Skip to content

✨ Add Quantity.From/TryFromUnitAbbreviation #1265

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

Merged
merged 4 commits into from
Jun 17, 2023
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
3 changes: 2 additions & 1 deletion CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public string Generate()
using UnitsNet.Units;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

#nullable enable

Expand Down Expand Up @@ -71,7 +72,7 @@ public static IQuantity FromQuantityInfo(QuantityInfo quantityInfo, QuantityValu
/// <param name=""unit"">Unit enum value.</param>
/// <param name=""quantity"">The resulting quantity if successful, otherwise <c>default</c>.</param>
/// <returns><c>True</c> if successful with <paramref name=""quantity""/> assigned the value, otherwise <c>false</c>.</returns>
public static bool TryFrom(QuantityValue value, Enum unit, [NotNullWhen(true)] out IQuantity? quantity)
public static bool TryFrom(QuantityValue value, Enum? unit, [NotNullWhen(true)] out IQuantity? quantity)
{
switch (unit)
{");
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ if (Quantity.TryFrom(value: 3, quantityName: "Length", unitName: "Centimeter", o
}
```

Or create by just the unit abbreviation, as long as there is exactly one unit with this abbreviation.
```c#
// Length with unit LengthUnit.Centimeter
IQuantity quantity = Quantity.FromUnitAbbreviation(3, "cm");

if (Quantity.TryFromUnitAbbreviation(3, "cm", out IQuantity? quantity2))
{
}
```

#### Parse quantity
Parse any string to a quantity instance of the given the quantity type.

Expand Down Expand Up @@ -261,6 +271,25 @@ Console.WriteLine(Convert(HowMuchUnit.Lots)); // 100 lts
Console.WriteLine(Convert(HowMuchUnit.Tons)); // 10 tns
```

#### Parse custom quantity
[QuantityParser](UnitsNet/CustomCode/QuantityParser.cs) parses quantity strings to `IQuantity` by providing a `UnitAbbreviationsCache` with custom units and unit abbreviations.

```c#
// Alternatively, manipulate the global UnitAbbreviationsCache.Default.
var unitAbbreviationsCache = new UnitAbbreviationsCache();
unitAbbreviationsCache.MapUnitToAbbreviation(HowMuchUnit.Some, "sm");
unitAbbreviationsCache.MapUnitToAbbreviation(HowMuchUnit.ATon, "tn");

var quantityParser = new QuantityParser(unitAbbreviationsCache);

// 1 Some
HowMuch q = quantityParser.Parse<HowMuch, HowMuchUnit>(
str: "1 sm",
formatProvider: null,
fromDelegate: (value, unit) => new HowMuch((double) value, unit));
```


### Example: Unit converter app
[Source code](https://github.com/angularsen/UnitsNet/tree/master/Samples/UnitConverter.Wpf) for `Samples/UnitConverter.Wpf`<br/>
[Download](https://github.com/angularsen/UnitsNet/releases/tag/UnitConverterWpf%2F2018-11-09) (release 2018-11-09 for Windows)
Expand Down
75 changes: 75 additions & 0 deletions UnitsNet.Tests/QuantityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace UnitsNet.Tests
{
public partial class QuantityTests
{
private static readonly CultureInfo Russian = CultureInfo.GetCultureInfo("ru-RU");

[Fact]
public void GetHashCodeForDifferentQuantitiesWithSameValuesAreNotEqual()
{
Expand Down Expand Up @@ -143,6 +145,79 @@ public void From_InvalidQuantityNameOrUnitName_ThrowsUnitNotFoundException()
Assert.Throws<UnitNotFoundException>(() => Quantity.From(5, "InvalidQuantity", "Kilogram"));
}

[Fact]
public void FromUnitAbbreviation_ReturnsQuantity()
{
IQuantity q = Quantity.FromUnitAbbreviation(5, "cm");
Assert.Equal(5, q.Value);
Assert.Equal(LengthUnit.Centimeter, q.Unit);
}

[Fact]
public void TryFromUnitAbbreviation_ReturnsQuantity()
{
Assert.True(Quantity.TryFromUnitAbbreviation(5, "cm", out IQuantity? q));
Assert.Equal(LengthUnit.Centimeter, q!.Unit);
}

[Fact]
public void FromUnitAbbreviation_MatchingCulture_ReturnsQuantity()
{
IQuantity q = Quantity.FromUnitAbbreviation(Russian, 5, "см");
Assert.Equal(5, q.Value);
Assert.Equal(LengthUnit.Centimeter, q.Unit);
}

[Fact]
public void TryFromUnitAbbreviation_MatchingCulture_ReturnsQuantity()
{
Assert.False(Quantity.TryFromUnitAbbreviation(Russian, 5, "cm", out IQuantity? q));
}

[Fact]
public void FromUnitAbbreviation_MismatchingCulture_ThrowsUnitNotFoundException()
{
Assert.Throws<UnitNotFoundException>(() => Quantity.FromUnitAbbreviation(Russian, 5, "cm")); // Expected "см"
}

[Fact]
public void TryFromUnitAbbreviation_MismatchingCulture_ThrowsUnitNotFoundException()
{
Assert.Throws<UnitNotFoundException>(() => Quantity.FromUnitAbbreviation(Russian, 5, "cm")); // Expected "см"
}

[Fact]
public void FromUnitAbbreviation_InvalidAbbreviation_ThrowsUnitNotFoundException()
{
Assert.Throws<UnitNotFoundException>(() => Quantity.FromUnitAbbreviation(5, "nonexisting-unit"));
}

[Fact]
public void TryFromUnitAbbreviation_InvalidAbbreviation_ThrowsUnitNotFoundException()
{
Assert.False(Quantity.TryFromUnitAbbreviation(5, "nonexisting-unit", out IQuantity? q));
Assert.Null(q);
}

[Fact]
public void FromUnitAbbreviation_AmbiguousAbbreviation_ThrowsAmbiguousUnitParseException()
{
// MassFraction.Percent
// Ratio.Percent
// VolumeConcentration.Percent
Assert.Throws<AmbiguousUnitParseException>(() => Quantity.FromUnitAbbreviation(5, "%"));
}

[Fact]
public void TryFromUnitAbbreviation_AmbiguousAbbreviation_ReturnsFalse()
{
// MassFraction.Percent
// Ratio.Percent
// VolumeConcentration.Percent
Assert.False(Quantity.TryFromUnitAbbreviation(5, "%", out IQuantity? q));
Assert.Null(q);
}

private static Length ParseLength(string str)
{
return Length.Parse(str, CultureInfo.InvariantCulture);
Expand Down
2 changes: 1 addition & 1 deletion UnitsNet.Tests/QuantityTypeConverterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public void ConvertFrom_GivenWrongQuantity_ThrowsArgumentException()
var converter = new QuantityTypeConverter<Length>();
ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { });

Assert.Throws<ArgumentException>(() => converter.ConvertFrom(context, Culture, "1m^2"));
Assert.Throws<UnitNotFoundException>(() => converter.ConvertFrom(context, Culture, "1m^2"));
}

[Theory]
Expand Down
129 changes: 124 additions & 5 deletions UnitsNet/CustomCode/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using UnitsNet.Units;

namespace UnitsNet
{
Expand Down Expand Up @@ -53,17 +54,17 @@ public static bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInf
UnitTypeAndNameToUnitInfoLazy.Value.TryGetValue((unitEnum.GetType(), unitEnum.ToString()), out unitInfo);

/// <summary>
/// Dynamically construct a quantity.
/// Dynamically constructs a quantity from a numeric value and a unit enum value.
/// </summary>
/// <param name="value">Numeric value.</param>
/// <param name="unit">Unit enum value.</param>
/// <returns>An <see cref="IQuantity"/> object.</returns>
/// <exception cref="ArgumentException">Unit value is not a know unit enum type.</exception>
/// <exception cref="UnitNotFoundException">Unit value is not a known unit enum type.</exception>
public static IQuantity From(QuantityValue value, Enum unit)
{
return TryFrom(value, unit, out IQuantity? quantity)
? quantity
: throw new ArgumentException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a third-party enum type defined outside UnitsNet library?");
: throw new UnitNotFoundException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a custom enum type defined outside the UnitsNet library?");
}

/// <summary>
Expand All @@ -73,7 +74,7 @@ public static IQuantity From(QuantityValue value, Enum unit)
/// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
/// <param name="unitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
/// <returns>An <see cref="IQuantity"/> object.</returns>
/// <exception cref="ArgumentException">Unit value is not a know unit enum type.</exception>
/// <exception cref="ArgumentException">Unit value is not a known unit enum type.</exception>
public static IQuantity From(QuantityValue value, string quantityName, string unitName)
{
// Get enum value for this unit, f.ex. LengthUnit.Meter for unit name "Meter".
Expand All @@ -82,6 +83,57 @@ public static IQuantity From(QuantityValue value, string quantityName, string un
: throw new UnitNotFoundException($"Unit [{unitName}] not found for quantity [{quantityName}].");
}

/// <summary>
/// Dynamically construct a quantity from a numeric value and a unit abbreviation using <see cref="CultureInfo.CurrentCulture"/>.
/// </summary>
/// <remarks>
/// This method is currently not optimized for performance and will enumerate all units and their unit abbreviations each time.<br/>
/// Unit abbreviation matching is case-insensitive.<br/>
/// <br/>
/// This will fail if more than one unit across all quantities share the same unit abbreviation.<br/>
/// Prefer <see cref="From(UnitsNet.QuantityValue,System.Enum)"/> or <see cref="From(UnitsNet.QuantityValue,string,string)"/> instead.
/// </remarks>
/// <param name="value">Numeric value.</param>
/// <param name="unitAbbreviation">Unit abbreviation, such as "kg" for <see cref="MassUnit.Kilogram"/>.</param>
/// <returns>An <see cref="IQuantity"/> object.</returns>
/// <exception cref="UnitNotFoundException">Unit abbreviation is not known.</exception>
/// <exception cref="AmbiguousUnitParseException">Multiple units found matching the given unit abbreviation.</exception>
public static IQuantity FromUnitAbbreviation(QuantityValue value, string unitAbbreviation) => FromUnitAbbreviation(null, value, unitAbbreviation);

/// <summary>
/// Dynamically construct a quantity from a numeric value and a unit abbreviation.
/// </summary>
/// <remarks>
/// This method is currently not optimized for performance and will enumerate all units and their unit abbreviations each time.<br/>
/// Unit abbreviation matching is case-insensitive.<br/>
/// <br/>
/// This will fail if more than one unit across all quantities share the same unit abbreviation.<br/>
/// Prefer <see cref="From(UnitsNet.QuantityValue,System.Enum)"/> or <see cref="From(UnitsNet.QuantityValue,string,string)"/> instead.
/// </remarks>
/// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
/// <param name="value">Numeric value.</param>
/// <param name="unitAbbreviation">Unit abbreviation, such as "kg" for <see cref="MassUnit.Kilogram"/>.</param>
/// <returns>An <see cref="IQuantity"/> object.</returns>
/// <exception cref="UnitNotFoundException">Unit abbreviation is not known.</exception>
/// <exception cref="AmbiguousUnitParseException">Multiple units found matching the given unit abbreviation.</exception>
public static IQuantity FromUnitAbbreviation(IFormatProvider? formatProvider, QuantityValue value, string unitAbbreviation)
{
// TODO Optimize this with UnitValueAbbreviationLookup via UnitAbbreviationsCache.TryGetUnitValueAbbreviationLookup.
List<Enum> units = GetUnitsForAbbreviation(formatProvider, unitAbbreviation);
if (units.Count > 1)
{
throw new AmbiguousUnitParseException($"Multiple units found matching the given unit abbreviation: {unitAbbreviation}");
}

if (units.Count == 0)
{
throw new UnitNotFoundException($"Unit abbreviation {unitAbbreviation} is not known. Did you pass in a custom unit abbreviation defined outside the UnitsNet library? This is currently not supported.");
}

Enum unit = units.Single();
return From(value, unit);
}

/// <inheritdoc cref="TryFrom(QuantityValue,System.Enum,out UnitsNet.IQuantity)"/>
public static bool TryFrom(double value, Enum unit, [NotNullWhen(true)] out IQuantity? quantity)
{
Expand Down Expand Up @@ -110,6 +162,54 @@ public static bool TryFrom(double value, string quantityName, string unitName, [
TryFrom(value, unitValue, out quantity);
}

/// <summary>
/// Dynamically construct a quantity from a numeric value and a unit abbreviation using <see cref="CultureInfo.CurrentCulture"/>.
/// </summary>
/// <remarks>
/// This method is currently not optimized for performance and will enumerate all units and their unit abbreviations each time.<br/>
/// Unit abbreviation matching is case-insensitive.<br/>
/// <br/>
/// This will fail if more than one unit across all quantities share the same unit abbreviation.<br/>
/// Prefer <see cref="From(UnitsNet.QuantityValue,System.Enum)"/> or <see cref="From(UnitsNet.QuantityValue,string,string)"/> instead.
/// </remarks>
/// <param name="value">Numeric value.</param>
/// <param name="unitAbbreviation">Unit abbreviation, such as "kg" for <see cref="MassUnit.Kilogram"/>.</param>
/// <param name="quantity">The quantity if successful, otherwise null.</param>
/// <returns>True if successful.</returns>
/// <exception cref="ArgumentException">Unit value is not a known unit enum type.</exception>
public static bool TryFromUnitAbbreviation(QuantityValue value, string unitAbbreviation, [NotNullWhen(true)] out IQuantity? quantity) =>
TryFromUnitAbbreviation(null, value, unitAbbreviation, out quantity);

/// <summary>
/// Dynamically construct a quantity from a numeric value and a unit abbreviation.
/// </summary>
/// <remarks>
/// This method is currently not optimized for performance and will enumerate all units and their unit abbreviations each time.<br/>
/// Unit abbreviation matching is case-insensitive.<br/>
/// <br/>
/// This will fail if more than one unit across all quantities share the same unit abbreviation.<br/>
/// Prefer <see cref="From(UnitsNet.QuantityValue,System.Enum)"/> or <see cref="From(UnitsNet.QuantityValue,string,string)"/> instead.
/// </remarks>
/// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
/// <param name="value">Numeric value.</param>
/// <param name="unitAbbreviation">Unit abbreviation, such as "kg" for <see cref="MassUnit.Kilogram"/>.</param>
/// <param name="quantity">The quantity if successful, otherwise null.</param>
/// <returns>True if successful.</returns>
/// <exception cref="ArgumentException">Unit value is not a known unit enum type.</exception>
public static bool TryFromUnitAbbreviation(IFormatProvider? formatProvider, QuantityValue value, string unitAbbreviation, [NotNullWhen(true)] out IQuantity? quantity)
{
// TODO Optimize this with UnitValueAbbreviationLookup via UnitAbbreviationsCache.TryGetUnitValueAbbreviationLookup.
List<Enum> units = GetUnitsForAbbreviation(formatProvider, unitAbbreviation);
if (units.Count == 1)
{
Enum? unit = units.SingleOrDefault();
return TryFrom(value, unit, out quantity);
}

quantity = default;
return false;
}

/// <inheritdoc cref="Parse(IFormatProvider, System.Type,string)"/>
public static IQuantity Parse(Type quantityType, string quantityString) => Parse(null, quantityType, quantityString);

Expand All @@ -121,6 +221,7 @@ public static bool TryFrom(double value, string quantityName, string unitName, [
/// <param name="quantityString">Quantity string representation, such as "1.5 kg". Must be compatible with given quantity type.</param>
/// <returns>The parsed quantity.</returns>
/// <exception cref="ArgumentException">Type must be of type UnitsNet.IQuantity -or- Type is not a known quantity type.</exception>
/// <exception cref="UnitNotFoundException">Type must be of type UnitsNet.IQuantity -or- Type is not a known quantity type.</exception>
public static IQuantity Parse(IFormatProvider? formatProvider, Type quantityType, string quantityString)
{
if (!typeof(IQuantity).IsAssignableFrom(quantityType))
Expand All @@ -129,7 +230,7 @@ public static IQuantity Parse(IFormatProvider? formatProvider, Type quantityType
if (TryParse(formatProvider, quantityType, quantityString, out IQuantity? quantity))
return quantity;

throw new ArgumentException($"Quantity string could not be parsed to quantity {quantityType}.");
throw new UnitNotFoundException($"Quantity string could not be parsed to quantity {quantityType}.");
}

/// <inheritdoc cref="TryParse(IFormatProvider,System.Type,string,out UnitsNet.IQuantity)"/>
Expand All @@ -144,5 +245,23 @@ public static IEnumerable<QuantityInfo> GetQuantitiesWithBaseDimensions(BaseDime
{
return InfosLazy.Value.Where(info => info.BaseDimensions.Equals(baseDimensions));
}

private static List<Enum> GetUnitsForAbbreviation(IFormatProvider? formatProvider, string unitAbbreviation)
{
// Use case-sensitive match to reduce ambiguity.
// Don't use UnitParser.TryParse() here, since it allows case-insensitive match per quantity as long as there are no ambiguous abbreviations for
// units of that quantity, but here we try all quantities and this results in too high of a chance for ambiguous matches,
// such as "cm" matching both LengthUnit.Centimeter (cm) and MolarityUnit.CentimolePerLiter (cM).
return Infos
.SelectMany(i => i.UnitInfos)
.Select(ui => UnitAbbreviationsCache.Default
.GetUnitAbbreviations(ui.Value.GetType(), Convert.ToInt32(ui.Value), formatProvider)
.Contains(unitAbbreviation, StringComparer.Ordinal)
? ui.Value
: null)
.Where(unitValue => unitValue != null)
.Select(unitValue => unitValue!)
.ToList();
}
}
}
3 changes: 2 additions & 1 deletion UnitsNet/GeneratedCode/Quantity.g.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.