From e3247764ce3bea958af894c1386390dd476a3175 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 18 Sep 2026 02:17:07 +0800 Subject: [PATCH] Fix week of year numbering for {Wmon}, {Wsun}, %W and %U The week number was computed from a week boundary anchored at the start of the year, which shifted every date by a week in years beginning on a Sunday or Monday and produced values below the first full week. Compute the number directly from the day of the year and the weekday offset, so that days before the first weekstart day of the year are week 0, matching strftime semantics. Correct the documented range of the affected directives to 00..53. --- CHANGELOG.md | 1 + lib/format/datetime/formatter.ex | 47 +++++----------------- lib/format/datetime/formatters/default.ex | 4 +- lib/format/datetime/formatters/strftime.ex | 4 +- test/format_default_test.exs | 17 ++++++++ test/format_strftime_test.exs | 16 ++++++++ 6 files changed, 49 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b0057e..c1ed4d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Updated tzdata to fix issues with 2024b - Fix deprecation: Module.eval_quoted/4 is deprecated. Use Code.eval_quoted/3 instead - Fix deprecation: "min..max inside match is deprecated" +- Fix week of year numbering for `{Wmon}`, `{Wsun}`, `%W` and `%U` in years starting on a Sunday or Monday --- diff --git a/lib/format/datetime/formatter.ex b/lib/format/datetime/formatter.ex index 2ec932c1..67337406 100644 --- a/lib/format/datetime/formatter.ex +++ b/lib/format/datetime/formatter.ex @@ -555,45 +555,13 @@ defmodule Timex.Format.DateTime.Formatter do pad_numeric(week, flags, width) end - def format_token(_locale, :week_mon, %{:year => year} = date, _modifiers, flags, width) do - new_year = Timex.Date.new!(year, 1, 1) - week_start = Timex.Date.beginning_of_week(new_year, :monday) - - # This date can be calculated by taking the day number of the year, - # shifting the day number of the year down by the number of days which - # occurred in the previous year, then dividing by 7 - day_num = - if Date.compare(week_start, new_year) == :lt do - prev_year_day_start = Date.day_of_year(week_start) - prev_year_day_end = Date.day_of_year(Timex.Date.new!(week_start.year, 12, 31)) - shift = prev_year_day_end - prev_year_day_start - shift + Date.day_of_year(Timex.Date.new!(year, date.month, date.day)) - else - Date.day_of_year(Timex.Date.new!(year, date.month, date.day)) - end - - div(day_num, 7) + def format_token(_locale, :week_mon, date, _modifiers, flags, width) do + week_number(date, :monday) |> pad_numeric(flags, width) end - def format_token(_locale, :week_sun, %{:year => year} = date, _modifiers, flags, width) do - new_year = Timex.Date.new!(year, 1, 1) - week_start = Timex.Date.beginning_of_week(new_year, :sunday) - - # This date can be calculated by taking the day number of the year, - # shifting the day number of the year down by the number of days which - # occurred in the previous year, then dividing by 7 - day_num = - if Date.compare(week_start, new_year) == :lt do - prev_year_day_start = Date.day_of_year(week_start) - prev_year_day_end = Date.day_of_year(Timex.Date.new!(week_start.year, 12, 31)) - shift = prev_year_day_end - prev_year_day_start - shift + Date.day_of_year(Timex.Date.new!(year, date.month, date.day)) - else - Date.day_of_year(Timex.Date.new!(year, date.month, date.day)) - end - - div(day_num, 7) + def format_token(_locale, :week_sun, date, _modifiers, flags, width) do + week_number(date, :sunday) |> pad_numeric(flags, width) end @@ -859,6 +827,13 @@ defmodule Timex.Format.DateTime.Formatter do {:error, {:formatter, :unsupported_token, token}} end + # Week of the year counted from the first `weekstart` day, as strftime + # defines %W and %U: days before that first day belong to week 0. + defp week_number(date, weekstart) do + weekday = Timex.weekday!(date, weekstart) - 1 + div(Timex.day(date) + 6 - weekday, 7) + end + defp pad_numeric(number, flags, width) when is_integer(number), do: pad_numeric("#{number}", flags, width) diff --git a/lib/format/datetime/formatters/default.ex b/lib/format/datetime/formatters/default.ex index 9c9ed848..3c630ded 100644 --- a/lib/format/datetime/formatters/default.ex +++ b/lib/format/datetime/formatters/default.ex @@ -47,8 +47,8 @@ defmodule Timex.Format.DateTime.Formatters.Default do ### Weeks * `{Wiso}` - ISO week number (01..53) - * `{Wmon}` - week number of the year, Monday first (01..53) - * `{Wsun}` - week number of the year, Sunday first (01..53) + * `{Wmon}` - week number of the year, Monday first (00..53) + * `{Wsun}` - week number of the year, Sunday first (00..53) ### Time diff --git a/lib/format/datetime/formatters/strftime.ex b/lib/format/datetime/formatters/strftime.ex index 5be9aff2..be1704c5 100644 --- a/lib/format/datetime/formatters/strftime.ex +++ b/lib/format/datetime/formatters/strftime.ex @@ -63,8 +63,8 @@ defmodule Timex.Format.DateTime.Formatters.Strftime do ### Weeks * `%V` - ISO week number (01..53) - * `%W` - week number of the year, Monday first (00..52) - * `%U` - week number of the year, Sunday first (00..52) + * `%W` - week number of the year, Monday first (00..53) + * `%U` - week number of the year, Sunday first (00..53) ### Time diff --git a/test/format_default_test.exs b/test/format_default_test.exs index df41d5b8..9e0fabf6 100644 --- a/test/format_default_test.exs +++ b/test/format_default_test.exs @@ -167,6 +167,23 @@ defmodule DateFormatTest.FormatDefault do date = Timex.to_datetime({2014, 12, 31}) assert {:ok, "52"} = format(date, "{Wmon}") assert {:ok, "52"} = format(date, "{Wsun}") + + # Years beginning on Sunday (2023) or Monday (2024) + date = Timex.to_datetime({2023, 4, 28}) + assert {:ok, "17"} = format(date, "{Wmon}") + assert {:ok, "17"} = format(date, "{Wsun}") + + date = Timex.to_datetime({2023, 4, 30}) + assert {:ok, "17"} = format(date, "{Wmon}") + assert {:ok, "18"} = format(date, "{Wsun}") + + date = Timex.to_datetime({2024, 1, 1}) + assert {:ok, "1"} = format(date, "{Wmon}") + assert {:ok, "0"} = format(date, "{Wsun}") + + date = Timex.to_datetime({2024, 1, 8}) + assert {:ok, "2"} = format(date, "{Wmon}") + assert {:ok, "1"} = format(date, "{Wsun}") end test "format simple compound date formats" do diff --git a/test/format_strftime_test.exs b/test/format_strftime_test.exs index 63a80f30..dd53b2ca 100644 --- a/test/format_strftime_test.exs +++ b/test/format_strftime_test.exs @@ -193,6 +193,14 @@ defmodule DateFormatTest.FormatStrftime do date = Timex.to_datetime({2013, 1, 7}) assert {:ok, "01"} = format(date, "%W") assert {:ok, "1"} = format(date, "%-W") + date = Timex.to_datetime({2023, 4, 28}) + assert {:ok, "17"} = format(date, "%W") + date = Timex.to_datetime({2023, 5, 1}) + assert {:ok, "18"} = format(date, "%W") + date = Timex.to_datetime({2024, 1, 1}) + assert {:ok, "01"} = format(date, "%W") + date = Timex.to_datetime({2024, 1, 8}) + assert {:ok, "02"} = format(date, "%W") end test "format %U" do @@ -206,6 +214,14 @@ defmodule DateFormatTest.FormatStrftime do date = Timex.to_datetime({2013, 1, 7}) assert {:ok, "01"} = format(date, "%U") assert {:ok, "1"} = format(date, "%-U") + date = Timex.to_datetime({2023, 4, 28}) + assert {:ok, "17"} = format(date, "%U") + date = Timex.to_datetime({2023, 4, 30}) + assert {:ok, "18"} = format(date, "%U") + date = Timex.to_datetime({2024, 1, 1}) + assert {:ok, "00"} = format(date, "%U") + date = Timex.to_datetime({2024, 1, 7}) + assert {:ok, "01"} = format(date, "%U") end test "various simple date combinations" do