taler-go

Utility functions in Go language
Log | Files | Refs | LICENSE

amount.go (4076B)


      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 GNU Taler Amount object
     31 type Amount struct {
     32 
     33 	// The type of currency, e.g. EUR
     34 	Currency string
     35 
     36 	// The value (before the ".")
     37 	Value uint64
     38 
     39 	// The fraction (after the ".", optional)
     40 	Fraction uint64
     41 }
     42 
     43 // The maximim length of a fraction (in digits)
     44 const FractionalLength = 8
     45 
     46 // The base of the fraction.
     47 const FractionalBase = 1e8
     48 
     49 // The maximum value
     50 var MaxAmountValue = uint64(math.Pow(2, 52))
     51 
     52 // Create a new amount from value and fraction in a currency
     53 func NewAmount(currency string, value uint64, fraction uint64) Amount {
     54 	return Amount{
     55 		Currency: currency,
     56 		Value:    value,
     57 		Fraction: fraction,
     58 	}
     59 }
     60 
     61 // Subtract the amount b from a and return the result.
     62 // a and b must be of the same currency and a >= b
     63 func (a *Amount) Sub(b Amount) (*Amount, error) {
     64 	if a.Currency != b.Currency {
     65 		return nil, errors.New("Currency mismatch!")
     66 	}
     67 	v := a.Value
     68 	f := a.Fraction
     69 	if a.Fraction < b.Fraction {
     70 		v -= 1
     71 		f += FractionalBase
     72 	}
     73 	f -= b.Fraction
     74 	if v < b.Value {
     75 		return nil, errors.New("Amount Overflow!")
     76 	}
     77 	v -= b.Value
     78 	r := Amount{
     79 		Currency: a.Currency,
     80 		Value:    v,
     81 		Fraction: f,
     82 	}
     83 	return &r, nil
     84 }
     85 
     86 // Add b to a and return the result.
     87 // Returns an error if the currencies do not match or the addition would
     88 // cause an overflow of the value
     89 func (a *Amount) Add(b Amount) (*Amount, error) {
     90 	if a.Currency != b.Currency {
     91 		return nil, errors.New("Currency mismatch!")
     92 	}
     93 	v := a.Value +
     94 		b.Value +
     95 		uint64(math.Floor((float64(a.Fraction)+float64(b.Fraction))/FractionalBase))
     96 
     97 	if v >= MaxAmountValue {
     98 		return nil, errors.New(fmt.Sprintf("Amount Overflow (%d > %d)!", v, MaxAmountValue))
     99 	}
    100 	f := uint64((a.Fraction + b.Fraction) % FractionalBase)
    101 	r := Amount{
    102 		Currency: a.Currency,
    103 		Value:    v,
    104 		Fraction: f,
    105 	}
    106 	return &r, nil
    107 }
    108 
    109 // Parses an amount string in the format <currency>:<value>[.<fraction>]
    110 func ParseAmount(s string) (*Amount, error) {
    111 	re, err := regexp.Compile(`^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$`)
    112 	parsed := re.FindStringSubmatch(s)
    113 
    114 	if nil != err {
    115 		return nil, errors.New(fmt.Sprintf("invalid amount: %s", s))
    116 	}
    117 	tail := "0.0"
    118 	if len(parsed) >= 4 {
    119 		tail = "0." + parsed[3]
    120 	}
    121 	if len(tail) > FractionalLength+1 {
    122 		return nil, errors.New("fraction too long")
    123 	}
    124 	value, err := strconv.ParseUint(parsed[2], 10, 64)
    125 	if nil != err {
    126 		return nil, errors.New(fmt.Sprintf("Unable to parse value %s", parsed[2]))
    127 	}
    128 	fractionF, err := strconv.ParseFloat(tail, 64)
    129 	if nil != err {
    130 		return nil, errors.New(fmt.Sprintf("Unable to parse fraction %s", tail))
    131 	}
    132 	fraction := uint64(math.Round(fractionF * FractionalBase))
    133 	currency := parsed[1]
    134 	a := NewAmount(currency, value, fraction)
    135 	return &a, nil
    136 }
    137 
    138 // Check if this amount is zero
    139 func (a *Amount) IsZero() bool {
    140 	return (a.Value == 0) && (a.Fraction == 0)
    141 }
    142 
    143 // Returns the string representation of the amount: <currency>:<value>[.<fraction>]
    144 // Omits trailing zeroes.
    145 func (a *Amount) String() string {
    146 	v := strconv.FormatUint(a.Value, 10)
    147 	if a.Fraction != 0 {
    148 		f := strconv.FormatUint(a.Fraction, 10)
    149 		f = strings.TrimRight(f, "0")
    150 		v = fmt.Sprintf("%s.%s", v, f)
    151 	}
    152 	return fmt.Sprintf("%s:%s", a.Currency, v)
    153 }