| File: src\Shared\DateFormatStringsHelpers.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Globalization; using System.Text.RegularExpressions; namespace Aspire.Dashboard.Utils; internal static partial class DateFormatStringsHelpers { // There are an unbound number of CultureInfo instances so we don't want to use it as the key. // Someone could have also customized their culture so we don't want to use the name as the key. // This struct contains required information from the culture that is used in cached format strings. private readonly record struct CultureDetailsKey(string LongTimePattern, string ShortDatePattern, string NumberDecimalSeparator, TimeFormat timeFormat); private sealed record MillisecondFormatStrings(CachedTimeFormatStrings LongTimePattern, CachedTimeFormatStrings ShortDateLongTimePattern); private sealed record CachedTimeFormatStrings(string None, string TruncatedMilliseconds, string FullMilliseconds); // Cache of format strings for each culture and 12h/24h override combination. Each entry is lazily initialized on demand. private static readonly ConcurrentDictionary<CultureDetailsKey, MillisecondFormatStrings> s_formatStrings = new(); // Colon and dot are the only time separators used by registered cultures. Regex checks for both. [GeneratedRegex(@"(:ss|\.ss|:s|\.s)")] private static partial Regex MatchSecondsInTimeFormatPattern(); // Matches 24-hour format specifiers: HH or H (but not h). [GeneratedRegex(@"HH?")] private static partial Regex MatchHourIn24HourTimeFormatPattern(); // Matches 12-hour format specifiers: hh or h (but not H). [GeneratedRegex(@"hh?")] private static partial Regex MatchHourIn12HourTimeFormatPattern(); // Matches AM/PM designator with optional leading whitespace. [GeneratedRegex(@"\s*tt")] private static partial Regex MatchAmPmDesignator(); private static MillisecondFormatStrings GetDateFormatStrings(CultureInfo cultureInfo, TimeFormat timeFormat) { var key = new CultureDetailsKey(cultureInfo.DateTimeFormat.LongTimePattern, cultureInfo.DateTimeFormat.ShortDatePattern, cultureInfo.NumberFormat.NumberDecimalSeparator, timeFormat); return s_formatStrings.GetOrAdd(key, static k => { var (none, truncated, full) = GetTimePatterns(k); return new MillisecondFormatStrings( new CachedTimeFormatStrings(none, truncated, full), new CachedTimeFormatStrings( k.ShortDatePattern + " " + none, k.ShortDatePattern + " " + truncated, k.ShortDatePattern + " " + full)); }); static (string None, string Truncated, string Full) GetTimePatterns(CultureDetailsKey key) { // From https://learn.microsoft.com/dotnet/standard/base-types/how-to-display-milliseconds-in-date-and-time-values // Intentionally use fff here instead of FFF so output has a consistent length. return ( FormatPattern(key, millisecondFormat: null), FormatPattern(key, "fff"), FormatPattern(key, "FFFFFFF")); } static string FormatPattern(CultureDetailsKey key, string? millisecondFormat) { // Gets the long time pattern, which is something like "h:mm:ss tt" (en-US), "H:mm:ss" (ja-JP), "HH:mm:ss" (fr-FR). var longTimePattern = key.LongTimePattern; // Apply 12h/24h override if requested. longTimePattern = key.timeFormat switch { TimeFormat.TwelveHour => ConvertTo12Hour(longTimePattern), TimeFormat.TwentyFourHour => ConvertTo24Hour(longTimePattern), _ => longTimePattern // System: use culture's pattern as-is }; if (millisecondFormat is null) { return longTimePattern; } return MatchSecondsInTimeFormatPattern().Replace(longTimePattern, $"$1'{key.NumberDecimalSeparator}'{millisecondFormat}"); } static string ConvertTo12Hour(string pattern) { // Replace H/HH with h (24h -> 12h). pattern = MatchHourIn24HourTimeFormatPattern().Replace(pattern, "h"); // Add AM/PM designator if not already present. if (!pattern.Contains("tt")) { pattern += " tt"; } return pattern; } static string ConvertTo24Hour(string pattern) { // Replace h/hh with H (12h -> 24h). pattern = MatchHourIn12HourTimeFormatPattern().Replace(pattern, "H"); // Remove AM/PM designator. pattern = MatchAmPmDesignator().Replace(pattern, "").TrimEnd(); return pattern; } } internal static string GetLongTimePattern(CultureInfo cultureInfo, TimeFormat timeFormat, MillisecondsDisplay millisecondsDisplay) => GetPattern(GetDateFormatStrings(cultureInfo, timeFormat).LongTimePattern, millisecondsDisplay); internal static string GetShortDateLongTimePattern(CultureInfo cultureInfo, TimeFormat timeFormat, MillisecondsDisplay millisecondsDisplay) => GetPattern(GetDateFormatStrings(cultureInfo, timeFormat).ShortDateLongTimePattern, millisecondsDisplay); private static string GetPattern(CachedTimeFormatStrings patterns, MillisecondsDisplay millisecondsDisplay) { return millisecondsDisplay switch { MillisecondsDisplay.None => patterns.None, MillisecondsDisplay.Truncated => patterns.TruncatedMilliseconds, MillisecondsDisplay.Full => patterns.FullMilliseconds, _ => throw new ArgumentException($"Unexpected {nameof(MillisecondsDisplay)} value: {millisecondsDisplay}.", nameof(millisecondsDisplay)) }; } }