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