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
67 changes: 67 additions & 0 deletions DesktopClock.Tests/FormatPresetsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Globalization;
using System.Linq;

namespace DesktopClock.Tests;

[UseUICulture("en-US")]
public class FormatPresetsTests
{
private static DateTimeFormatInfo Region(string culture) => CultureInfo.GetCultureInfo(culture).DateTimeFormat;

[Fact]
public void ForClock_KeepsTheUsFormats()
{
var presets = FormatPresets.ForClock(Region("en-US")).ToDictionary(p => p.Name, p => p.Format);

Assert.Equal("{h:mm tt}", presets["Time"]);
Assert.Equal("{HH:mm}", presets["Time, 24-hour"]);
Assert.Equal("{h:mm:ss tt}", presets["Time with seconds"]);
Assert.Equal("{ddd}, {MMM d}, {h:mm tt}", presets["Date and time"]);
Assert.Equal("{dddd}, {MMMM d}", presets["Date only"]);
}

[Fact]
public void ForClock_FollowsRegionalTimeAndDateOrder()
{
var german = FormatPresets.ForClock(Region("de-DE")).ToDictionary(p => p.Name, p => p.Format);
Assert.Equal("{HH:mm}", german["Time"]);
Assert.Equal("{dddd}, {d. MMMM}", german["Date only"]);

var chinese = FormatPresets.ForClock(Region("zh-CN")).ToDictionary(p => p.Name, p => p.Format);
Assert.Equal("{dddd}, {M月d日}", chinese["Date only"]);
}

[Theory]
[InlineData("en-US", true)]
[InlineData("ko-KR", true)]
[InlineData("de-DE", false)]
[InlineData("ja-JP", false)]
[InlineData("fr-CA", false)]
public void ForClock_OnlyOffersA24HourPresetWhereTheRegionUses12Hours(string culture, bool expected)
{
Assert.Equal(expected, FormatPresets.Uses12HourClock(Region(culture)));
Assert.Equal(expected, FormatPresets.ForClock(Region(culture)).Any(p => p.Name == "Time, 24-hour"));
}

[Fact]
public void DefaultClockFormat_FollowsTheRegion()
{
Assert.Equal("{ddd}, {MMM d}, {h:mm:ss tt}", FormatPresets.DefaultClockFormat(Region("en-US")));
Assert.Equal("{ddd}, {d. MMM}, {HH:mm:ss}", FormatPresets.DefaultClockFormat(Region("de-DE")));
}

[Fact]
public void AllPresets_RenderInEveryRegion()
{
var now = new DateTimeOffset(2026, 9, 22, 13, 1, 22, TimeSpan.Zero);

foreach (var culture in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
{
var formats = FormatPresets.ForClock(culture.DateTimeFormat).Select(p => p.Format).Append(FormatPresets.DefaultClockFormat(culture.DateTimeFormat));

foreach (var format in formats)
Assert.True(Tokenizer.FormatWithTokenizerOrFallBack(now, format, culture) != Tokenizer.FormatErrorMessage, $"{culture.Name}: {format}");
}
}
}
2 changes: 1 addition & 1 deletion DesktopClock/CountdownTargetEditor.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private void BuildPresetButtons()

var example = new TextBlock
{
Text = $"{getTarget():ddd, MMM d} {getTarget():t}",
Text = getTarget().ToString($"ddd, {FormatPresets.ShortMonthDayPattern(CultureInfo.CurrentCulture.DateTimeFormat)} {CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern}"),
FontSize = 12,
TextTrimming = TextTrimming.CharacterEllipsis,
};
Expand Down
16 changes: 3 additions & 13 deletions DesktopClock/FormatEditor.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
Expand All @@ -23,18 +24,7 @@ public enum FormatEditorMode
public partial class FormatEditor : UserControl
{
// Presets cover the scenarios users most commonly ask for; the raw box stays the escape hatch.
private static readonly (string Name, string Format)[] ClockPresets =
{
(Loc.Get("ClockPresetTime"), "{h:mm tt}"),
(Loc.Get("ClockPresetTime24"), "{HH:mm}"),
(Loc.Get("ClockPresetTimeSeconds"), "{h:mm:ss tt}"),
(Loc.Get("ClockPresetDayTime"), "{ddd}, {h:mm tt}"),
(Loc.Get("ClockPresetDateTime"), "{ddd}, {MMM dd}, {h:mm tt}"),
(Loc.Get("ClockPresetFullDateTime"), "{dddd}, {MMMM dd}, {h:mm tt}"),
(Loc.Get("ClockPresetDateOnly"), "{dddd}, {MMMM dd}"),
(Loc.Get("ClockPresetSortable"), "{yyyy-MM-dd} {HH:mm}"),
(Loc.Get("ClockPresetIsoWeek"), "{weekYear}-W{week}"),
};
private static readonly IReadOnlyList<(string Name, string Format)> ClockPresets = FormatPresets.ForClock(CultureInfo.CurrentCulture.DateTimeFormat);

private static readonly (string Name, string Format)[] CountdownPresets =
{
Expand All @@ -55,7 +45,7 @@ private static readonly (string Name, string Token)[] ClockTokens =
(Loc.Get("TokenMonth"), "{MMM}"),
(Loc.Get("TokenMonthFull"), "{MMMM}"),
(Loc.Get("TokenYear"), "{yyyy}"),
(Loc.Get("TokenTime"), "{h:mm tt}"),
(Loc.Get("TokenTime"), "{" + CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern + "}"),
(Loc.Get("TokenTime24"), "{HH:mm}"),
(Loc.Get("TokenSeconds"), "{ss}"),
(Loc.Get("TokenWeekNumber"), "{week}"),
Expand Down
3 changes: 2 additions & 1 deletion DesktopClock/Properties/Settings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media;
Expand Down Expand Up @@ -117,7 +118,7 @@ public static void TrySaveIfLoaded()
/// Change it when you want a different arrangement, such as adding the weekday, seconds, or a shorter date.
/// See: <see href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings">Custom date and time format strings</see>.
/// </remarks>
public string Format { get; set; } = "{ddd}, {MMM dd}, {h:mm:ss tt}";
public string Format { get; set; } = FormatPresets.DefaultClockFormat(CultureInfo.CurrentCulture.DateTimeFormat);

/// <summary>
/// Format string used while <see cref="CountdownTo"/> is enabled.
Expand Down
52 changes: 52 additions & 0 deletions DesktopClock/Utilities/FormatPresets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;

namespace DesktopClock;

/// <summary>
/// Clock formats built from the Windows regional format, so times are 24-hour where people expect it and dates follow the local day and month order.
/// </summary>
public static class FormatPresets
{
/// <summary>
/// The clock's default format: short weekday, short date, and time with seconds, such as "Tue, Sep 22, 1:01:22 PM" in the US or "Di., 22. Sep., 13:01:22" in Germany.
/// </summary>
public static string DefaultClockFormat(DateTimeFormatInfo format) =>
$"{{ddd}}, {Token(ShortMonthDayPattern(format))}, {Token(format.LongTimePattern)}";

/// <summary>
/// One-click clock formats for common scenarios.
/// </summary>
public static IReadOnlyList<(string Name, string Format)> ForClock(DateTimeFormatInfo format)
{
var time = Token(format.ShortTimePattern);
var monthDay = Token(format.MonthDayPattern);
var presets = new List<(string Name, string Format)> { (Loc.Get("ClockPresetTime"), time) };

// A 24-hour preset only adds something where the regional time is 12-hour.
if (Uses12HourClock(format))
presets.Add((Loc.Get("ClockPresetTime24"), "{HH:mm}"));

presets.Add((Loc.Get("ClockPresetTimeSeconds"), Token(format.LongTimePattern)));
presets.Add((Loc.Get("ClockPresetDayTime"), $"{{ddd}}, {time}"));
presets.Add((Loc.Get("ClockPresetDateTime"), $"{{ddd}}, {Token(ShortMonthDayPattern(format))}, {time}"));
presets.Add((Loc.Get("ClockPresetFullDateTime"), $"{{dddd}}, {monthDay}, {time}"));
presets.Add((Loc.Get("ClockPresetDateOnly"), $"{{dddd}}, {monthDay}"));
presets.Add((Loc.Get("ClockPresetSortable"), "{yyyy-MM-dd} {HH:mm}"));
presets.Add((Loc.Get("ClockPresetIsoWeek"), "{weekYear}-W{week}"));
return presets;
}

/// <summary>
/// The regional month and day with an abbreviated month, such as "MMM d" in the US or "d. MMM" in Germany.
/// </summary>
public static string ShortMonthDayPattern(DateTimeFormatInfo format) => format.MonthDayPattern.Replace("MMMM", "MMM");

/// <summary>
/// Whether the regional time format uses a 12-hour clock. Quoted text is skipped so a literal like the "h" in Canadian French "HH 'h' mm" doesn't count.
/// </summary>
public static bool Uses12HourClock(DateTimeFormatInfo format) => Regex.Replace(format.ShortTimePattern, "'[^']*'", "").Contains("h");

private static string Token(string pattern) => "{" + pattern + "}";
}
Loading