diff --git a/vlib/time/format.v b/vlib/time/format.v index 56d1dcceb2..f04c15c91f 100644 --- a/vlib/time/format.v +++ b/vlib/time/format.v @@ -416,14 +416,13 @@ pub fn (t Time) custom_format(s string) string { sb.write_string(ordinal_suffix(t.day)) } 'DDD' { - sb.write_string((t.day + days_before[t.month - 1] + int(is_leap_year(t.year))).str()) + sb.write_string((t.year_day()).str()) } 'DDDD' { - sb.write_string('${t.day + days_before[t.month - 1] + int(is_leap_year(t.year)):03}') + sb.write_string('${t.year_day():03}') } 'DDDo' { - sb.write_string(ordinal_suffix(t.day + days_before[t.month - 1] + - int(is_leap_year(t.year)))) + sb.write_string(ordinal_suffix(t.year_day())) } 'd' { sb.write_string('${t.day_of_week() % 7}') @@ -484,16 +483,13 @@ pub fn (t Time) custom_format(s string) string { sb.write_string('${(t.hour + 1):02}') } 'w' { - sb.write_string('${mceil((t.day + days_before[t.month - 1] + - int(is_leap_year(t.year))) / 7):.0}') + sb.write_string('${t.week_of_year():.0}') } 'ww' { - sb.write_string('${mceil((t.day + days_before[t.month - 1] + - int(is_leap_year(t.year))) / 7):02.0}') + sb.write_string('${t.week_of_year():02.0}') } 'wo' { - sb.write_string(ordinal_suffix(int(mceil((t.day + days_before[t.month - 1] + - int(is_leap_year(t.year))) / 7)))) + sb.write_string(ordinal_suffix(t.week_of_year())) } 'Q' { sb.write_string('${(t.month % 4) + 1}') diff --git a/vlib/time/time.v b/vlib/time/time.v index 7b44ae7569..dbbd4ddba3 100644 --- a/vlib/time/time.v +++ b/vlib/time/time.v @@ -296,6 +296,37 @@ pub fn (t Time) day_of_week() int { return day_of_week(t.year, t.month, t.day) } +// week_of_year returns the current week of year as an integer. +// follow ISO 8601 standard +pub fn (t Time) week_of_year() int { + // ISO 8601 Week of Year Rules: + // -------------------------------------------- + // 1. Week Definition: + // - A week starts on ​**Monday**​ (Day 1) and ends on ​**Sunday**​ (Day 7). + // 2. First Week of the Year: + // - The first week is the one containing the year's ​**first Thursday**. + // - Equivalently, the week with January 4th always belongs to Week 1. + // 3. Year Assignment: + // - Dates in December/January may belong to the previous/next ISO year, + // depending on the week's Thursday. + // 4. Week Number Format: + // - Expressed as `YYYY-Www` (e.g., `2026-W01` for the first week of 2026). + // -------------------------------------------- + // Algorithm Steps: + // 1. Find the Thursday of the current week: + // - If date is Monday-Wednesday, add days to reach Thursday. + // - If date is Thursday-Sunday, subtract days to reach Thursday. + // 2. The ISO year is the calendar year of this Thursday. + // 3. Compute the week number as: + // week_number = (thursday's day_of_year - 1) / 7 + 1 + day_of_week := t.day_of_week() + days_to_thursday := 4 - day_of_week + thursday_date := t.add_days(days_to_thursday) + thursday_day_of_year := thursday_date.year_day() + week_number := (thursday_day_of_year - 1) / 7 + 1 + return week_number +} + // year_day returns the current day of the year as an integer. // See also #Time.custom_format . pub fn (t Time) year_day() int { diff --git a/vlib/time/time_test.v b/vlib/time/time_test.v index 1185152d2e..e029e62ebf 100644 --- a/vlib/time/time_test.v +++ b/vlib/time/time_test.v @@ -201,6 +201,34 @@ fn test_day_of_week() { } } +fn test_week_of_year() { + // As windows use msvcrt.dll, which `strftime` does not support %V, so skip test + // TODO: newer version windows use ucrtbase.dll, which support %V + $if !windows { + for year in 2000 .. 2100 { + mut t := time.new(time.Time{ + year: year + month: 12 + day: 20 + }) + + // check from year.12.20 to next_year.1.8 + for _ in 0 .. 20 { + assert t.strftime('%V') == '${t.week_of_year():02}', '${t}' + t = t.add_days(1) + } + } + } + + t1 := time.Time{ + year: 2025 + month: 3 + day: 3 + } + assert t1.week_of_year() == 10 + assert t1.add_days(1).week_of_year() == 10 +} + fn test_year_day() { // testing if December 31st in a leap year is numbered as 366 assert time.parse('2024-12-31 20:00:00')!.year_day() == 366