summaryrefslogtreecommitdiff
path: root/talerbank/app/amount.py
blob: 83f91e02466782835a420f6f4ee898c6e8169e8a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
##
# This file is part of TALER
# (C) 2017 TALER SYSTEMS
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later
# version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library; if not, write to the Free
#  Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
#  Boston, MA  02110-1301  USA
#
#  @author Marcello Stanisci
#  @version 0.1
#  @repository https://git.taler.net/copylib.git/
#  This code is "copylib", it is versioned under the Git repository
#  mentioned above, and it is meant to be manually copied into
#  any project which might need it.


##
# Exception class to raise when an operation between two
# amounts of different currency is being attempted.
class CurrencyMismatch(Exception):
    hint = "Client gave amount with unsupported currency."
    http_status_code = 406

    ##
    # Init constructor.
    #
    # @param self the object itself.
    # @param curr1 first currency involved in the operation.
    # @param curr2 second currency involved in the operation.
    def __init__(self, curr1, curr2) -> None:
        super(CurrencyMismatch, self).__init__(
            "%s vs %s" % (curr1, curr2))

##
# Exception class to raise when a amount string is not valid.
class BadFormatAmount(Exception):

    hint = "Malformed amount string"

    ##
    # Init constructor.
    #
    # @param self the object itself.
    # @param faulty_str the invalid amount string.
    def __init__(self, faulty_str) -> None:
        super(BadFormatAmount, self).__init__(
            "Bad format amount: " + faulty_str)

##
# Main Amount class.
class NumberTooBig(Exception):
    hint = "Number given is too big"
    def __init__(self) -> None:
        super(NumberTooBig, self).__init__(
            "Number given is too big")

class NegativeNumber(Exception):
    hint = "Negative number given as value and/or fraction"
    def __init__(self) -> None:
        super(NegativeNumber, self).__init__(
            "Negative number given as value and/or fraction")

class Amount:
    ##
    # How many "fraction" units make one "value" unit of currency
    # (Taler requires 10^8).  Do not change this 'constant'.
    @staticmethod
    def _fraction() -> int:
        return 10 ** 8

    ##
    # Max value admitted: 2^53 - 1.  This constant is dictated
    # by the wallet: JavaScript does not go beyond this value.
    @staticmethod
    def _max_value() -> int:
        return (2 ** 53) - 1


    ##
    # Init constructor.
    #
    # @param self the object itself.
    # @param currency the amount's currency.
    # @param value integer part the amount
    # @param fraction fractional part of the amount
    def __init__(self, currency, value=0, fraction=0) -> None:
        if value < 0 or fraction < 0:
            raise NegativeNumber()
        self.value = value
        self.fraction = fraction
        self.currency = currency
        self.__normalize()
        if self.value > Amount._max_value():
            raise NumberTooBig()

    ##
    # Normalize amount.  It means it makes sure that the
    # fractional part is less than one unit, and transfers
    # the overhead to the integer part.
    def __normalize(self) -> None:
        if self.fraction >= Amount._fraction():
            self.value += int(self.fraction / Amount._fraction())
            self.fraction = self.fraction % Amount._fraction()

    ##
    # Parse a string matching the format "A:B.C",
    # instantiating an amount object.
    #
    # @param cls unused.
    # @param amount_str the stringified amount to parse.
    @classmethod
    def parse(cls, amount_str: str):
        exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.?([0-9]+)?\s*$'
        import re
        parsed = re.search(exp, amount_str)
        if not parsed:
            raise BadFormatAmount(amount_str)

        ##
        # Checks if the input overflows.
        #
        # @param arg the input number to check.
        # @return True if the overflow occurs, False otherwise.
        def check_overflow(arg):
            # Comes from 2^53 - 1
            JAVASCRIPT_MAX_INT = "9007199254740991"
            if len(JAVASCRIPT_MAX_INT) < len(arg):
                return True
            if len(JAVASCRIPT_MAX_INT) == len(arg):
                # Assume current system can afford to store
                # a number as big as JAVASCRIPT_MAX_INT.
                tmp = int(arg)
                tmp_js = int(JAVASCRIPT_MAX_INT)
                
                if tmp > tmp_js - 1: # - 1 leaves room for the fractional part
                    return True
            return False

        if check_overflow(parsed.group(2)):
            raise AmountOverflow("integer part")

        value = int(parsed.group(2))
        fraction = 0
        for i, digit in enumerate(parsed.group(3) or "0"):
            fraction += int(int(digit) * (Amount._fraction() / 10 ** (i+1)))
            if check_overflow(str(fraction)):
                raise AmountOverflow("fraction")

        return cls(parsed.group(1), value, fraction)

    ##
    # Compare two amounts.
    #
    # @param am1 first amount to compare.
    # @param am2 second amount to compare.
    # @return -1 if a < b
    #          0 if a == b
    #          1 if a > b
    @staticmethod
    def cmp(am1, am2) -> int:
        if am1.currency != am2.currency:
            raise CurrencyMismatch(am1.currency, am2.currency)
        if am1.value == am2.value:
            if am1.fraction < am2.fraction:
                return -1
            if am1.fraction > am2.fraction:
                return 1
            return 0
        if am1.value < am2.value:
            return -1
        return 1


    ##
    # Setter method for the current object.
    #
    # @param self the object itself.
    # @param currency the currency to set.
    # @param value the value to set.
    # @param fraction the fraction to set.
    def set(self, currency: str, value=0, fraction=0) -> None:
        self.currency = currency
        self.value = value
        self.fraction = fraction

    ##
    # Add the given amount to this one.
    #
    # @param self the object itself.
    # @param amount the amount to add to this one.
    def add(self, amount) -> None:
        if self.currency != amount.currency:
            raise CurrencyMismatch(self.currency, amount.currency)
        self.value += amount.value
        self.fraction += amount.fraction
        self.__normalize()

    ##
    # Subtract amount from this one.
    #
    # @param self this object.
    # @param amount the amount to subtract to this one.
    def subtract(self, amount) -> None:
        if self.currency != amount.currency:
            raise CurrencyMismatch(self.currency, amount.currency)
        if self.fraction < amount.fraction:
            self.fraction += Amount._fraction()
            self.value -= 1
        if self.value < amount.value:
            raise ValueError('self is lesser than amount to be subtracted')
        self.value -= amount.value
        self.fraction -= amount.fraction

    ##
    # Dump string from this amount, will put 'ndigits' numbers
    # after the dot.
    #
    # @param self this object.
    # @param ndigits how many digits we want for the fractional part.
    # @param pretty if True, put the currency in the last position and
    #        omit the colon.
    def stringify(self, ndigits: int, pretty=False) -> str:
        if ndigits <= 0:
            raise BadFormatAmount("ndigits must be > 0")
        tmp = self.fraction
        fraction_str = ""
        while ndigits > 0:
            fraction_str += str(int(tmp / (Amount._fraction() / 10)))
            tmp = (tmp * 10) % (Amount._fraction())
            ndigits -= 1
        if not pretty:
            return "%s:%d.%s" % (self.currency, self.value, fraction_str)
        return "%d.%s %s" % (self.value, fraction_str, self.currency)

    ##
    # Dump the Taler-compliant 'dict' amount from
    # this object.
    #
    # @param self this object.
    def dump(self) -> dict:
        return dict(value=self.value,
                    fraction=self.fraction,
                    currency=self.currency)