cashless2ecash

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

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 }