Problem 400681 · hard · Level 04 Non-Linear Data Structures

Money That Will Not Mix

py-dunder · value objects · exact arithmetic · largest remainder

A shared-expenses app adds, compares and splits sums of money in several currencies. Floats are not good enough (0.1 + 0.2 is not 0.3), and adding euros to dollars must be impossible. Write a class Money(amount, currency):

Creating. amount is an int (whole units) or a string made of an optional minus sign, digits and optionally a point followed by one or two digits, such as "12.5", "-0.05" or "7". A float raises TypeError; any other string (for example "1.", "1.234" or "abc") raises ValueError. currency must be three capital letters, otherwise ValueError.

Showing. str(m) is the amount with exactly two decimals and the currency: "12.50 EUR"; repr(m) is Money('12.50', 'EUR').

Arithmetic. a + b and a - b work for the same currency and raise the setup's CurrencyMismatch (a kind of ValueError) for different ones; with anything that is not Money they raise TypeError. sum(list_of_money) must work. -m, m * k and k * m work for an int k (anything else: TypeError). A zero amount is false in an if.

Comparing. == is True for the same amount and currency (different currencies are simply not equal), equal amounts hash equally, and <, <=, >, >= compare amounts of one currency but raise CurrencyMismatch across currencies.

Splitting. m.allocate(weights) splits m into len(weights) parts proportional to the non-negative integer weights (their total is positive), as a list of Money. Work in cents on the absolute amount: each part first gets the whole number of cents of its exact share, then the cents still missing go one each to the parts with the largest remainders of their exact shares, the earlier part first on a tie; for a negative amount every part is then negated. m.split(n) is m.allocate([1] * n).

The setup also has raises(fn, *args), which returns the name of the exception a call raises (or None).

Examples

Input:  a, b = Money("12.5", "EUR"), Money(3, "EUR")
        str(a + b), repr(a - b * 5), sum([a, b, b]) > a, raises(lambda: a + Money(1, "USD"))
Output: ('15.50 EUR', "Money('-2.50', 'EUR')", True, 'CurrencyMismatch')

Input:  [str(p) for p in Money("100", "GBP").allocate([1, 1, 1])], [str(p) for p in Money("-0.05", "GBP").split(2)]
Output: (['33.34 GBP', '33.33 GBP', '33.33 GBP'], ['-0.03 GBP', '-0.02 GBP'])

Constraints

  • Amounts up to 10**12 units; up to 1000 weights.
  • Use only whole numbers of cents internally: no floats anywhere.

Goals

  • Design a value type whose operators refuse meaningless combinations
  • Keep amounts exact by storing whole cents, and make `sum` work through `__radd__`
  • Split an amount into parts that add up exactly, with a fair rule for the leftover cents
Starting Python…