-
Notifications
You must be signed in to change notification settings - Fork 394
/
amount.go
172 lines (150 loc) · 5.8 KB
/
amount.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 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)
}