Skip to content

Commit

Permalink
fix: iso week usage
Browse files Browse the repository at this point in the history
Fava appears to use ISO 8601 weeks (e.g. `2025-W01`), but unfortunately
does so incorrectly due to a mismatch between ISO 8601 and Gregorian
calendar week definitions.

ISO 8601 defines precisely how to handle weeks, namely, that the first
week of a year is the week that contains 4 January. This definition
results in some occasional mismatches with the Gregorian calendar such
as:

- The first week of 2025 starts on 30 December 2024
- 3 January 2021 belongs to Week 53 of 2020

Fortunately, the fix is straightforward as `strftime` provide[^posix]:

- `%G` for the week-based year
- `%V` for the week number of the year (with no 'week 0' as `%W` can
  return).

This change will ensure that there is no ambiguity on weeks that
straddle the new year, though at the cost of introducing occasional
off-by-one errors compared to previously generated data.

[^posix]: See [POSIX.1-2024](https://pubs.opengroup.org/onlinepubs/9799919799/functions/strftime.html)
Signed-off-by: JP-Ellis <[email protected]>
  • Loading branch information
JP-Ellis committed Feb 5, 2025
1 parent ad5412a commit 356cb10
Show file tree
Hide file tree
Showing 4 changed files with 13 additions and 13 deletions.
4 changes: 2 additions & 2 deletions frontend/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const dateFormat: Record<Interval, DateFormatter> = {
quarter: (date) =>
`${date.getUTCFullYear().toString()}Q${(Math.floor(date.getUTCMonth() / 3) + 1).toString()}`,
month: utcFormat("%b %Y"),
week: utcFormat("%YW%W"),
week: utcFormat("%GW%V"),
day,
};

Expand All @@ -61,7 +61,7 @@ export const timeFilterDateFormat: Record<Interval, DateFormatter> = {
quarter: (date) =>
`${date.getUTCFullYear().toString()}-Q${(Math.floor(date.getUTCMonth() / 3) + 1).toString()}`,
month: utcFormat("%Y-%m"),
week: utcFormat("%Y-W%W"),
week: utcFormat("%G-W%V"),
day,
};

Expand Down
8 changes: 4 additions & 4 deletions frontend/test/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ test("time filter date formatting", () => {
assert.is(day(date), "2020-03-20");
assert.is(month(janfirst), "2020-01");
assert.is(month(date), "2020-03");
assert.is(week(janfirst), "2020-W00");
assert.is(week(date), "2020-W11");
assert.is(week(janfirst), "2020-W01");
assert.is(week(date), "2020-W12");
assert.is(quarter(janfirst), "2020-Q1");
assert.is(quarter(date), "2020-Q1");
assert.is(year(janfirst), "2020");
Expand All @@ -54,8 +54,8 @@ test("human-readable date formatting", () => {
assert.is(day(date), "2020-03-20");
assert.is(month(janfirst), "Jan 2020");
assert.is(month(date), "Mar 2020");
assert.is(week(janfirst), "2020W00");
assert.is(week(date), "2020W11");
assert.is(week(janfirst), "2020W01");
assert.is(week(date), "2020W12");
assert.is(quarter(janfirst), "2020Q1");
assert.is(quarter(date), "2020Q1");
assert.is(year(janfirst), "2020");
Expand Down
8 changes: 4 additions & 4 deletions src/fava/util/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def format_date(self, date: datetime.date) -> str:
if self is Interval.MONTH:
return date.strftime("%b %Y")
if self is Interval.WEEK:
return date.strftime("%YW%W")
return date.strftime("%GW%V")
return date.strftime("%Y-%m-%d")

def format_date_filter(self, date: datetime.date) -> str:
Expand All @@ -137,7 +137,7 @@ def format_date_filter(self, date: datetime.date) -> str:
if self is Interval.MONTH:
return date.strftime("%Y-%m")
if self is Interval.WEEK:
return date.strftime("%Y-W%W")
return date.strftime("%G-W%V")
return date.strftime("%Y-%m-%d")


Expand Down Expand Up @@ -313,7 +313,7 @@ def substitute(
if interval == "week":
string = string.replace(
complete_match,
(today + timedelta(offset * 7)).strftime("%Y-W%W"),
(today + timedelta(offset * 7)).strftime("%G-W%V"),
)
if interval == "day":
string = string.replace(
Expand Down Expand Up @@ -384,7 +384,7 @@ def parse_date( # noqa: PLR0911
if match:
year, week = map(int, match.group(1, 2))
start = (
datetime.datetime.strptime(f"{year}-W{week}-1", "%Y-W%W-%w")
datetime.datetime.strptime(f"{year}-W{week}-1", "%G-W%V-%w")
.replace(tzinfo=datetime.timezone.utc)
.date()
)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_util_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def test_interval_tuples() -> None:
("(month+24)", "2018-06"),
("week", "2016-W25"),
("week+20", "2016-W45"),
("week+2000", "2054-W42"),
("week+2000", "2054-W43"),
("day", "2016-06-24"),
("day+20", "2016-07-14"),
],
Expand Down Expand Up @@ -219,8 +219,8 @@ def test_fiscal_substitute(
("2000-01-01", "2001-01-01", " 2000 "),
("2010-10-01", "2010-11-01", "2010-10"),
("2000-01-03", "2000-01-04", "2000-01-03"),
("2015-01-05", "2015-01-12", "2015-W01"),
("2025-01-06", "2025-01-13", "2025-W01"),
("2014-12-29", "2015-01-05", "2015-W01"),
("2024-12-30", "2025-01-06", "2025-W01"),
("2015-04-01", "2015-07-01", "2015-Q2"),
("2014-01-01", "2016-01-01", "2014 to 2015"),
("2014-01-01", "2016-01-01", "2014-2015"),
Expand Down

0 comments on commit 356cb10

Please sign in to comment.