├── .gitignore ├── README.md ├── __init__.py ├── constants.py ├── database.py ├── db.sqlite ├── input.txt ├── models.py ├── program.py ├── requirements.txt ├── tests ├── __init__.py ├── conftest.py ├── test_database.py ├── test_db.sqlite ├── test_input.txt └── test_utils.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .vscode/ 3 | __pycache__/ 4 | .pytest_cache/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Coding Test Instructions 2 | 3 | Make a program that calculates and displays worker utilization over several weeks. It should take in entries from a log file in this format: 4 | 5 | ``` 6 | WORKER Alex "2019-10-10T12:00:00Z/2019-10-10T13:00:00Z" 7 | WORKER Derick "2019-10-10T12:00:00Z/2019-10-10T15:00:00Z" 8 | WORKER Alex "2019-10-10T13:00:00Z/2019-10-10T14:00:00Z" 9 | ``` 10 | 11 | Above, Alex has two entries of an hour each, and Derick has one entry that is 3 hours long. 12 | Worker utilization is calculated like so: `hours_worked_during_given_week / 40`. 13 | A manager might use something like this to see which employees should be given more work based on what percentage of the 40-hour workweek they have been working. The output should be formatted something like this: 14 | 15 | ``` 16 | Alex 5% 17 | Derick 7.5% 18 | ``` 19 | 20 | Alex was only utilized 5% because `2 / 40 = 0.05`. 21 | 22 | Additionally, workers who were utilized more than 100% should float to the top. If a worker goes over 8 hours in a single day, than any amount of time over 8 hours should count as double utilization. This way, a manager can easily see who is working overtime. 23 | 24 | The analysis will include: 25 | 26 | - Expertise in the selected language 27 | - Problem solving techniques as described in the readme and documentation 28 | - Testing approach 29 | - Overall software design 30 | 31 | ### Usage Instructions 32 | 33 | After downloading and extracting the `tar` file, ensure you have Python 3.6+ on your system. 34 | 35 | 1. From inside the `/CompanyCodingTest` dir, run `pip install -r requirements.txt`. 36 | > Note that if you have multiple Python installations on your system, this command may not work. If this occurs, try setting up a virtual environment with the correct Python version and trying the command again. 37 | 2. From inside the `/CompanyCodingTest` dir, run `python program.py input.txt`. To specify your own input file, simply change `input.txt` to the path of your desired input file. 38 | 39 | #### Tests 40 | 41 | From inside the `/CompanyCodingTest` dir, run `pytest`. 42 | 43 | ### Implementation Details 44 | 45 | Mission: Get average utilization over several weeks. 46 | 47 | > Note: Most of the documentation is kept close to the code it describes, where it should be. In this document, you'll find high-level descriptions of design decisions and problem-solving techniques I applied in the program. The documentation by the code is more granular. 48 | 49 | ### Architecture 50 | 51 | I designed the program with an MVC-style approach. 52 | 53 | Model: `database.py` 54 | 55 | View: `program.py` 56 | 57 | Controller: also `program.py` 58 | 59 | ### Problem-Solving 60 | 61 | #### program.py 62 | 63 | I used `pathlib` and `argparse` so as not to reinvent the wheel with `os`. The resulting code is much cleaner. Additionally, `pathlib` automatically formats the path correctly for the user's operating system. 64 | 65 | #### database.py 66 | 67 | Declaring a database and putting all of our information in it is far more explicit than an 'invisible database' made of dictionaries and lists floating around in memory. I used SQLite because that's all we really need here. 68 | 69 | ### Tests 70 | 71 | - I used `pytest` to avoid all the boilerplate that `unittest` requires. 72 | - Since `program.py` mainly just calls functions made elsewhere and formats things for display, I focused on testing `database.py` and `utils.py`. 73 | - The `conftest.py` file holds my fixtures (stuff that makes it easier for me to set up the right test conditions). 74 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeclopez/CompanyCodingTest/272f1876feb01f634b44deef35136a2deb456c3e/__init__.py -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | These have their own separate file 3 | so I don't have to define them again 4 | in other files. 5 | """ 6 | 7 | HOURS_IN_WORK_DAY = 8 8 | HOURS_IN_WORK_WEEK = 40 9 | SECONDS_IN_HOUR = 3600 10 | FOUR_SPACES = " " 11 | 12 | DB_PATH = "db.sqlite" 13 | DB_URI = f"sqlite:///{DB_PATH}" 14 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import sessionmaker 2 | from sqlalchemy import create_engine 3 | from sqlalchemy import func 4 | import os 5 | 6 | from models import Base, Worker, Entry, UtilReport 7 | import constants as c 8 | import utils 9 | 10 | 11 | def setup(db_path, db_uri): 12 | """ 13 | Start with a new db each time so 14 | we don't need to worry about duplicate entries. 15 | This method is simpler than wiping only the 16 | `Entry` table or checking for duplicate `Entry` 17 | objects. 18 | """ 19 | if os.path.exists(db_path): 20 | os.remove(db_path) 21 | 22 | engine = create_engine(db_uri) 23 | Base.metadata.create_all(engine) 24 | SessionMaker = sessionmaker(bind=engine) 25 | 26 | return SessionMaker 27 | 28 | 29 | def get_or_create(session: sessionmaker, model: object, **kwargs): 30 | """ 31 | Takes a database `Session`, 32 | some kind of model (i.e. `Worker`), 33 | and any information we would store 34 | about the given model instance. 35 | """ 36 | instance = session.query(model).filter_by(**kwargs).first() 37 | 38 | if not instance: 39 | instance = model(**kwargs) 40 | session.add(instance) 41 | session.commit() 42 | 43 | return instance 44 | 45 | 46 | def parse_entry(entry: str): 47 | """ 48 | Takes one line of the input file, 49 | returns all the information contained 50 | in that line in a tuple. 51 | """ 52 | entry_parts = entry.split() 53 | name = entry_parts[1] 54 | time_entry = entry_parts[2] 55 | time_entry = time_entry.replace("\"", "").split("/") 56 | 57 | year_number = utils.get_year_number(time_entry) 58 | week_number = utils.get_week_number(time_entry) 59 | day_number = utils.get_day_number(time_entry) 60 | hours = utils.calc_hours(time_entry) 61 | 62 | return name, year_number, week_number, day_number, hours 63 | 64 | 65 | def populate_from_entries(session: sessionmaker, entries: list): 66 | """ 67 | This function doesn't return 68 | the entries it puts in the database 69 | because that should be handled 70 | by a separate function. 71 | """ 72 | 73 | for e in entries: 74 | name, year_number, week_number, day_number, hours = parse_entry(e) 75 | 76 | worker = get_or_create(session, Worker, name=name) 77 | session.add(worker) 78 | 79 | entry = Entry( 80 | worker_id=worker.id, 81 | year_number=year_number, 82 | week_number=week_number, 83 | day_number=day_number, 84 | hours=hours 85 | ) 86 | session.add(entry) 87 | 88 | session.commit() 89 | 90 | 91 | def create_worker_util_reports(session: sessionmaker, worker_id: int, week_hours: dict): 92 | for week, hours in week_hours.items(): 93 | result = utils.calc_util_percent(hours) 94 | util_report = UtilReport( 95 | worker_id=worker_id, 96 | week_number=week, 97 | percent=result 98 | ) 99 | session.add(util_report) 100 | 101 | 102 | def create_all_workers_util_reports(session: sessionmaker): 103 | all_workers = session.query(Worker) 104 | 105 | for w in all_workers: 106 | # How many different days do we have on 107 | # record for this worker? 108 | # Also, grab the year number so we can 109 | # calculate the week number accurately. 110 | unique_days = ( 111 | session.query(Entry.day_number, Entry.year_number) 112 | .filter(Entry.worker_id == w.id) 113 | .distinct() 114 | ) 115 | 116 | # We start on the day level to ensure 117 | # that anything over 8 hours in one day 118 | # counts as double hours. 119 | week_hours = {} 120 | for ud in unique_days: 121 | day_hours = ( 122 | session.query(func.sum(Entry.hours)) 123 | .filter(Entry.worker_id == w.id) 124 | .filter(Entry.day_number == ud.day_number) 125 | .one() 126 | ) 127 | day_hours = day_hours[0] 128 | 129 | week_number = utils.get_week_number_from_day_number( 130 | ud.year_number, ud.day_number) 131 | # Excess and normal hours are 132 | # rolled into total hours for 133 | # the week here. 134 | weighted_day_hours = utils.get_weighted_day_hours(day_hours) 135 | 136 | week_hours[week_number] = weighted_day_hours 137 | 138 | create_worker_util_reports(session, w.id, week_hours) 139 | 140 | session.commit() 141 | 142 | 143 | def get_worker_id_from_name(session: sessionmaker, name): 144 | worker = session.query(Worker).filter(Worker.name == name).first() 145 | return worker.id 146 | 147 | 148 | def get_percent_util_on_week(session: sessionmaker, worker_name: str, week_number: int): 149 | worker_id = get_worker_id_from_name(session, worker_name) 150 | 151 | ut_report = ( 152 | session.query(UtilReport) 153 | .filter(UtilReport.week_number == week_number) 154 | .filter(UtilReport.worker_id == worker_id) 155 | .first() 156 | ) 157 | 158 | final_result = ut_report.percent if ut_report else 0 159 | 160 | return final_result 161 | 162 | 163 | def get_all_workers_names(session: sessionmaker): 164 | return [i[0] for i in session.query(Worker.name)] 165 | 166 | 167 | def get_unique_weeks(session: sessionmaker): 168 | weeks = session.query(Entry.week_number).distinct() 169 | return [i[0] for i in weeks] 170 | -------------------------------------------------------------------------------- /db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeclopez/CompanyCodingTest/272f1876feb01f634b44deef35136a2deb456c3e/db.sqlite -------------------------------------------------------------------------------- /input.txt: -------------------------------------------------------------------------------- 1 | WORKER Alex "2019-10-10T12:00:00Z/2019-10-10T13:00:00Z" 2 | WORKER Derick "2019-10-10T12:00:00Z/2019-10-10T15:00:00Z" 3 | WORKER Alex "2019-10-10T13:00:00Z/2019-10-10T14:00:00Z" 4 | WORKER Luke "2019-10-10T01:00:00Z/2019-10-10T23:00:00Z" 5 | WORKER Luke "2019-10-10T13:00:00Z/2019-10-10T23:00:00Z" 6 | WORKER Cindy "2019-11-10T13:00:00Z/2019-11-10T23:00:00Z" 7 | WORKER Cindy "2019-11-10T13:00:00Z/2019-11-10T23:00:00Z" 8 | WORKER Joe "2019-11-10T13:00:00Z/2019-11-10T23:00:00Z" 9 | WORKER Luke "2019-12-10T13:00:00Z/2019-12-10T23:00:00Z" 10 | WORKER Alex "2019-12-10T12:00:00Z/2019-12-10T13:00:00Z" 11 | WORKER Derick "2019-12-10T12:00:00Z/2019-12-10T15:00:00Z" 12 | WORKER Vivi "2019-12-25T12:00:00Z/2019-12-25T15:00:00Z" -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Table, Column, Integer, Float, String, MetaData, ForeignKey 2 | from sqlalchemy.ext.declarative import declarative_base 3 | 4 | Base = declarative_base() 5 | 6 | 7 | class Worker(Base): 8 | __tablename__ = 'workers' 9 | 10 | id = Column(Integer, primary_key=True) 11 | name = Column(String) 12 | 13 | 14 | class Entry(Base): 15 | __tablename__ = 'entries' 16 | 17 | id = Column(Integer, primary_key=True) 18 | worker_id = Column(Integer, ForeignKey('workers.id')) 19 | year_number = Column(Integer) 20 | week_number = Column(Integer) 21 | day_number = Column(Integer) 22 | hours = Column(Float) 23 | 24 | 25 | class UtilReport(Base): 26 | __tablename__ = 'util_reports' 27 | 28 | id = Column(Integer, primary_key=True) 29 | worker_id = Column(Integer, ForeignKey('workers.id')) 30 | week_number = Column(Integer) 31 | percent = Column(Float) 32 | -------------------------------------------------------------------------------- /program.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pathlib 3 | import sys 4 | 5 | import constants as c 6 | import database 7 | import utils 8 | 9 | 10 | def get_input_file_path_from_args(): 11 | my_parser = argparse.ArgumentParser( 12 | description="Calculate and display worker utilization over several weeks." 13 | ) 14 | 15 | my_parser.add_argument( 16 | "Path", 17 | metavar="path", 18 | type=str, 19 | help="the path to the input file" 20 | ) 21 | 22 | args = my_parser.parse_args() 23 | 24 | input_file_path = pathlib.Path(args.Path) 25 | 26 | return input_file_path 27 | 28 | 29 | def sort_all_util_reports(session, weeks: list): 30 | """ 31 | Since the final output will be sorted by week, 32 | the first `for` loop iterates over the 33 | week numbers. 34 | 35 | We store our weekly utilization reports in 36 | a list of pairs (tuples) because this makes it easy 37 | to sort with Python's `list.sort()` method. 38 | 39 | List comprehensions could reduce the line count, 40 | but ultimately they would make the code harder 41 | to follow in this function. 42 | """ 43 | names = database.get_all_workers_names(session) 44 | 45 | sorted_util_reports = [] 46 | for w in weeks: 47 | unsorted_week_of_util_reports = [] 48 | for n in names: 49 | percent = database.get_percent_util_on_week(session, n, w) 50 | pair = (n, percent) 51 | unsorted_week_of_util_reports.append(pair) 52 | 53 | sorted_week_of_util_reports = ( 54 | utils.apply_sorting_rules(unsorted_week_of_util_reports) 55 | ) 56 | 57 | sorted_util_reports.append(sorted_week_of_util_reports) 58 | 59 | return sorted_util_reports 60 | 61 | 62 | def print_all_util_reports(weeks: list, all_util_reports: list): 63 | """ 64 | Takes a list of week numbers and 65 | a list of all utilization reports. 66 | 67 | I print the worker entries with 68 | four spaces of indentation to 69 | increase output readability. 70 | I print a new line after each 71 | week for the same reason. 72 | """ 73 | for week_number, util_report in zip(weeks, all_util_reports): 74 | print(f"Week {week_number}") 75 | 76 | for ur in util_report: 77 | name = ur[0] 78 | percent = ur[1] 79 | print(c.FOUR_SPACES + name, f"{percent}%") 80 | 81 | print("") 82 | 83 | 84 | if __name__ == "__main__": 85 | input_path = get_input_file_path_from_args() 86 | 87 | if not input_path.is_file(): 88 | print("The given filename could not be found!") 89 | sys.exit() 90 | 91 | SessionMaker = database.setup(c.DB_PATH, c.DB_URI) 92 | session = SessionMaker() 93 | entries_list = utils.get_entries_from_input_file(input_path) 94 | 95 | database.populate_from_entries(session, entries_list) 96 | database.create_all_workers_util_reports(session) 97 | 98 | weeks = database.get_unique_weeks(session) 99 | 100 | all_util_reports = sort_all_util_reports(session, weeks) 101 | print_all_util_reports(weeks, all_util_reports) 102 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.3.0 2 | attrs==19.3.0 3 | autopep8==1.4.4 4 | colorama==0.4.3 5 | iso8601==0.1.12 6 | more-itertools==8.0.2 7 | numpy==1.18.0 8 | packaging==19.2 9 | pluggy==0.13.1 10 | py==1.8.0 11 | pycodestyle==2.5.0 12 | pyparsing==2.4.5 13 | pytest==5.3.2 14 | python-dateutil==2.8.1 15 | pytz==2019.3 16 | six==1.13.0 17 | SQLAlchemy==1.3.12 18 | wcwidth==0.1.7 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeclopez/CompanyCodingTest/272f1876feb01f634b44deef35136a2deb456c3e/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file sets up some things that 3 | I want to use in all my test files. 4 | This saves a lot of duplication, 5 | especially since I had to 6 | hack `sys.path`. 7 | """ 8 | 9 | from sqlalchemy.orm import sessionmaker 10 | from sqlalchemy import create_engine 11 | from pathlib import Path 12 | import pytest 13 | import sys 14 | import os 15 | 16 | # Add some directories to sys.path 17 | # so we can import stuff successfully 18 | # when running from `tests` or 19 | # `CompanyCodingTest`. 20 | current_dir = str(Path.cwd()) # NOQA: E402 (Tell autopep8 not to correct these lines) 21 | parent_dir = str(Path.cwd().parent) # NOQA: E402 22 | sys.path.append(current_dir) # NOQA: E402 23 | sys.path.append(parent_dir) # NOQA: E402 24 | 25 | from models import Base, Worker, Entry, UtilReport 26 | import program 27 | import database 28 | import utils 29 | 30 | 31 | @pytest.fixture 32 | def input_path(): 33 | yield Path("tests/test_input.txt") 34 | 35 | 36 | @pytest.fixture 37 | def time_entry(): 38 | time_entry = "2019-10-10T13:00:00Z/2019-10-10T23:00:00Z" 39 | return time_entry.split("/") 40 | 41 | 42 | @pytest.fixture 43 | def entries(input_path): 44 | yield utils.get_entries_from_input_file(input_path) 45 | 46 | 47 | @pytest.fixture 48 | def session(): 49 | DB_PATH = Path("tests/test_db.sqlite") 50 | DB_URI = f"sqlite:///{DB_PATH}" 51 | SessionMaker = database.setup(DB_PATH, DB_URI) 52 | session = SessionMaker() 53 | 54 | yield session 55 | 56 | # Everything after `yield` is the teardown code. 57 | session.close() 58 | 59 | 60 | @pytest.fixture 61 | def populated_database(session, entries): 62 | database.populate_from_entries(session, entries) 63 | database.create_all_workers_util_reports(session) 64 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pytest 3 | import sys 4 | import os 5 | 6 | from models import Base, Worker, Entry, UtilReport 7 | import program 8 | import database 9 | import utils 10 | 11 | 12 | def test_parse_entry(): 13 | entry = 'WORKER Vivi "2019-12-25T12:00:00Z/2019-12-25T15:00:00Z"' 14 | ( 15 | name, 16 | year_number, 17 | week_number, 18 | day_number, 19 | hours 20 | ) = database.parse_entry(entry) 21 | 22 | assert ( 23 | name == "Vivi" and 24 | year_number == 2019 and 25 | week_number == 52 and 26 | day_number == 359 and 27 | hours == 3 28 | ) 29 | 30 | 31 | def test_database_is_empty(session): 32 | result = session.query(Worker).all() 33 | 34 | assert result == [] 35 | 36 | 37 | def test_populate_from_entries_adds_all_entries_to_db(entries, session): 38 | database.populate_from_entries(session, entries) 39 | 40 | entries_in_db = session.query(Entry).all() 41 | 42 | assert len(entries_in_db) == len(entries) 43 | 44 | 45 | def test_create_all_workers_util_reports(entries, session): 46 | database.populate_from_entries(session, entries) 47 | 48 | database.create_all_workers_util_reports(session) 49 | 50 | util_reports_count = session.query(UtilReport).count() 51 | 52 | assert util_reports_count 53 | -------------------------------------------------------------------------------- /tests/test_db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeclopez/CompanyCodingTest/272f1876feb01f634b44deef35136a2deb456c3e/tests/test_db.sqlite -------------------------------------------------------------------------------- /tests/test_input.txt: -------------------------------------------------------------------------------- 1 | WORKER Alex "2019-10-10T12:00:00Z/2019-10-10T13:00:00Z" 2 | WORKER Derick "2019-10-10T12:00:00Z/2019-10-10T15:00:00Z" 3 | WORKER Alex "2019-10-10T13:00:00Z/2019-10-10T14:00:00Z" 4 | WORKER Luke "2019-10-10T01:00:00Z/2019-10-10T23:00:00Z" 5 | WORKER Luke "2019-10-10T13:00:00Z/2019-10-10T23:00:00Z" 6 | WORKER Cindy "2019-11-10T13:00:00Z/2019-11-10T23:00:00Z" 7 | WORKER Cindy "2019-11-10T13:00:00Z/2019-11-10T23:00:00Z" 8 | WORKER Joe "2019-11-10T13:00:00Z/2019-11-10T23:00:00Z" 9 | WORKER Luke "2019-12-10T13:00:00Z/2019-12-10T23:00:00Z" 10 | WORKER Alex "2019-12-10T12:00:00Z/2019-12-10T13:00:00Z" 11 | WORKER Derick "2019-12-10T12:00:00Z/2019-12-10T15:00:00Z" 12 | WORKER Vivi "2019-12-25T12:00:00Z/2019-12-25T15:00:00Z" -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | from pathlib import Path 3 | import random 4 | import pytest 5 | import sys 6 | import os 7 | 8 | from models import UtilReport 9 | from constants import * 10 | import database 11 | import iso8601 12 | import utils 13 | 14 | 15 | def test_get_entries_from_input_file_makes_entry_for_each_line(entries, input_path): 16 | entries = utils.get_entries_from_input_file(input_path) 17 | 18 | with open(input_path) as input_file: 19 | line_count = len(input_file.readlines()) 20 | 21 | assert len(entries) == line_count 22 | 23 | 24 | def test_parse_times_returns_two_datetime_objects(time_entry): 25 | start_time, end_time = utils.parse_times(time_entry) 26 | 27 | assert ( 28 | isinstance(start_time, datetime) and 29 | isinstance(end_time, datetime) 30 | ) 31 | 32 | 33 | def test_calc_hours_calculates_correctly(time_entry): 34 | hours = utils.calc_hours(time_entry) 35 | 36 | assert hours == 10 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "input_hours, expected_weighted_hours", 41 | [(0, 0), (1, 1), (8, 8), (9, 10), (10, 12), (16, 24)] 42 | ) 43 | def test_get_weighted_day_hours_calculates_correctly(input_hours, expected_weighted_hours): 44 | hours = utils.get_weighted_day_hours(input_hours) 45 | 46 | assert hours == expected_weighted_hours 47 | 48 | 49 | def test_put_percents_over_100_at_top(): 50 | over_100 = 105 51 | percents = [0, 50, 99, over_100] 52 | unsorted_util_reports = [("name", i) for i in percents] 53 | 54 | sorted_util_reports = utils.put_percents_over_100_at_top( 55 | unsorted_util_reports 56 | ) 57 | 58 | first_percent_in_list = sorted_util_reports[0][1] 59 | 60 | assert first_percent_in_list == over_100 61 | 62 | 63 | def test_apply_sorting_rules_does_ascending_order(): 64 | percents = [96, 42, 53, 99] 65 | unsorted_util_reports = [("name", i) for i in percents] 66 | 67 | percents.sort() 68 | expected_result = [("name", i) for i in percents] 69 | 70 | sorted_util_reports = utils.apply_sorting_rules( 71 | unsorted_util_reports 72 | ) 73 | 74 | assert sorted_util_reports == expected_result 75 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | import iso8601 3 | 4 | import constants as c 5 | 6 | 7 | def get_entries_from_input_file(input_path): 8 | """ 9 | Since each entry is on a 10 | separate line, split on line breaks. 11 | """ 12 | with open(input_path) as input_file: 13 | entries_list = input_file.read().strip().split("\n") 14 | 15 | return entries_list 16 | 17 | 18 | def parse_times(time_entry: str): 19 | times = [iso8601.parse_date(i) for i in time_entry] 20 | start_time = times[0] 21 | end_time = times[1] 22 | 23 | return start_time, end_time 24 | 25 | 26 | def calc_hours(time_entry: str): 27 | start_time, end_time = parse_times(time_entry) 28 | 29 | delta = end_time - start_time 30 | hours = delta.total_seconds() / c.SECONDS_IN_HOUR 31 | 32 | return hours 33 | 34 | 35 | def get_year_number(time_entry: str): 36 | start_time, end_time = parse_times(time_entry) 37 | year_number = end_time.isocalendar()[0] 38 | 39 | return year_number 40 | 41 | 42 | def get_week_number(time_entry: str): 43 | start_time, end_time = parse_times(time_entry) 44 | week_number = end_time.isocalendar()[1] 45 | 46 | return week_number 47 | 48 | 49 | def get_day_number(time_entry: str): 50 | start_time, end_time = parse_times(time_entry) 51 | day_number = end_time.timetuple().tm_yday 52 | 53 | return day_number 54 | 55 | 56 | def get_week_number_from_day_number(current_year: int, day_number: int): 57 | """ 58 | Takes the year and day number. 59 | We need the year to ensure 60 | precision, since the first 61 | day of the year can be a Wednesday 62 | on one year and a Sunday on another 63 | year. 64 | """ 65 | day_of_year = "%j" 66 | date = datetime.strptime(str(day_number), day_of_year) 67 | date = date.replace(year=current_year) 68 | 69 | return date.isocalendar()[1] 70 | 71 | 72 | def get_weighted_day_hours(day_hours: int): 73 | if day_hours > c.HOURS_IN_WORK_DAY: 74 | weighted_day_hours = ( 75 | day_hours + 76 | (day_hours - c.HOURS_IN_WORK_DAY) 77 | ) 78 | else: 79 | weighted_day_hours = day_hours 80 | 81 | return weighted_day_hours 82 | 83 | 84 | def calc_util_percent(week_hours: int): 85 | return (week_hours / c.HOURS_IN_WORK_WEEK) * 100 86 | 87 | 88 | def get_percent(x): 89 | """ 90 | Since our utilization report 91 | list is a list of pairs and 92 | the second item is the percent, 93 | we sort by the second item. 94 | """ 95 | return x[1] 96 | 97 | 98 | def put_percents_over_100_at_top(names_percents_pairs): 99 | # I copied the list because you 100 | # should never modify something 101 | # that you're iterating over. 102 | names_percents_pairs_copy = names_percents_pairs.copy() 103 | 104 | percents_over_100 = [] 105 | for ur in names_percents_pairs: 106 | name = ur[0] 107 | percent = ur[1] 108 | 109 | if percent > 100: 110 | index = names_percents_pairs_copy.index(ur) 111 | util_over_100 = names_percents_pairs_copy.pop(index) 112 | percents_over_100.append(util_over_100) 113 | 114 | # I sort the list again here so that even 115 | # the percents over 100 are in ascending 116 | # order. 117 | percents_over_100.sort(key=get_percent) 118 | 119 | return percents_over_100 + names_percents_pairs_copy 120 | 121 | 122 | def apply_sorting_rules(util_reports_for_week: list): 123 | util_reports_for_week.sort(key=get_percent) 124 | 125 | util_reports_for_week = put_percents_over_100_at_top(util_reports_for_week) 126 | 127 | return util_reports_for_week 128 | --------------------------------------------------------------------------------