// Copyright (C) 2021 Storj Labs, Inc. // See LICENSE for copying information. package monetary import ( "fmt" "math" "math/big" "github.com/shopspring/decimal" "github.com/zeebo/errs" ) // Currency represents a currency for the purpose of representing amounts in // that currency. Currency instances have a name, a symbol, and a number of // supported decimal places of supported precision. type Currency struct { name string symbol string decimalPlaces int32 } // NewCurrency creates a new Currency instance. func NewCurrency(name, symbol string, decimalPlaces int32) *Currency { return &Currency{name: name, symbol: symbol, decimalPlaces: decimalPlaces} } // Name returns the name of the currency. func (c *Currency) Name() string { return c.name } // Symbol returns the symbol of the currency. func (c *Currency) Symbol() string { return c.symbol } var ( // StorjToken is the currency for the STORJ ERC20 token, which powers // most payments on the current Storj network. StorjToken = NewCurrency("STORJ Token", "STORJ", 8) // USDollars is the currency of United States dollars, where fractional // cents are not supported. USDollars = NewCurrency("US dollars", "USD", 2) // Bitcoin is the currency for the well-known cryptocurrency Bitcoin // (a.k.a. BTC). Bitcoin = NewCurrency("Bitcoin (BTC)", "BTC", 8) // LiveGoats is the currency of live goats, which some Storj network // satellites may elect to support for payments. LiveGoats = NewCurrency("Live goats", "goats", 0) // Error is a class of errors encountered in the monetary package. Error = errs.Class("monetary error") ) // Amount represents a monetary amount, encapsulating a value and a currency. // // The value of the Amount is represented in "base units", meaning units of the // smallest indivisible portion of the given currency. For example, when // the currency is USDollars, the base unit would be cents. type Amount struct { baseUnits int64 currency *Currency } // AsFloat returns the monetary value in currency units expressed as a // floating point number. _Warning_ may lose precision! (float64 has the // equivalent of 53 bits of precision, as defined by big.Float.) func (a Amount) AsFloat() float64 { return float64(a.baseUnits) * math.Pow10(int(-a.currency.decimalPlaces)) } // AsBigFloat returns the monetary value in currency units expressed as an // instance of *big.Float with precision=64. _Warning_ may lose precision! func (a Amount) AsBigFloat() *big.Float { return a.AsBigFloatWithPrecision(64) } // AsBigFloatWithPrecision returns the monetary value in currency units // expressed as an instance of *big.Float with the given precision. // _Warning_ this may lose precision if the specified precision is not // large enough! func (a Amount) AsBigFloatWithPrecision(p uint) *big.Float { stringVal := a.AsDecimal().String() bf, _, err := big.ParseFloat(stringVal, 10, p, big.ToNearestEven) if err != nil { // it does not appear that this is possible, after a review of // decimal.Decimal{}.String() and big.ParseFloat(). panic(fmt.Sprintf("could not parse output of Decimal.String() (%q) as big.Float: %v", stringVal, err)) } return bf } // AsDecimal returns the monetary value in currency units expressed as an // arbitrary precision decimal number. func (a Amount) AsDecimal() decimal.Decimal { d := decimal.NewFromInt(a.baseUnits) return d.Shift(-a.currency.decimalPlaces) } // BaseUnits returns the monetary value expressed in its base units. func (a Amount) BaseUnits() int64 { return a.baseUnits } // Currency returns the currency of the amount. func (a Amount) Currency() *Currency { return a.currency } // Equal returns true if a and other are in the same currency and have the // same value. func (a Amount) Equal(other Amount) bool { return a.currency == other.currency && a.baseUnits == other.baseUnits } // AmountFromBaseUnits creates a new Amount instance from the given count of // base units and in the given currency. func AmountFromBaseUnits(units int64, currency *Currency) Amount { return Amount{ baseUnits: units, currency: currency, } } // AmountFromDecimal creates a new Amount instance from the given decimal // value and in the given currency. The decimal value is expected to be in // currency units. // // Example: // // AmountFromDecimal(decimal.NewFromFloat(3.50), USDollars) == Amount{baseUnits: 350, currency: USDollars} func AmountFromDecimal(d decimal.Decimal, currency *Currency) Amount { return AmountFromBaseUnits(d.Shift(currency.decimalPlaces).Round(0).IntPart(), currency) } // AmountFromString creates a new Amount instance from the given base 10 // value and in the given currency. The string is expected to give the // value of the amount in currency units. func AmountFromString(s string, currency *Currency) (Amount, error) { d, err := decimal.NewFromString(s) if err != nil { return Amount{}, Error.Wrap(err) } return AmountFromDecimal(d, currency), nil } // AmountFromBigFloat creates a new Amount instance from the given floating // point value and in the given currency. The big.Float is expected to give // the value of the amount in currency units. func AmountFromBigFloat(f *big.Float, currency *Currency) (Amount, error) { dec, err := DecimalFromBigFloat(f) if err != nil { return Amount{}, err } return AmountFromDecimal(dec, currency), nil } // DecimalFromBigFloat creates a new decimal.Decimal instance from the given // floating point value. func DecimalFromBigFloat(f *big.Float) (decimal.Decimal, error) { if f.IsInf() { return decimal.Decimal{}, Error.New("Cannot represent infinite amount") } // This is probably not computationally ideal, but it should be the most // straightforward way to convert (unless/until the decimal package adds // a NewFromBigFloat method). stringVal := f.Text('e', -1) dec, err := decimal.NewFromString(stringVal) return dec, Error.Wrap(err) }