Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / type / centcount.py
index 4181721bbc0d4ab7efe38608b821faa26ecf5bae..e78d068910e8281cbf28ee8674e7b6cff44f89fd 100644 (file)
@@ -1,26 +1,23 @@
 #!/usr/bin/env python3
 
-import re
-from typing import Optional, TypeVar, Tuple
+# © Copyright 2021-2022, Scott Gasch
 
-import math_utils
+"""An amount of money (USD) represented as an integral count of
+cents."""
 
+import re
+from typing import Optional, Tuple
 
-T = TypeVar('T', bound='CentCount')
+import math_utils
 
 
 class CentCount(object):
     """A class for representing monetary amounts potentially with
-    different currencies.
+    different currencies meant to avoid floating point rounding
+    issues by treating amount as a simple integral count of cents.
     """
 
-    def __init__ (
-            self,
-            centcount,
-            currency: str = 'USD',
-            *,
-            strict_mode = False
-    ):
+    def __init__(self, centcount, currency: str = 'USD', *, strict_mode=False):
         self.strict_mode = strict_mode
         if isinstance(centcount, str):
             ret = CentCount._parse(centcount)
@@ -36,7 +33,7 @@ class CentCount(object):
         if not currency:
             self.currency: Optional[str] = None
         else:
-            self.currency: Optional[str] = currency
+            self.currency = currency
 
     def __repr__(self):
         a = float(self.centcount)
@@ -44,9 +41,9 @@ class CentCount(object):
         a = round(a, 2)
         s = f'{a:,.2f}'
         if self.currency is not None:
-            return '%s %s' % (s, self.currency)
+            return f'{s} {self.currency}'
         else:
-            return '$%s' % s
+            return f'${s}'
 
     def __pos__(self):
         return CentCount(centcount=self.centcount, currency=self.currency)
@@ -58,8 +55,8 @@ class CentCount(object):
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
-                    centcount = self.centcount + other.centcount,
-                    currency = self.currency
+                    centcount=self.centcount + other.centcount,
+                    currency=self.currency,
                 )
             else:
                 raise TypeError('Incompatible currencies in add expression')
@@ -73,8 +70,8 @@ class CentCount(object):
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
-                    centcount = self.centcount - other.centcount,
-                    currency = self.currency
+                    centcount=self.centcount - other.centcount,
+                    currency=self.currency,
                 )
             else:
                 raise TypeError('Incompatible currencies in add expression')
@@ -89,8 +86,8 @@ class CentCount(object):
             raise TypeError('can not multiply monetary quantities')
         else:
             return CentCount(
-                centcount = int(self.centcount * float(other)),
-                currency = self.currency
+                centcount=int(self.centcount * float(other)),
+                currency=self.currency,
             )
 
     def __truediv__(self, other):
@@ -98,8 +95,8 @@ class CentCount(object):
             raise TypeError('can not divide monetary quantities')
         else:
             return CentCount(
-                centcount = int(float(self.centcount) / float(other)),
-                currency = self.currency
+                centcount=int(float(self.centcount) / float(other)),
+                currency=self.currency,
             )
 
     def __int__(self):
@@ -124,8 +121,8 @@ class CentCount(object):
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
-                    centcount = other.centcount - self.centcount,
-                    currency = self.currency
+                    centcount=other.centcount - self.centcount,
+                    currency=self.currency,
                 )
             else:
                 raise TypeError('Incompatible currencies in sub expression')
@@ -134,8 +131,8 @@ class CentCount(object):
                 raise TypeError('In strict_mode only two moneys can be added')
             else:
                 return CentCount(
-                    centcount = int(other) - self.centcount,
-                    currency = self.currency
+                    centcount=int(other) - self.centcount,
+                    currency=self.currency,
                 )
 
     __rmul__ = __mul__
@@ -147,10 +144,7 @@ class CentCount(object):
         if other is None:
             return False
         if isinstance(other, CentCount):
-            return (
-                self.centcount == other.centcount and
-                self.currency == other.currency
-            )
+            return self.centcount == other.centcount and self.currency == other.currency
         if self.strict_mode:
             raise TypeError("In strict mode only two CentCounts can be compared")
         else:
@@ -192,11 +186,11 @@ class CentCount(object):
     def __ge__(self, other):
         return self > other or self == other
 
-    def __hash__(self):
-        return self.__repr__
+    def __hash__(self) -> int:
+        return hash(self.__repr__)
 
-    CENTCOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
-    CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
+    CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
+    CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
 
     @classmethod
     def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
@@ -210,7 +204,7 @@ class CentCount(object):
                     centcount = int(float(chunk) * 100.0)
                 elif CentCount.CURRENCY_RE.match(chunk) is not None:
                     currency = chunk
-        except:
+        except Exception:
             pass
         if centcount is not None and currency is not None:
             return (centcount, currency)
@@ -219,7 +213,7 @@ class CentCount(object):
         return None
 
     @classmethod
-    def parse(cls, s: str) -> T:
+    def parse(cls, s: str) -> 'CentCount':
         chunks = CentCount._parse(s)
         if chunks is not None:
             return CentCount(chunks[0], chunks[1])