the only commit 😦
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
|
tags
|
||||||
22
CHALLENGE.md
Normal file
22
CHALLENGE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
There is no time limit to this exercise, and the exercise is intended to be relatively short. Please don’t 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 don’t worry, we won’t really be asking you to write banking software!
|
||||||
|
|
||||||
|
# Specification
|
||||||
|
We’re 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 that’s 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, there’s a twist, which is as follows. We’d like to be able to find out what each party’s balance is at a specified date. For example, Mary’s balance on the 16th of January is §0.00, but on the 17th it’s §125.00.
|
||||||
|
|
||||||
|
You don’t need to implement any kind of user interface for your program. We’ll experiment with your code in a REPL.
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
84
app/ledger.py
Normal file
84
app/ledger.py
Normal 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
0
app/tests/__init__.py
Normal file
61
app/tests/test_ledger.py
Normal file
61
app/tests/test_ledger.py
Normal 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
10
requirements.txt
Normal 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
|
||||||
Reference in New Issue
Block a user