aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md62
-rw-r--r--tom_harley_solution.py259
-rw-r--r--transactions.csv21
-rw-r--r--work_shifts.csv8
4 files changed, 350 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f5fecb9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+# Tenzo Coding Challenge
+
+Congratulations on making it to the first part of the Tenzo Interview process!
+
+The base file for this exercise is in Python 3 -- if you wish to change to a
+language of your choice, that is completely fine. Just remember to add
+instructions for how to run your solution.
+
+We want to understand the most and least profitable hour of the day for a given restaurant when looking at labour cost. You'll have two csvs, one describing the shifts, and one describing the hourly sales.
+
+## Labour Data
+
+A shift will include the pay-rate (per hour), the start and end time, and a text field where the manager will enter break info. This may vary depending on the individual manager.
+
+For example:
+```
+{
+ 'break_notes': '15-18',
+ 'start_time': '10:00',
+ 'end_time': '23:00',
+ 'pay_rate': 10.0
+}
+```
+
+The data given shows a shift started at 10AM and ended at 11PM. However, the break_notes "15-18" indicates that the staff member took a 3 hour break in the middle of the day (when they would not be paid). The employee was paid £10 per hour.
+
+## Sales Data
+
+This shows you a set of transactions:
+
+For example
+```
+[
+ {
+ 'time' : '10:31,
+ 'amount' : 50.32
+ }
+]
+```
+
+We are hoping that you can compute different metrics for the different hours,
+such as the total sales during this hour, the cost of labour for this hour, and
+the cost of labour as percentage of sales.
+
+e.g.,
+```
+Hour Sales Labour %
+7:00 100 30 30%
+8:00 300 60 20%
+```
+
+## Best and Worst Hours
+
+Lastly, we are hoping that you can output which hour was the best and worst in terms of labour cost as a percentage of sales. If the sales are null, then return -cost instead of percentage. (e.g -40).
+
+## Submission Instructions
+
+- The `EmptySolution.py` file contains some functions, where the output is well defined. This will be used for our tests, so be sure to output right answers (it is not that hard to compute the result by hand, and compare it to the output of your program) in the right format;
+- If you need to create other functions, you're free to do so, just keep in mind that the functions that are already defined need to work;
+- For the sake of testing, do not print any alert messages or anything to stdout;
+- Please write your name at the top and the bottom of your solution;
+- Submit a **single file** containing your solution and use `YOUR_NAME_solution.py` format (e.g. `john_doe_solution.py`).
diff --git a/tom_harley_solution.py b/tom_harley_solution.py
new file mode 100644
index 0000000..d950532
--- /dev/null
+++ b/tom_harley_solution.py
@@ -0,0 +1,259 @@
+"""
+Please write you name here: Tom Harley
+"""
+
+from datetime import timedelta, datetime
+from contextlib import contextmanager
+from collections import namedtuple
+import csv
+import re
+
+def parse_time(time_str):
+ time_str = time_str.strip()
+
+ if 'M' in time_str:
+ hour_format = "%I"
+ ampm_format = "%p"
+ else:
+ hour_format = "%H"
+ ampm_format = ""
+
+ if '.' in time_str:
+ minute_format = ".%M"
+ elif ':' in time_str:
+ minute_format = ":%M"
+ else:
+ minute_format = ""
+
+ format_str = f"{hour_format}{minute_format}{ampm_format}"
+ return datetime.strptime(time_str, format_str)
+
+def time_hour(hour):
+ return datetime.strptime(f"{hour}", "%H")
+
+def hours(start_time, end_time):
+ offset = 0 if end_time.minute == 0 else 1
+ iterator = iter(range(start_time.hour, end_time.hour + offset))
+ prev = time_hour(next(iterator))
+ for hour in iterator:
+ current = time_hour(hour)
+ yield (prev, current)
+ prev = current
+
+@contextmanager
+def opencsv(path_to_csv, lookahead_size=1024):
+ with open(path_to_csv, newline="") as csvfile:
+ sniffer = csv.Sniffer()
+ sample = csvfile.read(lookahead_size)
+
+ dialect = sniffer.sniff(sample)
+ header = sniffer.has_header(sample)
+
+ csvfile.seek(0)
+
+ reader = csv.reader(csvfile, dialect)
+
+ if header:
+ # skip header if it exists
+ next(reader)
+
+ yield reader
+
+def process_shifts(path_to_csv):
+ """
+
+ :param path_to_csv: The path to the work_shift.csv
+ :type string:
+ :return: A dictionary with time as key (string) with format %H:%M
+ (e.g. "18:00") and cost as value (Number)
+ For example, it should be something like :
+ {
+ "17:00": 50,
+ "22:00: 40,
+ }
+ In other words, for the hour beginning at 17:00, labour cost was
+ 50 pounds
+ :rtype dict:
+ """
+ with opencsv(path_to_csv) as reader:
+ result = dict()
+
+ for break_notes, end_time, pay_rate, start_time in reader:
+ break_start, break_end = map(parse_time, break_notes.split('-'))
+
+ start_time = parse_time(start_time)
+ end_time = parse_time(end_time)
+
+ pay_rate = float(pay_rate)
+
+ if not start_time < end_time:
+ # shift passes over midday/midnight
+ end_time = end_time + timedelta(hours=12)
+
+ if not start_time <= break_start:
+ # break starts in PM
+ break_start = break_start + timedelta(hours=12)
+
+ if not break_start < break_end:
+ # break passes over midday/midnight
+ break_end = break_end + timedelta(hours=12)
+
+ assert(break_start < break_end)
+ assert(start_time < end_time)
+ assert(start_time <= break_start)
+ assert(break_end <= end_time)
+
+ for hour_start, hour_end in hours(start_time, end_time):
+ paid_minutes = 60
+
+ if start_time > hour_start:
+ # shift started mid-hour
+ paid_minutes = paid_minutes - start_time.minute
+
+ if end_time < hour_end:
+ # shift ended mid-hour
+ paid_minutes = paid_minutes - (60 - end_time.minute)
+
+ if break_start <= hour_start:
+ # break starts before/on hour
+
+ if break_end >= hour_end:
+ # hour is contained in break
+ paid_minutes = 0
+
+ else:
+ # break ends mid-hour
+ paid_minutes = paid_minutes - break_end.minute
+
+ elif break_start <= hour_end:
+ # break starts mid-hour
+ paid_minutes = paid_minutes - (60 - break_start.hour)
+
+ if break_end < hour_end:
+ # break also ends mid-hour
+ paid_minutes = paid_minutes + (60 - break_end.hour)
+
+ else:
+ # break starts after hour
+ pass
+
+ hour_str = datetime.strftime(hour_start, "%H:%M")
+ hour_cost = pay_rate / 60 * paid_minutes
+ result[hour_str] = result.get(hour_str, 0) + hour_cost
+
+ return result
+
+def process_sales(path_to_csv):
+ """
+
+ :param path_to_csv: The path to the transactions.csv
+ :type string:
+ :return: A dictionary with time (string) with format %H:%M as key and
+ sales as value (string),
+ and corresponding value with format %H:%M (e.g. "18:00"),
+ and type float)
+ For example, it should be something like :
+ {
+ "17:00": 250,
+ "22:00": 0,
+ },
+ This means, for the hour beginning at 17:00, the sales were 250 dollars
+ and for the hour beginning at 22:00, the sales were 0.
+
+ :rtype dict:
+ """
+ with opencsv(path_to_csv) as reader:
+ result = dict()
+
+ for amount, time in reader:
+ amount = float(amount)
+ time = parse_time(time)
+
+ hour_str = datetime.strftime(time, "%H:00")
+ result[hour_str] = result.get(hour_str, 0) + amount
+
+ return result
+
+def compute_percentage(shifts, sales):
+ """
+
+ :param shifts:
+ :type shifts: dict
+ :param sales:
+ :type sales: dict
+ :return: A dictionary with time as key (string) with format %H:%M and
+ percentage of labour cost per sales as value (float),
+ If the sales are null, then return -cost instead of percentage
+ For example, it should be something like :
+ {
+ "17:00": 20,
+ "22:00": -40,
+ }
+ :rtype: dict
+ """
+ result = dict()
+
+ # things cannot be sold while there are no clerks working, so it should be
+ # safe to use the keys from the shift calculations dict.
+ for hour_str in shifts:
+ if hour_str not in sales:
+ result[hour_str] = - shifts[hour_str]
+
+ elif sales[hour_str] == 0:
+ # this won't happen with the version of process_sales I've written
+ # but may turn up in tests (it appears in the docstring of
+ # process_sales)
+ result[hour_str] = - shifts[hour_str]
+
+ else:
+ result[hour_str] = shifts[hour_str] / sales[hour_str] * 100
+
+ return result
+
+def best_and_worst_hour(percentages):
+ """
+
+ Args:
+ percentages: output of compute_percentage
+ Return: list of strings, the first element should be the best hour,
+ the second (and last) element should be the worst hour. Hour are
+ represented by string with format %H:%M
+ e.g. ["18:00", "20:00"]
+
+ """
+ Rank = namedtuple("Rank", "hour value")
+
+ iterator = iter(percentages.items())
+ first = Rank(*next(iterator))
+ best = first
+ worst = first
+
+ for hour_str, value in iterator:
+ if value < worst.value:
+ worst = Rank(hour_str, value)
+
+ if value > best.value:
+ best = Rank(hour_str, value)
+
+ return [ best.hour, worst.hour ]
+
+def main(path_to_shifts, path_to_sales):
+ """
+ Do not touch this function, but you can look at it, to have an idea of
+ how your data should interact with each other
+ """
+
+ shifts_processed = process_shifts(path_to_shifts)
+ sales_processed = process_sales(path_to_sales)
+ percentages = compute_percentage(shifts_processed, sales_processed)
+ best_hour, worst_hour = best_and_worst_hour(percentages)
+ return best_hour, worst_hour
+
+if __name__ == '__main__':
+ # You can change this to test your code, it will not be used
+ path_to_sales = "./transactions.csv"
+ path_to_shifts = "./work_shifts.csv"
+ best_hour, worst_hour = main(path_to_shifts, path_to_sales)
+ print(best_hour, worst_hour)
+
+# Please write you name here: Tom Harley
diff --git a/transactions.csv b/transactions.csv
new file mode 100644
index 0000000..796bbb8
--- /dev/null
+++ b/transactions.csv
@@ -0,0 +1,21 @@
+amount,time
+100.32,10:31
+30.56,10:56
+300.65,11:05
+20.0,11:31
+54.56,12:36
+220.09,12:45
+240.0,12:59
+270.43,13:10
+135.65,13:15
+15.43,14:04
+162.34,14:06
+63.43,15:04
+75.42,16:31
+142.34,17:31
+57.54,18:31
+450.54,18:31
+240.54,18:56
+240.54,19:15
+180.54,19:45
+240.54,21:45
diff --git a/work_shifts.csv b/work_shifts.csv
new file mode 100644
index 0000000..1b6ead9
--- /dev/null
+++ b/work_shifts.csv
@@ -0,0 +1,8 @@
+break_notes,end_time,pay_rate,start_time
+15-18,23:00,10.0,10:00
+18.30-19.00,23:00,12.0,18:00
+4PM-5PM,22:30,14.0,12:00
+3-4,18:00,10.0,09:00
+4-4.10PM,23:00,20.0,09:00
+15 - 17,23:00,10.0,11:00
+11 - 13,16:00,10.0,10:00