// Licensed under MIT No Attribution, see LICENSE file at the root.
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
using System.Globalization;
using System.Linq;
using System.Text;
// ReSharper disable once CheckNamespace
namespace UnitsNet;
///
/// Parses units given a unit abbreviations cache.
/// A static instance is created in the , which is used internally to parse
/// quantities and units using the
/// default abbreviations cache for all units and abbreviations defined in the library.
///
public sealed class UnitParser
{
///
/// Initializes a new instance of the class using the specified collection of quantity
/// information.
///
///
/// A read-only collection of quantity information used to initialize the unit abbreviations cache.
///
public UnitParser(IEnumerable quantities)
: this(new UnitAbbreviationsCache(quantities))
{
}
internal UnitParser(QuantityInfoLookup quantitiesLookup)
: this(new UnitAbbreviationsCache(quantitiesLookup))
{
}
///
/// Initializes a new instance of the class using the specified unit abbreviations cache.
///
///
/// The cache containing unit abbreviations.
///
///
/// If is null.
///
public UnitParser(UnitAbbreviationsCache unitAbbreviationsCache)
{
Abbreviations = unitAbbreviationsCache ?? throw new ArgumentNullException(nameof(unitAbbreviationsCache));
}
///
/// Gets the instance used by this .
/// This cache contains mappings of unit abbreviations to their corresponding units, enabling efficient
/// parsing and retrieval of unit information.
///
public UnitAbbreviationsCache Abbreviations { get; }
///
/// Gets the collection of quantities available in this instance.
///
///
/// This property provides access to the that contains
/// information about all quantities and their associated units.
///
public QuantityInfoLookup Quantities
{
get => Abbreviations.Quantities;
}
///
/// The default static instance used internally to parse quantities and units using the
/// default abbreviations cache for all units and abbreviations defined in the library.
///
public static UnitParser Default => UnitsNetSetup.Default.UnitParser;
///
/// Creates a default instance of the class with all the built-in unit abbreviations defined
/// in the library.
///
/// A instance with the default abbreviations cache.
public static UnitParser CreateDefault()
{
return new UnitParser(UnitAbbreviationsCache.CreateDefault());
}
///
/// Parses a unit abbreviation for a given unit enumeration type.
/// Example: Parse<LengthUnit>("km") => LengthUnit.Kilometer
///
///
/// The format provider to use for lookup. Defaults to if null.
///
/// Unit enum value, such as .
/// No quantity found matching the unit type.
/// No units match the abbreviation.
public TUnitType Parse(string unitAbbreviation, IFormatProvider? formatProvider = null)
where TUnitType : struct, Enum
{
if (unitAbbreviation == null) throw new ArgumentNullException(nameof(unitAbbreviation));
QuantityInfo quantityInfo = Quantities.GetQuantityByUnitType(typeof(TUnitType));
return Parse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider).UnitKey.ToUnit();
}
///
/// Parse a unit abbreviation, such as "kg" or "m", to the unit enum value of the enum type
/// .
///
///
/// Unit abbreviation, such as "kg" or "m" for and
/// respectively.
///
/// Unit enum type, such as and .
/// The format provider to use for lookup. Defaults to if null.
/// Unit enum value, such as .
/// The or the is null.
/// The is not a valid enumeration type.
/// No quantity found matching the unit type.
/// No units match the abbreviation.
/// More than one unit matches the abbreviation.
public Enum Parse(string unitAbbreviation, Type unitType, IFormatProvider? formatProvider = null)
{
if (unitAbbreviation == null) throw new ArgumentNullException(nameof(unitAbbreviation));
QuantityInfo quantityInfo = Quantities.GetQuantityByUnitType(unitType);
return Parse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider).Value;
}
///
/// Retrieves the corresponding to the specified unit abbreviation within a given quantity.
///
/// The name of the quantity to which the unit belongs.
/// The abbreviation of the unit to retrieve.
/// The format provider to use for lookup. Defaults to if null.
///
/// The that matches the specified unit abbreviation within the given quantity.
///
/// The or the is null.
///
/// Thrown if the specified does not correspond to a known quantity.
///
///
/// Thrown if the cannot be resolved to a valid unit for the specified quantity.
///
///
/// When a specific is provided, both localized and non-localized units would be compared.
/// Both the and the comparisons are case-insensitive.
///
public UnitInfo GetUnitFromAbbreviation(string quantityName, string unitAbbreviation, IFormatProvider? formatProvider)
{
if (quantityName == null) throw new ArgumentNullException(nameof(quantityName));
if (unitAbbreviation == null) throw new ArgumentNullException(nameof(unitAbbreviation));
QuantityInfo quantityInfo = Quantities.GetQuantityByName(quantityName);
return Parse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider);
}
///
/// Retrieves the corresponding to the specified unit abbreviation within a given quantity.
///
/// The type of the quantity to which the unit belongs.
/// The abbreviation of the unit to retrieve.
/// The format provider to use for lookup. Defaults to if null.
///
/// The that matches the specified unit abbreviation within the given quantity.
///
/// The or the is null.
///
/// Thrown if the specified does not correspond to a known quantity.
///
/// No quantity found matching the unit type.
/// No units match the abbreviation.
///
/// When a specific is provided, both localized and non-localized units would be compared.
/// The comparisons are case-insensitive.
///
public UnitInfo GetUnitFromAbbreviation(Type quantityType, string unitAbbreviation, IFormatProvider? formatProvider)
{
if (quantityType == null) throw new ArgumentNullException(nameof(quantityType));
if (unitAbbreviation == null) throw new ArgumentNullException(nameof(unitAbbreviation));
QuantityInfo quantityInfo = Quantities.GetQuantityInfo(quantityType);
return Parse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider);
}
///
/// Parses the specified unit abbreviation, such as "kg" or "m", to find the corresponding unit information.
///
/// The type of the unit information.
/// The abbreviation of the unit to parse.
/// A collection of unit information to search through.
/// The format provider to use for lookup. Defaults to if null.
/// The unit information that matches the specified abbreviation.
/// Thrown when is null.
/// Thrown when no matching unit is found.
/// Thrown when multiple matching units are found.
internal TUnitInfo Parse(string unitAbbreviation, IReadOnlyList units, IFormatProvider? formatProvider = null)
where TUnitInfo : UnitInfo
{
if (unitAbbreviation == null) throw new ArgumentNullException(nameof(unitAbbreviation));
if (formatProvider is not CultureInfo culture)
{
culture = CultureInfo.CurrentCulture;
}
unitAbbreviation = unitAbbreviation.Trim();
while (true)
{
List<(TUnitInfo UnitInfo, string Abbreviation)> matches = FindMatchingUnits(unitAbbreviation, units, culture);
switch (matches.Count)
{
case 1:
return matches[0].UnitInfo;
case 0:
// Retry with fallback culture, if different.
if (UnitAbbreviationsCache.HasFallbackCulture(culture))
{
culture = UnitAbbreviationsCache.FallbackCulture;
continue;
}
throw new UnitNotFoundException($"Unit not found with abbreviation [{unitAbbreviation}] for unit type [{typeof(TUnitInfo)}].");
default:
var unitsCsv = string.Join(", ", matches.Select(x => $"{x.UnitInfo.Name} (\"{x.Abbreviation}\")").OrderBy(x => x));
throw new AmbiguousUnitParseException($"Cannot parse \"{unitAbbreviation}\" since it matches multiple units: {unitsCsv}.");
}
}
}
internal static string NormalizeUnitString(string unitAbbreviation)
{
var abbreviationLength = unitAbbreviation.Length;
if (abbreviationLength == 0)
{
return unitAbbreviation;
}
// Remove all whitespace using StringBuilder
var sb = new StringBuilder(abbreviationLength);
for (var i = 0; i < unitAbbreviation.Length; i++)
{
var character = unitAbbreviation[i];
if (!char.IsWhiteSpace(character))
{
sb.Append(character);
}
}
// Perform replacements using StringBuilder
sb.Replace("^-9", "⁻⁹")
.Replace("^-8", "⁻⁸")
.Replace("^-7", "⁻⁷")
.Replace("^-6", "⁻⁶")
.Replace("^-5", "⁻⁵")
.Replace("^-4", "⁻⁴")
.Replace("^-3", "⁻³")
.Replace("^-2", "⁻²")
.Replace("^-1", "⁻¹")
.Replace("^1", "")
.Replace("^2", "²")
.Replace("^3", "³")
.Replace("^4", "⁴")
.Replace("^5", "⁵")
.Replace("^6", "⁶")
.Replace("^7", "⁷")
.Replace("^8", "⁸")
.Replace("^9", "⁹")
.Replace("*", "·")
.Replace("\u03bc", "\u00b5"); // Greek letter 'Mu' to Micro sign
return sb.ToString();
}
///
/// Try to parse a unit abbreviation.
///
/// The string value.
/// The unit enum value as out result.
/// Type of unit enum.
/// True if successful.
public bool TryParse([NotNullWhen(true)] string? unitAbbreviation, out TUnitType unit)
where TUnitType : struct, Enum
{
return TryParse(unitAbbreviation, null, out unit);
}
///
/// Try to parse a unit abbreviation.
///
/// The string value.
/// The format provider to use for lookup. Defaults to if null.
/// The unit enum value as out result.
/// Type of unit enum.
/// True if successful.
public bool TryParse([NotNullWhen(true)] string? unitAbbreviation, IFormatProvider? formatProvider, out TUnitType unit)
where TUnitType : struct, Enum
{
if (!TryParse(unitAbbreviation, typeof(TUnitType), formatProvider, out Enum? unitObj))
{
unit = default;
return false;
}
unit = (TUnitType)unitObj;
return true;
}
///
/// Try to parse a unit abbreviation.
///
/// The string value.
/// Type of unit enum.
/// The unit enum value as out result.
/// True if successful.
public bool TryParse([NotNullWhen(true)]string? unitAbbreviation, Type unitType, [NotNullWhen(true)] out Enum? unit)
{
return TryParse(unitAbbreviation, unitType, null, out unit);
}
///
/// Try to parse a unit abbreviation.
///
/// The string value.
/// Type of unit enum.
/// The format provider to use for lookup. Defaults to if null.
/// The unit enum value as out result.
/// True if successful.
public bool TryParse([NotNullWhen(true)] string? unitAbbreviation, Type unitType, IFormatProvider? formatProvider, [NotNullWhen(true)] out Enum? unit)
{
if (unitAbbreviation == null)
{
unit = null;
return false;
}
if (Quantities.TryGetQuantityByUnitType(unitType, out QuantityInfo? quantityInfo) &&
TryParse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider, out UnitInfo? unitInfo))
{
unit = unitInfo.Value;
return true;
}
unit = null;
return false;
}
///
/// Attempts to parse the specified unit abbreviation, such as "kg" or "m", into a unit of the specified type.
///
/// The type of the unit enumeration.
/// The unit abbreviation to parse.
/// The quantity information that provides context for the unit.
/// The format provider to use for lookup. Defaults to if null.
///
/// When this method returns, contains the parsed unit if the parsing succeeded, or null if the parsing failed.
///
///
/// true if the unit abbreviation was successfully parsed; otherwise, false.
///
internal bool TryParse([NotNullWhen(true)] string? unitAbbreviation, QuantityInfo quantityInfo, IFormatProvider? formatProvider,
out TUnit unit)
where TUnit : struct, Enum
{
if (TryParse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider, out UnitInfo? unitInfo))
{
unit = unitInfo.Value;
return true;
}
unit = default;
return false;
}
///
/// Attempts to retrieve the corresponding to the specified unit abbreviation within a given
/// quantity.
///
/// The name of the quantity to which the unit belongs.
///
/// The abbreviation of the unit to retrieve. Can be null, in which case the method will return false.
///
/// The format provider to use for lookup. Defaults to if null.
///
/// When this method returns, contains the that matches the specified unit abbreviation
/// within the given quantity, if the operation succeeds; otherwise, null.
///
///
/// true if the unit abbreviation was successfully resolved to a ; otherwise,
/// false.
///
///
/// This method does not throw exceptions for invalid input or unresolved unit abbreviations. Instead, it returns
/// false.
///
public bool TryGetUnitFromAbbreviation(string quantityName, string? unitAbbreviation, IFormatProvider? formatProvider, [NotNullWhen(true)] out UnitInfo? unitInfo)
{
if (unitAbbreviation != null && Quantities.TryGetQuantityByName(quantityName, out QuantityInfo? quantityInfo))
{
return TryParse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider, out unitInfo);
}
unitInfo = null;
return false;
}
///
/// Attempts to retrieve the corresponding to the specified unit abbreviation within a given
/// quantity.
///
/// The type of the quantity to which the unit belongs.
///
/// The abbreviation of the unit to retrieve. Can be null, in which case the method will return false.
///
/// The format provider to use for lookup. Defaults to if null.
///
/// When this method returns, contains the that matches the specified unit abbreviation
/// within the given quantity, if the operation succeeds; otherwise, null.
///
///
/// true if the unit abbreviation was successfully resolved to a ; otherwise,
/// false.
///
///
/// This method does not throw exceptions for invalid input or unresolved unit abbreviations. Instead, it returns
/// false.
///
public bool TryGetUnitFromAbbreviation(Type quantityType, string? unitAbbreviation, IFormatProvider? formatProvider, [NotNullWhen(true)] out UnitInfo? unitInfo)
{
if (unitAbbreviation != null && Quantities.TryGetQuantityInfo(quantityType, out QuantityInfo? quantityInfo))
{
return TryParse(unitAbbreviation, quantityInfo.UnitInfos, formatProvider, out unitInfo);
}
unitInfo = null;
return false;
}
///
/// Attempts to match the provided unit abbreviation against the defined abbreviations for the units and returns the
/// matching unit information.
///
/// The type of the unit information.
/// The unit abbreviation to match.
/// The collection of units to match against.
/// The format provider to use for lookup. Defaults to if null.
///
/// When this method returns, contains the matching unit information if the match was successful; otherwise, the
/// default value for the type of the unit parameter.
///
///
/// true if the unit abbreviation was successfully matched; otherwise, false.
///
internal bool TryParse([NotNullWhen(true)] string? unitAbbreviation, IReadOnlyList units, IFormatProvider? formatProvider,
[NotNullWhen(true)] out TUnitInfo? unit)
where TUnitInfo : UnitInfo
{
unit = null;
if (unitAbbreviation == null)
{
return false;
}
unitAbbreviation = unitAbbreviation.Trim();
if (formatProvider is not CultureInfo culture)
{
culture = CultureInfo.CurrentCulture;
}
List<(TUnitInfo UnitInfo, string Abbreviation)> matches = FindMatchingUnits(unitAbbreviation, units, culture);
if (matches.Count == 1)
{
unit = matches[0].UnitInfo;
return true;
}
if (matches.Count != 0 || !UnitAbbreviationsCache.HasFallbackCulture(culture))
{
return false; // either there are duplicates or nothing was matched and we're already using the fallback culture
}
// retry the lookup using the fallback culture
matches = FindMatchingUnits(unitAbbreviation, units, UnitAbbreviationsCache.FallbackCulture);
if (matches.Count != 1)
{
return false;
}
unit = matches[0].UnitInfo;
return true;
}
private List<(TUnitInfo UnitInfo, string Abbreviation)> FindMatchingUnits(string unitAbbreviation, IReadOnlyList units,
CultureInfo culture)
where TUnitInfo : UnitInfo
{
List<(TUnitInfo UnitInfo, string Abbreviation)> caseInsensitiveMatches = FindMatchingUnitsForCulture(units, unitAbbreviation, culture, StringComparison.OrdinalIgnoreCase);
if (caseInsensitiveMatches.Count == 0)
{
var normalizeUnitString = NormalizeUnitString(unitAbbreviation);
if (unitAbbreviation == normalizeUnitString)
{
return caseInsensitiveMatches;
}
unitAbbreviation = normalizeUnitString;
caseInsensitiveMatches = FindMatchingUnitsForCulture(units, unitAbbreviation, culture, StringComparison.OrdinalIgnoreCase);
if (caseInsensitiveMatches.Count == 0)
{
return caseInsensitiveMatches;
}
}
var nbAbbreviationsFound = caseInsensitiveMatches.Count;
if (nbAbbreviationsFound == 1)
{
return caseInsensitiveMatches;
}
// Narrow the search if too many hits, for example Megabar "Mbar" and Millibar "mbar" need to be distinguished
var caseSensitiveMatches = new List<(TUnitInfo UnitInfo, string Abbreviation)>(nbAbbreviationsFound);
for (var i = 0; i < nbAbbreviationsFound; i++)
{
(TUnitInfo UnitInfo, string Abbreviation) match = caseInsensitiveMatches[i];
if (unitAbbreviation == match.Abbreviation)
{
caseSensitiveMatches.Add(match);
}
}
return caseSensitiveMatches.Count == 0 ? caseInsensitiveMatches : caseSensitiveMatches;
}
private List<(TUnitInfo UnitInfo, string Abbreviation)> FindMatchingUnitsForCulture(IReadOnlyList unitInfos, string unitAbbreviation,
CultureInfo culture, StringComparison comparison)
where TUnitInfo: UnitInfo
{
var unitAbbreviationsPairs = new List<(TUnitInfo, string)>();
var nbUnits = unitInfos.Count;
for (var i = 0; i < nbUnits; i++)
{
TUnitInfo unitInfo = unitInfos[i];
IReadOnlyList abbreviations = Abbreviations.GetAbbreviationsForCulture(unitInfo, culture);
var nbAbbreviations = abbreviations.Count;
for (var p = 0; p < nbAbbreviations; p++)
{
var abbreviation = abbreviations[p];
if (unitAbbreviation.Equals(abbreviation, comparison))
{
unitAbbreviationsPairs.Add((unitInfo, abbreviation));
}
}
}
return unitAbbreviationsPairs;
}
///
/// Retrieves the unit information corresponding to the specified unit abbreviation.
///
/// The unit abbreviation to parse. Cannot be null or empty.
/// The format provider to use for lookup. Defaults to if null.
/// The instance representing the parsed unit.
///
/// Thrown when is null.
///
///
/// Thrown when the unit abbreviation is not recognized as a valid unit for the specified culture.
///
///
/// Thrown when multiple units match the given unit abbreviation, making the result ambiguous.
///
///
/// This method performs a series of searches to find the unit:
///
/// - Case-sensitive search using the current culture.
/// - Case-sensitive search using the fallback culture, if applicable.
/// - Case-insensitive search.
///
/// Note that this method is not optimized for performance, as it enumerates all units and their abbreviations
/// during each invocation.
///
public UnitInfo GetUnitFromAbbreviation(string unitAbbreviation, IFormatProvider? formatProvider)
{
if (unitAbbreviation == null) throw new ArgumentNullException(nameof(unitAbbreviation));
List<(UnitInfo UnitInfo, string Abbreviation)> matches = FindAllMatchingUnits(unitAbbreviation, formatProvider);
switch (matches.Count)
{
case 1:
return matches[0].UnitInfo;
case 0:
throw new UnitNotFoundException($"Unit not found with abbreviation [{unitAbbreviation}].");
default:
var unitsCsv = string.Join(", ", matches.Select(x => $"{x.UnitInfo.Name} (\"{x.Abbreviation}\")").OrderBy(x => x));
throw new AmbiguousUnitParseException($"Cannot parse \"{unitAbbreviation}\" since it matches multiple units: {unitsCsv}.");
}
}
///
/// Attempts to parse the specified unit abbreviation into an object.
///
/// The unit abbreviation to parse.
/// The format provider to use for lookup. Defaults to if null.
///
/// When this method returns, contains the parsed object if the parsing succeeded,
/// or null if the parsing failed. This parameter is passed uninitialized.
///
///
/// true if the unit abbreviation was successfully parsed; otherwise, false.
///
///
/// This method performs a series of searches to find the unit:
///
/// - Case-sensitive search using the current culture.
/// - Case-sensitive search using the fallback culture, if applicable.
/// - Case-insensitive search.
///
/// Note that this method is not optimized for performance, as it enumerates all units and their abbreviations
/// during each invocation.
///
public bool TryGetUnitFromAbbreviation([NotNullWhen(true)]string? unitAbbreviation, IFormatProvider? formatProvider, [NotNullWhen(true)] out UnitInfo? unit)
{
if (unitAbbreviation == null)
{
unit = null;
return false;
}
List<(UnitInfo UnitInfo, string Abbreviation)> matches = FindAllMatchingUnits(unitAbbreviation, formatProvider);
if (matches.Count == 1)
{
unit = matches[0].UnitInfo;
return true;
}
unit = null;
return false;
}
private List<(UnitInfo UnitInfo, string Abbreviation)> FindAllMatchingUnits(string unitAbbreviation, IFormatProvider? formatProvider)
{
if (formatProvider is not CultureInfo culture)
{
culture = CultureInfo.CurrentCulture;
}
unitAbbreviation = unitAbbreviation.Trim();
StringComparison comparison = StringComparison.Ordinal;
while (true)
{
List<(UnitInfo UnitInfo, string Abbreviation)> matches = FindAllMatchingUnitsForCulture(unitAbbreviation, culture, comparison);
if (matches.Count != 0)
{
return matches;
}
// Retry with fallback culture, if different.
if (UnitAbbreviationsCache.HasFallbackCulture(culture))
{
culture = UnitAbbreviationsCache.FallbackCulture;
continue;
}
var normalizedUnitString = NormalizeUnitString(unitAbbreviation);
if (normalizedUnitString != unitAbbreviation)
{
unitAbbreviation = normalizedUnitString;
continue;
}
if (comparison == StringComparison.Ordinal)
{
comparison = StringComparison.OrdinalIgnoreCase;
continue;
}
return matches;
}
}
private List<(UnitInfo UnitInfo, string Abbreviation)> FindAllMatchingUnitsForCulture(string unitAbbreviation, CultureInfo culture,
StringComparison comparison)
{
var unitAbbreviationsPairs = new List<(UnitInfo, string)>();
foreach (QuantityInfo quantityInfo in Quantities.Infos)
{
IReadOnlyList unitInfos = quantityInfo.UnitInfos;
var nbUnits = unitInfos.Count;
for (var i = 0; i < nbUnits; i++)
{
UnitInfo unitInfo = unitInfos[i];
IReadOnlyList abbreviations = Abbreviations.GetAbbreviationsForCulture(unitInfo, culture);
var nbAbbreviations = abbreviations.Count;
for (var p = 0; p < nbAbbreviations; p++)
{
var abbreviation = abbreviations[p];
if (unitAbbreviation.Equals(abbreviation, comparison))
{
unitAbbreviationsPairs.Add((unitInfo, abbreviation));
}
}
}
}
return unitAbbreviationsPairs;
}
///
/// Dynamically construct a quantity from a numeric value and a unit abbreviation.
///
///
/// This method is currently not optimized for performance and will enumerate all units and their unit abbreviations
/// each time.
/// Unit abbreviation matching in the overload is case-insensitive.
///
/// This will fail if more than one unit across all quantities share the same unit abbreviation.
///
/// Numeric value.
/// Unit abbreviation, such as "kg" for .
/// The format provider to use for lookup. Defaults to if null.
/// An object.
/// Unit abbreviation is not known.
/// Multiple units found matching the given unit abbreviation.
internal IQuantity FromUnitAbbreviation(double value, string unitAbbreviation, IFormatProvider? formatProvider)
{
return GetUnitFromAbbreviation(unitAbbreviation, formatProvider).From(value);
}
}