Skip to content

Commit 8873c9c

Browse files
committed
Refactor TimeFormatter
- `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`.
1 parent a809969 commit 8873c9c

File tree

12 files changed

+309
-161
lines changed

12 files changed

+309
-161
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"PluralRule": "de",
3+
"Ptxt_week": [ "{0} Woche", "{0} Wochen" ],
4+
"Ptxt_day": [ "{0} Tag", "{0} Tage" ],
5+
"Ptxt_hour": [ "{0} Stunde", "{0} Stunden" ],
6+
"Ptxt_minute": [ "{0} Minute", "{0} Minuten" ],
7+
"Ptxt_second": [ "{0} Sekunde", "{0} Sekunden" ],
8+
"Ptxt_millisecond": [ "{0} Millisekunde", "{0} Millisekunden" ],
9+
"Ptxt_w": [ "{0}w" ],
10+
"Ptxt_d": [ "{0}t" ],
11+
"Ptxt_h": [ "{0}h" ],
12+
"Ptxt_m": [ "{0}m" ],
13+
"Ptxt_s": [ "{0}s" ],
14+
"Ptxt_ms": [ "{0}ms" ],
15+
"Ptxt_lessThan": "weniger als {0}"
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"PluralRule": "en",
3+
"Ptxt_week": [ "{0} week", "{0} weeks" ],
4+
"Ptxt_day": [ "{0} day", "{0} days" ],
5+
"Ptxt_hour": [ "{0} hour", "{0} hours" ],
6+
"Ptxt_minute": [ "{0} minute", "{0} minutes" ],
7+
"Ptxt_second": [ "{0} second", "{0} seconds" ],
8+
"Ptxt_millisecond": [ "{0} millisecond", "{0} milliseconds" ],
9+
"Ptxt_w": [ "{0}w" ],
10+
"Ptxt_d": [ "{0}d" ],
11+
"Ptxt_h": [ "{0}h" ],
12+
"Ptxt_m": [ "{0}m" ],
13+
"Ptxt_s": [ "{0}s" ],
14+
"Ptxt_ms": [ "{0}ms" ],
15+
"Ptxt_lessThan": "less than {0}"
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"PluralRule": "es",
3+
"Ptxt_week": [ "{0} semana", "{0} semanas" ],
4+
"Ptxt_day": [ "{0} día", "{0} días" ],
5+
"Ptxt_hour": [ "{0} hora", "{0} horas" ],
6+
"Ptxt_minute": [ "{0} minuto", "{0} minutos" ],
7+
"Ptxt_second": [ "{0} segundo", "{0} segundos" ],
8+
"Ptxt_millisecond": [ "{0} milisegundo", "{0} milisegundos" ],
9+
"Ptxt_w": [ "{0}sem" ],
10+
"Ptxt_d": [ "{0}d" ],
11+
"Ptxt_h": [ "{0}h" ],
12+
"Ptxt_m": [ "{0}m" ],
13+
"Ptxt_s": [ "{0}s" ],
14+
"Ptxt_ms": [ "{0}ms" ],
15+
"Ptxt_lessThan": "menos que {0}"
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"PluralRule": "fr",
3+
"Ptxt_week": [ "{0} semaine", "{0} semaines" ],
4+
"Ptxt_day": [ "{0} jour", "{0} jours" ],
5+
"Ptxt_hour": [ "{0} heure", "{0} heures" ],
6+
"Ptxt_minute": [ "{0} minute", "{0} minutes" ],
7+
"Ptxt_second": [ "{0} seconde", "{0} secondes" ],
8+
"Ptxt_millisecond": [ "{0} milliseconde", "{0} millisecondes" ],
9+
"Ptxt_w": [ "{0}sem" ],
10+
"Ptxt_d": [ "{0}j" ],
11+
"Ptxt_h": [ "{0}h" ],
12+
"Ptxt_m": [ "{0}m" ],
13+
"Ptxt_s": [ "{0}s" ],
14+
"Ptxt_ms": [ "{0}ms" ],
15+
"Ptxt_lessThan": "moins que {0}"
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"PluralRule": "it",
3+
"Ptxt_week": [ "{0} settimana", "{0} settimane" ],
4+
"Ptxt_day": [ "{0} giorno", "{0} giorni" ],
5+
"Ptxt_hour": [ "{0} ora", "{0} ore" ],
6+
"Ptxt_minute": [ "{0} minuto", "{0} minuti" ],
7+
"Ptxt_second": [ "{0} secondo", "{0} secondi" ],
8+
"Ptxt_millisecond": [ "{0} millisecondo", "{0} millisecondi" ],
9+
"Ptxt_w": [ "{0}set" ],
10+
"Ptxt_d": [ "{0}g" ],
11+
"Ptxt_h": [ "{0}h" ],
12+
"Ptxt_m": [ "{0}m" ],
13+
"Ptxt_s": [ "{0}s" ],
14+
"Ptxt_ms": [ "{0}ms" ],
15+
"Ptxt_lessThan": "meno di {0}"
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"PluralRule": "pt",
3+
"Ptxt_week": [ "{0} semana", "{0} semanas" ],
4+
"Ptxt_day": [ "{0} dia", "{0} dias" ],
5+
"Ptxt_hour": [ "{0} hora", "{0} horas" ],
6+
"Ptxt_minute": [ "{0} minuto", "{0} minutos" ],
7+
"Ptxt_second": [ "{0} segundo", "{0} segundos" ],
8+
"Ptxt_millisecond": [ "{0} milissegundo", "{0} milissegundos" ],
9+
"Ptxt_w": [ "{0}sem" ],
10+
"Ptxt_d": [ "{0}d" ],
11+
"Ptxt_h": [ "{0}h" ],
12+
"Ptxt_m": [ "{0}m" ],
13+
"Ptxt_s": [ "{0}s" ],
14+
"Ptxt_ms": [ "{0}ms" ],
15+
"Ptxt_lessThan": "menos do que {0}"
16+
}

src/SmartFormat.Extensions.Time/SmartFormat.Extensions.Time.csproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ It uses extensions to provide named placeholders, localization, pluralization, g
1313
<PackageTags>string-format stringformat template templating string-composition smartformat smart-format netstandard netcore netframework csharp c-sharp</PackageTags>
1414
</PropertyGroup>
1515

16+
<ItemGroup>
17+
<None Remove="Resources\de.json" />
18+
<None Remove="Resources\en.json" />
19+
<None Remove="Resources\es.json" />
20+
<None Remove="Resources\fr.json" />
21+
<None Remove="Resources\it.json" />
22+
<None Remove="Resources\pt.json" />
23+
24+
<EmbeddedResource Include="Resources\de.json" />
25+
<EmbeddedResource Include="Resources\en.json" />
26+
<EmbeddedResource Include="Resources\es.json" />
27+
<EmbeddedResource Include="Resources\fr.json" />
28+
<EmbeddedResource Include="Resources\it.json" />
29+
<EmbeddedResource Include="Resources\pt.json" />
30+
</ItemGroup>
31+
1632
<ItemGroup>
1733
<ProjectReference Include="..\SmartFormat\SmartFormat.csproj" />
1834
<None Include="../../SmartFormat_365x365.png" Pack="true" Visible="false" PackagePath="/" />
@@ -24,6 +40,7 @@ It uses extensions to provide named placeholders, localization, pluralization, g
2440
<PrivateAssets>all</PrivateAssets>
2541
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2642
</PackageReference>
43+
<PackageReference Include="System.Text.Json" Version="8.0.5" />
2744
</ItemGroup>
2845

2946
</Project>

src/SmartFormat.Extensions.Time/TimeFormatter.cs

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

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

3234
///<inheritdoc/>
33-
public bool CanAutoDetect { get; set; } = true;
35+
public bool CanAutoDetect { get; set; } = false;
3436

3537
#region Constructors
3638

@@ -60,6 +62,7 @@ public TimeFormatter()
6062
/// <see cref="TimeFormatter"/> makes use of <see cref="PluralRules"/> and <see cref="PluralLocalizationFormatter"/>.
6163
/// </remarks>
6264
[Obsolete("This constructor is not required. Changed process to determine the default culture.", true)]
65+
[ExcludeFromCodeCoverage]
6366
public TimeFormatter(string defaultTwoLetterLanguageName)
6467
{
6568
if (CommonLanguagesTimeTextInfo.GetTimeTextInfo(defaultTwoLetterLanguageName) == null)
@@ -111,6 +114,7 @@ public string FallbackLanguage
111114
/// 3. The <see cref="CultureInfo.CurrentUICulture"/>.<br/>
112115
/// </remarks>
113116
[Obsolete("This property is not supported any more. Changed process to get or set the default culture.", true)]
117+
[ExcludeFromCodeCoverage]
114118
public string DefaultTwoLetterISOLanguageName { get; set; } = "en";
115119

116120
#endregion
@@ -121,19 +125,37 @@ public string FallbackLanguage
121125
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
122126
{
123127
var format = formattingInfo.Format;
124-
var current = formattingInfo.CurrentValue;
128+
129+
// Auto-detection calls just return a failure to evaluate
130+
if (string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName))
131+
return false;
132+
133+
#if NET6_0_OR_GREATER
134+
if (formattingInfo.CurrentValue is not (TimeSpan or DateTime or DateTimeOffset or TimeOnly))
135+
throw new FormattingException(formattingInfo.Format?.Items.FirstOrDefault(),
136+
$"'{nameof(TimeFormatter)}' can only process types of " +
137+
$"{nameof(TimeSpan)}, {nameof(DateTime)}, {nameof(DateTimeOffset)}, {nameof(TimeOnly)}, " +
138+
$"but not '{formattingInfo.CurrentValue?.GetType()}'", 0);
139+
#else
140+
if (formattingInfo.CurrentValue is not (TimeSpan or DateTime or DateTimeOffset))
141+
throw new FormattingException(formattingInfo.Format?.Items.FirstOrDefault(),
142+
$"'{nameof(TimeFormatter)}' can only process types of " +
143+
$"{nameof(TimeSpan)}, {nameof(DateTime)}, {nameof(DateTimeOffset)}, " +
144+
$"but not '{formattingInfo.CurrentValue?.GetType()}'", 0);
145+
#endif
125146

126147
// Now we have to check for a nested format.
127148
// That is the one needed for the ListFormatter
128149
var timeParts = GetTimeParts(formattingInfo);
129150
if (timeParts is null) return false;
130151

131-
if (format is { Length: > 0, HasNested: true })
152+
if (format is { Length: > 1, HasNested: true })
132153
{
133-
current = timeParts; // must be an IList to work with ListFormatter
134-
135-
format.Items.RemoveAt(0); // That's the format for the TimeFormatter
136-
formattingInfo.FormatAsChild(format, current);
154+
// Remove the format for the TimeFormatter
155+
format.Items.RemoveAt(0);
156+
// Try to invoke the child format - usually the ListFormatter
157+
// to format the list of time parts
158+
formattingInfo.FormatAsChild(format, timeParts);
137159
return true;
138160
}
139161

@@ -145,39 +167,28 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
145167
{
146168
var format = formattingInfo.Format;
147169
var formatterName = formattingInfo.Placeholder?.FormatterName ?? string.Empty;
170+
148171
var current = formattingInfo.CurrentValue;
149172

150173
var options = formattingInfo.FormatterOptions.Trim();
151174
var formatText = format?.RawText.Trim() ?? string.Empty;
152175

153-
// Not clear, whether we can process this format
154-
if (formatterName == string.Empty && options == string.Empty && formatText == string.Empty) return null;
155-
156176
// In SmartFormat 2.x, the format could be included in options, with empty format.
157177
// Using compatibility with v2, there is no reliable way to set a language as an option
158178
var v2Compatibility = options != string.Empty && formatText == string.Empty;
159179
var formattingOptions = v2Compatibility ? options : formatText;
160180

161-
var fromTime = GetFromTime(current, formattingOptions);
181+
var fromTime = GetFromTime(current);
162182

163-
if (fromTime is null)
164-
{
165-
// Auto detection calls just return a failure to evaluate
166-
if (formatterName == string.Empty)
167-
return null;
168-
169-
// throw, if the formatter has been called explicitly
170-
throw new FormatException(
171-
$"Formatter named '{formatterName}' can only process types of {nameof(TimeSpan)}, {nameof(DateTime)}, {nameof(DateTimeOffset)}");
172-
}
183+
if (fromTime == null) return null;
173184

174185
var timeTextInfo = GetTimeTextInfo(formattingInfo, v2Compatibility);
175186

176-
var timeSpanFormatOptions = TimeSpanFormatOptionsConverter.Parse(v2Compatibility ? options : formatText);
187+
var timeSpanFormatOptions = TimeSpanFormatOptionsConverter.Parse(formattingOptions);
177188
return fromTime.Value.ToTimeParts(timeSpanFormatOptions, timeTextInfo);
178189
}
179190

180-
private static TimeSpan? GetFromTime(object? current, string? formattingOptions)
191+
private static TimeSpan? GetFromTime(object? current)
181192
{
182193
TimeSpan? fromTime = null;
183194

@@ -186,17 +197,16 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
186197
case TimeSpan timeSpan:
187198
fromTime = timeSpan;
188199
break;
200+
#if NET6_0_OR_GREATER
201+
case TimeOnly timeOnly:
202+
fromTime = timeOnly.ToTimeSpan();
203+
break;
204+
#endif
189205
case DateTime dateTime:
190-
if (formattingOptions != string.Empty)
191-
{
192-
fromTime = SystemTime.Now().ToUniversalTime().Subtract(dateTime.ToUniversalTime());
193-
}
206+
fromTime = SystemTime.Now().ToUniversalTime().Subtract(dateTime.ToUniversalTime());
194207
break;
195208
case DateTimeOffset dateTimeOffset:
196-
if (formattingOptions != string.Empty)
197-
{
198-
fromTime = SystemTime.OffsetNow().UtcDateTime.Subtract(dateTimeOffset.UtcDateTime);
199-
}
209+
fromTime = SystemTime.OffsetNow().UtcDateTime.Subtract(dateTimeOffset.UtcDateTime);
200210
break;
201211
}
202212

@@ -224,7 +234,7 @@ private TimeTextInfo GetTimeTextInfo(IFormattingInfo formattingInfo, bool v2Comp
224234
throw new ArgumentException($"{nameof(TimeTextInfo)} could not be found for the given {nameof(IFormatProvider)}.", nameof(formattingInfo));
225235
}
226236

227-
#endregion
237+
#endregion
228238

229239
private static CultureInfo GetCultureInfo(IFormattingInfo formattingInfo, bool v2Compatibility)
230240
{

0 commit comments

Comments
 (0)