amount.go (5420B)
1 // This file is part of taler-go, the Taler Go implementation. 2 // Copyright (C) 2022 Martin Schanzenbach 3 // Copyright (C) 2024 Joel Häberli 4 // 5 // Taler Go is free software: you can redistribute it and/or modify it 6 // under the terms of the GNU Affero General Public License as published 7 // by the Free Software Foundation, either version 3 of the License, 8 // or (at your option) any later version. 9 // 10 // Taler Go is distributed in the hope that it will be useful, but 11 // WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 // 18 // SPDX-License-Identifier: AGPL3.0-or-later 19 20 package main 21 22 import ( 23 "errors" 24 "fmt" 25 "math" 26 "strconv" 27 "strings" 28 ) 29 30 // The GNU Taler Amount object 31 type Amount struct { 32 33 // The type of currency, e.g. EUR 34 Currency string `json:"currency"` 35 36 // The value (before the ".") 37 Value uint64 `json:"value"` 38 39 // The fraction (after the ".", optional) 40 Fraction uint64 `json:"fraction"` 41 } 42 43 func FormatAmount(amount *Amount, fractionalDigits int) string { 44 45 if amount == nil { 46 return "" 47 } 48 49 if amount.Currency == "" && amount.Value == 0 && amount.Fraction == 0 { 50 return "" 51 } 52 53 if amount.Fraction <= 0 { 54 return fmt.Sprintf("%s:%d", amount.Currency, amount.Value) 55 } 56 57 fractionStr := toFractionStr(int(amount.Fraction), fractionalDigits) 58 return fmt.Sprintf("%s:%d.%s", amount.Currency, amount.Value, fractionStr) 59 } 60 61 // The maximim length of a fraction (in digits) 62 const FractionalLength = 8 63 64 // The base of the fraction. 65 const FractionalBase = 1e8 66 67 // The maximum value 68 var MaxAmountValue = uint64(math.Pow(2, 52)) 69 70 // Create a new amount from value and fraction in a currency 71 func NewAmount(currency string, value uint64, fraction uint64) Amount { 72 return Amount{ 73 Currency: currency, 74 Value: value, 75 Fraction: fraction, 76 } 77 } 78 79 func toFractionStr(frac int, fractionalDigits int) string { 80 81 if fractionalDigits > 8 { 82 return "" 83 } 84 85 leadingZerosStr := "" 86 strLengthTens := int(math.Pow10(fractionalDigits - 1)) 87 strLength := int(math.Log10(float64(strLengthTens))) 88 leadingZeros := 0 89 if strLengthTens > frac { 90 for i := 0; i < strLength; i++ { 91 if strLengthTens > frac { 92 leadingZeros++ 93 strLengthTens = strLengthTens / 10 94 } 95 } 96 for i := 0; i < leadingZeros; i++ { 97 leadingZerosStr += "0" 98 } 99 } 100 101 return leadingZerosStr + strconv.Itoa(frac) 102 } 103 104 // Subtract the amount b from a and return the result. 105 // a and b must be of the same currency and a >= b 106 func (a *Amount) Sub(b Amount) (*Amount, error) { 107 if a.Currency != b.Currency { 108 return nil, errors.New("currency mismatch") 109 } 110 v := a.Value 111 f := a.Fraction 112 if a.Fraction < b.Fraction { 113 v -= 1 114 f += FractionalBase 115 } 116 f -= b.Fraction 117 if v < b.Value { 118 return nil, errors.New("amount overflow") 119 } 120 v -= b.Value 121 r := Amount{ 122 Currency: a.Currency, 123 Value: v, 124 Fraction: f, 125 } 126 return &r, nil 127 } 128 129 // Add b to a and return the result. 130 // Returns an error if the currencies do not match or the addition would 131 // cause an overflow of the value 132 func (a *Amount) Add(b Amount) (*Amount, error) { 133 if a.Currency != b.Currency { 134 return nil, errors.New("currency mismatch") 135 } 136 v := a.Value + 137 b.Value + 138 uint64(math.Floor((float64(a.Fraction)+float64(b.Fraction))/FractionalBase)) 139 140 if v >= MaxAmountValue { 141 return nil, fmt.Errorf("amount overflow (%d > %d)", v, MaxAmountValue) 142 } 143 f := uint64((a.Fraction + b.Fraction) % FractionalBase) 144 r := Amount{ 145 Currency: a.Currency, 146 Value: v, 147 Fraction: f, 148 } 149 return &r, nil 150 } 151 152 // Parses an amount string in the format <currency>:<value>[.<fraction>] 153 func ParseAmount(s string, fractionDigits int) (*Amount, error) { 154 155 if s == "" { 156 return &Amount{"", 0, 0}, nil 157 } 158 159 if !strings.Contains(s, ":") { 160 return nil, fmt.Errorf("invalid amount: %s", s) 161 } 162 163 currencyAndAmount := strings.Split(s, ":") 164 if len(currencyAndAmount) != 2 { 165 return nil, fmt.Errorf("invalid amount: %s", s) 166 } 167 168 currency := currencyAndAmount[0] 169 valueAndFraction := strings.Split(currencyAndAmount[1], ".") 170 if len(valueAndFraction) < 1 && len(valueAndFraction) > 2 { 171 return nil, fmt.Errorf("invalid value and fraction part in amount %s", s) 172 } 173 value, err := strconv.Atoi(valueAndFraction[0]) 174 if err != nil { 175 return nil, fmt.Errorf("invalid value in amount %s", s) 176 } 177 178 fraction := 0 179 if len(valueAndFraction) == 2 { 180 if len(valueAndFraction[1]) > fractionDigits { 181 return nil, fmt.Errorf("invalid amount: %s expected at max %d fractional digits", s, fractionDigits) 182 } 183 fractionInt, err := strconv.Atoi(valueAndFraction[1]) 184 if err != nil { 185 return nil, fmt.Errorf("invalid fraction in amount %s", s) 186 } 187 fraction = fractionInt 188 } 189 190 a := NewAmount(currency, uint64(value), uint64(fraction)) 191 return &a, nil 192 } 193 194 // Check if this amount is zero 195 func (a *Amount) IsZero() bool { 196 return (a.Value == 0) && (a.Fraction == 0) 197 } 198 199 // Returns the string representation of the amount: <currency>:<value>[.<fraction>] 200 // Omits trailing zeroes. 201 func (a *Amount) String() string { 202 v := strconv.FormatUint(a.Value, 10) 203 if a.Fraction != 0 { 204 f := strconv.FormatUint(a.Fraction, 10) 205 f = strings.TrimRight(f, "0") 206 v = fmt.Sprintf("%s.%s", v, f) 207 } 208 return fmt.Sprintf("%s:%s", a.Currency, v) 209 }