Skip to content
Open
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
18 changes: 18 additions & 0 deletions DesktopClock.Tests/TimeStringFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
35 changes: 35 additions & 0 deletions DesktopClock.Tests/TokenizerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
2 changes: 2 additions & 0 deletions DesktopClock/Utilities/DateFormatExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 71 additions & 1 deletion DesktopClock/Utilities/Tokenizer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;

namespace DesktopClock;
Expand Down Expand Up @@ -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);
});
}

Expand All @@ -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("}");
Expand Down