taler-mailbox

Service for asynchronous wallet-to-wallet payment messages
Log | Files | Refs | Submodules | README | LICENSE

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 }