From 5fbcc68578fae59441949b7b6c7ef0f9cf3fa13a Mon Sep 17 00:00:00 2001 From: Ondrej Sykora Date: Mon, 4 Dec 2023 08:26:56 +0000 Subject: [PATCH] Added human-readable time window formatting functions to `human_readable`. --- python/cfr/json/human_readable.py | 62 ++++++++++++ python/cfr/json/human_readable_test.py | 126 +++++++++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/python/cfr/json/human_readable.py b/python/cfr/json/human_readable.py index 81bf9b4a..992b90d7 100644 --- a/python/cfr/json/human_readable.py +++ b/python/cfr/json/human_readable.py @@ -1,5 +1,7 @@ """Provides functions for formatting CFR JSON objects to human-readable form.""" +from collections.abc import Iterable + from . import cfr_json @@ -12,6 +14,66 @@ def lat_lng(latlng: cfr_json.LatLng) -> str: return f"{latitude}, {longitude}" +def time_window(window: cfr_json.TimeWindow | None) -> str: + """Formats a time window as a human readable string. + + Args: + window: The time window to format. + + Returns: + A human-readable string representation of the time window. Returns an empty + string when `window` is None. + """ + if window is None: + return "" + parts = [] + start = window.get("startTime") + end = window.get("endTime") + if start is not None or end is not None: + parts.append( + f"{timestring_or_default(start, '...')} -" + f" {timestring_or_default(end, '...')}" + ) + soft_start = window.get("softStartTime") + soft_end = window.get("softEndTime") + if soft_start is not None or soft_end is not None: + parts.append( + f"soft: {timestring_or_default(soft_start, '...')} -" + f" {timestring_or_default(soft_end, '...')}" + ) + return " ".join(parts) + + +def time_windows( + windows: Iterable[cfr_json.TimeWindow] | None, separator: str = " | " +) -> str: + """Formats a collection of time windows as a human readable string. + + Args: + windows: The collection of time windows to be formatted. + separator: The separator string placed between the time windows. + + Returns: + A human-readable string representation of the time windows. Returns an empty + string when `windows` is None or empty. + """ + if not windows: + return "" + return separator.join(time_window(window) for window in windows) + + +def timestring_or_default( + value: cfr_json.TimeString | None, default: str +) -> str: + """Returns a formatted timestamp or `default`, if `value` is `None`.""" + # TODO(ondrasej): If the global span of the scenario is <= 24 hours, do not + # show the date. Also, normalize all timestamps to the same timezone and do + # not show the timezone suffix. + if value is not None: + return str(cfr_json.parse_time_string(value)) + return default + + def transition_duration(transition: cfr_json.Transition) -> str: """Returns human-readable travel duration info for a transition.""" # NOTE(ondrasej): Breaks have their own rows, so we do not need to include diff --git a/python/cfr/json/human_readable_test.py b/python/cfr/json/human_readable_test.py index 452de680..9fe7fea7 100644 --- a/python/cfr/json/human_readable_test.py +++ b/python/cfr/json/human_readable_test.py @@ -14,6 +14,132 @@ def test_lat_lng(self): ) +class TimeWindowTest(unittest.TestCase): + """Tests for time_window.""" + + def test_none(self): + self.assertEqual(human_readable.time_window(None), "") + + def test_start_and_end(self): + self.assertEqual( + human_readable.time_window({ + "startTime": "2023-11-21T12:00:32Z", + "endTime": "2023-11-21T17:00:00Z", + }), + "2023-11-21 12:00:32+00:00 - 2023-11-21 17:00:00+00:00", + ) + + def test_start_only(self): + self.assertEqual( + human_readable.time_window({"startTime": "2023-11-21T08:00:00Z"}), + "2023-11-21 08:00:00+00:00 - ...", + ) + + def test_end_only(self): + self.assertEqual( + human_readable.time_window({"endTime": "2023-11-21T18:15:00Z"}), + "... - 2023-11-21 18:15:00+00:00", + ) + + def test_soft_start_end(self): + self.assertEqual( + human_readable.time_window({ + "softStartTime": "2023-11-21T12:00:32Z", + "softEndTime": "2023-11-21T17:00:00Z", + }), + "soft: 2023-11-21 12:00:32+00:00 - 2023-11-21 17:00:00+00:00", + ) + + def test_soft_start_only(self): + self.assertEqual( + human_readable.time_window({ + "softStartTime": "2023-11-21T12:00:32Z", + }), + "soft: 2023-11-21 12:00:32+00:00 - ...", + ) + + def test_soft_end_only(self): + self.assertEqual( + human_readable.time_window({ + "softEndTime": "2023-11-21T21:00:00Z", + }), + "soft: ... - 2023-11-21 21:00:00+00:00", + ) + + def test_hard_and_soft(self): + self.assertEqual( + human_readable.time_window({ + "startTime": "2023-11-21T08:00:00Z", + "endTime": "2023-11-21T22:00:00Z", + "softStartTime": "2023-11-21T12:00:32Z", + "softEndTime": "2023-11-21T19:00:00Z", + }), + "2023-11-21 08:00:00+00:00 - 2023-11-21 22:00:00+00:00" + " soft: 2023-11-21 12:00:32+00:00 - 2023-11-21 19:00:00+00:00", + ) + + +class TimeWindowsTest(unittest.TestCase): + """Tests for time_windows.""" + + def test_none(self): + self.assertEqual(human_readable.time_windows(None), "") + + def test_empty(self): + self.assertEqual(human_readable.time_windows(()), "") + + def test_one_time_window(self): + self.assertEqual( + human_readable.time_windows(({"startTime": "2023-11-21T08:00:00Z"},)), + "2023-11-21 08:00:00+00:00 - ...", + ) + + def test_multiple_time_windows(self): + self.assertEqual( + human_readable.time_windows(( + { + "startTime": "2023-11-21T12:00:32Z", + "endTime": "2023-11-21T17:00:00Z", + }, + { + "softStartTime": "2023-11-21T12:00:32Z", + }, + )), + "2023-11-21 12:00:32+00:00 - 2023-11-21 17:00:00+00:00" + " | soft: 2023-11-21 12:00:32+00:00 - ...", + ) + + def test_non_default_separator(self): + self.assertEqual( + human_readable.time_windows( + ( + { + "startTime": "2023-11-21T12:00:32Z", + "endTime": "2023-11-21T17:00:00Z", + }, + { + "softStartTime": "2023-11-21T12:00:32Z", + }, + ), + separator="\n", + ), + "2023-11-21 12:00:32+00:00 - 2023-11-21 17:00:00+00:00" + "\nsoft: 2023-11-21 12:00:32+00:00 - ...", + ) + + +class TimeStringOrDefault(unittest.TestCase): + + def test_default(self): + self.assertEqual(human_readable.timestring_or_default(None, "..."), "...") + + def test_not_default(self): + self.assertEqual( + human_readable.timestring_or_default("2023-12-21T16:32:00Z", "..."), + "2023-12-21 16:32:00+00:00", + ) + + class TransitionDurationTest(unittest.TestCase): """Tests for transition_duration."""