Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions babel/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,8 @@ def __getitem__(self, name: str) -> str:
return self.format_day_of_year(num)
elif char == 'F':
return self.format_day_of_week_in_month()
elif char == 'g':
return self.format_julian_day(num)
elif char in ('E', 'e', 'c'):
return self.format_weekday(char, num)
elif char in ('a', 'b', 'B'):
Expand Down Expand Up @@ -1721,6 +1723,33 @@ def format_day_of_year(self, num: int) -> str:
def format_day_of_week_in_month(self) -> str:
return str((self.value.day - 1) // 7 + 1)

def format_julian_day(self, num: int) -> str:
"""
Return the modified Julian day.

This is different from the conventional Julian day number:
- It demarcates days at local zone midnight, rather than noon GMT.
- It depends on the local time zone.

>>> from datetime import date
>>> format = DateTimeFormat(date(2016, 2, 28), 'cs_CZ')
>>> format.format_julian_day(1)
'2457447'

:param num: count of format character

"""
# Python defines `dt.toordinal()` which gives us the Gregorian day for
# a given date. To compute the Julian day for a given date, we just
# need to know the offset between the Julian and Gregorian calendars.
# This offset can be computed by taking the `JAN_1_1_JULIAN_DAY`
# constant defined in ICU and subtracting `1` from it. This is because
# `date(1, 1, 1).toordinal() == 1` since January 1, 1 is the start of
# the Gregorian calendar.
# See: https://github.com/unicode-org/icu/blob/6d18ecab418ac4cf5a3f51ec3b69b77ba713d719/icu4j/main/core/src/main/java/com/ibm/icu/util/Calendar.java#L1136-L1141
JULIAN_DAY_OFFSET = 1721426 - 1
return self.format(self.value.toordinal() + JULIAN_DAY_OFFSET, num)

def format_period(self, char: str, num: int) -> str:
"""
Return period from parsed datetime according to format pattern.
Expand Down
2 changes: 1 addition & 1 deletion docs/dates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Date Fields
| +--------+--------------------------------------------------------+
| | ``F`` | Day of week in month. |
| +--------+--------------------------------------------------------+
| | ``g`` | ?? |
| | ``g`` | Modified Julian day (numeric). |
+----------+--------+--------------------------------------------------------+
| Week day | ``E`` | Day of week. Use one through three letters for the |
| | | short day, or four for the full name, or five for the |
Expand Down
17 changes: 17 additions & 0 deletions tests/test_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ def test_format_date_with_day_of_year_in_pattern_and_datetime_param():
assert dates.format_date(d, 'w', locale='en_US') == '14'


def test_format_julian_day(timezone_getter):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, I checked these test cases against ICU using this script:

from datetime import datetime, timezone
from icu import SimpleDateFormat, Locale, TimeZone


def format_date(pattern, tz, dt, locale='en'):
    fmt = SimpleDateFormat(pattern, Locale(locale))
    fmt.setTimeZone(TimeZone.createTimeZone(tz))
    return fmt.format(dt.timestamp())  


U = timezone.utc
print(format_date('g', 'UTC', datetime(476, 9, 4, tzinfo=U)))
print(format_date('g', 'UTC', datetime(1970, 1, 1, 12, tzinfo=U)))
print(format_date('g', 'UTC', datetime(1970, 1, 1, 12, 30, tzinfo=U)))
print(format_date('ggggggggg', 'UTC', datetime(2026, 11, 7, tzinfo=U)))

dt = datetime(2007, 3, 31, 23, 30, tzinfo=U)
print(format_date('g', 'UTC', dt))
print(format_date('g', 'Europe/Zurich', dt))

You can run it with uv:

uv run --with PyICU python

assert dates.format_date(date(476, 9, 4), 'g', locale='en') == '1895163'
assert dates.format_date(date(1970, 1, 1), 'g', locale='en') == '2440588'
# The time part of the datetime is ignored
assert dates.format_date(datetime(1970, 1, 1, 12, 30), 'g', locale='en') == '2440588'
# Zero padding
assert dates.format_date(date(2026, 11, 7), 'ggggggggg', locale='en') == '002461352'
# Local time is used for the calculation
# 23:30 UTC on March 31 is already April 1 in Zurich so the
# Julian day in the Zurich timezone should be 1 more than UTC
dt = datetime(2007, 3, 31, 23, 30, tzinfo=UTC)
jd_utc = dates.format_datetime(dt, 'g', locale='en')
jd_zurich = dates.format_datetime(dt, 'g', tzinfo=timezone_getter('Europe/Zurich'), locale='en')
assert jd_utc == '2454191'
assert jd_zurich == f'{int(jd_utc)+1}'


def test_format_datetime_with_float(timezone_getter):
UTC = timezone_getter('UTC')
d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=UTC)
Expand Down
Loading