diff --git a/src/SmartFormat.Extensions.Time/Resources/de.json b/src/SmartFormat.Extensions.Time/Resources/de.json new file mode 100644 index 00000000..857a7311 --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Resources/de.json @@ -0,0 +1,16 @@ +{ + "PluralRule": "de", + "Ptxt_week": [ "{0} Woche", "{0} Wochen" ], + "Ptxt_day": [ "{0} Tag", "{0} Tage" ], + "Ptxt_hour": [ "{0} Stunde", "{0} Stunden" ], + "Ptxt_minute": [ "{0} Minute", "{0} Minuten" ], + "Ptxt_second": [ "{0} Sekunde", "{0} Sekunden" ], + "Ptxt_millisecond": [ "{0} Millisekunde", "{0} Millisekunden" ], + "Ptxt_w": [ "{0}w" ], + "Ptxt_d": [ "{0}t" ], + "Ptxt_h": [ "{0}h" ], + "Ptxt_m": [ "{0}m" ], + "Ptxt_s": [ "{0}s" ], + "Ptxt_ms": [ "{0}ms" ], + "Ptxt_lessThan": "weniger als {0}" +} \ No newline at end of file diff --git a/src/SmartFormat.Extensions.Time/Resources/en.json b/src/SmartFormat.Extensions.Time/Resources/en.json new file mode 100644 index 00000000..ae0c49a7 --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Resources/en.json @@ -0,0 +1,16 @@ +{ + "PluralRule": "en", + "Ptxt_week": [ "{0} week", "{0} weeks" ], + "Ptxt_day": [ "{0} day", "{0} days" ], + "Ptxt_hour": [ "{0} hour", "{0} hours" ], + "Ptxt_minute": [ "{0} minute", "{0} minutes" ], + "Ptxt_second": [ "{0} second", "{0} seconds" ], + "Ptxt_millisecond": [ "{0} millisecond", "{0} milliseconds" ], + "Ptxt_w": [ "{0}w" ], + "Ptxt_d": [ "{0}d" ], + "Ptxt_h": [ "{0}h" ], + "Ptxt_m": [ "{0}m" ], + "Ptxt_s": [ "{0}s" ], + "Ptxt_ms": [ "{0}ms" ], + "Ptxt_lessThan": "less than {0}" +} \ No newline at end of file diff --git a/src/SmartFormat.Extensions.Time/Resources/es.json b/src/SmartFormat.Extensions.Time/Resources/es.json new file mode 100644 index 00000000..5dc9735a --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Resources/es.json @@ -0,0 +1,16 @@ +{ + "PluralRule": "es", + "Ptxt_week": [ "{0} semana", "{0} semanas" ], + "Ptxt_day": [ "{0} día", "{0} días" ], + "Ptxt_hour": [ "{0} hora", "{0} horas" ], + "Ptxt_minute": [ "{0} minuto", "{0} minutos" ], + "Ptxt_second": [ "{0} segundo", "{0} segundos" ], + "Ptxt_millisecond": [ "{0} milisegundo", "{0} milisegundos" ], + "Ptxt_w": [ "{0}sem" ], + "Ptxt_d": [ "{0}d" ], + "Ptxt_h": [ "{0}h" ], + "Ptxt_m": [ "{0}m" ], + "Ptxt_s": [ "{0}s" ], + "Ptxt_ms": [ "{0}ms" ], + "Ptxt_lessThan": "menos que {0}" +} diff --git a/src/SmartFormat.Extensions.Time/Resources/fr.json b/src/SmartFormat.Extensions.Time/Resources/fr.json new file mode 100644 index 00000000..59ec1084 --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Resources/fr.json @@ -0,0 +1,16 @@ +{ + "PluralRule": "fr", + "Ptxt_week": [ "{0} semaine", "{0} semaines" ], + "Ptxt_day": [ "{0} jour", "{0} jours" ], + "Ptxt_hour": [ "{0} heure", "{0} heures" ], + "Ptxt_minute": [ "{0} minute", "{0} minutes" ], + "Ptxt_second": [ "{0} seconde", "{0} secondes" ], + "Ptxt_millisecond": [ "{0} milliseconde", "{0} millisecondes" ], + "Ptxt_w": [ "{0}sem" ], + "Ptxt_d": [ "{0}j" ], + "Ptxt_h": [ "{0}h" ], + "Ptxt_m": [ "{0}m" ], + "Ptxt_s": [ "{0}s" ], + "Ptxt_ms": [ "{0}ms" ], + "Ptxt_lessThan": "moins que {0}" +} \ No newline at end of file diff --git a/src/SmartFormat.Extensions.Time/Resources/it.json b/src/SmartFormat.Extensions.Time/Resources/it.json new file mode 100644 index 00000000..1a2e8d5f --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Resources/it.json @@ -0,0 +1,16 @@ +{ + "PluralRule": "it", + "Ptxt_week": [ "{0} settimana", "{0} settimane" ], + "Ptxt_day": [ "{0} giorno", "{0} giorni" ], + "Ptxt_hour": [ "{0} ora", "{0} ore" ], + "Ptxt_minute": [ "{0} minuto", "{0} minuti" ], + "Ptxt_second": [ "{0} secondo", "{0} secondi" ], + "Ptxt_millisecond": [ "{0} millisecondo", "{0} millisecondi" ], + "Ptxt_w": [ "{0}set" ], + "Ptxt_d": [ "{0}g" ], + "Ptxt_h": [ "{0}h" ], + "Ptxt_m": [ "{0}m" ], + "Ptxt_s": [ "{0}s" ], + "Ptxt_ms": [ "{0}ms" ], + "Ptxt_lessThan": "meno di {0}" +} diff --git a/src/SmartFormat.Extensions.Time/Resources/pt.json b/src/SmartFormat.Extensions.Time/Resources/pt.json new file mode 100644 index 00000000..5310bcba --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Resources/pt.json @@ -0,0 +1,16 @@ +{ + "PluralRule": "pt", + "Ptxt_week": [ "{0} semana", "{0} semanas" ], + "Ptxt_day": [ "{0} dia", "{0} dias" ], + "Ptxt_hour": [ "{0} hora", "{0} horas" ], + "Ptxt_minute": [ "{0} minuto", "{0} minutos" ], + "Ptxt_second": [ "{0} segundo", "{0} segundos" ], + "Ptxt_millisecond": [ "{0} milissegundo", "{0} milissegundos" ], + "Ptxt_w": [ "{0}sem" ], + "Ptxt_d": [ "{0}d" ], + "Ptxt_h": [ "{0}h" ], + "Ptxt_m": [ "{0}m" ], + "Ptxt_s": [ "{0}s" ], + "Ptxt_ms": [ "{0}ms" ], + "Ptxt_lessThan": "menos do que {0}" +} diff --git a/src/SmartFormat.Extensions.Time/SmartFormat.Extensions.Time.csproj b/src/SmartFormat.Extensions.Time/SmartFormat.Extensions.Time.csproj index a9ff6e95..51edb255 100644 --- a/src/SmartFormat.Extensions.Time/SmartFormat.Extensions.Time.csproj +++ b/src/SmartFormat.Extensions.Time/SmartFormat.Extensions.Time.csproj @@ -13,6 +13,22 @@ It uses extensions to provide named placeholders, localization, pluralization, g string-format stringformat template templating string-composition smartformat smart-format netstandard netcore netframework csharp c-sharp + + + + + + + + + + + + + + + + @@ -24,6 +40,7 @@ It uses extensions to provide named placeholders, localization, pluralization, g all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/SmartFormat.Extensions.Time/TimeFormatter.cs b/src/SmartFormat.Extensions.Time/TimeFormatter.cs index 1fe7f16b..23cad2cf 100644 --- a/src/SmartFormat.Extensions.Time/TimeFormatter.cs +++ b/src/SmartFormat.Extensions.Time/TimeFormatter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using SmartFormat.Core.Extensions; @@ -24,13 +25,14 @@ public class TimeFormatter : IFormatter /// Obsolete. s only have one unique name. /// [Obsolete("Use property \"Name\" instead", true)] + [ExcludeFromCodeCoverage] public string[] Names { get; set; } = {"timespan", "time", string.Empty}; /// public string Name { get; set; } = "time"; /// - public bool CanAutoDetect { get; set; } = true; + public bool CanAutoDetect { get; set; } = false; #region Constructors @@ -60,6 +62,7 @@ public TimeFormatter() /// makes use of and . /// [Obsolete("This constructor is not required. Changed process to determine the default culture.", true)] + [ExcludeFromCodeCoverage] public TimeFormatter(string defaultTwoLetterLanguageName) { if (CommonLanguagesTimeTextInfo.GetTimeTextInfo(defaultTwoLetterLanguageName) == null) @@ -111,6 +114,7 @@ public string FallbackLanguage /// 3. The .
/// [Obsolete("This property is not supported any more. Changed process to get or set the default culture.", true)] + [ExcludeFromCodeCoverage] public string DefaultTwoLetterISOLanguageName { get; set; } = "en"; #endregion @@ -121,19 +125,37 @@ public string FallbackLanguage public bool TryEvaluateFormat(IFormattingInfo formattingInfo) { var format = formattingInfo.Format; - var current = formattingInfo.CurrentValue; + + // Auto-detection calls just return a failure to evaluate + if (string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName)) + return false; + +#if NET6_0_OR_GREATER + if (formattingInfo.CurrentValue is not (TimeSpan or DateTime or DateTimeOffset or TimeOnly)) + throw new FormattingException(formattingInfo.Format?.Items.FirstOrDefault(), + $"'{nameof(TimeFormatter)}' can only process types of " + + $"{nameof(TimeSpan)}, {nameof(DateTime)}, {nameof(DateTimeOffset)}, {nameof(TimeOnly)}, " + + $"but not '{formattingInfo.CurrentValue?.GetType()}'", 0); +#else + if (formattingInfo.CurrentValue is not (TimeSpan or DateTime or DateTimeOffset)) + throw new FormattingException(formattingInfo.Format?.Items.FirstOrDefault(), + $"'{nameof(TimeFormatter)}' can only process types of " + + $"{nameof(TimeSpan)}, {nameof(DateTime)}, {nameof(DateTimeOffset)}, " + + $"but not '{formattingInfo.CurrentValue?.GetType()}'", 0); +#endif // Now we have to check for a nested format. // That is the one needed for the ListFormatter var timeParts = GetTimeParts(formattingInfo); if (timeParts is null) return false; - if (format is { Length: > 0, HasNested: true }) + if (format is { Length: > 1, HasNested: true }) { - current = timeParts; // must be an IList to work with ListFormatter - - format.Items.RemoveAt(0); // That's the format for the TimeFormatter - formattingInfo.FormatAsChild(format, current); + // Remove the format for the TimeFormatter + format.Items.RemoveAt(0); + // Try to invoke the child format - usually the ListFormatter + // to format the list of time parts + formattingInfo.FormatAsChild(format, timeParts); return true; } @@ -145,39 +167,28 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) { var format = formattingInfo.Format; var formatterName = formattingInfo.Placeholder?.FormatterName ?? string.Empty; + var current = formattingInfo.CurrentValue; var options = formattingInfo.FormatterOptions.Trim(); var formatText = format?.RawText.Trim() ?? string.Empty; - // Not clear, whether we can process this format - if (formatterName == string.Empty && options == string.Empty && formatText == string.Empty) return null; - // In SmartFormat 2.x, the format could be included in options, with empty format. // Using compatibility with v2, there is no reliable way to set a language as an option var v2Compatibility = options != string.Empty && formatText == string.Empty; var formattingOptions = v2Compatibility ? options : formatText; - var fromTime = GetFromTime(current, formattingOptions); + var fromTime = GetFromTime(current); - if (fromTime is null) - { - // Auto detection calls just return a failure to evaluate - if (formatterName == string.Empty) - return null; - - // throw, if the formatter has been called explicitly - throw new FormatException( - $"Formatter named '{formatterName}' can only process types of {nameof(TimeSpan)}, {nameof(DateTime)}, {nameof(DateTimeOffset)}"); - } + if (fromTime == null) return null; var timeTextInfo = GetTimeTextInfo(formattingInfo, v2Compatibility); - var timeSpanFormatOptions = TimeSpanFormatOptionsConverter.Parse(v2Compatibility ? options : formatText); + var timeSpanFormatOptions = TimeSpanFormatOptionsConverter.Parse(formattingOptions); return fromTime.Value.ToTimeParts(timeSpanFormatOptions, timeTextInfo); } - private static TimeSpan? GetFromTime(object? current, string? formattingOptions) + private static TimeSpan? GetFromTime(object? current) { TimeSpan? fromTime = null; @@ -186,17 +197,16 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo) case TimeSpan timeSpan: fromTime = timeSpan; break; +#if NET6_0_OR_GREATER + case TimeOnly timeOnly: + fromTime = timeOnly.ToTimeSpan(); + break; +#endif case DateTime dateTime: - if (formattingOptions != string.Empty) - { - fromTime = SystemTime.Now().ToUniversalTime().Subtract(dateTime.ToUniversalTime()); - } + fromTime = SystemTime.Now().ToUniversalTime().Subtract(dateTime.ToUniversalTime()); break; case DateTimeOffset dateTimeOffset: - if (formattingOptions != string.Empty) - { - fromTime = SystemTime.OffsetNow().UtcDateTime.Subtract(dateTimeOffset.UtcDateTime); - } + fromTime = SystemTime.OffsetNow().UtcDateTime.Subtract(dateTimeOffset.UtcDateTime); break; } @@ -224,7 +234,7 @@ private TimeTextInfo GetTimeTextInfo(IFormattingInfo formattingInfo, bool v2Comp throw new ArgumentException($"{nameof(TimeTextInfo)} could not be found for the given {nameof(IFormatProvider)}.", nameof(formattingInfo)); } - #endregion +#endregion private static CultureInfo GetCultureInfo(IFormattingInfo formattingInfo, bool v2Compatibility) { diff --git a/src/SmartFormat.Extensions.Time/Utilities/CommonLanguagesTimeTextInfo.cs b/src/SmartFormat.Extensions.Time/Utilities/CommonLanguagesTimeTextInfo.cs index c3a1b709..22c54e58 100644 --- a/src/SmartFormat.Extensions.Time/Utilities/CommonLanguagesTimeTextInfo.cs +++ b/src/SmartFormat.Extensions.Time/Utilities/CommonLanguagesTimeTextInfo.cs @@ -2,7 +2,12 @@ // Copyright SmartFormat Project maintainers and contributors. // Licensed under the MIT license. +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Text.Json; using SmartFormat.Utilities; namespace SmartFormat.Extensions.Time.Utilities; @@ -17,129 +22,45 @@ public static class CommonLanguagesTimeTextInfo /// /// Gets the for the English language. /// - public static TimeTextInfo English => new() - { - PluralRule = PluralRules.GetPluralRule("en"), - Ptxt_week = new[] { "{0} week", "{0} weeks" }, - Ptxt_day = new[] { "{0} day", "{0} days" }, - Ptxt_hour = new[] { "{0} hour", "{0} hours" }, - Ptxt_minute = new[] { "{0} minute", "{0} minutes" }, - Ptxt_second = new[] { "{0} second", "{0} seconds" }, - Ptxt_millisecond = new[] { "{0} millisecond", "{0} milliseconds" }, - Ptxt_w = new[] { "{0}w" }, - Ptxt_d = new[] { "{0}d" }, - Ptxt_h = new[] { "{0}h" }, - Ptxt_m = new[] { "{0}m" }, - Ptxt_s = new[] { "{0}s" }, - Ptxt_ms = new[] { "{0}ms" }, - Ptxt_lessThan = "less than {0}" - }; - + [Obsolete("Use GetTimeTextInfo(\"en\") instead")] + [ExcludeFromCodeCoverage] + public static TimeTextInfo English => GetTimeTextInfo("en")!; + /// /// Gets the for the French language. /// - public static TimeTextInfo French => new() - { - PluralRule = PluralRules.GetPluralRule("fr"), - Ptxt_week = new[] { "{0} semaine", "{0} semaines" }, - Ptxt_day = new[] { "{0} jour", "{0} jours" }, - Ptxt_hour = new[] { "{0} heure", "{0} heures" }, - Ptxt_minute = new[] { "{0} minute", "{0} minutes" }, - Ptxt_second = new[] { "{0} seconde", "{0} secondes" }, - Ptxt_millisecond = new[] { "{0} milliseconde", "{0} millisecondes" }, - Ptxt_w = new[] { "{0}sem" }, - Ptxt_d = new[] { "{0}j" }, - Ptxt_h = new[] { "{0}h" }, - Ptxt_m = new[] { "{0}m" }, - Ptxt_s = new[] { "{0}s" }, - Ptxt_ms = new[] { "{0}ms" }, - Ptxt_lessThan = "moins que {0}" - }; + [Obsolete("Use GetTimeTextInfo(\"fr\") instead")] + [ExcludeFromCodeCoverage] + public static TimeTextInfo French => GetTimeTextInfo("fr")!; /// /// Gets the for the Spanish language. /// - public static TimeTextInfo Spanish => new() - { - PluralRule = PluralRules.GetPluralRule("es"), - Ptxt_week = new[] { "{0} semana", "{0} semanas" }, - Ptxt_day = new[] { "{0} día", "{0} días" }, - Ptxt_hour = new[] { "{0} hore", "{0} horas" }, - Ptxt_minute = new[] { "{0} minuto", "{0} minutos" }, - Ptxt_second = new[] { "{0} segundo", "{0} segundos" }, - Ptxt_millisecond = new[] { "{0} milisegundo", "{0} milisegundos" }, - Ptxt_w = new[] { "{0}sem" }, - Ptxt_d = new[] { "{0}d" }, - Ptxt_h = new[] { "{0}h" }, - Ptxt_m = new[] { "{0}m" }, - Ptxt_s = new[] { "{0}s" }, - Ptxt_ms = new[] { "{0}ms" }, - Ptxt_lessThan = "menos que {0}" - }; + [Obsolete("Use GetTimeTextInfo(\"es\") instead")] + [ExcludeFromCodeCoverage] + public static TimeTextInfo Spanish => GetTimeTextInfo("es")!; /// /// Gets the for the Portuguese language. /// - public static TimeTextInfo Portuguese => new() - { - PluralRule = PluralRules.GetPluralRule("pt"), - Ptxt_week = new[] { "{0} semana", "{0} semanas" }, - Ptxt_day = new[] { "{0} dia", "{0} dias" }, - Ptxt_hour = new[] { "{0} hora", "{0} horas" }, - Ptxt_minute = new[] { "{0} minuto", "{0} minutos" }, - Ptxt_second = new[] { "{0} segundo", "{0} segundos" }, - Ptxt_millisecond = new[] { "{0} milissegundo", "{0} milissegundos" }, - Ptxt_w = new[] { "{0}sem" }, - Ptxt_d = new[] { "{0}d" }, - Ptxt_h = new[] { "{0}h" }, - Ptxt_m = new[] { "{0}m" }, - Ptxt_s = new[] { "{0}s" }, - Ptxt_ms = new[] { "{0}ms" }, - Ptxt_lessThan = "menos do que {0}" - }; + [Obsolete("Use GetTimeTextInfo(\"pt\") instead")] + [ExcludeFromCodeCoverage] + public static TimeTextInfo Portuguese => GetTimeTextInfo("pt")!; /// /// Gets the for the Italian language. /// - public static TimeTextInfo Italian => new() - { - PluralRule = PluralRules.GetPluralRule("it"), - Ptxt_week = new[] { "{0} settimana", "{0} settimane" }, - Ptxt_day = new[] { "{0} giorno", "{0} giorni" }, - Ptxt_hour = new[] { "{0} ora", "{0} ore" }, - Ptxt_minute = new[] { "{0} minuto", "{0} minuti" }, - Ptxt_second = new[] { "{0} secondo", "{0} secondi" }, - Ptxt_millisecond = new[] { "{0} millisecondo", "{0} millisecondi" }, - Ptxt_w = new[] { "{0}set" }, - Ptxt_d = new[] { "{0}g" }, - Ptxt_h = new[] { "{0}h" }, - Ptxt_m = new[] { "{0}m" }, - Ptxt_s = new[] { "{0}s" }, - Ptxt_ms = new[] { "{0}ms" }, - Ptxt_lessThan = "meno di {0}" - }; + [Obsolete("Use GetTimeTextInfo(\"it\") instead")] + [ExcludeFromCodeCoverage] + public static TimeTextInfo Italian => GetTimeTextInfo("it")!; /// /// Gets the for the German language. /// - public static TimeTextInfo German => new() - { - PluralRule = PluralRules.GetPluralRule("de"), - Ptxt_week = new[] { "{0} Woche", "{0} Wochen" }, - Ptxt_day = new[] { "{0} Tag", "{0} Tage" }, - Ptxt_hour = new[] { "{0} Stunde", "{0} Stunden" }, - Ptxt_minute = new[] { "{0} Minute", "{0} Minuten" }, - Ptxt_second = new[] { "{0} Sekunde", "{0} Sekunden" }, - Ptxt_millisecond = new[] { "{0} Millisekunde", "{0} Millisekunden" }, - Ptxt_w = new[] { "{0}w" }, - Ptxt_d = new[] { "{0}t" }, - Ptxt_h = new[] { "{0}h" }, - Ptxt_m = new[] { "{0}m" }, - Ptxt_s = new[] { "{0}s" }, - Ptxt_ms = new[] { "{0}ms" }, - Ptxt_lessThan = "weniger als {0}" - }; - + [Obsolete("Use GetTimeTextInfo(\"de\") instead")] + [ExcludeFromCodeCoverage] + public static TimeTextInfo German => GetTimeTextInfo("de")!; + /// /// Adds a for a language. /// @@ -169,15 +90,41 @@ public static void AddLanguage(string twoLetterIsoLanguageName, TimeTextInfo tim if (_customLanguage.TryGetValue(twoLetterIsoLanguageName, out var timeTextInfo)) return timeTextInfo; - return twoLetterIsoLanguageName switch + timeTextInfo = LoadTimeTextInfo(twoLetterIsoLanguageName); + if (timeTextInfo is null) return null; + + _customLanguage.Add(twoLetterIsoLanguageName, timeTextInfo); + return timeTextInfo; + } + + private static TimeTextInfo? LoadTimeTextInfo(string languageCode) + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = $"SmartFormat.Extensions.Time.Resources.{languageCode}.json"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) return null; + + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + var data = JsonSerializer.Deserialize(json); + + return new TimeTextInfo { - "en" => English, - "fr" => French, - "es" => Spanish, - "pt" => Portuguese, - "it" => Italian, - "de" => German, - _ => null + PluralRule = PluralRules.GetPluralRule(data!.PluralRule), + Ptxt_week = data.Ptxt_week, + Ptxt_day = data.Ptxt_day, + Ptxt_hour = data.Ptxt_hour, + Ptxt_minute = data.Ptxt_minute, + Ptxt_second = data.Ptxt_second, + Ptxt_millisecond = data.Ptxt_millisecond, + Ptxt_w = data.Ptxt_w, + Ptxt_d = data.Ptxt_d, + Ptxt_h = data.Ptxt_h, + Ptxt_m = data.Ptxt_m, + Ptxt_s = data.Ptxt_s, + Ptxt_ms = data.Ptxt_ms, + Ptxt_lessThan = data.Ptxt_lessThan }; } -} \ No newline at end of file +} diff --git a/src/SmartFormat.Extensions.Time/Utilities/FileName.cs b/src/SmartFormat.Extensions.Time/Utilities/FileName.cs new file mode 100644 index 00000000..0aafde41 --- /dev/null +++ b/src/SmartFormat.Extensions.Time/Utilities/FileName.cs @@ -0,0 +1,24 @@ +// +// Copyright SmartFormat Project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace SmartFormat.Extensions.Time.Utilities; +internal struct TimeTextInfoData +{ + public string PluralRule { get; set; } + public string[] Ptxt_week { get; set; } + public string[] Ptxt_day { get; set; } + public string[] Ptxt_hour { get; set; } + public string[] Ptxt_minute { get; set; } + public string[] Ptxt_second { get; set; } + public string[] Ptxt_millisecond { get; set; } + public string[] Ptxt_w { get; set; } + public string[] Ptxt_d { get; set; } + public string[] Ptxt_h { get; set; } + public string[] Ptxt_m { get; set; } + public string[] Ptxt_s { get; set; } + public string[] Ptxt_ms { get; set; } + public string Ptxt_lessThan { get; set; } +} + diff --git a/src/SmartFormat.Extensions.Time/Utilities/TimeSpanUtility.cs b/src/SmartFormat.Extensions.Time/Utilities/TimeSpanUtility.cs index 90ed0281..32dfb8b8 100644 --- a/src/SmartFormat.Extensions.Time/Utilities/TimeSpanUtility.cs +++ b/src/SmartFormat.Extensions.Time/Utilities/TimeSpanUtility.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; namespace SmartFormat.Extensions.Time.Utilities; @@ -63,7 +63,7 @@ public static string ToTimeString(this TimeSpan fromTime, TimeSpanFormatOptions /// These will be combined with the default timeSpanFormatOptions. /// /// An object that supplies the text to use for output - public static IList ToTimeParts(this TimeSpan fromTime, TimeSpanFormatOptions options, + internal static IList ToTimeParts(this TimeSpan fromTime, TimeSpanFormatOptions options, TimeTextInfo timeTextInfo) { // If there are any missing options, merge with the defaults: diff --git a/src/SmartFormat.Tests/Extensions.Time/TimeFormatterTests.cs b/src/SmartFormat.Tests/Extensions.Time/TimeFormatterTests.cs index da23e875..5c02c272 100644 --- a/src/SmartFormat.Tests/Extensions.Time/TimeFormatterTests.cs +++ b/src/SmartFormat.Tests/Extensions.Time/TimeFormatterTests.cs @@ -5,7 +5,6 @@ using SmartFormat.Core.Settings; using SmartFormat.Extensions; using SmartFormat.Extensions.Time.Utilities; -using SmartFormat.Tests.TestUtils; using SmartFormat.Utilities; namespace SmartFormat.Tests.Extensions; @@ -42,7 +41,7 @@ private static object[] GetArgs() } [Test] - public void UseTimeFormatter_WithIllegalLanguage() + public void UseTimeFormatter_WithUnsupportedLanguage() { var smart = GetFormatter(); var timeFormatter = smart.GetFormatterExtension()!; @@ -72,35 +71,42 @@ public void Setting_Unknown_FallbackLanguage_Should_Throw() Assert.That(ex, Is.InstanceOf().And.Message.Contains(nameof(TimeTextInfo))); } + [Test] + public void UseTimeFormatter_WithSupportedlLanguage() + { + var smart = GetFormatter(); + + Assert.DoesNotThrow(() => smart.Format("{0:time(en):noless}", new TimeSpan(1,2,3))); + Assert.DoesNotThrow(() => smart.Format(CultureInfo.GetCultureInfo("en"), "{0:time:noless}", new TimeSpan(1,2,3))); + } + [TestCase(true)] [TestCase(false)] - public void UseTimeFormatter_With_Unimplemented_Language(bool useFallbackLanguage) + public void UseTimeFormatter_With_Unimplemented_FallbackLanguage(bool useFallbackLanguage) { var smart = GetFormatter(); var timeFormatter = smart.GetFormatterExtension()!; timeFormatter.FallbackLanguage = useFallbackLanguage ? "en" : string.Empty; - if(useFallbackLanguage) - Assert.That(() => smart.Format("{0:time(nl):noless}", new TimeSpan(1,2,3)), Throws.Nothing); + if (useFallbackLanguage) + Assert.That(() => smart.Format("{0:time(nl):noless}", new TimeSpan(1, 2, 3)), Throws.Nothing); else - Assert.That(() => smart.Format("{0:time(nl):noless}", new TimeSpan(1,2,3)), Throws.TypeOf().And.Message.Contains(nameof(TimeTextInfo))); + Assert.That(() => smart.Format("{0:time(nl):noless}", new TimeSpan(1, 2, 3)), Throws.TypeOf().And.Message.Contains(nameof(TimeTextInfo))); } - [Test] - public void UseTimeFormatter_WithLegalLanguage() + public void Explicit_Formatter_With_UnsupportedArgType_Should_Throw() { var smart = GetFormatter(); - - Assert.DoesNotThrow(() => smart.Format("{0:time(en):noless}", new TimeSpan(1,2,3))); - Assert.DoesNotThrow(() => smart.Format(CultureInfo.GetCultureInfo("en"), "{0:time:noless}", new TimeSpan(1,2,3))); + // Arrays are not supported + Assert.That(() => smart.Format("{0:time:}", Array.Empty()), Throws.Exception.TypeOf()); } [Test] - public void Explicit_Formatter_With_Unsupported_ArgType_Should_Throw() + public void Formatter_ReturnsFalse_For_Implicit_Invocation() { - var smart = GetFormatter(); - Assert.That(() => smart.Format("{0:time:}", DateTime.UtcNow), Throws.Exception.TypeOf()); + var tf = new TimeFormatter(); + Assert.That(tf.TryEvaluateFormat(new FormattingInfo()), Is.False); } [TestCase("{0:time: {:list:|, | and }}", "en", "less than 1 second")] @@ -171,6 +177,28 @@ public void Test_Options(string format, string expected) Assert.That(actual, Is.EqualTo(expected)); } + [Test] + public void TimeSpanRoundedToString() + { + var tsInfo = CommonLanguagesTimeTextInfo.GetTimeTextInfo("en")!; + var result = new TimeSpan(0, 23, 0, 0) + .Round(TimeSpan.FromDays(1).Ticks).ToTimeString(TimeSpanFormatOptions.None, tsInfo); + + Assert.That(result, Is.EqualTo("1 day")); + } + +#if NET6_0_OR_GREATER + [Test] + public void TimeOnlyArgument() + { + var args = GetArgs(); + var smart = GetFormatter(); + + var actual = smart.Format("{0:time(en):noless}", new TimeOnly(13, 12, 11)); + Assert.That(actual, Is.EqualTo("13 hours 12 minutes 11 seconds")); + } +#endif + [TestCase(0)] [TestCase(12)] [TestCase(23)] @@ -201,6 +229,32 @@ public void TimeSpanFromGivenTimeToCurrentTime(int diffHours) SystemTime.ResetDateTime(); } + [Test] + public void DateTimeOffset_As_Argument_Should_FormatAsTimeSpan_ToLocaltime() + { + var smart = GetFormatter(); + var dateTimeOffset = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero); + SystemTime.SetDateTimeOffset(dateTimeOffset.AddDays(1)); + var format = "{0:time(en):abbr hours noless:}"; + + var actual = smart.Format(format, dateTimeOffset); + Assert.That(actual, Is.EqualTo("24h")); + SystemTime.ResetDateTime(); + } + + [Test] + public void DateTime_As_Argument_Should_FormatAsTimeSpan_ToLocaltime() + { + var smart = GetFormatter(); + var dateTime = new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Unspecified); + SystemTime.SetDateTime(dateTime.AddDays(5)); + var format = "{0:time(en):abbr days noless:}"; + + var actual = smart.Format(format, dateTime); + Assert.That(actual, Is.EqualTo("5d")); + SystemTime.ResetDateTime(); + } + [TestCase(0)] [TestCase(12)] [TestCase(23)]