diff --git a/lingua_franca/format.py b/lingua_franca/format.py index a7137ebc..576ce34a 100755 --- a/lingua_franca/format.py +++ b/lingua_franca/format.py @@ -29,6 +29,7 @@ is_supported_full_lang, _raise_unsupported_language, \ UnsupportedLanguageError, NoneLangWarning, InvalidLangWarning, \ FunctionNotLocalizedError, resolve_resource_file, FunctionNotLocalizedError +from lingua_franca.time import now_local, to_local _REGISTERED_FUNCTIONS = ("nice_number", @@ -36,7 +37,8 @@ "pronounce_number", "pronounce_lang", "nice_response", - "nice_duration") + "nice_duration", + "nice_relative_time") populate_localized_function_dict("format", langs=get_active_langs()) @@ -578,3 +580,53 @@ def nice_response(text, lang=''): assertEqual(nice_response_de("10 ^ 2"), "10 hoch 2") """ + +@localized_function(run_own_code_on=[FunctionNotLocalizedError]) +def nice_relative_time(when, relative_to=None, lang=None): + """Create a relative phrase to roughly describe the period between two + datetimes. + + Examples are "25 seconds", "tomorrow", "7 days". + + Note: The reported period is currently limited to a number of days. Longer + periods such as multiple weeks or months will be reported in days. + + Args: + when (datetime): Local timezone + relative_to (datetime): Baseline for relative time, default is now() + lang (str, optional): Defaults to "en-us". + Returns: + str: Relative description of the given time + """ + now = relative_to if relative_to else now_local() + delta = to_local(when) - now + + if delta.total_seconds() < 1: + return "now" + + if delta.total_seconds() < 90: + if delta.total_seconds() == 1: + return "one second" + else: + return f"{int(delta.total_seconds())} seconds" + + minutes = int((delta.total_seconds() + 30) // 60) # +30 to round minutes + if minutes < 90: + if minutes == 1: + return "one minute" + else: + return "{} minutes".format(minutes) + + hours = int((minutes + 30) // 60) # +30 to round hours + if hours < 36: + if hours == 1: + return "one hour" + else: + return "{} hours".format(hours) + + # TODO: "2 weeks", "3 months", "4 years", etc + days = int((hours + 12) // 24) # +12 to round days + if days == 1: + return "1 day" + else: + return "{} days".format(days) diff --git a/lingua_franca/lang/format_eu.py b/lingua_franca/lang/format_eu.py index c1ea0c31..50e715c9 100644 --- a/lingua_franca/lang/format_eu.py +++ b/lingua_franca/lang/format_eu.py @@ -327,11 +327,15 @@ def nice_time_eu(dt, speech=True, use_24hour=False, use_ampm=False): # hemen dago tranpa # return str(dt.hour) + ":" + str(dt.minute) + def nice_relative_time_eu(when, relative_to=None, lang=None): """Create a relative phrase to roughly describe a datetime Examples are "25 seconds", "tomorrow", "7 days". + Note: The reported period is currently limited to a number of days. Longer + periods such as multiple weeks or months will be reported in days. + Args: when (datetime): Local timezone relative_to (datetime): Baseline for relative time, default is now() diff --git a/test/unittests/test_format_en.py b/test/unittests/test_format_en.py index fae8ff6d..fd117734 100644 --- a/test/unittests/test_format_en.py +++ b/test/unittests/test_format_en.py @@ -37,6 +37,7 @@ from lingua_franca.format import date_time_format from lingua_franca.format import join_list from lingua_franca.format import pronounce_lang +from lingua_franca.format import nice_relative_time from lingua_franca.time import default_timezone, set_default_tz, now_local, \ to_local @@ -534,6 +535,42 @@ def test_join(self): self.assertEqual(join_list([1, "b", 3, "d"], "or"), "1, b, 3 or d") +class TestNiceRelativeTime(unittest.TestCase): + def test_format_nice_relative_time(self): + base_datetime = datetime.datetime(2017, 1, 31, 13, 22, 3, + tzinfo=default_timezone()) + two_hours_from_base = base_datetime + datetime.timedelta(hours=2) + self.assertEqual( + nice_relative_time(when=two_hours_from_base, relative_to=base_datetime), + "2 hours" + ) + twoish_hours_from_base = base_datetime + datetime.timedelta(hours=2, minutes=27) + self.assertEqual( + nice_relative_time(when=twoish_hours_from_base, relative_to=base_datetime), + "2 hours" + ) + seconds_from_base = base_datetime + datetime.timedelta(seconds=47) + self.assertEqual( + nice_relative_time(when=seconds_from_base, relative_to=base_datetime), + "47 seconds" + ) + three_days_from_base = base_datetime + datetime.timedelta(days=3) + self.assertEqual( + nice_relative_time(when=three_days_from_base, relative_to=base_datetime), + "3 days" + ) + almost_four_days_from_base = base_datetime + datetime.timedelta(days=3, hours=20) + self.assertEqual( + nice_relative_time(when=almost_four_days_from_base, relative_to=base_datetime), + "4 days" + ) + long_time_from_base = base_datetime + datetime.timedelta(days=957, hours=2, seconds=12) + self.assertEqual( + nice_relative_time(when=long_time_from_base, relative_to=base_datetime), + "957 days" + ) + + class TestLangcode(unittest.TestCase): def test_format_lang_code(self): self.assertEqual(pronounce_lang(lang_code="en"), "English")