remove unnecessary __ne__ implementation
[utils.git] / src / budget_sync / money.py
1 LOG10_CENTS_PER_EURO = 2
2 CENTS_PER_EURO = 10 ** LOG10_CENTS_PER_EURO
3
4 __all__ = ["Money", "LOG10_CENTS_PER_EURO", "CENTS_PER_EURO"]
5
6
7 def _is_ascii_digits(s: str):
8 try:
9 s.encode("ascii")
10 except UnicodeEncodeError:
11 return False
12 return s.isdigit()
13
14
15 class Money:
16 """class for handling money, stored as an integer number of cents.
17
18 Note: float is not appropriate for dealing with monetary values due to
19 loss of precision and round-off error. Decimal has similar issues, but to
20 a smaller extent."""
21
22 cents: int
23
24 __slots__ = ["cents"]
25
26 def __init__(self, value=None, *, cents=None):
27 if value is None:
28 if cents is None:
29 cents = 0
30 assert isinstance(cents, int)
31 elif isinstance(value, Money):
32 cents = value.cents
33 elif isinstance(value, int):
34 cents = value * CENTS_PER_EURO
35 elif isinstance(value, float):
36 raise TypeError("float is not an appropriate type"
37 " for dealing with money")
38 else:
39 cents = self.from_str(value).cents
40 self.cents = cents
41
42 @staticmethod
43 def from_str(text: str):
44 if not isinstance(text, str):
45 raise TypeError("Can't use Money.from_str to "
46 "convert from non-str value")
47 parts = text.strip().split(".", maxsplit=2)
48 first_part = parts[0]
49 negative = first_part.startswith("-")
50 if first_part.startswith(("-", "+")):
51 first_part = first_part[1:]
52 if first_part == "":
53 euros = 0
54 elif _is_ascii_digits(first_part):
55 euros = int(first_part)
56 else:
57 raise ValueError("invalid Money string: characters after sign and"
58 " before first `.` must be ascii digits")
59 if len(parts) > 2:
60 raise ValueError("invalid Money string: too many `.` characters")
61 elif len(parts) == 2:
62 if parts[1] == "":
63 if first_part == "":
64 raise ValueError("invalid Money string: missing digits")
65 cents = 0
66 elif _is_ascii_digits(parts[1]):
67 shift_amount = LOG10_CENTS_PER_EURO - len(parts[1])
68 if shift_amount < 0:
69 raise ValueError("invalid Money string: too many digits"
70 " after `.`")
71 cents = int(parts[1]) * (10 ** shift_amount)
72 else:
73 raise ValueError("invalid Money string: characters"
74 " after `.` must be ascii digits")
75 elif first_part == "":
76 raise ValueError("invalid Money string: missing digits")
77 else:
78 cents = 0
79 cents += CENTS_PER_EURO * euros
80 if negative:
81 cents = -cents
82 return Money(cents=cents)
83
84 def __str__(self):
85 retval = "-" if self.cents < 0 else ""
86 retval += str(abs(self.cents) // CENTS_PER_EURO)
87 cents = abs(self.cents) % CENTS_PER_EURO
88 if cents != 0:
89 retval += "."
90 retval += str(cents).zfill(LOG10_CENTS_PER_EURO)
91 return retval
92
93 def __lt__(self, other):
94 return self.cents < Money(other).cents
95
96 def __le__(self, other):
97 return self.cents <= Money(other).cents
98
99 def __eq__(self, other):
100 return self.cents == Money(other).cents
101
102 def __ne__(self, other):
103 return self.cents != Money(other).cents
104
105 def __gt__(self, other):
106 return self.cents > Money(other).cents
107
108 def __ge__(self, other):
109 return self.cents >= Money(other).cents
110
111 def __repr__(self):
112 return f"Money({repr(str(self))})"
113
114 def __bool__(self):
115 return bool(self.cents)
116
117 def __add__(self, other):
118 cents = self.cents + Money(other).cents
119 return Money(cents=cents)
120
121 def __radd__(self, other):
122 cents = Money(other).cents + self.cents
123 return Money(cents=cents)
124
125 def __sub__(self, other):
126 cents = self.cents - Money(other).cents
127 return Money(cents=cents)
128
129 def __rsub__(self, other):
130 cents = Money(other).cents - self.cents
131 return Money(cents=cents)
132
133 def __mul__(self, other):
134 if not isinstance(other, int):
135 raise TypeError("can't multiply by non-int")
136 cents = self.cents * other
137 return Money(cents=cents)
138
139 def __rmul__(self, other):
140 if not isinstance(other, int):
141 raise TypeError("can't multiply by non-int")
142 cents = other * self.cents
143 return Money(cents=cents)