summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAarni Koskela <akx@iki.fi>2016-01-04 19:35:56 +0200
committerAarni Koskela <akx@iki.fi>2016-01-23 21:15:08 +0200
commit796c3d20bfb430f9ae1cf845b2cde35ab0573c2b (patch)
treec49c3ce25a3880c12e641a433dec62e1749b139e
parentb652f655deab93dddf35dd0fb4799f978778e275 (diff)
downloadbabel-796c3d20bfb430f9ae1cf845b2cde35ab0573c2b.tar.gz
dates: Add basic format_interval implementation
Refs #276
-rw-r--r--babel/dates.py107
-rw-r--r--docs/api/dates.rst2
-rw-r--r--tests/test_date_intervals.py47
3 files changed, 156 insertions, 0 deletions
diff --git a/babel/dates.py b/babel/dates.py
index b0ba9c0..3605a45 100644
--- a/babel/dates.py
+++ b/babel/dates.py
@@ -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.
diff --git a/docs/api/dates.rst b/docs/api/dates.rst
index 1b22cd7..67ada41 100644
--- a/docs/api/dates.rst
+++ b/docs/api/dates.rst
@@ -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
index 0000000..73fae46
--- /dev/null
+++ b/tests/test_date_intervals.py
@@ -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"