// 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.RegularExpressions; // ReSharper disable once CheckNamespace namespace UnitsNet; /// /// A method signature for creating a quantity given a numeric value and a strongly typed unit, for example 1.0 and /// . /// /// The type of quantity to create, such as . /// /// The type of unit enum that belongs to this quantity, such as for /// . /// public delegate TQuantity QuantityFromDelegate(double value, TUnitType fromUnit) where TQuantity : IQuantity where TUnitType : struct, Enum; /// /// Parses quantities from strings, such as "1.2 kg" to or "100 cm" to . /// public class QuantityParser { /// /// Allow integer, floating point or exponential number formats. /// private const NumberStyles ParseNumberStyles = NumberStyles.Number | NumberStyles.Float | NumberStyles.AllowExponent; private readonly UnitParser _unitParser; /// /// Initializes a new instance of the class using the specified /// . /// /// /// The cache containing mappings of units to their abbreviations, used for parsing quantities. /// public QuantityParser(UnitAbbreviationsCache unitAbbreviationsCache) : this(new UnitParser(unitAbbreviationsCache)) { } /// /// Initializes a new instance of the class using the specified . /// /// /// The unit parser to use for parsing units. /// public QuantityParser(UnitParser unitParser) { _unitParser = unitParser ?? throw new ArgumentNullException(nameof(unitParser)); } /// /// The default instance of , which uses the default /// unit abbreviations. /// public static QuantityParser Default { get => UnitsNetSetup.Default.QuantityParser; } /// /// Parses a quantity from a string, such as "1.2 kg" to or "100 cm" to . /// /// The string to parse, such as "1.2 kg". /// /// The culture for looking up localized unit abbreviations for a language, and for parsing /// the number formatted in this culture. Defaults to . /// /// A function to create a quantity given a numeric value and a unit enum value. /// The type of quantity to create, such as . /// /// The type of unit enum that belongs to this quantity, such as for /// . /// /// The parsed quantity if successful. /// The string was null. /// Failed to parse quantity. public TQuantity Parse(string str, IFormatProvider? formatProvider, QuantityFromDelegate fromDelegate) where TQuantity : IQuantity where TUnitType : struct, Enum { if (str == null) throw new ArgumentNullException(nameof(str)); str = str.Trim(); Regex regex = CreateRegexForQuantity(formatProvider); if (!TryExtractValueAndUnit(regex, str, out var valueString, out var unitString)) { throw new FormatException( "Unable to parse quantity. Expected the form \"{value} {unit abbreviation}\", such as \"5.5 m\". The spacing is optional.") { Data = { ["input"] = str } }; } return ParseWithRegex(valueString, unitString, fromDelegate, formatProvider); } /// internal IQuantity Parse(string str, IFormatProvider? formatProvider, QuantityInfo quantityInfo) { if (str == null) throw new ArgumentNullException(nameof(str)); str = str.Trim(); Regex regex = CreateRegexForQuantity(quantityInfo.UnitType, formatProvider); if (!TryExtractValueAndUnit(regex, str, out var valueString, out var unitString)) { throw new FormatException( "Unable to parse quantity. Expected the form \"{value} {unit abbreviation}\", such as \"5.5 m\". The spacing is optional.") { Data = { ["input"] = str } }; } return ParseWithRegex(valueString, unitString, quantityInfo.UnitInfos, formatProvider); } /// /// Tries to parse a quantity from a string, such as "1.2 kg" to or "100 cm" to /// . /// /// The string to parse, such as "1.2 kg". /// /// The culture for looking up localized unit abbreviations for a language, and for parsing /// the number formatted in this culture. Defaults to . /// /// A function to create a quantity given a numeric value and a unit enum value. /// The parsed quantity if successful, otherwise null. /// The type of quantity to create, such as . /// /// The type of unit enum that belongs to this quantity, such as for /// . /// /// True if successful. /// The string was null. /// Failed to parse quantity. public bool TryParse(string? str, IFormatProvider? formatProvider, QuantityFromDelegate fromDelegate, [NotNullWhen(true)] out TQuantity? result) where TQuantity : IQuantity where TUnitType : struct, Enum { result = default; if (string.IsNullOrWhiteSpace(str)) return false; str = str!.Trim(); // netstandard2.0 nullable quirk Regex regex = CreateRegexForQuantity(formatProvider); return TryExtractValueAndUnit(regex, str, out var valueString, out var unitString) && TryParseWithRegex(valueString, unitString, fromDelegate, formatProvider, out result); } /// internal bool TryParse(string? str, IFormatProvider? formatProvider, QuantityInfo quantityInfo, [NotNullWhen(true)] out IQuantity? result) { result = null; if (string.IsNullOrWhiteSpace(str)) return false; str = str!.Trim(); // netstandard2.0 nullable quirk Regex regex = CreateRegexForQuantity(quantityInfo.UnitType, formatProvider); return TryExtractValueAndUnit(regex, str, out var valueString, out var unitString) && TryParseWithRegex(valueString, unitString, quantityInfo.UnitInfos, formatProvider, out result); } internal string CreateRegexPatternForUnit(TUnitType unit, IFormatProvider? formatProvider, bool matchEntireString = true) where TUnitType : struct, Enum { IReadOnlyList unitAbbreviations = _unitParser.Abbreviations.GetUnitAbbreviations(unit, formatProvider); var pattern = GetRegexPatternForUnitAbbreviations(unitAbbreviations); return matchEntireString ? $"^{pattern}$" : pattern; } private static string GetRegexPatternForUnitAbbreviations(IEnumerable abbreviations) { var orderedAbbreviations = abbreviations .OrderByDescending(s => s.Length) // Important to order by length -- if "m" is before "mm" and the input is "mm", it will match just "m" .Select(Regex.Escape) // Escape special regex characters .ToArray(); var abbreviationsPiped = $"{string.Join("|", orderedAbbreviations)}"; return $@"(?.*?)\s?(?{abbreviationsPiped})"; } /// /// Parse a string given a particular regular expression. /// /// Error parsing string. private TQuantity ParseWithRegex(string valueString, string unitString, QuantityFromDelegate fromDelegate, IFormatProvider? formatProvider) where TQuantity : IQuantity where TUnitType : struct, Enum { var value = double.Parse(valueString, ParseNumberStyles, formatProvider); TUnitType parsedUnit = _unitParser.Parse(unitString, formatProvider); return fromDelegate(value, parsedUnit); } /// /// Parse a string given a particular regular expression. /// /// Error parsing string. private IQuantity ParseWithRegex(string valueString, string unitString, IReadOnlyList units, IFormatProvider? formatProvider) { var value = double.Parse(valueString, ParseNumberStyles, formatProvider); UnitInfo unitInfo = _unitParser.Parse(unitString, units, formatProvider); return unitInfo.From(value); } /// /// Parse a string given a particular regular expression. /// /// Error parsing string. private bool TryParseWithRegex(string? valueString, string? unitString, QuantityFromDelegate fromDelegate, IFormatProvider? formatProvider, [NotNullWhen(true)] out TQuantity? result) where TQuantity : IQuantity where TUnitType : struct, Enum { result = default; if (!double.TryParse(valueString, ParseNumberStyles, formatProvider, out var value)) { return false; } if (!_unitParser.TryParse(unitString, formatProvider, out TUnitType parsedUnit)) { return false; } result = fromDelegate(value, parsedUnit); return true; } /// /// Parse a string given a particular regular expression. /// /// Error parsing string. private bool TryParseWithRegex(string? valueString, string? unitString, IReadOnlyList units, IFormatProvider? formatProvider, [NotNullWhen(true)] out IQuantity? result) { result = null; if (!double.TryParse(valueString, ParseNumberStyles, formatProvider, out var value)) { return false; } if (!_unitParser.TryParse(unitString, units, formatProvider, out UnitInfo? parsedUnit)) { return false; } result = parsedUnit.From(value); return true; } private static bool TryExtractValueAndUnit(Regex regex, string str, [NotNullWhen(true)] out string? valueString, [NotNullWhen(true)] out string? unitString) { Match match = regex.Match(str); // the regex coming in contains all allowed units as strings. // That means if the unit in str is not formatted right // the regex.Match will either put str or string.empty into Groups[0] and Groups[1] // Therefore a mismatch can be detected by comparing the values of this two groups. if (match.Groups[0].Value == match.Groups[1].Value) { str = UnitParser.NormalizeUnitString(str); match = regex.Match(str); } GroupCollection groups = match.Groups; Group valueGroup = groups["value"]; Group unitGroup = groups["unit"]; if (!valueGroup.Success || !unitGroup.Success) { valueString = null; unitString = null; return false; } valueString = valueGroup.Value; unitString = unitGroup.Value; return true; } private string CreateRegexPatternForQuantity(Type unitType, IFormatProvider? formatProvider) { IReadOnlyList unitAbbreviations = _unitParser.Abbreviations.GetAllUnitAbbreviationsForQuantity(unitType, formatProvider); var pattern = GetRegexPatternForUnitAbbreviations(unitAbbreviations); // Match entire string exactly return $"^{pattern}$"; } private Regex CreateRegexForQuantity(Type unitType, IFormatProvider? formatProvider) { var pattern = CreateRegexPatternForQuantity(unitType, formatProvider); return new Regex(pattern, RegexOptions.Singleline | RegexOptions.IgnoreCase); } private Regex CreateRegexForQuantity(IFormatProvider? formatProvider) where TUnitType : struct, Enum { return CreateRegexForQuantity(typeof(TUnitType), formatProvider); } }