diff --git a/python/cfr/json/cfr_json.py b/python/cfr/json/cfr_json.py index 53ac0434..fa4af14e 100644 --- a/python/cfr/json/cfr_json.py +++ b/python/cfr/json/cfr_json.py @@ -167,6 +167,13 @@ class TransitionAttributes(TypedDict, total=False): delay: DurationString +class ShipmentTypeIncompatibility(TypedDict, total=False): + """Represents a shipment type incompatibility in the JSON CFR request.""" + + types: list[str] + incompatibilityMode: int + + class ShipmentModel(TypedDict, total=False): """Represents a shipment model in the JSON CFR request.""" @@ -176,6 +183,8 @@ class ShipmentModel(TypedDict, total=False): globalStartTime: TimeString globalEndTime: TimeString + shipmentTypeIncompatibilities: list[ShipmentTypeIncompatibility] + class Visit(TypedDict, total=False): """Represents a single visit on a route in the JSON CFR results.""" @@ -289,12 +298,25 @@ class OptimizeToursRequest(TypedDict, total=False): timeout: DurationString +class Metrics(TypedDict, total=False): + """Represents the metrics in the JSON CFR response.""" + + aggregatedRouteMetrics: AggregatedMetrics + skippedMandatoryShipmentCount: int + usedVehicleCount: int + earliestVehicleStartTime: TimeString + latestVehicleEndTime: TimeString + costs: dict[str, float] + totalCost: float + + class OptimizeToursResponse(TypedDict, total=False): """Represents the JSON CFR result.""" routes: list[ShipmentRoute] skippedShipments: list[SkippedShipment] totalCost: float + metrics: Metrics # pylint: enable=invalid-name @@ -447,6 +469,22 @@ def get_break_min_duration(break_request: BreakRequest) -> datetime.timedelta: return parse_duration_string(break_request["minDuration"]) +def get_transition_break_duration(transition: Transition) -> datetime.timedelta: + """Returns the break time of a transition. + + Args: + transition: The transition for which the break duration is computed. + + Returns: + When there is a break over the given transition, returns its duration. When + there is no break, returns zero duration. + """ + duration_string = transition.get("breakDuration") + if duration_string is None: + return datetime.timedelta() + return parse_duration_string(duration_string) + + def get_visit_request(model: ShipmentModel, visit: Visit) -> VisitRequest: """Returns the visit request used in `visit`.""" shipment_index = visit.get("shipmentIndex", 0) @@ -942,7 +980,9 @@ def recompute_route_metrics( end_time = parse_time_string(route["vehicleEndTime"]) if route_total_duration != end_time - start_time: raise ValueError( - "The total duration is inconsistent with vehicle start and end times" + "The total duration is inconsistent with vehicle start and end" + f" times. Start time: {start_time}, end time: {end_time}, total" + f" duration: {route_total_duration}" ) route["metrics"] = { diff --git a/python/cfr/json/cfr_json_test.py b/python/cfr/json/cfr_json_test.py index 7140afd6..891734dd 100644 --- a/python/cfr/json/cfr_json_test.py +++ b/python/cfr/json/cfr_json_test.py @@ -514,6 +514,28 @@ def test_get_min_duration(self): ) +class GetTransitionPropertiesTest(unittest.TestCase): + """Tests for accessor functions for Transition.""" + + def test_get_break_duration_no_break(self): + self.assertEqual( + cfr_json.get_transition_break_duration( + {"travelDuration": "32s", "totalDuration": "32s"} + ), + datetime.timedelta(), + ) + + def test_get_break_duration_with_break(self): + self.assertEqual( + cfr_json.get_transition_break_duration({ + "travelDuration": "16s", + "breakDuration": "3600s", + "totalDuration": "3616s", + }), + datetime.timedelta(hours=1), + ) + + class GetUnavoidableBreaksTest(unittest.TestCase): """Tests for get_unavoidable_breaks."""