"""Class for working with prices"""
import mcol
import mrich
CURRENCIES = {
"USD": "$",
"EUR": "€",
"GBP": "£",
}
[docs]
class Price:
"""Class to represent a certain amount of currency. Supported currencies:
::
CURRENCIES = {
'USD':'$',
'EUR':'€',
'GBP':'£',
}
"""
def __init__(self, amount: float | None, currency: str | None):
"""Price initialisation"""
if currency not in CURRENCIES:
assert currency is None, f"Unrecognised {currency=}"
assert not amount, f"Null Price can't have {amount=}"
amount = None
if amount is not None:
amount = float(amount)
self._amount = amount
self._currency = currency
### FACTORIES
[docs]
@classmethod
def null(cls) -> "Price":
"""Zero in any currency"""
self = cls.__new__(cls)
self.__init__(None, None)
return self
[docs]
@classmethod
def from_dict(
cls,
d: dict,
) -> "Price":
"""Create a :class:`.Price` object from a dictionary:
::
dict(amount: float, currency: str)
:param d: dictionary in the above format:
"""
self = cls.__new__(cls)
self.__init__(d["amount"], d["currency"])
return self
### PROPERTIES
@property
def symbol(self) -> str:
"""Currency symbol"""
return CURRENCIES[self.currency]
@property
def currency(self) -> str:
"""Currency string"""
return self._currency
@property
def amount(self) -> float:
"""Amount"""
return self._amount
@property
def is_null(self) -> bool:
"""Is this :meth:`.Price.null` or zero?"""
return self.amount is None
### METHODS
[docs]
def get_dict(self) -> dict:
"""Dictionary in the format:
::
dict(amount: float, currency: str)
"""
return dict(amount=self.amount, currency=self.currency)
[docs]
def copy(self) -> "Price":
"""Return a copy of this :class:`.Price`"""
return Price(amount=self.amount, currency=self.currency)
### DUNDERS
[docs]
def __str__(self) -> str:
"""Unformatted string representation"""
if self.currency is None:
return "Null Price"
return f"{self.symbol}{self.amount:.2f} {self.currency}"
[docs]
def __repr__(self) -> str:
"""ANSI Formatted string representation"""
return f"{mcol.bold}{mcol.underline}{self}{mcol.unbold}{mcol.ununderline}"
def __rich__(self) -> str:
"""Rich Formatted string representation"""
return f"[bold underline]{self}"
[docs]
def __add__(self, other: "Price") -> "Price":
"""Add two :class:`.Price` objects
:param other: :class:`.Price` object
:returns: :class:`.Price` object
"""
if other is None:
return self
if other.is_null:
return self
if self.is_null:
return other
if self.currency != other.currency:
raise NotImplementedError(
f"Adding two different currencies: {self.currency} != {other.currency}"
)
return Price(self.amount + other.amount, self.currency)
[docs]
def __truediv__(self, other: "Price | float | int") -> "Price | float":
"""Divide this :class:`.Price` by another object
:param other: :class:`.Price` or float or int
:returns: :class:`.Price` object or float
"""
if isinstance(other, int) or isinstance(other, float):
if self.is_null:
return self
return Price(amount=self.amount / other, currency=self.currency)
elif isinstance(other, Price):
assert self.currency == other.currency
assert not other.is_null
return self.amount / other.amount
raise TypeError(f"Division not supported between Price and {type(other)}")
[docs]
def __mul__(self, other: "Price | float | int") -> "Price | float":
"""Multiply this :class:`.Price` by another object
:param other: :class:`.Price` or float or int
:returns: :class:`.Price` object or float
"""
if isinstance(other, int) or isinstance(other, float):
if self.is_null:
return self
return Price(amount=self.amount * other, currency=self.currency)
raise TypeError(f"Multiplication not supported between Price and {type(other)}")
[docs]
def __eq__(self, other: "Price") -> bool:
"""Compare two :class:`.Price` objects"""
if isinstance(other, int) or isinstance(other, float):
if self.is_null:
return other == 0
return self.amount == other
if self.is_null and other.is_null:
return True
if self.is_null and not other.is_null:
return False
if not self.is_null and other.is_null:
return False
assert (
self.currency == other.currency
), f"Comparing different currencies: {self.currency} != {other.currency}"
return self.amount == other.amount
[docs]
def __lt__(self, other: "Price") -> bool:
"""Compare two :class:`.Price` objects"""
if isinstance(other, int) or isinstance(other, float):
if self.is_null:
return False
return self.amount > other
if self.is_null and other.is_null:
return False
if self.is_null and not other.is_null:
return True
if not self.is_null and other.is_null:
return False
assert (
self.currency == other.currency
), f"Comparing different currencies: {self.currency} != {other.currency}"
return self.amount < other.amount
[docs]
def __gt__(self, other: "Price") -> bool:
"""Compare two :class:`.Price` objects"""
if isinstance(other, int) or isinstance(other, float):
if self.is_null:
return False
return self.amount < other
if self.is_null and other.is_null:
return False
if self.is_null and not other.is_null:
return False
if not self.is_null and other.is_null:
return True
assert (
self.currency == other.currency
), f"Comparing different currencies: {self.currency} != {other.currency}"
return self.amount > other.amount
[docs]
def __hash__(self) -> int:
"""Allow for Prices to be hashed for comparison"""
if self.is_null:
return hash("NULL")
return hash(f"{self.currency} {self.amount}")