amount.go (6410B)
1 // This file is part of taler-go, the Taler Go implementation. 2 // Copyright (C) 2022 Martin Schanzenbach 3 // 4 // Taler Go is free software: you can redistribute it and/or modify it 5 // under the terms of the GNU Affero General Public License as published 6 // by the Free Software Foundation, either version 3 of the License, 7 // or (at your option) any later version. 8 // 9 // Taler Go is distributed in the hope that it will be useful, but 10 // WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 // Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 // 17 // SPDX-License-Identifier: AGPL3.0-or-later 18 19 package util 20 21 import ( 22 "errors" 23 "fmt" 24 "math" 25 "regexp" 26 "strconv" 27 "strings" 28 ) 29 30 // The DD51 currency specification for formatting 31 type CurrencySpecification struct { 32 // e.g. “Japanese Yen” or "Bitcoin (Mainnet)" 33 Name string `json:"name"` 34 35 // Currency 36 Currency string `json:"currency"` 37 38 // how many digits the user may enter after the decimal separator 39 NumFractionalInputDigits uint `json:"num_fractional_input_digits"` 40 41 // €,$,£: 2; some arabic currencies: 3, ¥: 0 42 NumFractionalNormalDigits uint `json:"num_fractional_normal_digits"` 43 44 // usually same as fractionalNormalDigits, but e.g. might be 2 for ¥ 45 NumFractionalTrailingZeroDigits uint `json:"num_fractional_trailing_zero_digits"` 46 47 // map of powers of 10 to alternative currency names / symbols, 48 // must always have an entry under "0" that defines the base name, 49 // e.g. "0 : €" or "3 : k€". For BTC, would be "0 : BTC, -3 : mBTC". 50 // This way, we can also communicate the currency symbol to be used. 51 AltUnitNames map[int]string `json:"alt_unit_names"` 52 } 53 54 var Currencies = map[string]CurrencySpecification{ 55 "KUDOS": { 56 Name: "KUDOS", 57 NumFractionalInputDigits: 2, 58 NumFractionalNormalDigits: 2, 59 AltUnitNames: map[int]string{ 60 0: "KUDOS", 61 }, 62 }, 63 "USD": { 64 Name: "US Dollar", 65 NumFractionalInputDigits: 2, 66 NumFractionalNormalDigits: 2, 67 AltUnitNames: map[int]string{ 68 0: "$", 69 }, 70 }, 71 "EUR": { 72 Name: "Euro", 73 NumFractionalInputDigits: 2, 74 NumFractionalNormalDigits: 2, 75 AltUnitNames: map[int]string{ 76 0: "€", 77 }, 78 }, 79 "JPY": { 80 Name: "Japanese Yen", 81 NumFractionalInputDigits: 2, 82 NumFractionalNormalDigits: 0, 83 AltUnitNames: map[int]string{ 84 0: "¥", 85 }, 86 }, 87 } 88 89 // The GNU Taler Amount object 90 type Amount struct { 91 92 // The type of currency, e.g. EUR 93 Currency string 94 95 // The value (before the ".") 96 Value uint64 97 98 // The fraction (after the ".", optional) 99 Fraction uint64 100 } 101 102 // The maximim length of a fraction (in digits) 103 const FractionalLength = 8 104 105 // The base of the fraction. 106 const FractionalBase = 1e8 107 108 // The maximum value 109 var MaxAmountValue = uint64(math.Pow(2, 52)) 110 111 // Create a new amount from value and fraction in a currency 112 func NewAmount(currency string, value uint64, fraction uint64) Amount { 113 return Amount{ 114 Currency: currency, 115 Value: value, 116 Fraction: fraction, 117 } 118 } 119 120 // FIXME also use altUnitNames. 121 func (a *Amount) FormatWithCurrencySpecification(cf CurrencySpecification) (string, error) { 122 123 if cf.NumFractionalNormalDigits == 0 { 124 return fmt.Sprintf("%s %d", cf.AltUnitNames[0], a.Value), nil 125 } 126 return fmt.Sprintf("%s %d.%0*d", cf.AltUnitNames[0], a.Value, cf.NumFractionalNormalDigits, a.Fraction/1e6), nil 127 } 128 129 func (a *Amount) Format() (string, error) { 130 cf, idx := Currencies[a.Currency] 131 if idx { 132 return a.FormatWithCurrencySpecification(cf) 133 } 134 return "", errors.New("No currency specification found for " + a.Currency) 135 } 136 137 // Subtract the amount b from a and return the result. 138 // a and b must be of the same currency and a >= b 139 func (a *Amount) Sub(b Amount) (*Amount, error) { 140 if a.Currency != b.Currency { 141 return nil, errors.New("Currency mismatch!") 142 } 143 v := a.Value 144 f := a.Fraction 145 if a.Fraction < b.Fraction { 146 v -= 1 147 f += FractionalBase 148 } 149 f -= b.Fraction 150 if v < b.Value { 151 return nil, errors.New("Amount Overflow!") 152 } 153 v -= b.Value 154 r := Amount{ 155 Currency: a.Currency, 156 Value: v, 157 Fraction: f, 158 } 159 return &r, nil 160 } 161 162 // Add b to a and return the result. 163 // Returns an error if the currencies do not match or the addition would 164 // cause an overflow of the value 165 func (a *Amount) Add(b Amount) (*Amount, error) { 166 if a.Currency != b.Currency { 167 return nil, errors.New("Currency mismatch!") 168 } 169 v := a.Value + 170 b.Value + 171 uint64(math.Floor((float64(a.Fraction)+float64(b.Fraction))/FractionalBase)) 172 173 if v >= MaxAmountValue { 174 return nil, errors.New(fmt.Sprintf("Amount Overflow (%d > %d)!", v, MaxAmountValue)) 175 } 176 f := uint64((a.Fraction + b.Fraction) % FractionalBase) 177 r := Amount{ 178 Currency: a.Currency, 179 Value: v, 180 Fraction: f, 181 } 182 return &r, nil 183 } 184 185 // Amounts in GNU Taler must match this regular expression 186 var rexAmount = regexp.MustCompile(`^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$`) 187 188 // Parses an amount string in the format <currency>:<value>[.<fraction>] 189 func ParseAmount(s string) (*Amount, error) { 190 parsed := rexAmount.FindStringSubmatch(s) 191 if nil == parsed { 192 return nil, errors.New(fmt.Sprintf("invalid amount: %s", s)) 193 } 194 tail := "0.0" 195 if len(parsed) >= 4 { 196 tail = "0." + parsed[3] 197 } 198 if len(tail) > FractionalLength+1 { 199 return nil, errors.New("fraction too long") 200 } 201 value, err := strconv.ParseUint(parsed[2], 10, 64) 202 if nil != err { 203 return nil, errors.New(fmt.Sprintf("Unable to parse value %s", parsed[2])) 204 } 205 fractionF, err := strconv.ParseFloat(tail, 64) 206 if nil != err { 207 return nil, errors.New(fmt.Sprintf("Unable to parse fraction %s", tail)) 208 } 209 fraction := uint64(math.Round(fractionF * FractionalBase)) 210 currency := parsed[1] 211 a := NewAmount(currency, value, fraction) 212 return &a, nil 213 } 214 215 // Check if this amount is zero 216 func (a *Amount) IsZero() bool { 217 return (a.Value == 0) && (a.Fraction == 0) 218 } 219 220 // Returns the string representation of the amount: <currency>:<value>[.<fraction>] 221 // Omits trailing zeroes. 222 func (a *Amount) String() string { 223 v := strconv.FormatUint(a.Value, 10) 224 if a.Fraction != 0 { 225 f := strconv.FormatUint(a.Fraction, 10) 226 f = strings.TrimRight(f, "0") 227 v = fmt.Sprintf("%s.%s", v, f) 228 } 229 return fmt.Sprintf("%s:%s", a.Currency, v) 230 }