cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

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 }