Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor TimeFormatter #460

Merged
merged 1 commit into from
Jan 14, 2025
Merged
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
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