def test_there_should_be_at_least_x_days_between_ops_of_shift_types( model, build_run_data, build_expressions, people): constraint = ThereShouldBeAtLeastXDaysBetweenOpsOfShiftTypes( priority=0, x=8, shift_types=["SPECIAL_A", "SPECIAL_B"]) history = History.build(past_shifts=[ AssignedShift("shift", ShiftType.SPECIAL_A, date(2018, 12, 29), people[0]), AssignedShift("shift", ShiftType.SPECIAL_B, date(2018, 12, 22), people[0]), AssignedShift("shift", ShiftType.SPECIAL_A, date(2018, 12, 23), people[1]), AssignedShift("shift", ShiftType.STANDARD, date(2018, 12, 28), people[1]), ]) data = build_run_data(history=history) assignments = init_assignments(model, data) expressions = build_expressions(constraint, data, assignments) # Second person is ok to assign because they had more than 8 free days assert evaluate(assignments, ((1, 0, 0, 0), ), expressions) assert evaluate(assignments, ((1, 0, 4, 0), ), expressions) assert evaluate(assignments, ((1, 0, 5, 0), ), expressions) # First person has just been on a special_a shift assert evaluate(assignments, ((0, 0, 0, 0), ), expressions) assert not evaluate(assignments, ((0, 0, 4, 0), ), expressions) assert not evaluate(assignments, ((0, 0, 5, 0), ), expressions)
def test_basic_assignment(): shifts = [ Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 1)), Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 2)), Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 3)), Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 4)), Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 5)), Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 6)), Shift(name="shift", shift_type=ShiftType.STANDARD, day=date(2019, 1, 7)), ] config = Config.build( people=[Person(name=f"person_{index}") for index in range(7)], max_shifts_per_person=1, shifts_by_day={shift.day: [shift] for shift in shifts}, history=History.build(), ) solution = solve(config) assert len(list(solution)) == 7
def build(history=History.build()): run_data = Config.build( people=people, max_shifts_per_person=2, shifts_by_day=shifts_per_day, history=history, ) return run_data
def _parse_history(history) -> History: offsets = [] shifts = [] for offset in history["offsets"]: offsets.append(PastShiftOffset.from_json(offset)) for shift in history["shifts"]: shifts.append(AssignedShift.from_json(shift)) return History.build(past_shifts=shifts, offsets=offsets)
def test_date_last_on_shift(): person_a = Person("a") person_b = Person("b") person_c = Person("c") person_d = Person("d") history = History.build([ AssignedShift( "shift", ShiftType.SPECIAL_A, date(2019, 8, 31), person_a, ), AssignedShift( "shift", ShiftType.SPECIAL_B, date(2019, 9, 1), person_b, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 2), person_a, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 3), person_b, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 4), person_a, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 5), person_c, ), ]) metrics = HistoryMetrics.build(history, [person_a, person_b, person_d], date(2019, 9, 8)) assert metrics.date_last_on_shift == { person_a: date(2019, 9, 4), person_b: date(2019, 9, 3), person_d: NEVER, }
def test_objective_function_for_weekdays(model, build_run_data, people, now): history = History.build(past_shifts=[ # A has done 2 past shifts, most recent is 4 days ago AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=4), people[0], ), AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=5), people[0], ), # B has done 2 past shifts, most recent is 3 days ago AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=3), people[1], ), AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=7), people[1], ), # C has done 1 past shifts, most recent is 6 days ago AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=6), people[2], ), # D has done no past shifts ]) data = build_run_data(history=history) assignments = init_assignments(model, data) objective = RankingWeight() assert evaluate( assignments, ((3, 0, 0, 0), (2, 1, 1, 0), (1, 0, 2, 0)), objective.objective(assignments, data), ) == (107 + 2 + 104)
def test_there_should_be_at_least_x_days_between_ops(model, build_run_data, build_expressions, people, shifts_per_day): constraint = ThereShouldBeAtLeastXDaysBetweenOps(priority=0, x=1) history = History.build(past_shifts=[ AssignedShift("shift", ShiftType.STANDARD, date(2018, 12, 31), people[0]) ]) data = build_run_data(history=history) assignments = init_assignments(model, data) expressions = build_expressions(constraint, data, assignments) # One day gap between shifts assert evaluate(assignments, ((0, 0, 1, 0), ), expressions) # Shifts are back to back assert not evaluate(assignments, ((0, 0, 0, 0), ), expressions)
def test_num_of_shifts(): person_a = Person("a") person_b = Person("b") person_c = Person("c") history = History.build([ AssignedShift( "shift", ShiftType.SPECIAL_A, date(2019, 8, 31), person_a, ), AssignedShift( "shift", ShiftType.SPECIAL_B, date(2019, 9, 1), person_b, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 2), person_a, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 3), person_b, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 4), person_a, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 5), person_c, ), ]) metrics = HistoryMetrics.build(history, [person_a, person_b], date(2019, 9, 5)) assert metrics.num_of_shifts == { ShiftType.STANDARD: { person_a: 2, person_b: 1 }, ShiftType.SPECIAL_A: { person_a: 1, person_b: 0 }, ShiftType.SPECIAL_B: { person_a: 0, person_b: 1 }, }
def test_num_of_shifts_with_offsets(): person_a = Person("a") person_b = Person("b") person_c = Person("c") history = History.build( past_shifts=[ AssignedShift( "shift", ShiftType.SPECIAL_A, date(2019, 8, 31), person_a, ), AssignedShift( "shift", ShiftType.SPECIAL_B, date(2019, 9, 1), person_b, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 2), person_a, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 3), person_b, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 4), person_a, ), AssignedShift( "shift", ShiftType.STANDARD, date(2019, 9, 5), person_c, ), ], offsets=[ PastShiftOffset(person=person_a, shift_type=ShiftType.STANDARD, offset=2), PastShiftOffset(person=person_b, shift_type=ShiftType.SPECIAL_A, offset=1), PastShiftOffset(person=person_c, shift_type=ShiftType.SPECIAL_B, offset=5), ], ) metrics = HistoryMetrics.build(history, [person_a, person_b], date(2019, 9, 5)) assert metrics.num_of_shifts == { ShiftType.STANDARD: { person_a: 4, person_b: 1 }, ShiftType.SPECIAL_A: { person_a: 1, person_b: 1 }, ShiftType.SPECIAL_B: { person_a: 0, person_b: 1, person_c: 5 }, }
def test_objective_function_for_entire_week(model, build_run_data, people, now): history = History.build(past_shifts=[ # STANDARD # A has done 2 past shifts, most recent is 4 days ago AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=4), people[0], ), AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=5), people[0], ), # B has done 2 past shifts, most recent is 3 days ago AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=3), people[1], ), AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=7), people[1], ), # C has done 1 past shifts, most recent is 6 days ago AssignedShift( "shift", ShiftType.STANDARD, now - timedelta(days=6), people[2], ), # D has done no past shifts # SPECIAL_A # A has done 1 past shift, most recent is 2 days ago AssignedShift( "shift", ShiftType.SPECIAL_A, now - timedelta(days=2), people[0], ), # B has done no past shifts # C has done no past shifts # D has done 1 past shift, most recent is 9 days ago AssignedShift( "shift", ShiftType.SPECIAL_A, now - timedelta(days=9), people[3], ), # SPECIAL_B # A has done no past shifts # B has done no past shifts # C has done 1 past shift, most recent is 1 days ago AssignedShift( "shift", ShiftType.SPECIAL_B, now - timedelta(days=1), people[2], ), # Sun # D has done no past shifts ]) data = build_run_data(history=history) assignments = init_assignments(model, data) objective = RankingWeight() expected_standard_weight = 107 + 2 + 105 expected_special_a_weight = 105 expected_special_b_weight = 104 assert (evaluate( assignments, ( # Standard (2, 1, 1, 0), (1, 0, 2, 0), (3, 0, 3, 0), ), objective.objective(assignments, data), ) == expected_standard_weight) assert (evaluate( assignments, ( # Special A (3, 0, 4, 1), ), objective.objective(assignments, data), ) == expected_special_a_weight) assert (evaluate( assignments, ( # Special B (2, 0, 5, 2), ), objective.objective(assignments, data), ) == expected_special_b_weight) assert (evaluate( assignments, ( # Standard (2, 1, 1, 0), (1, 0, 2, 0), (3, 0, 3, 0), # Special A (3, 0, 4, 1), # Special B (2, 0, 5, 2), ), objective.objective(assignments, data), ) == expected_standard_weight + expected_special_a_weight + expected_special_b_weight)