diff --git a/DesktopClock.Tests/TimeStringFormatterTests.cs b/DesktopClock.Tests/TimeStringFormatterTests.cs index 941cd3d..037bbd3 100644 --- a/DesktopClock.Tests/TimeStringFormatterTests.cs +++ b/DesktopClock.Tests/TimeStringFormatterTests.cs @@ -133,6 +133,24 @@ public void Format_ConvertsUsingSelectedTimeZone() Assert.Equal("18:30", result); } + [Fact] + public void Format_WeekTokenUsesSelectedTimeZone() + { + var now = new DateTimeOffset(2024, 12, 29, 23, 30, 0, TimeSpan.Zero); + var timeZone = TimeZoneInfo.CreateCustomTimeZone("UtcPlus2", TimeSpan.FromHours(2), "UtcPlus2", "UtcPlus2"); + + var result = TimeStringFormatter.Format( + now, + now.DateTime, + timeZone, + default, + "{weekYear}-W{week}", + null, + CultureInfo.InvariantCulture); + + Assert.Equal("2025-W1", result); + } + [Fact] public void Format_InvalidClockFormatShowsBriefErrorMessage() { diff --git a/DesktopClock.Tests/TokenizerTests.cs b/DesktopClock.Tests/TokenizerTests.cs index f7f88db..4a0aa46 100644 --- a/DesktopClock.Tests/TokenizerTests.cs +++ b/DesktopClock.Tests/TokenizerTests.cs @@ -111,6 +111,41 @@ public void FormatWithTokenizer_DateTimeOffset_ShouldWork() Assert.Equal("Sunday, Sep 24", result); } + [Theory] + [InlineData("2024-01-01", "Week 1")] + [InlineData("2024-12-29", "Week 52")] + [InlineData("2024-12-30", "Week 1")] + [InlineData("2021-01-01", "Week 53")] + public void FormatWithTokenizer_WeekToken_ShouldUseIsoWeek(string date, string expected) + { + // Arrange + var dateTime = DateTime.Parse(date, CultureInfo.InvariantCulture); + var format = "Week {week}"; + + // Act + var result = Tokenizer.FormatWithTokenizerOrFallBack(dateTime, format, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("2024-12-30", "2025-W1")] + [InlineData("2021-01-01", "2020-W53")] + [InlineData("2026-05-06", "2026-W19")] + public void FormatWithTokenizer_WeekYearToken_ShouldUseIsoWeekYear(string date, string expected) + { + // Arrange + var dateTime = DateTime.Parse(date, CultureInfo.InvariantCulture); + var format = "{weekYear}-W{week}"; + + // Act + var result = Tokenizer.FormatWithTokenizerOrFallBack(dateTime, format, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal(expected, result); + } + [Fact] public void FormatWithTokenizer_StandardFormat_WithoutBraces_ShouldWork() { diff --git a/DesktopClock/Utilities/DateFormatExample.cs b/DesktopClock/Utilities/DateFormatExample.cs index 1a9cf0f..be65c32 100644 --- a/DesktopClock/Utilities/DateFormatExample.cs +++ b/DesktopClock/Utilities/DateFormatExample.cs @@ -58,6 +58,8 @@ public static DateFormatExample FromFormat(string format, DateTimeOffset dateTim "{dddd}, {MMM dd}, {h:mm tt}", // Custom format: "Monday, Apr 10, 2:30 PM" "{dddd}, {MMM dd}, {HH:mm:ss}", // Custom format: "Monday, Apr 10, 14:30:45" "{dddd}, {MMM dd}, {h:mm:ss tt}", // Custom format: "Monday, Apr 10, 2:30:45 PM" + "Week {week}", // Custom token: "Week 15" + "Week {week}, {weekYear}", // Custom token: "Week 15, 2023" // Standard formats "D", // Long date pattern: Monday, June 15, 2009 (en-US) diff --git a/DesktopClock/Utilities/Tokenizer.cs b/DesktopClock/Utilities/Tokenizer.cs index 9d4c9a6..8a21412 100644 --- a/DesktopClock/Utilities/Tokenizer.cs +++ b/DesktopClock/Utilities/Tokenizer.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.RegularExpressions; namespace DesktopClock; @@ -31,7 +32,7 @@ public static string FormatWithTokenizerOrFallBack(IFormattable formattable, str return _tokenizerRegex.Replace(format, (m) => { var formatString = m.Groups[1].Value; - return formattable.ToString(formatString, formatProvider); + return FormatToken(formattable, formatString, formatProvider); }); } @@ -48,6 +49,75 @@ public static string FormatWithTokenizerOrFallBack(IFormattable formattable, str return formattable.ToString(); } + private static string FormatToken(IFormattable formattable, string format, IFormatProvider formatProvider) + { + if (TryFormatWeekToken(formattable, format, out var result)) + { + return result; + } + + return formattable.ToString(format, formatProvider); + } + + private static bool TryFormatWeekToken(IFormattable formattable, string format, out string result) + { + result = null; + + if (!TryGetDateTime(formattable, out var dateTime)) + { + return false; + } + + switch (format) + { + case "week": + result = GetIsoWeek(dateTime).ToString(CultureInfo.InvariantCulture); + return true; + + case "weekYear": + result = GetIsoWeekYear(dateTime).ToString(CultureInfo.InvariantCulture); + return true; + + default: + return false; + } + } + + private static bool TryGetDateTime(IFormattable formattable, out DateTime dateTime) + { + switch (formattable) + { + case DateTime value: + dateTime = value; + return true; + + case DateTimeOffset value: + dateTime = value.DateTime; + return true; + + default: + dateTime = default; + return false; + } + } + + private static int GetIsoWeek(DateTime dateTime) + { + if (dateTime.DayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Wednesday) + { + dateTime = dateTime.AddDays(3); + } + + var calendar = CultureInfo.InvariantCulture.Calendar; + return calendar.GetWeekOfYear(dateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); + } + + private static int GetIsoWeekYear(DateTime dateTime) + { + var day = dateTime.DayOfWeek == DayOfWeek.Sunday ? 7 : (int)dateTime.DayOfWeek; + return dateTime.AddDays(4 - day).Year; + } + private static bool UsesTokenSyntax(string format) { return format.Contains("{") || format.Contains("}");