Skip to content

Commit

Permalink
Refactor TimeFormatter
Browse files Browse the repository at this point in the history
- `SmartFormat.Extensions.Time.csproj` includes `System.Text.Json`.
- Enhance `TimeFormatter` to handle `TimeOnly` type (in supported frameworks).
- Update `CommonLanguagesTimeTextInfo` to load data from JSON resources and marked old properties as obsolete.
- Add JSON resource files for de, en, es, fr, it, pt languages.
- Accept `TimeOnly` argument for NET6_0_OR_GREATER
- Introduce `TimeTextInfoData` struct to read localization data from JSON
- Update and add tests in `TimeFormatterTests`.
  • Loading branch information
axunonb committed Jan 14, 2025
1 parent a809969 commit 8873c9c
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 161 deletions.
16 changes: 16 additions & 0 deletions src/SmartFormat.Extensions.Time/Resources/de.json
Original file line number Diff line number Diff line change
@@ -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}"
}
16 changes: 16 additions & 0 deletions src/SmartFormat.Extensions.Time/Resources/en.json
Original file line number Diff line number Diff line change
@@ -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}"
}
16 changes: 16 additions & 0 deletions src/SmartFormat.Extensions.Time/Resources/es.json
Original file line number Diff line number Diff line change
@@ -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}"
}
16 changes: 16 additions & 0 deletions src/SmartFormat.Extensions.Time/Resources/fr.json
Original file line number Diff line number Diff line change
@@ -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}"
}
16 changes: 16 additions & 0 deletions src/SmartFormat.Extensions.Time/Resources/it.json
Original file line number Diff line number Diff line change
@@ -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}"
}
16 changes: 16 additions & 0 deletions src/SmartFormat.Extensions.Time/Resources/pt.json
Original file line number Diff line number Diff line change
@@ -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}"
}
17 changes: 17 additions & 0 deletions src/SmartFormat.Extensions.Time/SmartFormat.Extensions.Time.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ It uses extensions to provide named placeholders, localization, pluralization, g
<PackageTags>string-format stringformat template templating string-composition smartformat smart-format netstandard netcore netframework csharp c-sharp</PackageTags>
</PropertyGroup>

<ItemGroup>
<None Remove="Resources\de.json" />
<None Remove="Resources\en.json" />
<None Remove="Resources\es.json" />
<None Remove="Resources\fr.json" />
<None Remove="Resources\it.json" />
<None Remove="Resources\pt.json" />

<EmbeddedResource Include="Resources\de.json" />
<EmbeddedResource Include="Resources\en.json" />
<EmbeddedResource Include="Resources\es.json" />
<EmbeddedResource Include="Resources\fr.json" />
<EmbeddedResource Include="Resources\it.json" />
<EmbeddedResource Include="Resources\pt.json" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SmartFormat\SmartFormat.csproj" />
<None Include="../../SmartFormat_365x365.png" Pack="true" Visible="false" PackagePath="/" />
Expand All @@ -24,6 +40,7 @@ It uses extensions to provide named placeholders, localization, pluralization, g
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

</Project>
74 changes: 42 additions & 32 deletions src/SmartFormat.Extensions.Time/TimeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using SmartFormat.Core.Extensions;
Expand All @@ -24,13 +25,14 @@ public class TimeFormatter : IFormatter
/// Obsolete. <see cref="IFormatter"/>s only have one unique name.
/// </summary>
[Obsolete("Use property \"Name\" instead", true)]
[ExcludeFromCodeCoverage]
public string[] Names { get; set; } = {"timespan", "time", string.Empty};

///<inheritdoc/>
public string Name { get; set; } = "time";

///<inheritdoc/>
public bool CanAutoDetect { get; set; } = true;
public bool CanAutoDetect { get; set; } = false;

#region Constructors

Expand Down Expand Up @@ -60,6 +62,7 @@ public TimeFormatter()
/// <see cref="TimeFormatter"/> makes use of <see cref="PluralRules"/> and <see cref="PluralLocalizationFormatter"/>.
/// </remarks>
[Obsolete("This constructor is not required. Changed process to determine the default culture.", true)]
[ExcludeFromCodeCoverage]
public TimeFormatter(string defaultTwoLetterLanguageName)
{
if (CommonLanguagesTimeTextInfo.GetTimeTextInfo(defaultTwoLetterLanguageName) == null)
Expand Down Expand Up @@ -111,6 +114,7 @@ public string FallbackLanguage
/// 3. The <see cref="CultureInfo.CurrentUICulture"/>.<br/>
/// </remarks>
[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
Expand All @@ -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;
}

Expand All @@ -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;

Expand All @@ -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;
}

Expand Down Expand Up @@ -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)
{
Expand Down
Loading

0 comments on commit 8873c9c

Please sign in to comment.