Skip to content

Instantly share code, notes, and snippets.

@cellofellow
Last active August 15, 2019 21:22
Show Gist options
  • Select an option

  • Save cellofellow/69b445664c67386794caee767515acb4 to your computer and use it in GitHub Desktop.

Select an option

Save cellofellow/69b445664c67386794caee767515acb4 to your computer and use it in GitHub Desktop.
'''
Toy module that handles old pre-decimal British £sd (pounds, shillings, pence)
money values. It stores all as integers of farthings (Farthing class), and can
convert a number of farthings into £sd notation or into a number of all British
coins. It can also take a bundle of coins and simplify those according to a set
of available coins.
'''
from typing import (Union, Tuple, FrozenSet, Set, Type, NamedTuple, Dict)
Farthinglike = Union[int, 'Farthing', Type['Farthing']]
StrSet = Union[FrozenSet[str], Set[str]]
class Farthing(int):
'''
Represent one farthing (¼ penny) of £sd British currency.
'''
def lsd(self) -> 'LSD':
'Notation of this number of farthings in £sd, pounds shillings pence.'
return LSD.make(self)
def coin(self, with_guineas=False) -> 'Coins':
'Best coinage for this number of farthings.'
return Coins.split(self,
(WITH_GUINEAS
if with_guineas
else WITHOUT_GUINEAS))
def split(self, other: 'Farthing') -> Tuple[int, 'Farthing']:
return int(self) // other, Farthing(self % other)
def __add__(self, other: Farthinglike) -> 'Farthing':
right: int
if other is Farthing:
right = 1
elif isinstance(other, Farthing):
right = int(other)
elif isinstance(other, int):
right = other
return Farthing(int(self) + right)
def __sub__(self, other: Farthinglike) -> 'Farthing':
right: int
if other is Farthing:
right = 1
elif isinstance(other, Farthing):
right = int(other)
elif isinstance(other, int):
right = other
return Farthing(int(self) - right)
def __mul__(self, other: Farthinglike) -> 'Farthing':
right: int
if other is Farthing:
right = 1
elif isinstance(other, Farthing):
right = int(other)
elif isinstance(other, int):
right = other
return Farthing(int(self) * right)
def __rmul__(self, other: Farthinglike) -> 'Farthing':
left: int
if other is Farthing:
left = 1
elif isinstance(other, Farthing):
left = int(other)
elif isinstance(other, int):
left = other
return Farthing(int(self) * left)
def __repr__(self):
return f'Farthing({int(self)})'
# Coins in denominations of farthings.
Zero = Farthing(0)
One = Farthing(1)
Halfpenny = Farthing(2)
Penny = Farthing(4)
Twopence = Penny * 2
Threepence = Penny * 3
Sixpence = Penny * 6
Shilling = Penny * 12
Florin = Shilling * 2
HalfCrown = (Shilling * 2) + Sixpence
Crown = Shilling * 5
HalfSovereign = Shilling * 10
Pound = Shilling * 20
Sovereign = Pound
Guinea = Pound + Shilling
def test_farthing_add():
two = One + 1
assert isinstance(two, Farthing), 'expected a Farthing instance'
assert two == 2
assert two == Halfpenny
def test_farthing_sub():
three = Penny - 1
assert isinstance(three, Farthing), 'expected a Farthing instance'
assert three == 3
def test_farthing_mul():
tuppence = Penny * 2
assert isinstance(tuppence, Farthing), 'expected a Farthing instance'
assert tuppence == 8
assert tuppence == Twopence
def test_farthing_rmul():
tuppence = 2 * Penny
assert isinstance(tuppence, Farthing), 'expected a Farthing instance'
assert tuppence == 8
assert tuppence == Twopence
def test_farthing_split():
penny, one = Farthing(5).split(Penny)
assert not isinstance(penny, Farthing), 'expected not a Farthing instance'
assert isinstance(one, Farthing), 'expected a Farthing instance'
assert penny == 1
assert one == 1
def test_farthing_repr():
assert repr(Zero) == 'Farthing(0)'
assert repr(Guinea) == 'Farthing(1008)'
def test_farthing_lsd():
one_of_each = Pound + Shilling + Penny + One
lsd = one_of_each.lsd()
assert isinstance(lsd, LSD), 'expected LSD instance'
assert lsd.value() == one_of_each
def test_farthing_coin():
one_of_each = Pound + Shilling + Penny + One
coins = one_of_each.coin()
assert isinstance(coins, Coins), 'expected Coins instance'
assert coins.value() == one_of_each
class LSD(NamedTuple):
'''
Representation of £sd, pounds shillings pence, from a number of farthings.
'''
pounds: int
shillings: int
pennies: int
farthings: int
@classmethod
def make(cls, value: Farthing) -> 'LSD':
pounds, remainder = Farthing(value).split(Pound)
shillings, remainder = remainder.split(Shilling)
pennies, farthings = remainder.split(Penny)
return cls(pounds, shillings, pennies, farthings)
def value(self) -> Farthing:
return sum([self.pounds * Pound,
self.shillings * Shilling,
self.pennies * Penny,
Farthing(self.farthings)],
Zero)
def __str__(self):
FARTHING_MAP = {1: '¼', 2: '½', 3: '¾'}
pounds = f'£{self.pounds}' if self.pounds else ''
shillings = f'{self.shillings}s' if self.shillings else ''
farthings = FARTHING_MAP.get(self.farthings, '')
pence = (f'{self.pennies}{farthings}d.'
if (self.pennies or farthings)
else '')
return '.'.join(s for s in [pounds, shillings, pence] if s)
def test_lsd_make():
lsd = LSD.make(Pound + Shilling + Penny + One)
assert isinstance(lsd, LSD), 'expected LSD instance'
assert (1, 1, 1, 1) == lsd
def test_lsd_value():
lsd = LSD(1, 1, 1, 1)
assert lsd.value() == Pound + Shilling + Penny + One
def test_lsd_str():
lsd = LSD(1, 1, 1, 1)
assert str(lsd) == '£1.1s.1¼d.'
NAME_MAP = {
# Field name, Display Name, Plural or append S, Value
'guineas': ('Guinea', None, Guinea),
'sovereigns': ('Sovereign', None, Sovereign),
'half_sovereigns': ('Half Sovereign', None, HalfSovereign),
'crowns': ('Crown', None, Crown),
'half_crowns': ('Half Crown', None, HalfCrown),
'florins': ('Florin', None, Florin),
'shillings': ('Shilling', None, Shilling),
'sixpence': ('Sixpence', 'Sixpence', Sixpence),
'threepence': ('Threepence', 'Threepence', Threepence),
'twopence': ('Twopence', 'Twopence', Twopence),
'pennies': ('Penny', 'Pence', Penny),
'halfpennies': ('Halfpenny', 'Halfpence', Halfpenny),
'farthings': ('Farthing', None, One),
}
ALL = frozenset(NAME_MAP)
WITHOUT_GUINEAS = frozenset(ALL - {'guineas'})
WITH_GUINEAS = frozenset(ALL - {'sovereigns'})
COMMON = frozenset({'sovereigns', 'shillings', 'pennies'})
class Coins(NamedTuple):
'''
A bundle of coins of all types from the British pre-decimal times.
Convert a Farthing instance into coins using the split method. Split takes
a set of coins to consider valid; by default it uses all except the odd
Guinea, which has a value of 21 shillings and is a little weird.
A bundle of coins can be simplified by calling the simplify method, which
likewise takes a set of valid coins to use.
Get the value in farthings of a bundle by calling value.
'''
guineas: int
sovereigns: int
half_sovereigns: int
crowns: int
half_crowns: int
florins: int
shillings: int
sixpence: int
threepence: int
twopence: int
pennies: int
halfpennies: int
farthings: int
@classmethod
def split(cls, value: Farthing,
coin_set: StrSet = WITHOUT_GUINEAS) -> 'Coins':
'''
Create an optimum bundle of coins from a value as Farthing instance.
'''
remainder = Farthing(value)
coins: Dict[str, int] = {}
*fields_except_farthings, _ = cls._fields
for name in fields_except_farthings:
if name in coin_set:
*_, multiplier = NAME_MAP[name]
coins[name], remainder = remainder.split(multiplier)
else:
coins[name] = 0
coins['farthings'] = int(remainder)
return cls(**coins)
def make_change(self, value: Farthing) -> Tuple['Coins', 'Coins']:
'''
Given a value, make change from this instance as the till, returning a
new till and the change.
If there are insufficient funds, raises ValueError with message
'Insufficient funds'.
If change cannot be made, a ValueError is raised with message
'Imperfect change' and the balance that cannot be changed.
'''
if self.value() < value:
raise ValueError('Insufficient funds')
remainder = Farthing(value)
new_till: Dict[str, int] = {}
change: Dict[str, int] = {}
*fields_except_farthings, _ = self._fields
for name in fields_except_farthings:
till_count = getattr(self, name)
_, _, multiplier = NAME_MAP[name]
change_required, change_remains = remainder.split(multiplier)
if change_required and change_required <= till_count:
new_till[name] = till_count - change_required
change[name] = change_required
remainder = change_remains
else:
new_till[name] = till_count
change[name] = 0
if remainder > self.farthings:
raise ValueError('Imperfect change', remainder - self.farthings)
new_till['farthings'] = self.farthings - remainder
change['farthings'] = int(remainder)
return Coins(**new_till), Coins(**change)
@classmethod
def _farthings(cls, name: str, value: int) -> Farthing:
*_, multiplier = NAME_MAP[name]
return Farthing(value * multiplier)
def value(self) -> Farthing:
'Convert bundle of coins to value as Farthing instance.'
return sum((self._farthings(k, v) for k, v in self._asdict().items()),
Zero)
def simplify(self, coin_set: StrSet = WITHOUT_GUINEAS) -> 'Coins':
'''
Convert bundle of coins into a simpler bundle of coins using provided
set of valid coins.
'''
return self.split(self.value(), coin_set)
@classmethod
def _singular_or_plural(cls, name: str, value: int) -> str:
singular, plural, _ = NAME_MAP[name]
return singular if value == 1 else (plural or f'{singular}s')
def __str__(self):
return ', '.join(f'{v} {self._singular_or_plural(k, v)}'
for k, v in self._asdict().items() if v)
def __add__(self, other: 'Coins') -> 'Coins':
return Coins(*(a + b for a, b in zip(self, other)))
def test_coins_split():
from operator import eq
coins = Coins.split(One)
assert isinstance(coins, Coins), 'expected Coins instance'
assert coins.farthings == 1
assert sum(coins[:-1]) == 0
one_of_each = Pound + Shilling + Penny + Farthing
coins = Coins.split(one_of_each)
assert coins.value() == one_of_each
assert eq(
(coins.guineas, coins.sovereigns, coins.shillings, coins.pennies,
coins.farthings),
(0, 1, 1, 1, 1))
coins = Coins.split(one_of_each, WITH_GUINEAS)
assert eq(
(coins.guineas, coins.sovereigns, coins.shillings, coins.pennies,
coins.farthings),
(1, 0, 0, 1, 1))
def test_coins_value():
from operator import eq
coins = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
assert eq(
coins.value(),
sum((val for _, _, val in NAME_MAP.values()),
Zero))
def test_coins_simplify():
coins = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
simplified = coins.simplify(COMMON)
assert {k for k, v in simplified._asdict().items()
if v} <= COMMON | {'farthings'}
def test_coins_str():
from operator import eq
# Singular Forms
coins = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
assert eq(
str(coins),
'1 Guinea, 1 Sovereign, 1 Half Sovereign, 1 Crown, '
'1 Half Crown, 1 Florin, 1 Shilling, 1 Sixpence, '
'1 Threepence, 1 Twopence, 1 Penny, 1 Halfpenny, 1 Farthing')
# Plural Forms, Special
coins = Coins(sixpence=2, threepence=2, twopence=2, pennies=2,
halfpennies=2,
guineas=0, sovereigns=0, half_sovereigns=0, crowns=0,
half_crowns=0, florins=0, shillings=0, farthings=0)
assert eq(str(coins),
'2 Sixpence, 2 Threepence, 2 Twopence, 2 Pence, 2 Halfpence')
# Plural Forms, normal (with an "s")
coins = Coins(sixpence=0, threepence=0, twopence=0, pennies=0,
halfpennies=0,
guineas=2, sovereigns=2, half_sovereigns=2, crowns=2,
half_crowns=2, florins=2, shillings=2, farthings=2)
assert eq(
str(coins),
'2 Guineas, 2 Sovereigns, 2 Half Sovereigns, 2 Crowns, 2 Half Crowns, '
'2 Florins, 2 Shillings, 2 Farthings')
def test_coins_make_change():
till = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
try:
till.make_change(4 * Pound)
except ValueError as exc:
assert exc.args[0] == 'Insufficient funds'
else:
assert False, 'Expected ValueError'
till = Coins(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0)
try:
till.make_change(Halfpenny + One)
except ValueError as exc:
message, balance = exc.args
assert (message, balance) == ('Imperfect change', 1)
else:
assert False, 'Expected ValueError'
till = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
new_till, change = till.make_change(6 * Shilling + Sixpence)
assert new_till + change == till
assert change.value() == 6 * Shilling + Sixpence
assert new_till.value() == till.value() - change.value()
expected_change = Coins(crowns=1, shillings=1, sixpence=1,
guineas=0, sovereigns=0, half_sovereigns=0,
half_crowns=0, florins=0, threepence=0, twopence=0,
pennies=0, halfpennies=0, farthings=0)
expected_new_till = Coins(crowns=0, shillings=0, sixpence=0,
guineas=1, sovereigns=1, half_sovereigns=1,
half_crowns=1, florins=1, threepence=1,
twopence=1, pennies=1, halfpennies=1,
farthings=1)
assert change == expected_change
assert new_till == expected_new_till
def test_coins_add():
a = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
b = Coins(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
c = a + b
assert c.value() == a.value() + b.value()
assert c == (2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment