Skip to content

Commit f814884

Browse files
authored
🚸Describe why Parse fails due to case-insensitive ambiguity (#1484)
Fixes #1423 `UnitParser.Parse<LengthUnit>("MM")` fails due to matching both `Megameter` and `Millimeter` in case-insensitive matching, but matches neither of them in the case-sensitive fallback. It was confusing to get `UnitsNotFoundException` in this case, since case-insensitive usually works for most units. ### Changes - Handle this case and throw `AmbiguousUnitParseException` instead of `UnitNotFoundException` - Describe the case-insensitive units that matched - Fix existing test `Parse_WithMultipleCaseInsensitiveMatchesButNoExactMatches_ThrowsUnitNotFoundException` - Skip retrying with fallback culture if no specific `formatProvider` was given
1 parent bb1420e commit f814884

File tree

4 files changed

+28
-7
lines changed

4 files changed

+28
-7
lines changed

UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ protected string GetQuantityType(IQuantity quantity)
190190
}
191191

192192
/// <summary>
193-
/// Attempt to find an a unique (non-ambiguous) unit matching the provided abbreviation.
193+
/// Attempt to find a unique (non-ambiguous) unit matching the provided abbreviation.
194194
/// <remarks>
195195
/// An exhaustive search using all quantities is very likely to fail with an
196196
/// <exception cref="AmbiguousUnitParseException" />, so make sure you're using the minimum set of supported quantities.

UnitsNet.Tests/QuantityParserTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed under MIT No Attribution, see LICENSE file at the root.
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
22
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
33

44
using UnitsNet.Tests.CustomQuantities;
@@ -40,7 +40,7 @@ public void Parse_WithOneCaseInsensitiveMatchAndOneExactMatch_ParsesWithTheExact
4040
}
4141

4242
[Fact]
43-
public void Parse_WithMultipleCaseInsensitiveMatchesButNoExactMatches_ThrowsUnitNotFoundException()
43+
public void Parse_WithMultipleCaseInsensitiveMatchesButNoExactMatches_ThrowsAmbiguousUnitParseException()
4444
{
4545
var unitAbbreviationsCache = new UnitAbbreviationsCache();
4646
unitAbbreviationsCache.MapUnitToAbbreviation(HowMuchUnit.Some, "foo");
@@ -52,7 +52,8 @@ void Act()
5252
quantityParser.Parse<HowMuch, HowMuchUnit>("1 Foo", null, (value, unit) => new HowMuch((double) value, unit));
5353
}
5454

55-
Assert.Throws<UnitNotFoundException>(Act);
55+
var ex = Assert.Throws<AmbiguousUnitParseException>(Act);
56+
Assert.Equal("Cannot parse \"Foo\" since it matched multiple units [Some, ATon] with case-insensitive comparison, but zero units with case-sensitive comparison. To resolve the ambiguity, pass a unit abbreviation with the correct casing.", ex.Message);
5657
}
5758

5859
[Fact]

UnitsNet.Tests/UnitParserTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,13 @@ public void Parse_MappedCustomUnit()
136136

137137
Assert.Equal(HowMuchUnit.Some, parsedUnit);
138138
}
139+
140+
[Fact]
141+
public void Parse_LengthUnit_MM_ThrowsExceptionDescribingTheAmbiguity()
142+
{
143+
var ex = Assert.Throws<AmbiguousUnitParseException>(() => UnitsNetSetup.Default.UnitParser.Parse<LengthUnit>("MM"));
144+
Assert.Contains("Cannot parse \"MM\" since it matched multiple units [Millimeter, Megameter] with case-insensitive comparison, but zero units with case-sensitive comparison. To resolve the ambiguity, pass a unit abbreviation with the correct casing.", ex.Message);
145+
}
146+
139147
}
140148
}

UnitsNet/CustomCode/UnitParser.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,19 @@ public Enum Parse(string? unitAbbreviation, Type unitType, IFormatProvider? form
7272
var stringUnitPairs = _unitAbbreviationsCache.GetStringUnitPairs(enumValues, formatProvider);
7373
var matches = stringUnitPairs.Where(pair => pair.Item1.Equals(unitAbbreviation, StringComparison.OrdinalIgnoreCase)).ToArray();
7474

75+
// No match? Retry after normalizing the unit abbreviation.
7576
if(matches.Length == 0)
7677
{
7778
unitAbbreviation = NormalizeUnitString(unitAbbreviation);
7879
matches = stringUnitPairs.Where(pair => pair.Item1.Equals(unitAbbreviation, StringComparison.OrdinalIgnoreCase)).ToArray();
7980
}
8081

81-
// Narrow the search if too many hits, for example Megabar "Mbar" and Millibar "mbar" need to be distinguished
82-
if(matches.Length > 1)
82+
var caseInsensitiveMatches = matches;
83+
84+
// More than one case-insensitive match? Retry with case-sensitive match.
85+
// For example, Megabar "Mbar" and Millibar "mbar" need to be distinguished.
86+
bool hasMultipleCaseInsensitiveMatches = matches.Length > 1;
87+
if (hasMultipleCaseInsensitiveMatches)
8388
matches = stringUnitPairs.Where(pair => pair.Item1.Equals(unitAbbreviation)).ToArray();
8489

8590
switch(matches.Length)
@@ -88,11 +93,18 @@ public Enum Parse(string? unitAbbreviation, Type unitType, IFormatProvider? form
8893
return (Enum)Enum.ToObject(unitType, matches[0].Unit);
8994
case 0:
9095
// Retry with fallback culture, if different.
91-
if(!Equals(formatProvider, UnitAbbreviationsCache.FallbackCulture))
96+
if (formatProvider != null && !Equals(formatProvider, UnitAbbreviationsCache.FallbackCulture))
9297
{
9398
return Parse(unitAbbreviation, unitType, UnitAbbreviationsCache.FallbackCulture);
9499
}
95100

101+
if (hasMultipleCaseInsensitiveMatches)
102+
{
103+
string ciUnitsCsv = string.Join(", ", caseInsensitiveMatches.Select(x => Enum.GetName(unitType, x.Unit)));
104+
throw new AmbiguousUnitParseException(
105+
$"Cannot parse \"{unitAbbreviation}\" since it matched multiple units [{ciUnitsCsv}] with case-insensitive comparison, but zero units with case-sensitive comparison. To resolve the ambiguity, pass a unit abbreviation with the correct casing.");
106+
}
107+
96108
throw new UnitNotFoundException($"Unit not found with abbreviation [{unitAbbreviation}] for unit type [{unitType}].");
97109
default:
98110
string unitsCsv = string.Join(", ", matches.Select(x => Enum.GetName(unitType, x.Unit)).ToArray());

0 commit comments

Comments
 (0)