// 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); } }