Skip to content

Commit

Permalink
dates: Add basic format_interval implementation
Browse files Browse the repository at this point in the history
Refs #276
  • Loading branch information
akx committed Jan 23, 2016
1 parent b652f65 commit 796c3d2
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 0 deletions.
107 changes: 107 additions & 0 deletions babel/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,107 @@ def _iter_patterns(a_unit):
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.
Expand Down Expand Up @@ -1208,8 +1309,14 @@ def get_week_number(self, day_of_period, day_of_week=None):
'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.
Expand Down
2 changes: 2 additions & 0 deletions docs/api/dates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Date and Time Formatting

.. autofunction:: format_skeleton

.. autofunction:: format_interval

Timezone Functionality
----------------------

Expand Down
47 changes: 47 additions & 0 deletions tests/test_date_intervals.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit 796c3d2

Please sign in to comment.