def completion_time(request): start_time = request.GET.get("start", "") hours = request.GET.get("hours", "") try: start_time = datetime.fromisoformat(start_time) except ValueError: return JsonResponse( {"error": "Invalid start time format. Expected ISO format.", "resp": None} ) try: hours = int(hours) except ValueError: return JsonResponse( {"error": "Invalid number of hours. Expected an integer.", "resp": None} ) end_time = utils.compute_end_time(start_time, hours) return JsonResponse({"error": None, "resp": end_time.isoformat()})
def completion_time(self): if self.override_hours: return self.start_time + timedelta( hours=float(self.override_hours)) return utils.compute_end_time(self.start_time, self.hours)
def schedule( items: List[Item], existing_items: Mapping[str, List[ExistingItem]], start: datetime, end: datetime, ) -> List[Mapping[str, str]]: """ Schedules the items according the following algorithm: :param items: a list of Item objects to be scheduled :param existing_items: a mapping from group to ExistingItem objects that are already on the timeline and therefore cannot be changed. :param start: the earliest time when the items can be scheduled to start :param end: the latest time when the items can be scheduled to finish :return: a list of schedules """ schedules: List[Schedule] = [] logger.info( "Scheduling %d items from %s to %s", len(items), start.isoformat(), end.isoformat(), ) # Prioritize item with earliest deadline, with a tiebreaker of shortest # time first items.sort(key=lambda item: (item.item.goal.deadline, item.hours)) ml_schedules = _get_available_blocks(existing_items, start, end) for item in items: min_earliest_time = end min_ml = None min_block_id = None for ml in item.groups: earliest_time = end earliest_block_id = None for i, (st, et) in enumerate(ml_schedules[ml]): if compute_end_time(st, item.hours) <= et: earliest_time = st earliest_block_id = i break if earliest_time < min_earliest_time: min_earliest_time = earliest_time min_ml = ml min_block_id = earliest_block_id if min_ml is None: raise ScheduleException("Unable to schedule: too many items.") else: item_end_time = compute_end_time(min_earliest_time, item.hours) if timezone.make_naive(item_end_time) > datetime.combine( item.item.goal.deadline, WORK_HOURS_END ): raise ScheduleException("Unable to schedule: would exceed deadline.") schedules.append( Schedule(item.item.id, min_earliest_time, item_end_time, min_ml) ) st, et = ml_schedules[min_ml][min_block_id] if et == item_end_time: del ml_schedules[min_ml][min_block_id] else: ml_schedules[min_ml][min_block_id] = (item_end_time, et) logger.info("Scheduled %d items: %s", len(schedules), schedules) return list(map(Schedule.to_dict, schedules))
def schedule_max_profit( items: List[Item], existing_items: Mapping[str, List[ExistingItem]], start: datetime, end: datetime, ) -> List[Mapping[str, str]]: """ Schedules the items according the following algorithm: :param items: a list of Item objects to be scheduled :param existing_items: a mapping from group to ExistingItem objects that are already on the timeline and therefore cannot be changed. :param start: the earliest time when the items can be scheduled to start :param end: the latest time when the items can be scheduled to finish :return: a list of schedules """ schedules: List[Schedule] = [] logger.info( "Scheduling %d items from %s to %s", len(items), start.isoformat(), end.isoformat(), ) # Prioritize item with earliest deadline, with a tiebreaker of shortest # time first items.sort( key=lambda item: ( item.item.sku.sales.aggregate(revenue=Sum(F("price") * F("sales")))[ "revenue" ], item.hours, ) ) ml_schedules = _get_available_blocks(existing_items, start, end) for item in items: max_latest_time = start opt_ml = None opt_block_id = None item_deadline = start.tzinfo.localize( datetime.combine(item.item.goal.deadline, WORK_HOURS_END) ) for ml in item.groups: latest_time = end latest_block_id = None found = False for i, (st, et) in reversed(list(enumerate(ml_schedules[ml]))): deadline = et if et <= item_deadline else item_deadline if compute_start_time(deadline, item.hours) >= st: latest_time = compute_start_time(deadline, item.hours) latest_block_id = i found = True break if found: max_latest_time = latest_time opt_ml = ml opt_block_id = latest_block_id if opt_ml is None: raise ScheduleException("Unable to schedule: too many items.") else: item_end_time = compute_end_time(max_latest_time, item.hours) schedules.append( Schedule(item.item.id, max_latest_time, item_end_time, opt_ml) ) st, et = ml_schedules[opt_ml][opt_block_id] if st == max_latest_time: del ml_schedules[opt_ml][opt_block_id] else: ml_schedules[opt_ml][opt_block_id] = (st, max_latest_time) logger.info("Scheduled %d items: %s", len(schedules), schedules) return list(map(Schedule.to_dict, schedules))
def test_compute_end_time(self): tz = timezone.get_current_timezone() test_cases = [ ( (tz.localize(datetime(2019, 2, 21, 9, 0, 0)), 1), tz.localize(datetime(2019, 2, 21, 10, 0, 0)), "9AM, 1 hour, 10AM", ), ( (tz.localize(datetime(2019, 2, 21, 9, 0, 0)), 10), tz.localize(datetime(2019, 2, 22, 9, 0, 0)), "9AM, 10 hours, 9AM next day", ), ( (tz.localize(datetime(2019, 2, 21, 9, 0, 0)), 24), tz.localize(datetime(2019, 2, 23, 13, 0, 0)), "9AM, 24 hours, 1PM two days later", ), ( (tz.localize(datetime(2019, 2, 21, 20, 0, 0)), 11), tz.localize(datetime(2019, 2, 23, 9, 0, 0)), "8PM, 11 hours, 9AM two days later (no work first day)", ), ( (tz.localize(datetime(2019, 2, 21, 18, 0, 0)), 10), tz.localize(datetime(2019, 2, 22, 18, 0, 0)), "6PM, 10 hours, 6PM next day", ), ( (tz.localize(datetime(2019, 2, 21, 8, 0, 0)), 10), tz.localize(datetime(2019, 2, 21, 18, 0, 0)), "8AM, 10 hours, 6PM", ), ( (tz.localize(datetime(2019, 2, 21, 8, 0, 0)), 0.5), tz.localize(datetime(2019, 2, 21, 8, 30, 0)), "8AM, 30 minutes, 8:30AM", ), ( (tz.localize(datetime(2019, 2, 21, 8, 0, 0)), 10.5), tz.localize(datetime(2019, 2, 22, 8, 30, 0)), "8AM, 10.5 hours, 8:30AM next day", ), ( (tz.localize(datetime(2019, 2, 21, 1, 0, 0)), 4), tz.localize(datetime(2019, 2, 21, 12, 0, 0)), "1AM, 4 hours, 12PM same day", ), ((tz.localize(datetime(2019, 3, 9, 16, 0, 0)), 4), tz.localize(datetime(2019, 3, 10, 10, 0, 0)), "4PM, 4 hours, 10 AM next day, across DST switch") ] for (start_time, hours), expected, msg in test_cases: print( "start: ", start_time.strftime("%Y-%m-%d %H:%M:%S %Z%z"), "+", hours, "hours", ) end_time = utils.compute_end_time(start_time, hours) print("end: ", end_time.strftime("%Y-%m-%d %H:%M:%S %Z%z")) print("expected:", expected.strftime("%Y-%m-%d %H:%M:%S %Z%z")) self.assertEqual(end_time, expected, msg)