amount.go (6698B)
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 internal_utils 21 22 import ( 23 "c2ec/pkg/config" 24 "errors" 25 "fmt" 26 "math" 27 "strconv" 28 "strings" 29 ) 30 31 // The GNU Taler Amount object 32 type Amount struct { 33 34 // The type of currency, e.g. EUR 35 Currency string `json:"currency"` 36 37 // The value (before the ".") 38 Value uint64 `json:"value"` 39 40 // The fraction (after the ".", optional) 41 Fraction uint64 `json:"fraction"` 42 } 43 44 type TalerAmountCurrency struct { 45 Val int64 `db:"val"` 46 Frac int32 `db:"frac"` 47 Curr string `db:"curr"` 48 } 49 50 func ToAmount(amount *TalerAmountCurrency) (*Amount, error) { 51 52 if amount == nil { 53 return &Amount{ 54 Currency: "", 55 Value: 0, 56 Fraction: 0, 57 }, nil 58 } 59 a := new(Amount) 60 a.Currency = amount.Curr 61 a.Value = uint64(amount.Val) 62 a.Fraction = uint64(amount.Frac) 63 return a, nil 64 } 65 66 func FormatAmount(amount *Amount, fractionalDigits int) string { 67 68 if amount == nil { 69 return "" 70 } 71 72 if amount.Currency == "" && amount.Value == 0 && amount.Fraction == 0 { 73 return "" 74 } 75 76 if amount.Fraction <= 0 { 77 return fmt.Sprintf("%s:%d", amount.Currency, amount.Value) 78 } 79 80 fractionStr := toFractionStr(int(amount.Fraction), fractionalDigits) 81 return fmt.Sprintf("%s:%d.%s", amount.Currency, amount.Value, fractionStr) 82 } 83 84 // The maximim length of a fraction (in digits) 85 const FractionalLength = 8 86 87 // The base of the fraction. 88 const FractionalBase = 1e8 89 90 // The maximum value 91 var MaxAmountValue = uint64(math.Pow(2, 52)) 92 93 // Create a new amount from value and fraction in a currency 94 func NewAmount(currency string, value uint64, fraction uint64) Amount { 95 return Amount{ 96 Currency: currency, 97 Value: value, 98 Fraction: fraction, 99 } 100 } 101 102 func toFractionStr(frac int, fractionalDigits int) string { 103 104 if fractionalDigits > 8 { 105 return "" 106 } 107 108 leadingZerosStr := "" 109 strLengthTens := int(math.Pow10(fractionalDigits - 1)) 110 strLength := int(math.Log10(float64(strLengthTens))) 111 leadingZeros := 0 112 if strLengthTens > frac { 113 for i := 0; i < strLength; i++ { 114 if strLengthTens > frac { 115 leadingZeros++ 116 strLengthTens = strLengthTens / 10 117 } 118 } 119 for i := 0; i < leadingZeros; i++ { 120 leadingZerosStr += "0" 121 } 122 } 123 124 return leadingZerosStr + strconv.Itoa(frac) 125 } 126 127 // checks if a < b 128 // returns error if the currencies do not match. 129 func (a *Amount) IsSmallerThan(b Amount) (bool, error) { 130 131 if !strings.EqualFold(a.Currency, b.Currency) { 132 return false, errors.New("unable tos compare different currencies") 133 } 134 135 if a.Value < b.Value { 136 return true, nil 137 } 138 139 if a.Value == b.Value && a.Fraction < b.Fraction { 140 return true, nil 141 } 142 143 return false, nil 144 } 145 146 // checks if a = b 147 // returns error if the currencies do not match. 148 func (a *Amount) IsEqualTo(b Amount) (bool, error) { 149 150 if !strings.EqualFold(a.Currency, b.Currency) { 151 return false, errors.New("unable tos compare different currencies") 152 } 153 154 return a.Value == b.Value && a.Fraction == b.Fraction, nil 155 } 156 157 // Subtract the amount b from a and return the result. 158 // a and b must be of the same currency and a >= b 159 func (a *Amount) Sub(b Amount, fractionalDigits int) (*Amount, error) { 160 if a.Currency != b.Currency { 161 return nil, errors.New("currency mismatch") 162 } 163 v := a.Value 164 f := a.Fraction 165 if a.Fraction < b.Fraction { 166 v -= 1 167 f += uint64(math.Pow10(fractionalDigits)) 168 } 169 f -= b.Fraction 170 if v < b.Value { 171 return nil, errors.New("amount overflow") 172 } 173 v -= b.Value 174 r := Amount{ 175 Currency: a.Currency, 176 Value: v, 177 Fraction: f, 178 } 179 return &r, nil 180 } 181 182 // Add b to a and return the result. 183 // Returns an error if the currencies do not match or the addition would 184 // cause an overflow of the value 185 func (a *Amount) Add(b Amount, fractionalDigits int) (*Amount, error) { 186 if a.Currency != b.Currency { 187 return nil, errors.New("currency mismatch") 188 } 189 v := a.Value + 190 b.Value + 191 uint64(math.Floor((float64(a.Fraction)+float64(b.Fraction))/FractionalBase)) 192 193 if v >= MaxAmountValue { 194 return nil, fmt.Errorf("amount overflow (%d > %d)", v, MaxAmountValue) 195 } 196 f := uint64((a.Fraction + b.Fraction) % uint64(math.Pow10(fractionalDigits))) 197 r := Amount{ 198 Currency: a.Currency, 199 Value: v, 200 Fraction: f, 201 } 202 return &r, nil 203 } 204 205 // Parses an amount string in the format <currency>:<value>[.<fraction>] 206 func ParseAmount(s string, fractionDigits int) (*Amount, error) { 207 208 if s == "" { 209 return &Amount{config.CONFIG.Server.Currency, 0, 0}, nil 210 } 211 212 if !strings.Contains(s, ":") { 213 return nil, fmt.Errorf("invalid amount: %s", s) 214 } 215 216 currencyAndAmount := strings.Split(s, ":") 217 if len(currencyAndAmount) != 2 { 218 return nil, fmt.Errorf("invalid amount: %s", s) 219 } 220 221 currency := currencyAndAmount[0] 222 valueAndFraction := strings.Split(currencyAndAmount[1], ".") 223 if len(valueAndFraction) < 1 && len(valueAndFraction) > 2 { 224 return nil, fmt.Errorf("invalid value and fraction part in amount %s", s) 225 } 226 value, err := strconv.Atoi(valueAndFraction[0]) 227 if err != nil { 228 LogError("amount", err) 229 return nil, fmt.Errorf("invalid value in amount %s", s) 230 } 231 232 fraction := 0 233 if len(valueAndFraction) == 2 { 234 if len(valueAndFraction[1]) > fractionDigits { 235 return nil, fmt.Errorf("invalid amount: %s expected at max %d fractional digits", s, fractionDigits) 236 } 237 k := 0 238 if len(valueAndFraction[1]) < fractionDigits { 239 k = fractionDigits - len(valueAndFraction[1]) 240 } 241 fractionInt, err := strconv.Atoi(valueAndFraction[1]) 242 if err != nil { 243 LogError("amount", err) 244 return nil, fmt.Errorf("invalid fraction in amount %s", s) 245 } 246 fraction = fractionInt * int(math.Pow10(k)) 247 } 248 249 a := NewAmount(currency, uint64(value), uint64(fraction)) 250 return &a, nil 251 } 252 253 // Check if this amount is zero 254 func (a *Amount) IsZero() bool { 255 return (a.Value == 0) && (a.Fraction == 0) 256 } 257 258 // Returns the string representation of the amount: <currency>:<value>[.<fraction>] 259 // Omits trailing zeroes. 260 func (a *Amount) String(fractionalDigits int) string { 261 262 return FormatAmount(a, fractionalDigits) 263 }