the only commit 😦

This commit is contained in:
2020-10-13 01:49:01 +02:00
commit fc91dbdf07
7 changed files with 180 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
__pycache__
.venv
tags

22
CHALLENGE.md Normal file
View File

@@ -0,0 +1,22 @@
There is no time limit to this exercise, and the exercise is intended to be relatively short. Please dont spend more than a handful of hours on it.
Please write the code as you would if you were intending to put it into production. Please write in Python.
Oh, and dont worry, we wont really be asking you to write banking software!
# Specification
Were going to be keeping track of financial transactions between different parties — people and organisations. In our system, these parties are identified by a simple string such as "john" or "supermarket", and you will be provided with a ledger of transactions that looks like this:
2015-01-16,john,mary,125.00
2015-01-17,john,supermarket,20.00
2015-01-17,mary,insurance,100.00
Note: You will need to generate the necessary data to demonstrate the capabilities of your program.
In this example, John pays Mary §125.00 (§ is our fictional currency) on the 16th of January, and the next day he pays the supermarket §20.00, and Mary pays her insurance company, which costs her §100.00.
Your task will be to write a software system that can process a ledger in this format, and provide access to the accounts of each of the named parties, assuming they all started with a balance of zero. For example, the supermarket has received §20.00, so thats its balance. John has paid out §125.00 to Mary and §20.00 to the supermarket, so his balance is in debit by §145.00. In other words, his balance is §-145.00.
Of course, theres a twist, which is as follows. Wed like to be able to find out what each partys balance is at a specified date. For example, Marys balance on the 16th of January is §0.00, but on the 17th its §125.00.
You dont need to implement any kind of user interface for your program. Well experiment with your code in a REPL.

0
app/__init__.py Normal file
View File

84
app/ledger.py Normal file
View File

@@ -0,0 +1,84 @@
from decimal import Decimal
import datetime as dt
import re
from typing import List, Tuple, NamedTuple
from dateutil import parser
DATE_REGEX = r"[1-2][9|0]\d\d-\d\d-\d\d"
class ValidationError(Exception):
pass
class Transaction(NamedTuple):
date: dt.date
source: str
target: str
amount: Decimal
def ingest(transaction_strings: List[str]):
"""
Parse comma-separated strings into Transactions.
Build `entities` dictionary, with keys of entity names and values of
lists of tuples, 0-date 1-amount
"""
transactions = []
entity_names = set()
for transaction_string in transaction_strings:
date, source, target, amount = parse_and_validate_transaction_string(
transaction_string
)
transaction = Transaction(date, source, target, amount)
transactions.append(transaction)
entity_names.update((source, target))
global entities
entities = {entity_name: [] for entity_name in entity_names}
for t in sorted(transactions, key=lambda t: t.date):
entities[t.source].append((t.date, -t.amount))
entities[t.target].append((t.date, t.amount))
def _get_transactions():
"""for testing"""
return [item for sublist in list(entities.values()) for item in sublist]
def get_balance(entity_name: str, on_date: dt.date = None) -> Decimal:
"""
Get the balance _at closing_ of a given date. If no date is given,
all transactions are summed -> "current balance".
"""
return sum(
amount
for date, amount in entities[entity_name]
if not on_date or date <= on_date
)
def parse_and_validate_transaction_string(
transaction_string: str,
) -> Tuple[dt.date, str, str, Decimal]:
try:
date_string, source, target, amount_string = transaction_string.split(",")
except ValueError:
raise ValidationError(
f"Please provide a date, source, target and amount: {transaction_string}"
)
if any(not arg for arg in (date_string, source, target, amount_string)):
raise ValidationError(f"All items must have a value: {transaction_string}")
if not re.match(DATE_REGEX, date_string):
raise ValidationError(f"date is invalid: {date_string}")
date = parser.parse(date_string).date()
amount = Decimal(amount_string)
return date, source, target, amount

0
app/tests/__init__.py Normal file
View File

61
app/tests/test_ledger.py Normal file
View File

@@ -0,0 +1,61 @@
import datetime as dt
from decimal import Decimal
from random import choice
from pytest import fixture
from app import ledger
ENTITIES = ("mary", "john", "insurance", "supermarket", "lottery", "joe", "gas station")
@fixture
def ledger_data():
return (
"2015-01-16,john,mary,125.00",
"2015-01-17,john,supermarket,20.00",
"2015-01-17,mary,insurance,100.00",
)
@fixture
def random_data():
N = 10_000
data = []
for _ in range(N):
date = f"{choice(range(2014, 2018))}-{choice(range(1, 13)):02}-{choice(range(1, 29)):02}"
source = choice(ENTITIES)
target = choice([i for i in ENTITIES if i != source])
amount = Decimal(choice(range(5, 250)))
data.append(f"{date},{source},{target},{amount}")
return data
@fixture
def ingest(ledger_data):
ledger.ingest(ledger_data)
def test_ledger_ingest(ingest):
assert len(ledger._get_transactions()) == 6
assert len(ledger.entities) == 4
assert ledger.entities["john"]
assert ledger.entities["mary"]
assert ledger.entities["insurance"]
assert ledger.entities["supermarket"]
def test_get_balance(ingest):
assert ledger.get_balance("john") == Decimal(-145)
assert ledger.get_balance("john", dt.date(2015, 1, 16)) == Decimal(-125)
assert ledger.get_balance("mary", dt.date(2015, 1, 16)) == Decimal(125)
assert ledger.get_balance("mary", dt.date(2015, 1, 17)) == Decimal(25)
assert ledger.get_balance("mary") == Decimal(25)
def test_get_balance_random(random_data):
ledger.ingest(random_data)
for entity_name, transactions in ledger.entities.items():
assert ledger.get_balance(entity_name) == sum(t[1] for t in transactions)
assert ledger.get_balance(entity_name, dt.date(2017, 3, 5)) == sum(amount for date, amount in
transactions if date <= dt.date(2017, 3, 5))

10
requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
attrs==20.2.0
iniconfig==1.0.1
packaging==20.4
pluggy==0.13.1
py==1.9.0
pyparsing==2.4.7
pytest==6.1.1
python-dateutil==2.8.1
six==1.15.0
toml==0.10.1