]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
dates: Add basic format_interval implementation
authorAarni Koskela <akx@iki.fi>
Mon, 4 Jan 2016 17:35:56 +0000 (19:35 +0200)
committerAarni Koskela <akx@iki.fi>
Sat, 23 Jan 2016 19:15:08 +0000 (21:15 +0200)
Refs #276

babel/dates.py
docs/api/dates.rst
tests/test_date_intervals.py [new file with mode: 0644]

index b0ba9c082aa1d74d37cc6de268bfbdd701a833d3..3605a45194782474d3c7ce7e401f9a06a9f0deef 100644 (file)
@@ -882,6 +882,107 @@ def format_timedelta(delta, granularity='second', threshold=.85,
     return u''
 
 
+def _format_fallback_interval(start, end, skeleton, tzinfo, locale):
+    if skeleton in locale.datetime_skeletons:  # Use the given skeleton
+        format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale)
+    elif all((isinstance(d, date) and not isinstance(d, datetime)) for d in (start, end)):  # Both are just dates
+        format = lambda dt: format_date(dt, locale=locale)
+    elif all((isinstance(d, time) and not isinstance(d, date)) for d in (start, end)):  # Both are times
+        format = lambda dt: format_time(dt, tzinfo=tzinfo, locale=locale)
+    else:
+        format = lambda dt: format_datetime(dt, tzinfo=tzinfo, locale=locale)
+
+    formatted_start = format(start)
+    formatted_end = format(end)
+
+    if formatted_start == formatted_end:
+        return format(start)
+
+    return (
+        locale.interval_formats.get(None, "{0}-{1}").
+        replace("{0}", formatted_start).
+        replace("{1}", formatted_end)
+    )
+
+
+def format_interval(start, end, skeleton, tzinfo=None, locale=LC_TIME):
+    """
+    Format an interval between two instants according to the locale's rules.
+
+    >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "yMd", locale="fi")
+    u'15.\u201317.1.2016'
+
+    >>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB")
+    '12:12 \u2013 16:16'
+
+    >>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US")
+    '5:12 AM \u2013 4:16 PM'
+
+    >>> format_interval(time(16, 18), time(16, 24), "Hm", locale="it")
+    '16:18\u201316:24'
+
+    If the start instant equals the end instant, the interval is formatted like the instant.
+
+    >>> format_interval(time(16, 18), time(16, 18), "Hm", locale="it")
+    '16:18'
+
+    :param start: First instant (datetime/date/time)
+    :param end: Second instant (datetime/date/time)
+    :param skeleton: The "skeleton format" to use for formatting.
+    :param tzinfo: tzinfo to use (if none is already attached)
+    :param locale: A locale object or identifier.
+    :return: Formatted interval
+    """
+    locale = Locale.parse(locale)
+
+    # NB: The quote comments below are from the algorithm description in
+    #     http://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
+
+    # > Look for the intervalFormatItem element that matches the "skeleton",
+    # > starting in the current locale and then following the locale fallback
+    # > chain up to, but not including root.
+
+    if skeleton not in locale.interval_formats:
+        # > If no match was found from the previous step, check what the closest
+        # > match is in the fallback locale chain, as in availableFormats. That
+        # > is, this allows for adjusting the string value field's width,
+        # > including adjusting between "MMM" and "MMMM", and using different
+        # > variants of the same field, such as 'v' and 'z'.
+        # TODO: Implement closest-match instead of immediately falling back
+        return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
+
+    skel_formats = locale.interval_formats[skeleton]
+
+    if start == end:
+        return format_skeleton(skeleton, start, tzinfo, locale)
+
+    start = _ensure_datetime_tzinfo(_get_datetime(start), tzinfo=tzinfo)
+    end = _ensure_datetime_tzinfo(_get_datetime(end), tzinfo=tzinfo)
+
+    start_fmt = DateTimeFormat(start, locale=locale)
+    end_fmt = DateTimeFormat(end, locale=locale)
+
+    # > If a match is found from previous steps, compute the calendar field
+    # > with the greatest difference between start and end datetime. If there
+    # > is no difference among any of the fields in the pattern, format as a
+    # > single date using availableFormats, and return.
+
+    for field in PATTERN_CHAR_ORDER:  # These are in largest-to-smallest order
+        if field in skel_formats:
+            if start_fmt.extract(field) != end_fmt.extract(field):
+                # > If there is a match, use the pieces of the corresponding pattern to
+                # > format the start and end datetime, as above.
+                return "".join(
+                    parse_pattern(pattern).apply(instant, locale)
+                    for pattern, instant
+                    in zip(skel_formats[field], (start, end))
+                )
+
+    # > Otherwise, format the start and end datetime using the fallback pattern.
+
+    return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
+
+
 def parse_date(string, locale=LC_TIME):
     """Parse a date from a string.
 
@@ -1208,8 +1309,14 @@ PATTERN_CHARS = {
     'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4], 'v': [1, 4], 'V': [1, 4]  # zone
 }
 
+#: The pattern characters declared in the Date Field Symbol Table
+#: (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
+#: in order of decreasing magnitude.
+PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZvV"
+
 _pattern_cache = {}
 
+
 def parse_pattern(pattern):
     """Parse date, time, and datetime format patterns.
 
index 1b22cd74bf9f6bb243c707a3e1fc38431abef5b2..67ada41591fc7bfa90aee4523247081606ea860a 100644 (file)
@@ -19,6 +19,8 @@ Date and Time Formatting
 
 .. autofunction:: format_skeleton
 
+.. autofunction:: format_interval
+
 Timezone Functionality
 ----------------------
 
diff --git a/tests/test_date_intervals.py b/tests/test_date_intervals.py
new file mode 100644 (file)
index 0000000..73fae46
--- /dev/null
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import datetime
+
+from babel import dates
+from babel.dates import get_timezone
+from babel.util import UTC
+
+TEST_DT = datetime.datetime(2016, 1, 8, 11, 46, 15)
+TEST_TIME = TEST_DT.time()
+TEST_DATE = TEST_DT.date()
+
+
+def test_format_interval_same_instant_1():
+    assert dates.format_interval(TEST_DT, TEST_DT, "yMMMd", locale="fi") == "8. tammikuuta 2016"
+
+
+def test_format_interval_same_instant_2():
+    assert dates.format_interval(TEST_DT, TEST_DT, "xxx", locale="fi") == "8.1.2016 klo 11.46.15"
+
+
+def test_format_interval_same_instant_3():
+    assert dates.format_interval(TEST_TIME, TEST_TIME, "xxx", locale="fi") == "11.46.15"
+
+
+def test_format_interval_same_instant_4():
+    assert dates.format_interval(TEST_DATE, TEST_DATE, "xxx", locale="fi") == "8.1.2016"
+
+
+def test_format_interval_no_difference():
+    t1 = TEST_DT
+    t2 = t1 + datetime.timedelta(minutes=8)
+    assert dates.format_interval(t1, t2, "yMd", locale="fi") == "8.1.2016"
+
+
+def test_format_interval_in_tz():
+    t1 = TEST_DT.replace(tzinfo=UTC)
+    t2 = t1 + datetime.timedelta(minutes=18)
+    hki_tz = get_timezone("Europe/Helsinki")
+    assert dates.format_interval(t1, t2, "Hmv", tzinfo=hki_tz, locale="fi") == "13.46\u201314.04 aikavyöhyke: Suomi"
+
+
+def test_format_interval_12_hour():
+    t2 = TEST_DT
+    t1 = t2 - datetime.timedelta(hours=1)
+    assert dates.format_interval(t1, t2, "hm", locale="en") == "10:46 \u2013 11:46 AM"