// Package dec is a fixed-point decimal with 18 fractional digits, the same // precision as the Cosmos SDK LegacyDec the Injective exchange module uses for // prices, quantities and fee rates. Values are immutable. package dec import ( "errors" "math/big" "strings" ) const Precision = 18 var ( scale = new(big.Int).Exp(big.NewInt(10), big.NewInt(Precision), nil) // ErrSyntax is returned for strings that are not plain decimals. ErrSyntax = errors.New("dec: invalid decimal string") // ErrPrecision is returned when a string has more than 18 fractional digits. ErrPrecision = errors.New("dec: more than 18 fractional digits") ) // Dec holds value * 10^18 as an integer. type Dec struct{ i *big.Int } func Zero() Dec { return Dec{new(big.Int)} } func FromInt(n int64) Dec { return Dec{new(big.Int).Mul(big.NewInt(n), scale)} } func (d Dec) bi() *big.Int { if d.i == nil { return new(big.Int) } return d.i } // Parse reads "123", "-0.5", "8.105000000000000000". No exponents. func Parse(s string) (Dec, error) { s = strings.TrimSpace(s) if s == "" { return Dec{}, ErrSyntax } neg := false if s[0] == '-' || s[0] == '+' { neg = s[0] == '-' s = s[1:] } intPart, frac, hasDot := strings.Cut(s, ".") if intPart == "" && frac == "" || (hasDot && frac == "" && intPart == "") { return Dec{}, ErrSyntax } if intPart == "" { intPart = "0" } if len(frac) > Precision { return Dec{}, ErrPrecision } for _, c := range intPart + frac { if c < '0' || c > '9' { return Dec{}, ErrSyntax } } digits := intPart + frac + strings.Repeat("0", Precision-len(frac)) v, ok := new(big.Int).SetString(digits, 10) if !ok { return Dec{}, ErrSyntax } if neg { v.Neg(v) } return Dec{v}, nil } // MustParse panics on error; for constants and tests. func MustParse(s string) Dec { d, err := Parse(s) if err != nil { panic(err) } return d } func (d Dec) Add(o Dec) Dec { return Dec{new(big.Int).Add(d.bi(), o.bi())} } func (d Dec) Sub(o Dec) Dec { return Dec{new(big.Int).Sub(d.bi(), o.bi())} } func (d Dec) Neg() Dec { return Dec{new(big.Int).Neg(d.bi())} } // Mul rounds half away from zero to 18 digits. func (d Dec) Mul(o Dec) Dec { p := new(big.Int).Mul(d.bi(), o.bi()) return Dec{roundQuo(p, scale)} } // Quo rounds half away from zero to 18 digits. Division by zero panics. func (d Dec) Quo(o Dec) Dec { if o.IsZero() { panic("dec: division by zero") } n := new(big.Int).Mul(d.bi(), scale) return Dec{roundQuo(n, o.bi())} } func roundQuo(n, m *big.Int) *big.Int { q, r := new(big.Int).QuoRem(n, m, new(big.Int)) if r.Sign() == 0 { return q } twice := new(big.Int).Abs(r) twice.Lsh(twice, 1) if twice.Cmp(new(big.Int).Abs(m)) >= 0 { if (n.Sign() < 0) != (m.Sign() < 0) { q.Sub(q, big.NewInt(1)) } else { q.Add(q, big.NewInt(1)) } } return q } func (d Dec) Cmp(o Dec) int { return d.bi().Cmp(o.bi()) } func (d Dec) Equal(o Dec) bool { return d.Cmp(o) == 0 } func (d Dec) GT(o Dec) bool { return d.Cmp(o) > 0 } func (d Dec) GTE(o Dec) bool { return d.Cmp(o) >= 0 } func (d Dec) LT(o Dec) bool { return d.Cmp(o) < 0 } func (d Dec) LTE(o Dec) bool { return d.Cmp(o) <= 0 } func (d Dec) IsZero() bool { return d.bi().Sign() == 0 } func (d Dec) IsPositive() bool { return d.bi().Sign() > 0 } func (d Dec) IsNegative() bool { return d.bi().Sign() < 0 } func Min(a, b Dec) Dec { if a.LT(b) { return a } return b } func Max(a, b Dec) Dec { if a.GT(b) { return a } return b } // IsMultipleOf reports whether d is an exact multiple of tick (tick > 0). func (d Dec) IsMultipleOf(tick Dec) bool { if !tick.IsPositive() { return true } return new(big.Int).Rem(d.bi(), tick.bi()).Sign() == 0 } // FloorTo rounds d down (toward negative infinity) to a multiple of tick. func (d Dec) FloorTo(tick Dec) Dec { if !tick.IsPositive() { return d } q := new(big.Int).Div(d.bi(), tick.bi()) // Euclidean, floors for positive tick return Dec{q.Mul(q, tick.bi())} } // CeilTo rounds d up to a multiple of tick. func (d Dec) CeilTo(tick Dec) Dec { f := d.FloorTo(tick) if f.Equal(d) { return f } return f.Add(tick) } // String prints the shortest exact form ("8.105", "-0.5", "12"). func (d Dec) String() string { v := d.bi() neg := v.Sign() < 0 a := new(big.Int).Abs(v) q, r := new(big.Int).QuoRem(a, scale, new(big.Int)) s := q.String() if r.Sign() != 0 { fs := r.String() fs = strings.Repeat("0", Precision-len(fs)) + fs s += "." + strings.TrimRight(fs, "0") } if neg { s = "-" + s } return s } // Round returns d rounded half away from zero to n fractional digits. func (d Dec) Round(n int) Dec { if n >= Precision { return d } unit := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(Precision-n)), nil) q := roundQuo(d.bi(), unit) return Dec{q.Mul(q, unit)} } // Float64 is for display and charts only, never for matching. func (d Dec) Float64() float64 { f, _ := new(big.Rat).SetFrac(d.bi(), scale).Float64() return f } func (d Dec) MarshalJSON() ([]byte, error) { return []byte(`"` + d.String() + `"`), nil } func (d *Dec) UnmarshalJSON(b []byte) error { s := strings.Trim(string(b), `"`) v, err := Parse(s) if err != nil { return err } *d = v return nil }