Skip to content

Commit 9fbd6b4

Browse files
lipchevangularsen
andauthored
UnitParser.TryParse doesn't try with FallbackCulture (v6) (#1466)
Fixes #1443 - [x] `UnitParser`: extracting a `FindMatchingUnits` function (used by both `Parse` and `TryParse` for a given abbreviation and culture) - [x] `UnitAbbreviationsCache`: just a cosmetic change (flipping the order of the items in the tuple) - [x] `UnitParserTests`: added the missing tests for the `FallbackCulture` - [x] `UnitTestBaseClassGenerator`: improving the code-coverage of the `ParseUnit` and `TryParseUnit` tests (parsing without specifying a culture, which should always work with abbreviations for the `FallbackCulture`) - Add some extra tests regarding CurrentCulture and explicit culture for parsing --------- Co-authored-by: Andreas Gullberg Larsen <[email protected]>
1 parent dc10bf7 commit 9fbd6b4

File tree

128 files changed

+24374
-24909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+24374
-24909
lines changed

CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs

Lines changed: 251 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using CodeGen.JsonTypes;
45

@@ -50,6 +51,38 @@ internal class UnitTestBaseClassGenerator : GeneratorBase
5051
/// </summary>
5152
private readonly string _otherOrBaseUnitFullName;
5253

54+
/// <summary>
55+
/// Stores a mapping of culture names to their corresponding unique unit abbreviations.
56+
/// Each culture maps to a dictionary where the key is the unit abbreviation and the value is the corresponding
57+
/// <see cref="Unit" />.
58+
/// This ensures that unit abbreviations are unique within the context of a specific culture.
59+
/// </summary>
60+
/// <remarks>
61+
/// Used for testing culture-specific parsing with non-ambiguous (unique) abbreviations.
62+
/// </remarks>
63+
private readonly Dictionary<string, Dictionary<string, Unit>> _uniqueAbbreviationsForCulture;
64+
65+
/// <summary>
66+
/// Stores a mapping of culture names to their respective ambiguous unit abbreviations.
67+
/// Each culture maps to a dictionary where the key is the ambiguous abbreviation, and the value is a list of
68+
/// <see cref="Unit" /> objects
69+
/// that share the same abbreviation within that culture.
70+
/// </summary>
71+
/// <remarks>
72+
/// This field is used to identify and handle unit abbreviations that are not unique within a specific culture.
73+
/// Ambiguities arise when multiple units share the same abbreviation, requiring additional logic to resolve.
74+
/// </remarks>
75+
private readonly Dictionary<string, Dictionary<string, List<Unit>>> _ambiguousAbbreviationsForCulture;
76+
77+
/// <summary>
78+
/// The default or fallback culture for unit localizations.
79+
/// </summary>
80+
/// <remarks>
81+
/// This culture, "en-US", is used as a fallback when a specific <see cref="System.Globalization.CultureInfo" />
82+
/// is not available for the defined unit localizations.
83+
/// </remarks>
84+
private const string FallbackCultureName = "en-US";
85+
5386
public UnitTestBaseClassGenerator(Quantity quantity)
5487
{
5588
_quantity = quantity;
@@ -65,6 +98,52 @@ public UnitTestBaseClassGenerator(Quantity quantity)
6598
// Try to pick another unit, or fall back to base unit if only a single unit.
6699
_otherOrBaseUnit = quantity.Units.Where(u => u != _baseUnit).DefaultIfEmpty(_baseUnit).First();
67100
_otherOrBaseUnitFullName = $"{_unitEnumName}.{_otherOrBaseUnit.SingularName}";
101+
102+
var abbreviationsForCulture = new Dictionary<string, Dictionary<string, List<Unit>>>();
103+
foreach (Unit unit in quantity.Units)
104+
{
105+
if (unit.ObsoleteText != null)
106+
{
107+
continue;
108+
}
109+
110+
foreach (Localization localization in unit.Localization)
111+
{
112+
if (!abbreviationsForCulture.TryGetValue(localization.Culture, out Dictionary<string, List<Unit>>? localizationsForCulture))
113+
{
114+
abbreviationsForCulture[localization.Culture] = localizationsForCulture = new Dictionary<string, List<Unit>>();
115+
}
116+
117+
foreach (var abbreviation in localization.Abbreviations)
118+
{
119+
if (localizationsForCulture.TryGetValue(abbreviation, out List<Unit>? matchingUnits))
120+
{
121+
matchingUnits.Add(unit);
122+
}
123+
else
124+
{
125+
localizationsForCulture[abbreviation] = [unit];
126+
}
127+
}
128+
}
129+
}
130+
131+
_uniqueAbbreviationsForCulture = new Dictionary<string, Dictionary<string, Unit>>();
132+
_ambiguousAbbreviationsForCulture = new Dictionary<string, Dictionary<string, List<Unit>>>();
133+
foreach ((var cultureName, Dictionary<string, List<Unit>>? abbreviations) in abbreviationsForCulture)
134+
{
135+
var uniqueAbbreviations = abbreviations.Where(pair => pair.Value.Count == 1).ToDictionary(pair => pair.Key, pair => pair.Value[0]);
136+
if (uniqueAbbreviations.Count != 0)
137+
{
138+
_uniqueAbbreviationsForCulture.Add(cultureName, uniqueAbbreviations);
139+
}
140+
141+
var ambiguousAbbreviations = abbreviations.Where(pair => pair.Value.Count > 1).ToDictionary();
142+
if (ambiguousAbbreviations.Count != 0)
143+
{
144+
_ambiguousAbbreviationsForCulture.Add(cultureName, ambiguousAbbreviations);
145+
}
146+
}
68147
}
69148

70149
private string GetUnitFullName(Unit unit) => $"{_unitEnumName}.{unit.SingularName}";
@@ -90,6 +169,7 @@ public string Generate()
90169
using System.Globalization;
91170
using System.Linq;
92171
using System.Threading;
172+
using UnitsNet.Tests.Helpers;
93173
using UnitsNet.Tests.TestsBase;
94174
using UnitsNet.Units;
95175
using Xunit;
@@ -323,45 +403,193 @@ public void TryParse()
323403
}
324404
Writer.WL($@"
325405
}}
406+
");
326407

327-
[Fact]
328-
public void ParseUnit()
329-
{{");
330-
foreach (var unit in _quantity.Units.Where(u => string.IsNullOrEmpty(u.ObsoleteText)))
331-
foreach (var localization in unit.Localization)
332-
foreach (var abbreviation in localization.Abbreviations)
408+
Writer.WL($@"
409+
[Theory]");
410+
foreach ((var abbreviation, Unit unit) in _uniqueAbbreviationsForCulture[FallbackCultureName])
333411
{
334412
Writer.WL($@"
335-
try
336-
{{
337-
var parsedUnit = {_quantity.Name}.ParseUnit(""{abbreviation}"", CultureInfo.GetCultureInfo(""{localization.Culture}""));
338-
Assert.Equal({GetUnitFullName(unit)}, parsedUnit);
339-
}} catch (AmbiguousUnitParseException) {{ /* Some units have the same abbreviations */ }}
413+
[InlineData(""{abbreviation}"", {GetUnitFullName(unit)})]");
414+
}
415+
Writer.WL($@"
416+
public void ParseUnit_WithUsEnglishCurrentCulture(string abbreviation, {_unitEnumName} expectedUnit)
417+
{{
418+
// Fallback culture ""{FallbackCultureName}"" is always localized
419+
using var _ = new CultureScope(""{FallbackCultureName}"");
420+
{_unitEnumName} parsedUnit = {_quantity.Name}.ParseUnit(abbreviation);
421+
Assert.Equal(expectedUnit, parsedUnit);
422+
}}
340423
");
424+
425+
Writer.WL($@"
426+
[Theory]");
427+
foreach ((var abbreviation, Unit unit) in _uniqueAbbreviationsForCulture[FallbackCultureName])
428+
{
429+
Writer.WL($@"
430+
[InlineData(""{abbreviation}"", {GetUnitFullName(unit)})]");
341431
}
342432
Writer.WL($@"
433+
public void ParseUnit_WithUnsupportedCurrentCulture_FallsBackToUsEnglish(string abbreviation, {_unitEnumName} expectedUnit)
434+
{{
435+
// Currently, no abbreviations are localized for Icelandic, so it should fall back to ""{FallbackCultureName}"" when parsing.
436+
using var _ = new CultureScope(""is-IS"");
437+
{_unitEnumName} parsedUnit = {_quantity.Name}.ParseUnit(abbreviation);
438+
Assert.Equal(expectedUnit, parsedUnit);
343439
}}
440+
");
344441

345-
[Fact]
346-
public void TryParseUnit()
347-
{{");
348-
foreach (var unit in _quantity.Units.Where(u => string.IsNullOrEmpty(u.ObsoleteText)))
349-
foreach (var localization in unit.Localization)
350-
foreach (var abbreviation in localization.Abbreviations)
442+
Writer.WL($@"
443+
[Theory]");
444+
foreach ((var cultureName, Dictionary<string, Unit> abbreviations) in _uniqueAbbreviationsForCulture)
351445
{
352-
// Skip units with ambiguous abbreviations, since there is no exception to describe this is why TryParse failed.
353-
if (IsAmbiguousAbbreviation(localization, abbreviation)) continue;
446+
foreach ((var abbreviation, Unit unit) in abbreviations)
447+
{
448+
Writer.WL($@"
449+
[InlineData(""{cultureName}"", ""{abbreviation}"", {GetUnitFullName(unit)})]");
450+
}
451+
}
452+
Writer.WL($@"
453+
public void ParseUnit_WithCurrentCulture(string culture, string abbreviation, {_unitEnumName} expectedUnit)
454+
{{
455+
using var _ = new CultureScope(culture);
456+
{_unitEnumName} parsedUnit = {_quantity.Name}.ParseUnit(abbreviation);
457+
Assert.Equal(expectedUnit, parsedUnit);
458+
}}
459+
");
460+
461+
Writer.WL($@"
462+
[Theory]");
463+
foreach ((var cultureName, Dictionary<string, Unit> abbreviations) in _uniqueAbbreviationsForCulture)
464+
{
465+
foreach ((var abbreviation, Unit unit) in abbreviations)
466+
{
467+
Writer.WL($@"
468+
[InlineData(""{cultureName}"", ""{abbreviation}"", {GetUnitFullName(unit)})]");
469+
}
470+
}
471+
Writer.WL($@"
472+
public void ParseUnit_WithCulture(string culture, string abbreviation, {_unitEnumName} expectedUnit)
473+
{{
474+
{_unitEnumName} parsedUnit = {_quantity.Name}.ParseUnit(abbreviation, CultureInfo.GetCultureInfo(culture));
475+
Assert.Equal(expectedUnit, parsedUnit);
476+
}}
477+
");
354478

479+
// we only generate these for a few of the quantities
480+
if (_ambiguousAbbreviationsForCulture.Count != 0)
481+
{
355482
Writer.WL($@"
356-
{{
357-
Assert.True({_quantity.Name}.TryParseUnit(""{abbreviation}"", CultureInfo.GetCultureInfo(""{localization.Culture}""), out var parsedUnit));
358-
Assert.Equal({GetUnitFullName(unit)}, parsedUnit);
359-
}}
483+
[Theory]");
484+
foreach ((var cultureName, Dictionary<string, List<Unit>>? abbreviations) in _ambiguousAbbreviationsForCulture)
485+
{
486+
foreach (KeyValuePair<string, List<Unit>> ambiguousPair in abbreviations)
487+
{
488+
Writer.WL($@"
489+
[InlineData(""{cultureName}"", ""{ambiguousPair.Key}"")] // [{string.Join(", ", ambiguousPair.Value.Select(x => x.SingularName))}] ");
490+
}
491+
}
492+
Writer.WL($@"
493+
public void ParseUnitWithAmbiguousAbbreviation(string culture, string abbreviation)
494+
{{
495+
Assert.Throws<AmbiguousUnitParseException>(() => {_quantity.Name}.ParseUnit(abbreviation, CultureInfo.GetCultureInfo(culture)));
496+
}}
360497
");
498+
} // ambiguousAbbreviations
499+
500+
Writer.WL($@"
501+
[Theory]");
502+
foreach ((var abbreviation, Unit unit) in _uniqueAbbreviationsForCulture[FallbackCultureName])
503+
{
504+
Writer.WL($@"
505+
[InlineData(""{abbreviation}"", {GetUnitFullName(unit)})]");
361506
}
362507
Writer.WL($@"
508+
public void TryParseUnit_WithUsEnglishCurrentCulture(string abbreviation, {_unitEnumName} expectedUnit)
509+
{{
510+
// Fallback culture ""{FallbackCultureName}"" is always localized
511+
using var _ = new CultureScope(""{FallbackCultureName}"");
512+
Assert.True({_quantity.Name}.TryParseUnit(abbreviation, out {_unitEnumName} parsedUnit));
513+
Assert.Equal(expectedUnit, parsedUnit);
363514
}}
515+
");
364516

517+
Writer.WL($@"
518+
[Theory]");
519+
foreach ((var abbreviation, Unit unit) in _uniqueAbbreviationsForCulture[FallbackCultureName])
520+
{
521+
Writer.WL($@"
522+
[InlineData(""{abbreviation}"", {GetUnitFullName(unit)})]");
523+
}
524+
Writer.WL($@"
525+
public void TryParseUnit_WithUnsupportedCurrentCulture_FallsBackToUsEnglish(string abbreviation, {_unitEnumName} expectedUnit)
526+
{{
527+
// Currently, no abbreviations are localized for Icelandic, so it should fall back to ""{FallbackCultureName}"" when parsing.
528+
using var _ = new CultureScope(""is-IS"");
529+
Assert.True({_quantity.Name}.TryParseUnit(abbreviation, out {_unitEnumName} parsedUnit));
530+
Assert.Equal(expectedUnit, parsedUnit);
531+
}}
532+
");
533+
534+
Writer.WL($@"
535+
[Theory]");
536+
foreach ((var cultureName, Dictionary<string, Unit> abbreviations) in _uniqueAbbreviationsForCulture)
537+
{
538+
foreach ((var abbreviation, Unit unit) in abbreviations)
539+
{
540+
Writer.WL($@"
541+
[InlineData(""{cultureName}"", ""{abbreviation}"", {GetUnitFullName(unit)})]");
542+
}
543+
}
544+
Writer.WL($@"
545+
public void TryParseUnit_WithCurrentCulture(string culture, string abbreviation, {_unitEnumName} expectedUnit)
546+
{{
547+
using var _ = new CultureScope(culture);
548+
Assert.True({_quantity.Name}.TryParseUnit(abbreviation, out {_unitEnumName} parsedUnit));
549+
Assert.Equal(expectedUnit, parsedUnit);
550+
}}
551+
");
552+
553+
Writer.WL($@"
554+
[Theory]");
555+
foreach ((var cultureName, Dictionary<string, Unit> abbreviations) in _uniqueAbbreviationsForCulture)
556+
{
557+
foreach ((var abbreviation, Unit unit) in abbreviations)
558+
{
559+
Writer.WL($@"
560+
[InlineData(""{cultureName}"", ""{abbreviation}"", {GetUnitFullName(unit)})]");
561+
}
562+
}
563+
Writer.WL($@"
564+
public void TryParseUnit_WithCulture(string culture, string abbreviation, {_unitEnumName} expectedUnit)
565+
{{
566+
Assert.True({_quantity.Name}.TryParseUnit(abbreviation, CultureInfo.GetCultureInfo(culture), out {_unitEnumName} parsedUnit));
567+
Assert.Equal(expectedUnit, parsedUnit);
568+
}}
569+
");
570+
571+
// we only generate these for a few of the quantities
572+
if (_ambiguousAbbreviationsForCulture.Count != 0)
573+
{
574+
Writer.WL($@"
575+
[Theory]");
576+
foreach ((var cultureName, Dictionary<string, List<Unit>>? abbreviations) in _ambiguousAbbreviationsForCulture)
577+
{
578+
foreach (KeyValuePair<string, List<Unit>> ambiguousPair in abbreviations)
579+
{
580+
Writer.WL($@"
581+
[InlineData(""{cultureName}"", ""{ambiguousPair.Key}"")] // [{string.Join(", ", ambiguousPair.Value.Select(x => x.SingularName))}] ");
582+
}
583+
}
584+
Writer.WL($@"
585+
public void TryParseUnitWithAmbiguousAbbreviation(string culture, string abbreviation)
586+
{{
587+
Assert.False({_quantity.Name}.TryParseUnit(abbreviation, CultureInfo.GetCultureInfo(culture), out _));
588+
}}
589+
");
590+
} // ambiguousAbbreviations
591+
592+
Writer.WL($@"
365593
[Theory]
366594
[MemberData(nameof(UnitTypes))]
367595
public void ToUnit({_unitEnumName} unit)

0 commit comments

Comments
 (0)