diff --git a/Build/build-functions.psm1 b/Build/build-functions.psm1
index 1f5525757c..dd29014efa 100644
--- a/Build/build-functions.psm1
+++ b/Build/build-functions.psm1
@@ -63,7 +63,8 @@ function Start-Tests {
$projectPaths = @(
"UnitsNet.Tests\UnitsNet.Tests.csproj",
"UnitsNet.NumberExtensions.Tests\UnitsNet.NumberExtensions.Tests.csproj",
- "UnitsNet.Serialization.JsonNet.Tests\UnitsNet.Serialization.JsonNet.Tests.csproj"
+ "UnitsNet.Serialization.JsonNet.Tests\UnitsNet.Serialization.JsonNet.Tests.csproj",
+ "UnitsNet.Serialization.SystemTextJson.Tests\UnitsNet.Serialization.SystemTextJson.csproj"
)
# Parent dir must exist before xunit tries to write files to it
@@ -102,6 +103,7 @@ function Start-PackNugets([boolean] $IncludeNanoFramework = $false) {
$projectPaths = @(
"UnitsNet\UnitsNet.csproj",
"UnitsNet.Serialization.JsonNet\UnitsNet.Serialization.JsonNet.csproj",
+ "UnitsNet.Serialization.SystemTextJson\UnitsNet.Serialization.SystemTextJson.csproj",
"UnitsNet.NumberExtensions\UnitsNet.NumberExtensions.csproj"
)
diff --git a/CodeGen/CodeGen.csproj b/CodeGen/CodeGen.csproj
index 24c93d5eca..a162ff9fae 100644
--- a/CodeGen/CodeGen.csproj
+++ b/CodeGen/CodeGen.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -10,6 +10,7 @@
+
diff --git a/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs b/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs
index 4765e49d9e..2965ebccbb 100644
--- a/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs
+++ b/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs
@@ -40,7 +40,7 @@ public static class NumberTo{_quantityName}Extensions
continue;
Writer.WL(2, $@"
-/// ");
+/// ");
// Include obsolete text from the quantity per extension method, to make it visible when the class is not explicitly referenced in code.
Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit.ObsoleteText ?? _quantity.ObsoleteText));
@@ -49,10 +49,10 @@ public static class NumberTo{_quantityName}Extensions
where T : notnull
#if NET7_0_OR_GREATER
, INumber
- => {_quantityName}.From{unit.PluralName}(double.CreateChecked(value));
+ => {_quantityName}.From{unit.PluralName}(QuantityValue.CreateChecked(value));
#else
, IConvertible
- => {_quantityName}.From{unit.PluralName}(value.ToDouble(null));
+ => {_quantityName}.From{unit.PluralName}(value.ToQuantityValue());
#endif
");
}
diff --git a/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs b/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
index 63cab0add0..82ccf14824 100644
--- a/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
+++ b/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
@@ -4,7 +4,10 @@
using System;
using System.Linq;
using CodeGen.Helpers;
+using CodeGen.Helpers.ExpressionAnalyzer;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
using CodeGen.JsonTypes;
+using Fractions;
namespace CodeGen.Generators.UnitsNetGen
{
@@ -34,16 +37,10 @@ public string Generate()
{
Writer.WL(GeneratedFileHeader);
Writer.WL(@"
-using System;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.Linq;
+using System.Resources;
using System.Runtime.Serialization;
-using UnitsNet.Units;
-#if NET
-using System.Numerics;
-#endif
+using UnitsNet.Debug;
#nullable enable
@@ -65,9 +62,64 @@ namespace UnitsNet
Writer.WLIfText(1, GetObsoleteAttributeOrNull(_quantity));
Writer.WL(@$"
[DataContract]
- [DebuggerTypeProxy(typeof(QuantityDisplay))]
- public readonly partial struct {_quantity.Name} :
- {(_quantity.GenerateArithmetic ? "IArithmeticQuantity" : "IQuantity")}<{_quantity.Name}, {_unitEnumName}>,");
+ [DebuggerDisplay(QuantityDebugProxy.DisplayFormat)]
+ [DebuggerTypeProxy(typeof(QuantityDebugProxy))]
+ public readonly partial struct {_quantity.Name} :");
+ GenerateInterfaceExtensions();
+
+ Writer.WL($@"
+ {{
+ ///
+ /// The numeric value this quantity was constructed with.
+ ///
+ [DataMember(Name = ""Value"", Order = 1, EmitDefaultValue = false)]
+ private readonly QuantityValue _value;
+
+ ///
+ /// The unit this quantity was constructed with.
+ ///
+ [DataMember(Name = ""Unit"", Order = 2, EmitDefaultValue = false)]
+ private readonly {_unitEnumName}? _unit;
+");
+ GenerateQuantityInfo();
+ GenerateStaticConstructor();
+ GenerateInstanceConstructors();
+ GenerateStaticProperties();
+ GenerateProperties();
+ GenerateConversionProperties();
+ GenerateStaticMethods();
+ GenerateStaticFactoryMethods();
+ GenerateStaticParseMethods();
+ GenerateArithmeticOperators();
+ GenerateRelationalOperators();
+ GenerateEqualityAndComparison();
+ GenerateConversionMethods();
+ GenerateToString();
+
+ Writer.WL($@"
+ }}
+}}");
+ return Writer.ToString();
+ }
+
+ private void GenerateInterfaceExtensions()
+ {
+ // generate the base interface (either IVectorQuantity, IAffineQuantity or ILogarithmicQuantity)
+ if (_quantity.Logarithmic)
+ {
+ Writer.WL(@$"
+ ILogarithmicQuantity<{_quantity.Name}, {_unitEnumName}>,");
+ }
+ else if (!string.IsNullOrEmpty(_quantity.AffineOffsetType))
+ {
+ Writer.WL(@$"
+ IAffineQuantity<{_quantity.Name}, {_unitEnumName}, {_quantity.AffineOffsetType}>,");
+ }
+ else // the default quantity type implements the IVectorQuantity interface
+ {
+ Writer.WL(@$"
+ IArithmeticQuantity<{_quantity.Name}, {_unitEnumName}>,");
+ }
if (_quantity.Relations.Any(r => r.Operator is "*" or "/"))
{
@@ -90,14 +142,13 @@ namespace UnitsNet
default:
continue;
}
- Writer.WL($"<{relation.LeftQuantity.Name}, {relation.RightQuantity.Name}, {relation.ResultQuantity.Name}>,");
+ Writer.WL($"<{relation.LeftQuantity.Name}, {relation.RightQuantity.Name}, {relation.ResultQuantity.Name.Replace("double", "QuantityValue")}>,");
}
}
Writer.WL(@$"
#endif");
}
-
Writer.WL(@$"
#if NET7_0_OR_GREATER
IComparisonOperators<{_quantity.Name}, {_quantity.Name}, bool>,
@@ -105,74 +156,89 @@ namespace UnitsNet
#endif
IComparable,
IComparable<{_quantity.Name}>,
- IConvertible,
IEquatable<{_quantity.Name}>,
IFormattable");
-
- Writer.WL($@"
- {{
- ///
- /// The numeric value this quantity was constructed with.
- ///
- [DataMember(Name = ""Value"", Order = 1)]
- private readonly double _value;
-
- ///
- /// The unit this quantity was constructed with.
- ///
- [DataMember(Name = ""Unit"", Order = 2)]
- private readonly {_unitEnumName}? _unit;
-");
- GenerateStaticConstructor();
- GenerateInstanceConstructors();
- GenerateStaticProperties();
- GenerateProperties();
- GenerateConversionProperties();
- GenerateStaticMethods();
- GenerateStaticFactoryMethods();
- GenerateStaticParseMethods();
- GenerateArithmeticOperators();
- GenerateRelationalOperators();
- GenerateEqualityAndComparison();
- GenerateConversionMethods();
- GenerateToString();
- GenerateIConvertibleMethods();
-
- Writer.WL($@"
- }}
-}}");
- return Writer.ToString();
}
- private void GenerateStaticConstructor()
+ private void GenerateQuantityInfo()
{
+ var quantityInfoClassName = $"{_quantity.Name}Info";
BaseDimensions baseDimensions = _quantity.BaseDimensions;
+ var createDimensionsExpression = _isDimensionless
+ ? "BaseDimensions.Dimensionless"
+ : $"new BaseDimensions({baseDimensions.L}, {baseDimensions.M}, {baseDimensions.T}, {baseDimensions.I}, {baseDimensions.Θ}, {baseDimensions.N}, {baseDimensions.J})";
+
Writer.WL($@"
- static {_quantity.Name}()
+ ///
+ /// Provides detailed information about the quantity, including its name, base unit, unit mappings, base dimensions, and conversion functions.
+ ///
+ public sealed class {quantityInfoClassName}: QuantityInfo<{_quantity.Name}, {_unitEnumName}>
{{");
- Writer.WL(_isDimensionless ? $@"
- BaseDimensions = BaseDimensions.Dimensionless;" : $@"
- BaseDimensions = new BaseDimensions({baseDimensions.L}, {baseDimensions.M}, {baseDimensions.T}, {baseDimensions.I}, {baseDimensions.Θ}, {baseDimensions.N}, {baseDimensions.J});");
-
Writer.WL($@"
- BaseUnit = {_unitEnumName}.{_quantity.BaseUnit};
- Units = Enum.GetValues(typeof({_unitEnumName})).Cast<{_unitEnumName}>().ToArray();
- Zero = new {_quantity.Name}(0, BaseUnit);
- Info = new QuantityInfo<{_unitEnumName}>(""{_quantity.Name}"",
- new UnitInfo<{_unitEnumName}>[]
- {{");
+ ///
+ public {quantityInfoClassName}(string name, {_unitEnumName} baseUnit, IEnumerable> unitMappings, {_quantity.Name} zero, BaseDimensions baseDimensions,
+ QuantityFromDelegate<{_quantity.Name}, {_unitEnumName}> fromDelegate, ResourceManager? unitAbbreviations)
+ : base(name, baseUnit, unitMappings, zero, baseDimensions, fromDelegate, unitAbbreviations)
+ {{
+ }}
+ ///
+ public {quantityInfoClassName}(string name, {_unitEnumName} baseUnit, IEnumerable> unitMappings, {_quantity.Name} zero, BaseDimensions baseDimensions)
+ : this(name, baseUnit, unitMappings, zero, baseDimensions, {_quantity.Name}.From, new ResourceManager(""UnitsNet.GeneratedCode.Resources.{_quantity.Name}"", typeof({_quantity.Name}).Assembly))
+ {{
+ }}
+
+ ///
+ /// Creates a new instance of the class with the default settings for the {_quantity.Name} quantity.
+ ///
+ /// A new instance of the class with the default settings.
+ public static {quantityInfoClassName} CreateDefault()
+ {{
+ return new {quantityInfoClassName}(nameof({_quantity.Name}), DefaultBaseUnit, GetDefaultMappings(), new {_quantity.Name}(0, DefaultBaseUnit), DefaultBaseDimensions);
+ }}
+
+ ///
+ /// Creates a new instance of the class with the default settings for the {_quantity.Name} quantity and a callback for customizing the default unit mappings.
+ ///
+ ///
+ /// A callback function for customizing the default unit mappings.
+ ///
+ ///
+ /// A new instance of the class with the default settings.
+ ///
+ public static {quantityInfoClassName} CreateDefault(Func>, IEnumerable>> customizeUnits)
+ {{
+ return new {quantityInfoClassName}(nameof({_quantity.Name}), DefaultBaseUnit, customizeUnits(GetDefaultMappings()), new {_quantity.Name}(0, DefaultBaseUnit), DefaultBaseDimensions);
+ }}
+
+ ///
+ /// The for is {_quantity.BaseDimensions}.
+ ///
+ public static BaseDimensions DefaultBaseDimensions {{ get; }} = {createDimensionsExpression};
+
+ ///
+ /// The default base unit of {_quantity.Name} is {_baseUnit.SingularName}. All conversions, as defined in the , go via this value.
+ ///
+ public static {_unitEnumName} DefaultBaseUnit {{ get; }} = {_unitEnumName}.{_baseUnit.SingularName};
+
+ ///
+ /// Retrieves the default mappings for .
+ ///
+ /// An of representing the default unit mappings for {_quantity.Name}.
+ public static IEnumerable> GetDefaultMappings()
+ {{");
+
foreach (Unit unit in _quantity.Units)
{
BaseUnits? baseUnits = unit.BaseUnits;
+ string baseUnitsFormat;
if (baseUnits == null)
{
- Writer.WL($@"
- new UnitInfo<{_unitEnumName}>({_unitEnumName}.{unit.SingularName}, ""{unit.PluralName}"", BaseUnits.Undefined, ""{_quantity.Name}""),");
+ baseUnitsFormat = "BaseUnits.Undefined";
}
else
{
- var baseUnitsCtorArgs = string.Join(", ",
+ baseUnitsFormat = $"new BaseUnits({string.Join(", ",
new[]
{
baseUnits.L != null ? $"length: LengthUnit.{baseUnits.L}" : null,
@@ -182,19 +248,49 @@ private void GenerateStaticConstructor()
baseUnits.Θ != null ? $"temperature: TemperatureUnit.{baseUnits.Θ}" : null,
baseUnits.N != null ? $"amount: AmountOfSubstanceUnit.{baseUnits.N}" : null,
baseUnits.J != null ? $"luminousIntensity: LuminousIntensityUnit.{baseUnits.J}" : null
- }.Where(str => str != null));
+ }.Where(str => str != null))})";
+ }
+ if (unit.SingularName == _quantity.BaseUnit)
+ {
Writer.WL($@"
- new UnitInfo<{_unitEnumName}>({_unitEnumName}.{unit.SingularName}, ""{unit.PluralName}"", new BaseUnits({baseUnitsCtorArgs}), ""{_quantity.Name}""),");
+ yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat});");
+ }
+ else
+ {
+ // note: omitting the extra parameter (where possible) saves us 36 KB
+ CompositeExpression expressionFromBaseToUnit = ExpressionEvaluator.Evaluate(unit.FromBaseToUnitFunc, "{x}");
+ if (expressionFromBaseToUnit.Terms.Count == 1 && expressionFromBaseToUnit.Degree == Fraction.One)
+ {
+ Writer.WL($@"
+ yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat},
+ {expressionFromBaseToUnit.GetConversionExpressionFormat()}
+ );");
+ }
+ else
+ {
+ Writer.WL($@"
+ yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat},
+ {expressionFromBaseToUnit.GetConversionExpressionFormat()},
+ {unit.GetUnitToBaseConversionExpressionFormat()}
+ );");
+ }
}
}
Writer.WL($@"
- }},
- BaseUnit, Zero, BaseDimensions);
+ }}
+ }}
+");
+ }
- DefaultConversionFunctions = new UnitConverter();
- RegisterDefaultConversions(DefaultConversionFunctions);
+ private void GenerateStaticConstructor()
+ {
+ Writer.WL($@"
+ static {_quantity.Name}()
+ {{");
+ Writer.WL($@"
+ Info = UnitsNetSetup.CreateQuantityInfo({_quantity.Name}Info.CreateDefault);
}}
");
}
@@ -207,7 +303,7 @@ private void GenerateInstanceConstructors()
///
/// The numeric value to construct this quantity with.
/// The unit representation to construct this quantity with.
- public {_quantity.Name}(double value, {_unitEnumName} unit)
+ public {_quantity.Name}(QuantityValue value, {_unitEnumName} unit)
{{");
Writer.WL(@"
_value = value;");
@@ -226,7 +322,7 @@ private void GenerateInstanceConstructors()
/// The unit system to create the quantity with.
/// The given is null.
/// No unit was found for the given .
- public {_quantity.Name}(double value, UnitSystem unitSystem)
+ public {_quantity.Name}(QuantityValue value, UnitSystem unitSystem)
{{
_value = value;
_unit = Info.GetDefaultUnit(unitSystem);
@@ -243,37 +339,38 @@ private void GenerateStaticProperties()
///
/// The containing the default generated conversion functions for instances.
///
- public static UnitConverter DefaultConversionFunctions {{ get; }}
+ [Obsolete(""Replaced by UnitConverter.Default"")]
+ public static UnitConverter DefaultConversionFunctions => UnitConverter.Default;
///
- public static QuantityInfo<{_unitEnumName}> Info {{ get; }}
+ public static QuantityInfo<{_quantity.Name}, {_unitEnumName}> Info {{ get; }}
///
/// The of this quantity.
///
- public static BaseDimensions BaseDimensions {{ get; }}
+ public static BaseDimensions BaseDimensions => Info.BaseDimensions;
///
/// The base unit of {_quantity.Name}, which is {_quantity.BaseUnit}. All conversions go via this value.
///
- public static {_unitEnumName} BaseUnit {{ get; }}
+ public static {_unitEnumName} BaseUnit => Info.BaseUnitInfo.Value;
///
/// All units of measurement for the {_quantity.Name} quantity.
///
- public static {_unitEnumName}[] Units {{ get; }}
+ public static IReadOnlyCollection<{_unitEnumName}> Units => Info.Units;
///
/// Gets an instance of this quantity with a value of 0 in the base unit {_quantity.BaseUnit}.
///
- public static {_quantity.Name} Zero {{ get; }}
+ public static {_quantity.Name} Zero => Info.Zero;
");
-
- if (_quantity.GenerateArithmetic)
+
+ if (_quantity.Logarithmic)
{
Writer.WL($@"
- ///
- public static {_quantity.Name} AdditiveIdentity => Zero;
+ ///
+ public static QuantityValue LogarithmicScalingFactor {{get;}} = {10 * _quantity.LogarithmicScalingFactor};
");
}
@@ -287,30 +384,51 @@ private void GenerateProperties()
Writer.WL($@"
#region Properties
- ///
- /// The numeric value this quantity was constructed with.
- ///
- public double Value => _value;
-
///
- double IQuantity.Value => _value;
-
- Enum IQuantity.Unit => Unit;
+ public QuantityValue Value => _value;
///
public {_unitEnumName} Unit => _unit.GetValueOrDefault(BaseUnit);
///
- public QuantityInfo<{_unitEnumName}> QuantityInfo => Info;
-
- ///
- QuantityInfo IQuantity.QuantityInfo => Info;
+ public QuantityInfo<{_quantity.Name}, {_unitEnumName}> QuantityInfo => Info;
///
/// The of this quantity.
///
public BaseDimensions Dimensions => {_quantity.Name}.BaseDimensions;
+ #region Explicit implementations
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ Enum IQuantity.Unit => Unit;
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ UnitKey IQuantity.UnitKey => UnitKey.ForUnit(Unit);
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ QuantityInfo IQuantity.QuantityInfo => Info;
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ QuantityInfo<{_unitEnumName}> IQuantity<{_unitEnumName}>.QuantityInfo => Info;
+
+#if NETSTANDARD2_0
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ IQuantityInstanceInfo<{_quantity.Name}> IQuantityInstance<{_quantity.Name}>.QuantityInfo => Info;
+#endif
+");
+ if (_quantity.Logarithmic)
+ {
+ Writer.WL($@"
+#if NETSTANDARD2_0
+ QuantityValue ILogarithmicQuantity<{_quantity.Name}>.LogarithmicScalingFactor => LogarithmicScalingFactor;
+#endif
+");
+ }
+
+ Writer.WL($@"
+ #endregion
+
#endregion
");
}
@@ -326,11 +444,11 @@ private void GenerateConversionProperties()
Writer.WL($@"
///
- /// Gets a value of this quantity converted into
+ /// Gets a value of this quantity converted into
/// ");
Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit));
Writer.WL($@"
- public double {unit.PluralName} => As({_unitEnumName}.{unit.SingularName});
+ public QuantityValue {unit.PluralName} => this.As({_unitEnumName}.{unit.SingularName});
");
}
@@ -346,41 +464,6 @@ private void GenerateStaticMethods()
#region Static Methods
- ///
- /// Registers the default conversion functions in the given instance.
- ///
- /// The to register the default conversion functions in.
- internal static void RegisterDefaultConversions(UnitConverter unitConverter)
- {{
- // Register in unit converter: {_unitEnumName} -> BaseUnit");
-
- foreach (Unit unit in _quantity.Units)
- {
- if (unit.SingularName == _quantity.BaseUnit) continue;
-
- Writer.WL($@"
- unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity.ToUnit({_unitEnumName}.{_quantity.BaseUnit}));");
- }
-
- Writer.WL();
- Writer.WL($@"
-
- // Register in unit converter: BaseUnit <-> BaseUnit
- unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity);
-
- // Register in unit converter: BaseUnit -> {_unitEnumName}");
-
- foreach (Unit unit in _quantity.Units)
- {
- if (unit.SingularName == _quantity.BaseUnit) continue;
-
- Writer.WL($@"
- unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}, quantity => quantity.ToUnit({_unitEnumName}.{unit.SingularName}));");
- }
-
- Writer.WL($@"
- }}
-
///
/// Get unit abbreviation string.
///
@@ -421,7 +504,7 @@ private void GenerateStaticFactoryMethods()
/// ");
Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit));
Writer.WL($@"
- public static {_quantity.Name} From{unit.PluralName}(double value)
+ public static {_quantity.Name} From{unit.PluralName}(QuantityValue value)
{{
return new {_quantity.Name}(value, {_unitEnumName}.{unit.SingularName});
}}
@@ -435,7 +518,7 @@ private void GenerateStaticFactoryMethods()
/// Value to convert from.
/// Unit to convert from.
/// {_quantity.Name} unit value.
- public static {_quantity.Name} From(double value, {_unitEnumName} fromUnit)
+ public static {_quantity.Name} From(QuantityValue value, {_unitEnumName} fromUnit)
{{
return new {_quantity.Name}(value, fromUnit);
}}
@@ -501,10 +584,7 @@ private void GenerateStaticParseMethods()
/// Format to use when parsing number and unit. Defaults to if null.
public static {_quantity.Name} Parse(string str, IFormatProvider? provider)
{{
- return UnitsNetSetup.Default.QuantityParser.Parse<{_quantity.Name}, {_unitEnumName}>(
- str,
- provider,
- From);
+ return QuantityParser.Default.Parse<{_quantity.Name}, {_unitEnumName}>(str, provider, From);
}}
///
@@ -532,11 +612,7 @@ public static bool TryParse([NotNullWhen(true)]string? str, out {_quantity.Name}
/// Format to use when parsing number and unit. Defaults to if null.
public static bool TryParse([NotNullWhen(true)]string? str, IFormatProvider? provider, out {_quantity.Name} result)
{{
- return UnitsNetSetup.Default.QuantityParser.TryParse<{_quantity.Name}, {_unitEnumName}>(
- str,
- provider,
- From,
- out result);
+ return QuantityParser.Default.TryParse<{_quantity.Name}, {_unitEnumName}>(str, provider, From, out result);
}}
///
@@ -557,7 +633,7 @@ public static bool TryParse([NotNullWhen(true)]string? str, IFormatProvider? pro
/// Parse a unit string.
///
/// String to parse. Typically in the form: {{number}} {{unit}}
- /// Format to use when parsing number and unit. Defaults to if null.
+ /// Format to use when parsing the unit. Defaults to if null.
///
/// Length.ParseUnit(""m"", CultureInfo.GetCultureInfo(""en-US""));
///
@@ -565,10 +641,10 @@ public static bool TryParse([NotNullWhen(true)]string? str, IFormatProvider? pro
/// Error parsing string.
public static {_unitEnumName} ParseUnit(string str, IFormatProvider? provider)
{{
- return UnitsNetSetup.Default.UnitParser.Parse<{_unitEnumName}>(str, provider);
+ return UnitParser.Default.Parse(str, Info.UnitInfos, provider).Value;
}}
- ///
+ ///
public static bool TryParseUnit([NotNullWhen(true)]string? str, out {_unitEnumName} unit)
{{
return TryParseUnit(str, null, out unit);
@@ -583,10 +659,10 @@ public static bool TryParseUnit([NotNullWhen(true)]string? str, out {_unitEnumNa
///
/// Length.TryParseUnit(""m"", CultureInfo.GetCultureInfo(""en-US""));
///
- /// Format to use when parsing number and unit. Defaults to if null.
+ /// Format to use when parsing the unit. Defaults to if null.
public static bool TryParseUnit([NotNullWhen(true)]string? str, IFormatProvider? provider, out {_unitEnumName} unit)
{{
- return UnitsNetSetup.Default.UnitParser.TryParse<{_unitEnumName}>(str, provider, out unit);
+ return UnitParser.Default.TryParse(str, Info, provider, out unit);
}}
#endregion
@@ -595,7 +671,7 @@ public static bool TryParseUnit([NotNullWhen(true)]string? str, IFormatProvider?
private void GenerateArithmeticOperators()
{
- if (!_quantity.GenerateArithmetic) return;
+ if (_quantity.IsAffine) return;
// Logarithmic units required different arithmetic
if (_quantity.Logarithmic)
@@ -616,35 +692,35 @@ private void GenerateArithmeticOperators()
/// Get from adding two .
public static {_quantity.Name} operator +({_quantity.Name} left, {_quantity.Name} right)
{{
- return new {_quantity.Name}(left.Value + right.ToUnit(left.Unit).Value, left.Unit);
+ return new {_quantity.Name}(left.Value + right.As(left.Unit), left.Unit);
}}
/// Get from subtracting two .
public static {_quantity.Name} operator -({_quantity.Name} left, {_quantity.Name} right)
{{
- return new {_quantity.Name}(left.Value - right.ToUnit(left.Unit).Value, left.Unit);
+ return new {_quantity.Name}(left.Value - right.As(left.Unit), left.Unit);
}}
/// Get from multiplying value and .
- public static {_quantity.Name} operator *(double left, {_quantity.Name} right)
+ public static {_quantity.Name} operator *(QuantityValue left, {_quantity.Name} right)
{{
return new {_quantity.Name}(left * right.Value, right.Unit);
}}
/// Get from multiplying value and .
- public static {_quantity.Name} operator *({_quantity.Name} left, double right)
+ public static {_quantity.Name} operator *({_quantity.Name} left, QuantityValue right)
{{
return new {_quantity.Name}(left.Value * right, left.Unit);
}}
/// Get from dividing by value.
- public static {_quantity.Name} operator /({_quantity.Name} left, double right)
+ public static {_quantity.Name} operator /({_quantity.Name} left, QuantityValue right)
{{
return new {_quantity.Name}(left.Value / right, left.Unit);
}}
/// Get ratio value from dividing by .
- public static double operator /({_quantity.Name} left, {_quantity.Name} right)
+ public static QuantityValue operator /({_quantity.Name} left, {_quantity.Name} right)
{{
return left.{_baseUnit.PluralName} / right.{_baseUnit.PluralName};
}}
@@ -662,53 +738,61 @@ private void GenerateLogarithmicArithmeticOperators()
#region Logarithmic Arithmetic Operators
/// Negate the value.
- public static {_quantity.Name} operator -({_quantity.Name} right)
+ public static {_quantity.Name} operator -({_quantity.Name} quantity)
{{
- return new {_quantity.Name}(-right.Value, right.Unit);
+ return new {_quantity.Name}(-quantity.Value, quantity.Unit);
}}
/// Get from logarithmic addition of two .
+ /// This operation involves a conversion of the values to linear space, which is not guaranteed to produce an exact value.
+ /// The final result is rounded to 15 significant digits.
+ ///
public static {_quantity.Name} operator +({_quantity.Name} left, {_quantity.Name} right)
{{
// Logarithmic addition
// Formula: {x} * log10(10^(x/{x}) + 10^(y/{x}))
- return new {_quantity.Name}({x} * Math.Log10(Math.Pow(10, left.Value / {x}) + Math.Pow(10, right.ToUnit(left.Unit).Value / {x})), left.Unit);
+ var leftUnit = left.Unit;
+ return new {_quantity.Name}(QuantityValueExtensions.AddWithLogScaling(left.Value, right.As(leftUnit), LogarithmicScalingFactor), leftUnit);
}}
/// Get from logarithmic subtraction of two .
+ /// This operation involves a conversion of the values to linear space, which is not guaranteed to produce an exact value.
+ /// The final result is rounded to 15 significant digits.
+ ///
public static {_quantity.Name} operator -({_quantity.Name} left, {_quantity.Name} right)
{{
// Logarithmic subtraction
// Formula: {x} * log10(10^(x/{x}) - 10^(y/{x}))
- return new {_quantity.Name}({x} * Math.Log10(Math.Pow(10, left.Value / {x}) - Math.Pow(10, right.ToUnit(left.Unit).Value / {x})), left.Unit);
+ var leftUnit = left.Unit;
+ return new {_quantity.Name}(QuantityValueExtensions.SubtractWithLogScaling(left.Value, right.As(leftUnit), LogarithmicScalingFactor), leftUnit);
}}
/// Get from logarithmic multiplication of value and .
- public static {_quantity.Name} operator *(double left, {_quantity.Name} right)
+ public static {_quantity.Name} operator *(QuantityValue left, {_quantity.Name} right)
{{
// Logarithmic multiplication = addition
return new {_quantity.Name}(left + right.Value, right.Unit);
}}
/// Get from logarithmic multiplication of value and .
- public static {_quantity.Name} operator *({_quantity.Name} left, double right)
+ public static {_quantity.Name} operator *({_quantity.Name} left, QuantityValue right)
{{
// Logarithmic multiplication = addition
return new {_quantity.Name}(left.Value + right, left.Unit);
}}
/// Get from logarithmic division of by value.
- public static {_quantity.Name} operator /({_quantity.Name} left, double right)
+ public static {_quantity.Name} operator /({_quantity.Name} left, QuantityValue right)
{{
// Logarithmic division = subtraction
return new {_quantity.Name}(left.Value - right, left.Unit);
}}
/// Get ratio value from logarithmic division of by .
- public static double operator /({_quantity.Name} left, {_quantity.Name} right)
+ public static QuantityValue operator /({_quantity.Name} left, {_quantity.Name} right)
{{
// Logarithmic division = subtraction
- return Convert.ToDouble(left.Value - right.ToUnit(left.Unit).Value);
+ return left.Value - right.As(left.Unit);
}}
#endregion
@@ -735,46 +819,137 @@ private void GenerateRelationalOperators()
/// The corresponding inverse quantity, .
public {relation.RightQuantity.Name} Inverse()
{{
- return {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(1 / {relation.LeftUnit.PluralName});
+ return UnitConverter.Default.ConvertTo(Value, Unit, {relation.RightQuantity.Name}.Info);
}}
");
}
else
{
- var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
+ var leftParameterType = relation.LeftQuantity.Name;
+ var leftParameterName = leftParameterType.ToCamelCase();
var leftConversionProperty = relation.LeftUnit.PluralName;
- var rightParameter = relation.RightQuantity.Name.ToCamelCase();
+ var rightParameterType = relation.RightQuantity.Name;
+ var rightParameterName = relation.RightQuantity.Name.ToCamelCase();
var rightConversionProperty = relation.RightUnit.PluralName;
- if (leftParameter == rightParameter)
+ if (leftParameterName == rightParameterName)
{
- leftParameter = "left";
- rightParameter = "right";
+ leftParameterName = "left";
+ rightParameterName = "right";
}
- var leftPart = $"{leftParameter}.{leftConversionProperty}";
- var rightPart = $"{rightParameter}.{rightConversionProperty}";
+ var leftPart = $"{leftParameterName}.{leftConversionProperty}";
+ var rightPart = $"{rightParameterName}.{rightConversionProperty}";
- if (leftParameter is "double")
+ if (leftParameterName is "double")
{
- leftParameter = leftPart = "value";
+ leftParameterType = "QuantityValue";
+ leftParameterName = leftPart = "value";
}
- if (rightParameter is "double")
+ if (rightParameterName is "double")
{
- rightParameter = rightPart = "value";
+ rightParameterType = "QuantityValue";
+ rightParameterName = rightPart = "value";
}
var expression = $"{leftPart} {relation.Operator} {rightPart}";
- if (relation.ResultQuantity.Name is not "double")
+ var resultType = relation.ResultQuantity.Name;
+ if (resultType is "double")
{
- expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
+ resultType = "QuantityValue";
+ }
+ else
+ {
+ expression = $"{resultType}.From{relation.ResultUnit.PluralName}({expression})";
}
Writer.WL($@"
- /// Get from {relation.Operator} .
- public static {relation.ResultQuantity.Name} operator {relation.Operator}({relation.LeftQuantity.Name} {leftParameter}, {relation.RightQuantity.Name} {rightParameter})
+ /// Get from {relation.Operator} .
+ public static {resultType} operator {relation.Operator}({leftParameterType} {leftParameterName}, {rightParameterType} {rightParameterName})
+ {{
+ return {expression};
+ }}
+");
+ }
+ }
+
+ Writer.WL($@"
+
+ #endregion
+");
+ }
+
+ ///
+ /// Generates operators that express relations between quantities as applied by .
+ ///
+ private void GenerateRelationalOperatorsWithFixedUnits()
+ {
+ if (!_quantity.Relations.Any()) return;
+
+ Writer.WL($@"
+ #region Relational Operators
+");
+
+ foreach (QuantityRelation relation in _quantity.Relations)
+ {
+ if (relation.Operator == "inverse")
+ {
+ Writer.WL($@"
+ /// Calculates the inverse of this quantity.
+ /// The corresponding inverse quantity, .
+ public {relation.RightQuantity.Name} Inverse()
+ {{
+ return {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(QuantityValue.Inverse({relation.LeftUnit.PluralName}));
+ }}
+");
+ }
+ else
+ {
+ var leftParameterType = relation.LeftQuantity.Name;
+ var leftParameterName = leftParameterType.ToCamelCase();
+ var leftConversionProperty = relation.LeftUnit.PluralName;
+ var rightParameterType = relation.RightQuantity.Name;
+ var rightParameterName = relation.RightQuantity.Name.ToCamelCase();
+ var rightConversionProperty = relation.RightUnit.PluralName;
+
+ if (leftParameterName == rightParameterName)
+ {
+ leftParameterName = "left";
+ rightParameterName = "right";
+ }
+
+ var leftPart = $"{leftParameterName}.{leftConversionProperty}";
+ var rightPart = $"{rightParameterName}.{rightConversionProperty}";
+
+ if (leftParameterName is "double")
+ {
+ leftParameterType = "QuantityValue";
+ leftParameterName = leftPart = "value";
+ }
+
+ if (rightParameterName is "double")
+ {
+ rightParameterType = "QuantityValue";
+ rightParameterName = rightPart = "value";
+ }
+
+ var expression = $"{leftPart} {relation.Operator} {rightPart}";
+
+ var resultType = relation.ResultQuantity.Name;
+ if (resultType is "double")
+ {
+ resultType = "QuantityValue";
+ }
+ else
+ {
+ expression = $"{resultType}.From{relation.ResultUnit.PluralName}({expression})";
+ }
+
+ Writer.WL($@"
+ /// Get from {relation.Operator} .
+ public static {resultType} operator {relation.Operator}({leftParameterType} {leftParameterName}, {rightParameterType} {rightParameterName})
{{
return {expression};
}}
@@ -796,88 +971,82 @@ private void GenerateEqualityAndComparison()
/// Returns true if less or equal to.
public static bool operator <=({_quantity.Name} left, {_quantity.Name} right)
{{
- return left.Value <= right.ToUnit(left.Unit).Value;
+ return left.Value <= right.As(left.Unit);
}}
/// Returns true if greater than or equal to.
public static bool operator >=({_quantity.Name} left, {_quantity.Name} right)
{{
- return left.Value >= right.ToUnit(left.Unit).Value;
+ return left.Value >= right.As(left.Unit);
}}
/// Returns true if less than.
public static bool operator <({_quantity.Name} left, {_quantity.Name} right)
{{
- return left.Value < right.ToUnit(left.Unit).Value;
+ return left.Value < right.As(left.Unit);
}}
/// Returns true if greater than.
public static bool operator >({_quantity.Name} left, {_quantity.Name} right)
{{
- return left.Value > right.ToUnit(left.Unit).Value;
+ return left.Value > right.As(left.Unit);
}}
- // We use obsolete attribute to communicate the preferred equality members to use.
- // CS0809: Obsolete member 'memberA' overrides non-obsolete member 'memberB'.
- #pragma warning disable CS0809
-
- /// Indicates strict equality of two quantities, where both and are exactly equal.
- [Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For equality checks, use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
+ /// Indicates strict equality of two quantities.
public static bool operator ==({_quantity.Name} left, {_quantity.Name} right)
{{
return left.Equals(right);
}}
- /// Indicates strict inequality of two quantities, where both and are exactly equal.
- [Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For equality checks, use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
+ /// Indicates strict inequality of two quantities.
public static bool operator !=({_quantity.Name} left, {_quantity.Name} right)
{{
return !(left == right);
}}
///
- /// Indicates strict equality of two quantities, where both and are exactly equal.
- [Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
+ /// Indicates strict equality of two quantities.
public override bool Equals(object? obj)
{{
- if (obj is null || !(obj is {_quantity.Name} otherQuantity))
+ if (obj is not {_quantity.Name} otherQuantity)
return false;
return Equals(otherQuantity);
}}
///
- /// Indicates strict equality of two quantities, where both and are exactly equal.
- [Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
+ /// Indicates strict equality of two quantities.
public bool Equals({_quantity.Name} other)
{{
- return new {{ Value, Unit }}.Equals(new {{ other.Value, other.Unit }});
+ return _value.Equals(other.As(this.Unit));
}}
- #pragma warning restore CS0809
-
- /// Compares the current with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other when converted to the same unit.
+ ///
+ /// Returns the hash code for this instance.
+ ///
+ /// A hash code for the current {_quantity.Name}.
+ public override int GetHashCode()
+ {{
+ return Comparison.GetHashCode(typeof({_quantity.Name}), this.As(BaseUnit));
+ }}
+
+ ///
/// An object to compare with this instance.
///
/// is not the same type as this instance.
///
- /// A value that indicates the relative order of the quantities being compared. The return value has these meanings:
- ///
- /// Value Meaning
- /// - Less than zero This instance precedes in the sort order.
- /// - Zero This instance occurs in the same position in the sort order as .
- /// - Greater than zero This instance follows in the sort order.
- ///
- ///
public int CompareTo(object? obj)
{{
- if (obj is null) throw new ArgumentNullException(nameof(obj));
- if (!(obj is {_quantity.Name} otherQuantity)) throw new ArgumentException(""Expected type {_quantity.Name}."", nameof(obj));
+ if (obj is not {_quantity.Name} otherQuantity)
+ throw obj is null ? new ArgumentNullException(nameof(obj)) : ExceptionHelper.CreateArgumentException<{_quantity.Name}>(obj, nameof(obj));
return CompareTo(otherQuantity);
}}
- /// Compares the current with another and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other when converted to the same unit.
+ ///
+ /// Compares the current with another and returns an integer that indicates
+ /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the other quantity, when converted to the same unit.
+ ///
/// A quantity to compare with this instance.
/// A value that indicates the relative order of the quantities being compared. The return value has these meanings:
///
@@ -889,257 +1058,88 @@ public int CompareTo(object? obj)
///
public int CompareTo({_quantity.Name} other)
{{
- return _value.CompareTo(other.ToUnit(this.Unit).Value);
- }}
-
- ///
- ///
- /// Compare equality to another {_quantity.Name} within the given absolute or relative tolerance.
- ///
- ///
- /// Relative tolerance is defined as the maximum allowable absolute difference between this quantity's value and
- /// as a percentage of this quantity's value. will be converted into
- /// this quantity's unit for comparison. A relative tolerance of 0.01 means the absolute difference must be within +/- 1% of
- /// this quantity's value to be considered equal.
- ///
- /// In this example, the two quantities will be equal if the value of b is within +/- 1% of a (0.02m or 2cm).
- ///
- /// var a = Length.FromMeters(2.0);
- /// var b = Length.FromInches(50.0);
- /// a.Equals(b, 0.01, ComparisonType.Relative);
- ///
- ///
- ///
- ///
- /// Absolute tolerance is defined as the maximum allowable absolute difference between this quantity's value and
- /// as a fixed number in this quantity's unit. will be converted into
- /// this quantity's unit for comparison.
- ///
- /// In this example, the two quantities will be equal if the value of b is within 0.01 of a (0.01m or 1cm).
- ///
- /// var a = Length.FromMeters(2.0);
- /// var b = Length.FromInches(50.0);
- /// a.Equals(b, 0.01, ComparisonType.Absolute);
- ///
- ///
- ///
- ///
- /// Note that it is advised against specifying zero difference, due to the nature
- /// of floating-point operations and using double internally.
- ///
- ///
- /// The other quantity to compare to.
- /// The absolute or relative tolerance value. Must be greater than or equal to 0.
- /// The comparison type: either relative or absolute.
- /// True if the absolute difference between the two values is not greater than the specified relative or absolute tolerance.
- [Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")]
- public bool Equals({_quantity.Name} other, double tolerance, ComparisonType comparisonType)
- {{
- if (tolerance < 0)
- throw new ArgumentOutOfRangeException(nameof(tolerance), ""Tolerance must be greater than or equal to 0."");
-
- return UnitsNet.Comparison.Equals(
- referenceValue: this.Value,
- otherValue: other.As(this.Unit),
- tolerance: tolerance,
- comparisonType: comparisonType);
+ return _value.CompareTo(other.As(this.Unit));
}}
-
+");
+ // TODO see about removing this
+#if EXTENDED_EQUALS_INTERFACE
+
+ if (_quantity.IsAffine)
+ {
+ Writer.WL($@"
///
public bool Equals(IQuantity? other, IQuantity tolerance)
{{
- return other is {_quantity.Name} otherTyped
- && (tolerance is {_quantity.Name} toleranceTyped
- ? true
- : throw new ArgumentException($""Tolerance quantity ({{tolerance.QuantityInfo.Name}}) did not match the other quantities of type '{_quantity.Name}'."", nameof(tolerance)))
- && Equals(otherTyped, toleranceTyped);
+ #if NET
+ return this.Equals<{_quantity.Name}, {_quantity.AffineOffsetType}>(other, tolerance);
+ #else
+ return AffineQuantityExtensions.Equals(this, other, tolerance);
+ #endif
}}
///
- public bool Equals({_quantity.Name} other, {_quantity.Name} tolerance)
+ public bool Equals({_quantity.Name} other, {_quantity.AffineOffsetType} tolerance)
{{
- return UnitsNet.Comparison.Equals(
- referenceValue: this.Value,
- otherValue: other.As(this.Unit),
- tolerance: tolerance.As(this.Unit),
- comparisonType: ComparisonType.Absolute);
+ return this.EqualsAbsolute(other, tolerance);
}}
-
- ///
- /// Returns the hash code for this instance.
- ///
- /// A hash code for the current {_quantity.Name}.
- public override int GetHashCode()
- {{
- return new {{ Info.Name, Value, Unit }}.GetHashCode();
- }}
-
- #endregion
");
- }
-
- private void GenerateConversionMethods()
- {
- Writer.WL($@"
- #region Conversion Methods
-
- ///
- /// Convert to the unit representation .
- ///
- /// Value converted to the specified unit.
- public double As({_unitEnumName} unit)
+ }
+ else if (_quantity.Logarithmic)
+ {
+ Writer.WL($@"
+ ///
+ public bool Equals(IQuantity? other, IQuantity tolerance)
{{
- if (Unit == unit)
- return Value;
-
- return ToUnit(unit).Value;
+ return LogarithmicQuantityExtensions.Equals(this, other, tolerance);
}}
-");
- Writer.WL( $@"
-
- ///
- public double As(UnitSystem unitSystem)
+ ///
+ public bool Equals({_quantity.Name} other, {_quantity.Name} tolerance)
{{
- return As(Info.GetDefaultUnit(unitSystem));
+ return this.EqualsAbsolute(other, tolerance);
}}
");
-
- Writer.WL($@"
- ///
- /// Converts this {_quantity.Name} to another {_quantity.Name} with the unit representation .
- ///
- /// The unit to convert to.
- /// A {_quantity.Name} with the specified unit.
- public {_quantity.Name} ToUnit({_unitEnumName} unit)
+ }
+ else
+ {
+ Writer.WL($@"
+ ///
+ public bool Equals(IQuantity? other, IQuantity tolerance)
{{
- return ToUnit(unit, DefaultConversionFunctions);
+ return VectorQuantityExtensions.Equals(this, other, tolerance);
}}
- ///
- /// Converts this to another using the given with the unit representation .
- ///
- /// The unit to convert to.
- /// The to use for the conversion.
- /// A {_quantity.Name} with the specified unit.
- public {_quantity.Name} ToUnit({_unitEnumName} unit, UnitConverter unitConverter)
+ ///
+ public bool Equals({_quantity.Name} other, {_quantity.Name} tolerance)
{{
- if (TryToUnit(unit, out var converted))
- {{
- // Try to convert using the auto-generated conversion methods.
- return converted!.Value;
- }}
- else if (unitConverter.TryGetConversionFunction((typeof({_quantity.Name}), Unit, typeof({_quantity.Name}), unit), out var conversionFunction))
- {{
- // See if the unit converter has an extensibility conversion registered.
- return ({_quantity.Name})conversionFunction(this);
- }}
- else if (Unit != BaseUnit)
- {{
- // Conversion to requested unit NOT found. Try to convert to BaseUnit, and then from BaseUnit to requested unit.
- var inBaseUnits = ToUnit(BaseUnit);
- return inBaseUnits.ToUnit(unit);
- }}
- else
- {{
- // No possible conversion
- throw new NotImplementedException($""Can not convert {{Unit}} to {{unit}}."");
- }}
+ return this.EqualsAbsolute(other, tolerance);
}}
-
- ///
- /// Attempts to convert this to another with the unit representation .
- ///
- /// The unit to convert to.
- /// The converted in , if successful.
- /// True if successful, otherwise false.
- private bool TryToUnit({_unitEnumName} unit, [NotNullWhen(true)] out {_quantity.Name}? converted)
- {{
- if (Unit == unit)
- {{
- converted = this;
- return true;
- }}
-
- {_quantity.Name}? convertedOrNull = (Unit, unit) switch
- {{
- // {_unitEnumName} -> BaseUnit");
-
- foreach (Unit unit in _quantity.Units)
- {
- if (unit.SingularName == _quantity.BaseUnit) continue;
-
- var func = unit.FromUnitToBaseFunc.Replace("{x}", "_value");
- Writer.WL($@"
- ({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}) => new {_quantity.Name}({func}, {_unitEnumName}.{_quantity.BaseUnit}),");
- }
-
- Writer.WL();
- Writer.WL($@"
-
- // BaseUnit -> {_unitEnumName}");
- foreach(Unit unit in _quantity.Units)
- {
- if (unit.SingularName == _quantity.BaseUnit) continue;
-
- var func = unit.FromBaseToUnitFunc.Replace("{x}", "_value");
- Writer.WL($@"
- ({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}) => new {_quantity.Name}({func}, {_unitEnumName}.{unit.SingularName}),");
+");
+
}
-
- Writer.WL();
+#endif
Writer.WL($@"
- _ => null
- }};
- if (convertedOrNull is null)
- {{
- converted = default;
- return false;
- }}
-
- converted = convertedOrNull.Value;
- return true;
- }}
-");
- Writer.WL($@"
- ///
- public {_quantity.Name} ToUnit(UnitSystem unitSystem)
- {{
- return ToUnit(Info.GetDefaultUnit(unitSystem));
- }}
+ #endregion
");
+ }
+ private void GenerateConversionMethods()
+ {
Writer.WL($@"
- #region Explicit implementations
+ #region Conversion Methods (explicit implementations for netstandard2.0)
- double IQuantity.As(Enum unit)
- {{
- if (unit is not {_unitEnumName} typedUnit)
- throw new ArgumentException($""The given unit is of type {{unit.GetType()}}. Only {{typeof({_unitEnumName})}} is supported."", nameof(unit));
+#if NETSTANDARD2_0
+ QuantityValue IQuantity.As(Enum unit) => UnitConverter.Default.ConvertValue(Value, UnitKey.ForUnit(Unit), unit);
- return As(typedUnit);
- }}
+ IQuantity IQuantity.ToUnit(Enum unit) => UnitConverter.Default.ConvertTo(this, unit);
- ///
- IQuantity IQuantity.ToUnit(Enum unit)
- {{
- if (!(unit is {_unitEnumName} typedUnit))
- throw new ArgumentException($""The given unit is of type {{unit.GetType()}}. Only {{typeof({_unitEnumName})}} is supported."", nameof(unit));
-
- return ToUnit(typedUnit, DefaultConversionFunctions);
- }}
+ IQuantity IQuantity.ToUnit(UnitSystem unitSystem) => this.ToUnit(unitSystem);
- ///
- IQuantity IQuantity.ToUnit(UnitSystem unitSystem) => ToUnit(unitSystem);
-
- ///
- IQuantity<{_unitEnumName}> IQuantity<{_unitEnumName}>.ToUnit({_unitEnumName} unit) => ToUnit(unit);
+ IQuantity<{_unitEnumName}> IQuantity<{_unitEnumName}>.ToUnit({_unitEnumName} unit) => this.ToUnit(unit);
- ///
- IQuantity<{_unitEnumName}> IQuantity<{_unitEnumName}>.ToUnit(UnitSystem unitSystem) => ToUnit(unitSystem);
-
- #endregion
+ IQuantity<{_unitEnumName}> IQuantity<{_unitEnumName}>.ToUnit(UnitSystem unitSystem) => this.ToUnit(unitSystem);
+#endif
#endregion
");
@@ -1159,145 +1159,19 @@ public override string ToString()
return ToString(null, null);
}}
- ///
- /// Gets the default string representation of value and unit using the given format provider.
- ///
- /// String representation.
- /// Format to use for localization and number formatting. Defaults to if null.
- public string ToString(IFormatProvider? provider)
- {{
- return ToString(null, provider);
- }}
-
- ///
- ///
- /// Gets the string representation of this instance in the specified format string using .
- ///
- /// The format string.
- /// The string representation.
- public string ToString(string? format)
- {{
- return ToString(format, null);
- }}
-
- ///
+ ///
///
/// Gets the string representation of this instance in the specified format string using the specified format provider, or if null.
///
- /// The format string.
- /// Format to use for localization and number formatting. Defaults to if null.
- /// The string representation.
public string ToString(string? format, IFormatProvider? provider)
{{
- return QuantityFormatter.Format<{_unitEnumName}>(this, format, provider);
+ return QuantityFormatter.Default.Format(this, format, provider);
}}
#endregion
" );
}
- private void GenerateIConvertibleMethods()
- {
- Writer.WL($@"
- #region IConvertible Methods
-
- TypeCode IConvertible.GetTypeCode()
- {{
- return TypeCode.Object;
- }}
-
- bool IConvertible.ToBoolean(IFormatProvider? provider)
- {{
- throw new InvalidCastException($""Converting {{typeof({_quantity.Name})}} to bool is not supported."");
- }}
-
- byte IConvertible.ToByte(IFormatProvider? provider)
- {{
- return Convert.ToByte(_value);
- }}
-
- char IConvertible.ToChar(IFormatProvider? provider)
- {{
- throw new InvalidCastException($""Converting {{typeof({_quantity.Name})}} to char is not supported."");
- }}
-
- DateTime IConvertible.ToDateTime(IFormatProvider? provider)
- {{
- throw new InvalidCastException($""Converting {{typeof({_quantity.Name})}} to DateTime is not supported."");
- }}
-
- decimal IConvertible.ToDecimal(IFormatProvider? provider)
- {{
- return Convert.ToDecimal(_value);
- }}
-
- double IConvertible.ToDouble(IFormatProvider? provider)
- {{
- return Convert.ToDouble(_value);
- }}
-
- short IConvertible.ToInt16(IFormatProvider? provider)
- {{
- return Convert.ToInt16(_value);
- }}
-
- int IConvertible.ToInt32(IFormatProvider? provider)
- {{
- return Convert.ToInt32(_value);
- }}
-
- long IConvertible.ToInt64(IFormatProvider? provider)
- {{
- return Convert.ToInt64(_value);
- }}
-
- sbyte IConvertible.ToSByte(IFormatProvider? provider)
- {{
- return Convert.ToSByte(_value);
- }}
-
- float IConvertible.ToSingle(IFormatProvider? provider)
- {{
- return Convert.ToSingle(_value);
- }}
-
- string IConvertible.ToString(IFormatProvider? provider)
- {{
- return ToString(null, provider);
- }}
-
- object IConvertible.ToType(Type conversionType, IFormatProvider? provider)
- {{
- if (conversionType == typeof({_quantity.Name}))
- return this;
- else if (conversionType == typeof({_unitEnumName}))
- return Unit;
- else if (conversionType == typeof(QuantityInfo))
- return {_quantity.Name}.Info;
- else if (conversionType == typeof(BaseDimensions))
- return {_quantity.Name}.BaseDimensions;
- else
- throw new InvalidCastException($""Converting {{typeof({_quantity.Name})}} to {{conversionType}} is not supported."");
- }}
-
- ushort IConvertible.ToUInt16(IFormatProvider? provider)
- {{
- return Convert.ToUInt16(_value);
- }}
-
- uint IConvertible.ToUInt32(IFormatProvider? provider)
- {{
- return Convert.ToUInt32(_value);
- }}
-
- ulong IConvertible.ToUInt64(IFormatProvider? provider)
- {{
- return Convert.ToUInt64(_value);
- }}
-
- #endregion");
- }
-
///
private static string? GetObsoleteAttributeOrNull(Quantity quantity) => GetObsoleteAttributeOrNull(quantity.ObsoleteText);
diff --git a/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs b/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs
index 69443fa2d5..1577fd7f5d 100644
--- a/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs
+++ b/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs
@@ -1,4 +1,5 @@
-using CodeGen.Helpers;
+using System.Collections.Generic;
+using System.Linq;
using CodeGen.JsonTypes;
namespace CodeGen.Generators.UnitsNetGen
@@ -16,123 +17,64 @@ public string Generate()
{
Writer.WL(GeneratedFileHeader);
Writer.WL(@"
-using System;
-using System.Globalization;
-using UnitsNet.Units;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
#nullable enable
-namespace UnitsNet
+namespace UnitsNet;
+
+///
+/// Dynamically parse or construct quantities when types are only known at runtime.
+///
+public partial class Quantity
{
///
- /// Dynamically parse or construct quantities when types are only known at runtime.
+ /// Serves as a repository for predefined quantity conversion mappings, facilitating the automatic generation and retrieval of unit conversions in the UnitsNet library.
///
- public partial class Quantity
+ internal static class Provider
{
///
- /// All QuantityInfo instances mapped by quantity name that are present in UnitsNet by default.
+ /// All QuantityInfo instances that are present in UnitsNet by default.
///
- public static readonly IDictionary ByName = new Dictionary
+ internal static IReadOnlyList DefaultQuantities => new QuantityInfo[]
{");
foreach (var quantity in _quantities)
Writer.WL($@"
- {{ ""{quantity.Name}"", {quantity.Name}.Info }},");
+ {quantity.Name}.Info,");
Writer.WL(@"
};
///
- /// Dynamically constructs a quantity of the given with the value in the quantity's base units.
+ /// All implicit quantity conversions that exist by default.
///
- /// The of the quantity to create.
- /// The value to construct the quantity with.
- /// The created quantity.
- public static IQuantity FromQuantityInfo(QuantityInfo quantityInfo, double value)
- {
- return quantityInfo.Name switch
- {");
- foreach (var quantity in _quantities)
- {
- var quantityName = quantity.Name;
+ internal static readonly IReadOnlyList DefaultConversions = new QuantityConversionMapping[]
+ {");
+ foreach (var quantityRelation in _quantities.SelectMany(quantity => quantity.Relations.Where(x => x.Operator == "inverse")).Distinct(new CumulativeRelationshipEqualityComparer()).OrderBy(relation => relation.LeftQuantity.Name))
Writer.WL($@"
- ""{quantityName}"" => {quantityName}.From(value, {quantityName}.BaseUnit),");
- }
-
+ new (typeof({quantityRelation.LeftQuantity.Name}), typeof({quantityRelation.RightQuantity.Name})),");
Writer.WL(@"
- _ => throw new ArgumentException($""{quantityInfo.Name} is not a supported quantity."")
- };
+ };
+ }
+}");
+ return Writer.ToString();
}
+ }
- ///
- /// Try to dynamically construct a quantity.
- ///
- /// Numeric value.
- /// Unit enum value.
- /// The resulting quantity if successful, otherwise default.
- /// True if successful with assigned the value, otherwise false.
- public static bool TryFrom(double value, Enum? unit, [NotNullWhen(true)] out IQuantity? quantity)
+ internal class CumulativeRelationshipEqualityComparer: IEqualityComparer{
+ public bool Equals(QuantityRelation? x, QuantityRelation? y)
{
- quantity = unit switch
- {");
- foreach (var quantity in _quantities)
- {
- var quantityName = quantity.Name;
- var unitTypeName = $"{quantityName}Unit";
- var unitValue = unitTypeName.ToCamelCase();
- Writer.WL($@"
- {unitTypeName} {unitValue} => {quantityName}.From(value, {unitValue}),");
- }
-
- Writer.WL(@"
- _ => null
- };
-
- return quantity is not null;
+ if (ReferenceEquals(x, y)) return true;
+ if (x is null) return false;
+ if (y is null) return false;
+ if (x.GetType() != y.GetType()) return false;
+ return
+ x.ResultQuantity == y.ResultQuantity && (
+ (x.LeftQuantity.Equals(y.LeftQuantity) && x.RightQuantity.Equals(y.RightQuantity))
+ || (x.LeftQuantity.Equals(y.RightQuantity) && x.RightQuantity.Equals(y.LeftQuantity)));
}
- ///
- /// Try to dynamically parse a quantity string representation.
- ///
- /// The format provider to use for lookup. Defaults to if null.
- /// Type of quantity, such as .
- /// Quantity string representation, such as ""1.5 kg"". Must be compatible with given quantity type.
- /// The resulting quantity if successful, otherwise default.
- /// The parsed quantity.
- public static bool TryParse(IFormatProvider? formatProvider, Type quantityType, [NotNullWhen(true)] string? quantityString, [NotNullWhen(true)] out IQuantity? quantity)
+ public int GetHashCode(QuantityRelation obj)
{
- quantity = default(IQuantity);
-
- if (!typeof(IQuantity).IsAssignableFrom(quantityType))
- return false;
-
- var parser = UnitsNetSetup.Default.QuantityParser;
-
- return quantityType switch
- {");
- foreach (var quantity in _quantities)
- {
- var quantityName = quantity.Name;
- Writer.WL($@"
- Type _ when quantityType == typeof({quantityName}) => parser.TryParse<{quantityName}, {quantityName}Unit>(quantityString, formatProvider, {quantityName}.From, out quantity),");
- }
-
- Writer.WL(@"
- _ => false
- };
- }
-
- internal static IEnumerable GetQuantityTypes()
- {");
- foreach (var quantity in _quantities)
- Writer.WL($@"
- yield return typeof({quantity.Name});");
- Writer.WL(@"
- }
- }
-}");
- return Writer.ToString();
+ return obj.LeftQuantity.GetHashCode() ^ obj.RightQuantity.GetHashCode();
}
}
}
diff --git a/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs b/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs
index 625e9a8661..18320577ef 100644
--- a/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs
+++ b/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using CodeGen.JsonTypes;
@@ -58,7 +59,7 @@ internal class UnitTestBaseClassGenerator : GeneratorBase
/// A dimensionless quantity has all base dimensions (L, M, T, I, Θ, N, J) equal to zero.
///
private readonly bool _isDimensionless;
-
+
///
/// Stores a mapping of culture names to their corresponding unique unit abbreviations.
/// Each culture maps to a dictionary where the key is the unit abbreviation and the value is the corresponding
@@ -82,6 +83,16 @@ internal class UnitTestBaseClassGenerator : GeneratorBase
///
private readonly Dictionary>> _ambiguousAbbreviationsForCulture;
+ ///
+ /// A dictionary that maps culture names to their respective dictionaries of units and their default abbreviations.
+ ///
+ ///
+ /// This field is used to store the default abbreviation for each unit in a specific culture.
+ /// The outer dictionary key represents the culture name (e.g., "en-US"), while the inner dictionary maps a
+ /// to its default abbreviation.
+ ///
+ private readonly Dictionary> _defaultAbbreviationsForCulture;
+
///
/// The default or fallback culture for unit localizations.
///
@@ -90,7 +101,7 @@ internal class UnitTestBaseClassGenerator : GeneratorBase
/// is not available for the defined unit localizations.
///
private const string FallbackCultureName = "en-US";
-
+
public UnitTestBaseClassGenerator(Quantity quantity)
{
_quantity = quantity;
@@ -107,7 +118,8 @@ public UnitTestBaseClassGenerator(Quantity quantity)
_otherOrBaseUnit = quantity.Units.Where(u => u != _baseUnit).DefaultIfEmpty(_baseUnit).First();
_otherOrBaseUnitFullName = $"{_unitEnumName}.{_otherOrBaseUnit.SingularName}";
_isDimensionless = quantity.BaseDimensions is { L: 0, M: 0, T: 0, I: 0, Θ: 0, N: 0, J: 0 };
-
+
+ _defaultAbbreviationsForCulture = new Dictionary>();
var abbreviationsForCulture = new Dictionary>>();
foreach (Unit unit in quantity.Units)
{
@@ -118,6 +130,11 @@ public UnitTestBaseClassGenerator(Quantity quantity)
foreach (Localization localization in unit.Localization)
{
+ if (localization.Abbreviations.Length == 0)
+ {
+ continue;
+ }
+
if (!abbreviationsForCulture.TryGetValue(localization.Culture, out Dictionary>? localizationsForCulture))
{
abbreviationsForCulture[localization.Culture] = localizationsForCulture = new Dictionary>();
@@ -134,6 +151,13 @@ public UnitTestBaseClassGenerator(Quantity quantity)
localizationsForCulture[abbreviation] = [unit];
}
}
+
+ if (!_defaultAbbreviationsForCulture.TryGetValue(localization.Culture, out Dictionary? defaultLocalizationsForCulture))
+ {
+ _defaultAbbreviationsForCulture[localization.Culture] = defaultLocalizationsForCulture = new Dictionary();
+ }
+
+ defaultLocalizationsForCulture.Add(unit, localization.Abbreviations[0]);
}
}
@@ -238,6 +262,7 @@ public abstract partial class {_quantity.Name}TestsBase : QuantityTestsBase
Writer.WL($@"
new object[] {{ {GetUnitFullName(unit)} }},");
}
+
Writer.WL($@"
}};
@@ -282,7 +307,7 @@ public virtual void Ctor_SIUnitSystem_ReturnsQuantityWithSIUnits()
{{
var quantity = new {_quantity.Name}(value: 1, unitSystem: UnitSystem.SI);
Assert.Equal(1, quantity.Value);
- Assert.True(quantity.QuantityInfo.UnitInfos.First(x => x.Value == quantity.Unit).BaseUnits.IsSubsetOf(UnitSystem.SI.BaseUnits));
+ Assert.True(quantity.QuantityInfo[quantity.Unit].BaseUnits.IsSubsetOf(UnitSystem.SI.BaseUnits));
}}
[Fact]
@@ -299,15 +324,33 @@ public void Ctor_UnitSystem_ThrowsArgumentExceptionIfNotSupported()
[Fact]
public void {_quantity.Name}_QuantityInfo_ReturnsQuantityInfoDescribingQuantity()
{{
+ {_unitEnumName}[] unitsOrderedByName = EnumUtils.GetEnumValues<{_unitEnumName}>().OrderBy(x => x.ToString()).ToArray();
var quantity = new {_quantity.Name}(1, {_baseUnitFullName});
- QuantityInfo<{_unitEnumName}> quantityInfo = quantity.QuantityInfo;
+ QuantityInfo<{_quantity.Name}, {_unitEnumName}> quantityInfo = quantity.QuantityInfo;
- Assert.Equal({_quantity.Name}.Zero, quantityInfo.Zero);
Assert.Equal(""{_quantity.Name}"", quantityInfo.Name);
+ Assert.Equal({_quantity.Name}.Zero, quantityInfo.Zero);
+ Assert.Equal({_quantity.Name}.BaseUnit, quantityInfo.BaseUnitInfo.Value);
+ Assert.Equal(unitsOrderedByName, quantityInfo.Units);
+ Assert.Equal(unitsOrderedByName, quantityInfo.UnitInfos.Select(x => x.Value));
+ Assert.Equal({_quantity.Name}.Info, quantityInfo);
+ Assert.Equal(quantityInfo, ((IQuantity)quantity).QuantityInfo);
+ Assert.Equal(quantityInfo, ((IQuantity<{_unitEnumName}>)quantity).QuantityInfo);
+ }}
+
+ [Fact]
+ public void {_quantity.Name}Info_CreateWithCustomUnitInfos()
+ {{
+ {_unitEnumName}[] expectedUnits = [{_baseUnitFullName}];
+
+ {_quantity.Name}.{_quantity.Name}Info quantityInfo = {_quantity.Name}.{_quantity.Name}Info.CreateDefault(mappings => mappings.SelectUnits(expectedUnits));
- var units = EnumUtils.GetEnumValues<{_unitEnumName}>().OrderBy(x => x.ToString()).ToArray();
- var unitNames = units.Select(x => x.ToString());
+ Assert.Equal(""{_quantity.Name}"", quantityInfo.Name);
+ Assert.Equal({_quantity.Name}.Zero, quantityInfo.Zero);
+ Assert.Equal({_quantity.Name}.BaseUnit, quantityInfo.BaseUnitInfo.Value);
+ Assert.Equal(expectedUnits, quantityInfo.Units);
+ Assert.Equal(expectedUnits, quantityInfo.UnitInfos.Select(x => x.Value));
}}
[Fact]
@@ -329,7 +372,7 @@ public void From_ValueAndUnit_ReturnsQuantityWithSameValueAndUnit()
var quantityVariable = $"quantity{i++:D2}";
Writer.WL($@"
var {quantityVariable} = {_quantity.Name}.From(1, {GetUnitFullName(unit)});
- AssertEx.EqualTolerance(1, {quantityVariable}.{unit.PluralName}, {unit.PluralName}Tolerance);
+ Assert.Equal(1, {quantityVariable}.{unit.PluralName});
Assert.Equal({GetUnitFullName(unit)}, {quantityVariable}.Unit);
");
@@ -547,50 +590,93 @@ public void ToUnit_UnitSystem_ThrowsArgumentExceptionIfNotSupported()
}}
");
}
-
+
Writer.WL($@"
-
- [Fact]
- public void Parse()
- {{");
- foreach (var unit in _quantity.Units.Where(u => string.IsNullOrEmpty(u.ObsoleteText)))
- foreach (var localization in unit.Localization)
- foreach (var abbreviation in localization.Abbreviations)
+ [Theory]");
+ foreach ((var cultureName, Dictionary abbreviations) in _uniqueAbbreviationsForCulture)
{
- Writer.WL($@"
- try
- {{
- var parsed = {_quantity.Name}.Parse(""1 {abbreviation}"", CultureInfo.GetCultureInfo(""{localization.Culture}""));
- AssertEx.EqualTolerance(1, parsed.{unit.PluralName}, {unit.PluralName}Tolerance);
- Assert.Equal({GetUnitFullName(unit)}, parsed.Unit);
- }} catch (AmbiguousUnitParseException) {{ /* Some units have the same abbreviations */ }}
-");
+ var culture = CultureInfo.GetCultureInfo(cultureName);
+ foreach ((var abbreviation, Unit unit) in abbreviations)
+ {
+ Writer.WL($@"
+ [InlineData(""{cultureName}"", ""{4.2m.ToString(culture)} {abbreviation}"", {GetUnitFullName(unit)}, 4.2)]");
+ }
}
Writer.WL($@"
+ public void Parse(string culture, string quantityString, {_unitEnumName} expectedUnit, decimal expectedValue)
+ {{
+ using var _ = new CultureScope(culture);
+ var parsed = {_quantity.Name}.Parse(quantityString);
+ Assert.Equal(expectedUnit, parsed.Unit);
+ Assert.Equal(expectedValue, parsed.Value);
}}
+");
- [Fact]
- public void TryParse()
- {{");
- foreach (var unit in _quantity.Units.Where(u => string.IsNullOrEmpty(u.ObsoleteText)))
- foreach (var localization in unit.Localization)
- foreach (var abbreviation in localization.Abbreviations)
+ // we only generate these for a few of the quantities
+ if (_ambiguousAbbreviationsForCulture.Count != 0)
{
- // Skip units with ambiguous abbreviations, since there is no exception to describe this is why TryParse failed.
- if (IsAmbiguousAbbreviation(localization, abbreviation)) continue;
-
Writer.WL($@"
- {{
- Assert.True({_quantity.Name}.TryParse(""1 {abbreviation}"", CultureInfo.GetCultureInfo(""{localization.Culture}""), out var parsed));
- AssertEx.EqualTolerance(1, parsed.{unit.PluralName}, {unit.PluralName}Tolerance);
- Assert.Equal({GetUnitFullName(unit)}, parsed.Unit);
- }}
+ [Theory]");
+ foreach ((var cultureName, Dictionary>? abbreviations) in _ambiguousAbbreviationsForCulture)
+ {
+ foreach (KeyValuePair> ambiguousPair in abbreviations)
+ {
+ Writer.WL($@"
+ [InlineData(""{cultureName}"", ""1 {ambiguousPair.Key}"")] // [{string.Join(", ", ambiguousPair.Value.Select(x => x.SingularName))}] ");
+ }
+ }
+ Writer.WL($@"
+ public void ParseWithAmbiguousAbbreviation(string culture, string quantityString)
+ {{
+ Assert.Throws(() => {_quantity.Name}.Parse(quantityString, CultureInfo.GetCultureInfo(culture)));
+ }}
");
+ } // ambiguousAbbreviations
+
+
+ Writer.WL($@"
+ [Theory]");
+ foreach ((var cultureName, Dictionary abbreviations) in _uniqueAbbreviationsForCulture)
+ {
+ var culture = CultureInfo.GetCultureInfo(cultureName);
+ foreach ((var abbreviation, Unit unit) in abbreviations)
+ {
+ Writer.WL($@"
+ [InlineData(""{cultureName}"", ""{4.2m.ToString(culture)} {abbreviation}"", {GetUnitFullName(unit)}, 4.2)]");
+ }
}
Writer.WL($@"
+ public void TryParse(string culture, string quantityString, {_unitEnumName} expectedUnit, decimal expectedValue)
+ {{
+ using var _ = new CultureScope(culture);
+ Assert.True({_quantity.Name}.TryParse(quantityString, out {_quantity.Name} parsed));
+ Assert.Equal(expectedUnit, parsed.Unit);
+ Assert.Equal(expectedValue, parsed.Value);
}}
");
+
+ // we only generate these for a few of the quantities
+ if (_ambiguousAbbreviationsForCulture.Count != 0)
+ {
+ Writer.WL($@"
+ [Theory]");
+ foreach ((var cultureName, Dictionary>? abbreviations) in _ambiguousAbbreviationsForCulture)
+ {
+ foreach (KeyValuePair> ambiguousPair in abbreviations)
+ {
+ Writer.WL($@"
+ [InlineData(""{cultureName}"", ""1 {ambiguousPair.Key}"")] // [{string.Join(", ", ambiguousPair.Value.Select(x => x.SingularName))}] ");
+ }
+ }
+ Writer.WL($@"
+ public void TryParseWithAmbiguousAbbreviation(string culture, string quantityString)
+ {{
+ Assert.False({_quantity.Name}.TryParse(quantityString, CultureInfo.GetCultureInfo(culture), out _));
+ }}
+");
+ } // ambiguousAbbreviations
+
Writer.WL($@"
[Theory]");
foreach ((var abbreviation, Unit unit) in _uniqueAbbreviationsForCulture[FallbackCultureName])
@@ -775,6 +861,38 @@ public void TryParseUnitWithAmbiguousAbbreviation(string culture, string abbrevi
");
} // ambiguousAbbreviations
+ Writer.WL($@"
+ [Theory]");
+ foreach ((var cultureName, Dictionary abbreviations) in _defaultAbbreviationsForCulture)
+ {
+ foreach ((Unit unit, var abbreviation) in abbreviations)
+ {
+ Writer.WL($@"
+ [InlineData(""{cultureName}"", {GetUnitFullName(unit)}, ""{abbreviation}"")]");
+ }
+ }
+ Writer.WL($@"
+ public void GetAbbreviationForCulture(string culture, {_unitEnumName} unit, string expectedAbbreviation)
+ {{
+ var defaultAbbreviation = {_quantity.Name}.GetAbbreviation(unit, CultureInfo.GetCultureInfo(culture));
+ Assert.Equal(expectedAbbreviation, defaultAbbreviation);
+ }}
+");
+ Writer.WL($@"
+ [Fact]
+ public void GetAbbreviationWithDefaultCulture()
+ {{
+ Assert.All({_quantity.Name}.Units, unit =>
+ {{
+ var expectedAbbreviation = UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(unit);
+
+ var defaultAbbreviation = {_quantity.Name}.GetAbbreviation(unit);
+
+ Assert.Equal(expectedAbbreviation, defaultAbbreviation);
+ }});
+ }}
+");
+
Writer.WL($@"
[Theory]
[MemberData(nameof(UnitTypes))]
@@ -806,6 +924,7 @@ public void ToUnit_FromNonBaseUnit_ReturnsQuantityWithGivenUnit({_unitEnumName}
var quantity = {_quantity.Name}.From(3.0, fromUnit);
var converted = quantity.ToUnit(unit);
Assert.Equal(converted.Unit, unit);
+ Assert.Equal(quantity, converted);
}});
}}
@@ -829,20 +948,22 @@ public void ToUnit_FromIQuantity_ReturnsTheExpectedIQuantity({_unitEnumName} uni
IQuantity<{_unitEnumName}> quantityToConvert = quantity;
IQuantity<{_unitEnumName}> convertedQuantity = quantityToConvert.ToUnit(unit);
Assert.Equal(unit, convertedQuantity.Unit);
+ Assert.Equal(expectedQuantity, convertedQuantity);
}}, () =>
{{
IQuantity quantityToConvert = quantity;
IQuantity convertedQuantity = quantityToConvert.ToUnit(unit);
Assert.Equal(unit, convertedQuantity.Unit);
+ Assert.Equal(expectedQuantity, convertedQuantity);
}});
}}
[Fact]
public void ConversionRoundTrip()
{{
- {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(1);");
+ {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(3);");
foreach (var unit in _quantity.Units) Writer.WL($@"
- AssertEx.EqualTolerance(1, {_quantity.Name}.From{unit.PluralName}({baseUnitVariableName}.{unit.PluralName}).{_baseUnit.PluralName}, {unit.PluralName}Tolerance);");
+ Assert.Equal(3, {_quantity.Name}.From{unit.PluralName}({baseUnitVariableName}.{unit.PluralName}).{_baseUnit.PluralName});");
Writer.WL($@"
}}
");
@@ -854,13 +975,13 @@ public void ConversionRoundTrip()
public void LogarithmicArithmeticOperators()
{{
{_quantity.Name} v = {_quantity.Name}.From{_baseUnit.PluralName}(40);
- AssertEx.EqualTolerance(-40, -v.{_baseUnit.PluralName}, {unit.PluralName}Tolerance);
+ Assert.Equal(-40, -v.{_baseUnit.PluralName});
AssertLogarithmicAddition();
AssertLogarithmicSubtraction();
- AssertEx.EqualTolerance(50, (v*10).{_baseUnit.PluralName}, {unit.PluralName}Tolerance);
- AssertEx.EqualTolerance(50, (10*v).{_baseUnit.PluralName}, {unit.PluralName}Tolerance);
- AssertEx.EqualTolerance(35, (v/5).{_baseUnit.PluralName}, {unit.PluralName}Tolerance);
- AssertEx.EqualTolerance(35, v/{_quantity.Name}.From{_baseUnit.PluralName}(5), {unit.PluralName}Tolerance);
+ Assert.Equal(50, (v * 10).{_baseUnit.PluralName});
+ Assert.Equal(50, (10 * v).{_baseUnit.PluralName});
+ Assert.Equal(35, (v / 5).{_baseUnit.PluralName});
+ Assert.Equal(35, v / {_quantity.Name}.From{_baseUnit.PluralName}(5));
}}
protected abstract void AssertLogarithmicAddition();
@@ -868,20 +989,20 @@ public void LogarithmicArithmeticOperators()
protected abstract void AssertLogarithmicSubtraction();
");
}
- else if (_quantity.GenerateArithmetic)
+ else if (!_quantity.IsAffine)
{
Writer.WL($@"
[Fact]
public void ArithmeticOperators()
{{
{_quantity.Name} v = {_quantity.Name}.From{_baseUnit.PluralName}(1);
- AssertEx.EqualTolerance(-1, -v.{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance);
- AssertEx.EqualTolerance(2, ({_quantity.Name}.From{_baseUnit.PluralName}(3)-v).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance);
- AssertEx.EqualTolerance(2, (v + v).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance);
- AssertEx.EqualTolerance(10, (v*10).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance);
- AssertEx.EqualTolerance(10, (10*v).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance);
- AssertEx.EqualTolerance(2, ({_quantity.Name}.From{_baseUnit.PluralName}(10)/5).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance);
- AssertEx.EqualTolerance(2, {_quantity.Name}.From{_baseUnit.PluralName}(10)/{_quantity.Name}.From{_baseUnit.PluralName}(5), {_baseUnit.PluralName}Tolerance);
+ Assert.Equal(-1, -v.{_baseUnit.PluralName});
+ Assert.Equal(2, ({_quantity.Name}.From{_baseUnit.PluralName}(3) - v).{_baseUnit.PluralName});
+ Assert.Equal(2, (v + v).{_baseUnit.PluralName});
+ Assert.Equal(10, (v * 10).{_baseUnit.PluralName});
+ Assert.Equal(10, (10 * v).{_baseUnit.PluralName});
+ Assert.Equal(2, ({_quantity.Name}.From{_baseUnit.PluralName}(10) / 5).{_baseUnit.PluralName});
+ Assert.Equal(2, {_quantity.Name}.From{_baseUnit.PluralName}(10) / {_quantity.Name}.From{_baseUnit.PluralName}(5));
}}
");
}
@@ -934,13 +1055,6 @@ public void CompareToThrowsOnNull()
[Theory]
[InlineData(1, {_baseUnitFullName}, 1, {_baseUnitFullName}, true)] // Same value and unit.
[InlineData(1, {_baseUnitFullName}, 2, {_baseUnitFullName}, false)] // Different value.
- [InlineData(2, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different value and unit.");
- if (_baseUnit != _otherOrBaseUnit)
- {
- Writer.WL($@"
- [InlineData(1, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different unit.");
- }
- Writer.WL($@"
public void Equals_ReturnsTrue_IfValueAndUnitAreEqual(double valueA, {_unitEnumName} unitA, double valueB, {_unitEnumName} unitB, bool expectEqual)
{{
var a = new {_quantity.Name}(valueA, unitA);
@@ -978,35 +1092,104 @@ public void Equals_Null_ReturnsFalse()
}}
[Fact]
- public void Equals_RelativeTolerance_IsImplemented()
+ public void EqualsReturnsFalseOnTypeMismatch()
{{
- var v = {_quantity.Name}.From{_baseUnit.PluralName}(1);
- Assert.True(v.Equals({_quantity.Name}.From{_baseUnit.PluralName}(1), {_baseUnit.PluralName}Tolerance, ComparisonType.Relative));
- Assert.False(v.Equals({_quantity.Name}.Zero, {_baseUnit.PluralName}Tolerance, ComparisonType.Relative));
- Assert.True({_quantity.Name}.From{_baseUnit.PluralName}(100).Equals({_quantity.Name}.From{_baseUnit.PluralName}(120), 0.3, ComparisonType.Relative));
- Assert.False({_quantity.Name}.From{_baseUnit.PluralName}(100).Equals({_quantity.Name}.From{_baseUnit.PluralName}(120), 0.1, ComparisonType.Relative));
+ {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(1);
+ Assert.False({baseUnitVariableName}.Equals(new object()));
}}
[Fact]
- public void Equals_NegativeRelativeTolerance_ThrowsArgumentOutOfRangeException()
+ public void EqualsReturnsFalseOnNull()
{{
- var v = {_quantity.Name}.From{_baseUnit.PluralName}(1);
- Assert.Throws(() => v.Equals({_quantity.Name}.From{_baseUnit.PluralName}(1), -1, ComparisonType.Relative));
+ {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(1);
+ Assert.False({baseUnitVariableName}.Equals(null));
+ }}
+");
+ var differenceResultType = _quantity.AffineOffsetType ?? _quantity.Name;
+ if (_quantity.Logarithmic)
+ {
+ Writer.WL($@"
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(100, 110)]
+ [InlineData(100, 90)]
+ public void Equals_WithTolerance_IsImplemented(double firstValue, double secondValue)
+ {{
+ var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(firstValue);
+ var otherQuantity = {_quantity.Name}.From{_baseUnit.PluralName}(secondValue);
+ {differenceResultType} maxTolerance = quantity > otherQuantity ? quantity - otherQuantity : otherQuantity - quantity;
+ var largerTolerance = maxTolerance * 1.1m;
+ var smallerTolerance = maxTolerance / 1.1m;
+ Assert.True(quantity.Equals(quantity, {differenceResultType}.Zero));
+ Assert.True(quantity.Equals(quantity, maxTolerance));
+ Assert.True(quantity.Equals(otherQuantity, largerTolerance));
+ Assert.False(quantity.Equals(otherQuantity, smallerTolerance));
+ // note: it's currently not possible to test this due to the rounding error from (quantity - otherQuantity)
+ // Assert.True(quantity.Equals(otherQuantity, maxTolerance));
}}
[Fact]
- public void EqualsReturnsFalseOnTypeMismatch()
+ public void Equals_WithNegativeTolerance_DoesNotThrowArgumentOutOfRangeException()
{{
- {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(1);
- Assert.False({baseUnitVariableName}.Equals(new object()));
+ // note: unlike with vector quantities- a small tolerance maybe positive in one unit and negative in another
+ var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1);
+ var negativeTolerance = {_quantity.Name}.From{_baseUnit.PluralName}(-1);
+ Assert.True(quantity.Equals(quantity, negativeTolerance));
}}
+");
+ }
+ else // quantities with a linear scale
+ {
+ Writer.WL($@"
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(100, 110)]
+ [InlineData(100, 90)]
+ public void Equals_WithTolerance_IsImplemented(double firstValue, double secondValue)
+ {{
+ var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(firstValue);
+ var otherQuantity = {_quantity.Name}.From{_baseUnit.PluralName}(secondValue);
+ {differenceResultType} maxTolerance = quantity > otherQuantity ? quantity - otherQuantity : otherQuantity - quantity;
+ var largerTolerance = maxTolerance * 1.1m;
+ var smallerTolerance = maxTolerance / 1.1m;
+ Assert.True(quantity.Equals(quantity, {differenceResultType}.Zero));
+ Assert.True(quantity.Equals(quantity, maxTolerance));
+ Assert.True(quantity.Equals(otherQuantity, maxTolerance));
+ Assert.True(quantity.Equals(otherQuantity, largerTolerance));
+ Assert.False(quantity.Equals(otherQuantity, smallerTolerance));
+ }}
+");
+ if (_quantity.IsAffine)
+ {
+ Writer.WL($@"
[Fact]
- public void EqualsReturnsFalseOnNull()
+ public void Equals_WithNegativeTolerance_ThrowsArgumentOutOfRangeException()
{{
- {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(1);
- Assert.False({baseUnitVariableName}.Equals(null));
+ var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1);
+ {differenceResultType} negativeTolerance = quantity - {_quantity.Name}.From{_baseUnit.PluralName}(2);
+ Assert.Throws(() => quantity.Equals(quantity, negativeTolerance));
}}
+");
+ }
+ else // vector quantities
+ {
+ Writer.WL($@"
+
+ [Fact]
+ public void Equals_WithNegativeTolerance_ThrowsArgumentOutOfRangeException()
+ {{
+ var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1);
+ var negativeTolerance = {_quantity.Name}.From{_baseUnit.PluralName}(-1);
+ Assert.Throws(() => quantity.Equals(quantity, negativeTolerance));
+ }}
+");
+ }
+ }
+
+ Writer.WL($@"
[Fact]
public void HasAtLeastOneAbbreviationSpecified()
@@ -1024,6 +1207,18 @@ public void BaseDimensionsShouldNeverBeNull()
Assert.False({_quantity.Name}.BaseDimensions is null);
}}
+ [Fact]
+ public void Units_ReturnsTheQuantityInfoUnits()
+ {{
+ Assert.Equal({_quantity.Name}.Info.Units, {_quantity.Name}.Units);
+ }}
+
+ [Fact]
+ public void DefaultConversionFunctions_ReturnsTheDefaultUnitConverter()
+ {{
+ Assert.Equal(UnitConverter.Default, {_quantity.Name}.DefaultConversionFunctions);
+ }}
+
[Fact]
public void ToString_ReturnsValueAndUnitAbbreviationInCurrentCulture()
{{
@@ -1092,162 +1287,16 @@ public void ToString_NullProvider_EqualsCurrentCulture(string format)
Assert.Equal(quantity.ToString(format, CultureInfo.CurrentCulture), quantity.ToString(format, null));
}}
- [Fact]
- public void Convert_ToBool_ThrowsInvalidCastException()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Throws(() => Convert.ToBoolean(quantity));
- }}
-
- [Fact]
- public void Convert_ToByte_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((byte)quantity.Value, Convert.ToByte(quantity));
- }}
-
- [Fact]
- public void Convert_ToChar_ThrowsInvalidCastException()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Throws(() => Convert.ToChar(quantity));
- }}
-
- [Fact]
- public void Convert_ToDateTime_ThrowsInvalidCastException()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Throws(() => Convert.ToDateTime(quantity));
- }}
-
- [Fact]
- public void Convert_ToDecimal_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((decimal)quantity.Value, Convert.ToDecimal(quantity));
- }}
-
- [Fact]
- public void Convert_ToDouble_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((double)quantity.Value, Convert.ToDouble(quantity));
- }}
-
- [Fact]
- public void Convert_ToInt16_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((short)quantity.Value, Convert.ToInt16(quantity));
- }}
-
- [Fact]
- public void Convert_ToInt32_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((int)quantity.Value, Convert.ToInt32(quantity));
- }}
-
- [Fact]
- public void Convert_ToInt64_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((long)quantity.Value, Convert.ToInt64(quantity));
- }}
-
- [Fact]
- public void Convert_ToSByte_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((sbyte)quantity.Value, Convert.ToSByte(quantity));
- }}
-
- [Fact]
- public void Convert_ToSingle_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((float)quantity.Value, Convert.ToSingle(quantity));
- }}
-
- [Fact]
- public void Convert_ToString_EqualsToString()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal(quantity.ToString(), Convert.ToString(quantity));
- }}
-
- [Fact]
- public void Convert_ToUInt16_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((ushort)quantity.Value, Convert.ToUInt16(quantity));
- }}
-
- [Fact]
- public void Convert_ToUInt32_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((uint)quantity.Value, Convert.ToUInt32(quantity));
- }}
-
- [Fact]
- public void Convert_ToUInt64_EqualsValueAsSameType()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal((ulong)quantity.Value, Convert.ToUInt64(quantity));
- }}
-
- [Fact]
- public void Convert_ChangeType_SelfType_EqualsSelf()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal(quantity, Convert.ChangeType(quantity, typeof({_quantity.Name})));
- }}
-
- [Fact]
- public void Convert_ChangeType_UnitType_EqualsUnit()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal(quantity.Unit, Convert.ChangeType(quantity, typeof({_unitEnumName})));
- }}
-
- [Fact]
- public void Convert_ChangeType_QuantityInfo_EqualsQuantityInfo()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal({_quantity.Name}.Info, Convert.ChangeType(quantity, typeof(QuantityInfo)));
- }}
-
- [Fact]
- public void Convert_ChangeType_BaseDimensions_EqualsBaseDimensions()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal({_quantity.Name}.BaseDimensions, Convert.ChangeType(quantity, typeof(BaseDimensions)));
- }}
-
- [Fact]
- public void Convert_ChangeType_InvalidType_ThrowsInvalidCastException()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Throws(() => Convert.ChangeType(quantity, typeof(QuantityFormatter)));
- }}
-
- [Fact]
- public void Convert_GetTypeCode_Returns_Object()
- {{
- var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal(TypeCode.Object, Convert.GetTypeCode(quantity));
- }}
-
[Fact]
public void GetHashCode_Equals()
{{
var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0);
- Assert.Equal(new {{{_quantity.Name}.Info.Name, quantity.Value, quantity.Unit}}.GetHashCode(), quantity.GetHashCode());
+ var expected = Comparison.GetHashCode(typeof({_quantity.Name}), quantity.As({_quantity.Name}.BaseUnit));
+ Assert.Equal(expected, quantity.GetHashCode());
}}
");
- if (_quantity.GenerateArithmetic)
+ if (!_quantity.IsAffine)
{
Writer.WL($@"
[Theory]
diff --git a/CodeGen/Generators/UnitsNetGenerator.cs b/CodeGen/Generators/UnitsNetGenerator.cs
index 907f70e4a6..ca23308212 100644
--- a/CodeGen/Generators/UnitsNetGenerator.cs
+++ b/CodeGen/Generators/UnitsNetGenerator.cs
@@ -153,9 +153,6 @@ private static void GenerateResourceFiles(Quantity[] quantities, string resource
$"{resourcesDirectory}/{quantity.Name}.restext" :
$"{resourcesDirectory}/{quantity.Name}.{culture}.restext";
- // Ensure parent folder exists
- Directory.CreateDirectory(resourcesDirectory);
-
using var writer = File.CreateText(fileName);
foreach(Unit unit in quantity.Units)
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluationTerm.cs b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluationTerm.cs
new file mode 100644
index 0000000000..559e476240
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluationTerm.cs
@@ -0,0 +1,14 @@
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer;
+
+///
+/// A term of the form "P^n" where P is a term that hasn't been parsed, raised to the given power.
+///
+/// The actual expression to parse
+/// The exponent to use on the parsed expression (default is 1)
+///
+/// Since we're tokenizing the expressions from top to bottom, the first step is parsing the exponent of the
+/// expression: e.g. Math.Pow(P, 2)
+///
+public record ExpressionEvaluationTerm(string Expression, Fraction Exponent);
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluator.cs
new file mode 100644
index 0000000000..2f850a9be4
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluator.cs
@@ -0,0 +1,296 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+using CodeGen.Helpers.ExpressionAnalyzer.Functions;
+using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
+using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math.Trigonometry;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer;
+
+internal class ExpressionEvaluator // TODO make public (and move out in a separate project)
+{
+ public static readonly Fraction Pi = FractionExtensions.FromDoubleRounded(Math.PI, 16);
+ private readonly IReadOnlyDictionary _constantValues;
+ private readonly Dictionary _expressionsEvaluated = [];
+
+ private readonly IReadOnlyDictionary _functionEvaluators;
+
+ public ExpressionEvaluator(string parameterName, params IFunctionEvaluator[] functionEvaluators)
+ : this(parameterName, functionEvaluators.ToDictionary(x => x.FunctionName), new Dictionary())
+ {
+ }
+
+ public ExpressionEvaluator(string parameterName, IReadOnlyDictionary constantValues, params IFunctionEvaluator[] functionEvaluators)
+ : this(parameterName, functionEvaluators.ToDictionary(x => x.FunctionName), constantValues)
+ {
+ }
+
+ public ExpressionEvaluator(string parameterName, IReadOnlyDictionary functionEvaluators,
+ IReadOnlyDictionary constantValues)
+ {
+ ParameterName = parameterName;
+ _constantValues = constantValues;
+ _functionEvaluators = functionEvaluators;
+ }
+
+ public string ParameterName { get; }
+
+ protected string Add(CompositeExpression expression)
+ {
+ var label = "{" + (char)('a' + _expressionsEvaluated.Count) + "}";
+ _expressionsEvaluated[label] = expression;
+ return label;
+ }
+
+ public CompositeExpression Evaluate(ExpressionEvaluationTerm expressionEvaluationTerm) // TODO either replace by string or add a coefficient
+ {
+ if (TryParseExpressionTerm(expressionEvaluationTerm.Expression, expressionEvaluationTerm.Exponent, out ExpressionTerm? expressionTerm))
+ {
+ return expressionTerm;
+ }
+
+ var expressionToParse = expressionEvaluationTerm.Expression;
+ Fraction exponent = expressionEvaluationTerm.Exponent;
+ string previousExpression;
+ do
+ {
+ previousExpression = expressionToParse;
+ // the regex captures the innermost occurrence of a function group: "Sin(x)", "Pow(x, y)", "(x + 1)" are all valid matches
+ expressionToParse = Regex.Replace(expressionToParse, @"(\w*)\(([^()]*)\)", match =>
+ {
+ var functionName = match.Groups[1].Value;
+ var functionBodyToParse = match.Groups[2].Value;
+ var evaluationTerm = new ExpressionEvaluationTerm(functionBodyToParse, exponent);
+ if (string.IsNullOrEmpty(functionName)) // standard grouping (technically this is equivalent to f(x) -> x)
+ {
+ // all terms within the group are expanded: extract the simplified expression
+ CompositeExpression expression = ReplaceTokenizedExpressions(evaluationTerm);
+ return Add(expression);
+ }
+
+ if (_functionEvaluators.TryGetValue(functionName, out IFunctionEvaluator? functionEvaluator))
+ {
+ // resolve the expression using the custom function evaluator
+ CompositeExpression expression = functionEvaluator.CreateExpression(evaluationTerm, ReplaceTokenizedExpressions);
+ return Add(expression);
+ }
+
+ throw new FormatException($"No function evaluator available for {functionName}({functionBodyToParse})");
+ });
+ } while (previousExpression != expressionToParse);
+
+ return ReplaceTokenizedExpressions(expressionEvaluationTerm with { Expression = expressionToParse });
+ }
+
+ private CompositeExpression ReplaceTokenizedExpressions(ExpressionEvaluationTerm tokenizedExpression)
+ {
+ // all groups and function are expanded: we're left with a standard arithmetic expression such as "4 * a + 2 * b * x - c - d + 5"
+ // with a, b, c, d representing the previously evaluated expressions
+ var result = new CompositeExpression();
+ var stringBuilder = new StringBuilder();
+ ArithmeticOperationToken lastToken = ArithmeticOperationToken.Addition;
+ CompositeExpression? runningExpression = null;
+ foreach (var character in tokenizedExpression.Expression)
+ {
+ if (!TryReadToken(character, out ArithmeticOperationToken currentToken)) // TODO use None?
+ {
+ continue;
+ }
+
+ switch (currentToken)
+ {
+ case ArithmeticOperationToken.Addition or ArithmeticOperationToken.Subtraction:
+ {
+ if (stringBuilder.Length == 0) // ignore the leading sign
+ {
+ lastToken = currentToken;
+ continue;
+ }
+
+ // we're at the end of a term expression
+ CompositeExpression lastTerm = ParseTerm();
+ if (runningExpression is null)
+ {
+ result.AddTerms(lastTerm);
+ }
+ else // the last term is part of a running multiplication
+ {
+ result.AddTerms(runningExpression * lastTerm);
+ runningExpression = null;
+ }
+
+ lastToken = currentToken;
+ break;
+ }
+ case ArithmeticOperationToken.Multiplication or ArithmeticOperationToken.Division:
+ {
+ CompositeExpression previousTerm = ParseTerm();
+ if (runningExpression is null)
+ {
+ runningExpression = previousTerm;
+ }
+ else // the previousTerm term is part of a running multiplication (which is going to be followed by at least one more multiplication/division)
+ {
+ runningExpression *= previousTerm;
+ }
+
+ lastToken = currentToken;
+ break;
+ }
+ }
+ }
+
+ CompositeExpression finalTerm = ParseTerm();
+ if (runningExpression is null)
+ {
+ result.AddTerms(finalTerm);
+ }
+ else
+ {
+ result.AddTerms(runningExpression * finalTerm);
+ }
+
+ return result;
+
+ bool TryReadToken(char character, out ArithmeticOperationToken token)
+ {
+ switch (character)
+ {
+ case '+':
+ token = ArithmeticOperationToken.Addition;
+ return true;
+ case '-':
+ token = ArithmeticOperationToken.Subtraction;
+ return true;
+ case '*':
+ token = ArithmeticOperationToken.Multiplication;
+ return true;
+ case '/':
+ token = ArithmeticOperationToken.Division;
+ return true;
+ case not ' ':
+ stringBuilder.Append(character);
+ break;
+ }
+
+ token = default;
+ return false;
+ }
+
+ CompositeExpression ParseTerm()
+ {
+ var previousExpression = stringBuilder.ToString();
+ stringBuilder.Clear();
+ if (_expressionsEvaluated.TryGetValue(previousExpression, out CompositeExpression? expression))
+ {
+ return lastToken switch
+ {
+ ArithmeticOperationToken.Subtraction => expression.Negate(),
+ ArithmeticOperationToken.Division => expression.Invert(),
+ _ => expression
+ };
+ }
+
+ if (TryParseExpressionTerm(previousExpression, tokenizedExpression.Exponent, out ExpressionTerm? expressionTerm))
+ {
+ return lastToken switch
+ {
+ ArithmeticOperationToken.Subtraction => expressionTerm.Negate(),
+ ArithmeticOperationToken.Division => expressionTerm.Invert(),
+ _ => expressionTerm
+ };
+ }
+
+ throw new FormatException($"Failed to parse the previous token: {previousExpression}");
+ }
+ }
+
+
+ private bool TryParseExpressionTerm(string expressionToParse, Fraction exponent, [MaybeNullWhen(false)] out ExpressionTerm expressionTerm)
+ {
+ if (expressionToParse == ParameterName)
+ {
+ expressionTerm = new ExpressionTerm(Fraction.One, exponent);
+ return true;
+ }
+
+ if (_constantValues.TryGetValue(expressionToParse, out Fraction constantExpression) || Fraction.TryParse(expressionToParse, out constantExpression))
+ {
+ if (exponent.Numerator == exponent.Denominator)
+ {
+ expressionTerm = ExpressionTerm.Constant(constantExpression);
+ return true;
+ }
+
+ if (exponent.Denominator.IsOne)
+ {
+ expressionTerm = ExpressionTerm.Constant(Fraction.Pow(constantExpression, (int)exponent.Numerator));
+ return true;
+ }
+
+ // constant expression using a non-integer power: there is currently no Fraction.Pow(Fraction, Fraction)
+ expressionTerm = ExpressionTerm.Constant(FractionExtensions.FromDoubleRounded(Math.Pow(constantExpression.ToDouble(), exponent.ToDouble())));
+ return true;
+ }
+
+ expressionTerm = null;
+ return false;
+ }
+
+ public static string ReplaceDecimalNotations(string expression, Dictionary constantValues)
+ {
+ return Regex.Replace(expression, @"\d*(\.\d*)?[eE][-\+]?\d*[dD]?", match =>
+ {
+ var tokens = match.Value.ToLower().Replace("d", "").Split('e');
+ if (tokens.Length != 2 || !Fraction.TryParse(tokens[0], out Fraction mantissa) || !int.TryParse(tokens[1], out var exponent))
+ {
+ throw new FormatException($"The expression contains invalid tokens: {expression}");
+ }
+
+ var label = $"{{v{constantValues.Count}}}";
+ constantValues[label] = mantissa * Fraction.Pow(10, exponent);
+ return label;
+ }).Replace("d", string.Empty); // TODO these are force-generated for the BitRate (we should stop doing it)
+ }
+
+ public static string ReplaceMathPi(string expression, Dictionary constantValues)
+ {
+ return Regex.Replace(expression, @"Math\.PI", _ =>
+ {
+ constantValues[nameof(Pi)] = Pi;
+ return nameof(Pi);
+ });
+ }
+
+ public static CompositeExpression Evaluate(string expression, string parameter)
+ {
+ var constantExpressions = new Dictionary();
+
+ expression = ReplaceDecimalNotations(expression, constantExpressions); // TODO expose an IPreprocessor (or something)
+ expression = ReplaceMathPi(expression, constantExpressions);
+ expression = expression.Replace("Math.", string.Empty);
+
+ // these are no longer necessary
+ // var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions,
+ // new SqrtFunctionEvaluator(),
+ // new PowFunctionEvaluator(),
+ // new SinFunctionEvaluator(),
+ // new AsinFunctionEvaluator());
+ var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions);
+
+ return expressionEvaluator.Evaluate(new ExpressionEvaluationTerm(expression, Fraction.One));
+ }
+
+ private enum ArithmeticOperationToken
+ {
+ Addition,
+ Subtraction,
+ Multiplication,
+ Division
+ }
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CompositeExpression.cs b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CompositeExpression.cs
new file mode 100644
index 0000000000..7175578acb
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CompositeExpression.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+
+///
+/// A set of terms, ordered by their degree: "P(x)^2 + P(x) + 1"
+///
+internal class CompositeExpression : IEnumerable
+{
+ private readonly SortedSet _terms;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// This constructor creates an empty CompositeExpression with terms sorted in descending order.
+ ///
+ public CompositeExpression()
+ {
+ _terms = new SortedSet(DescendingOrderComparer);
+ }
+
+ ///
+ /// Initializes a new instance of the class with a single term.
+ ///
+ /// The initial term of the composite expression.
+ ///
+ /// This constructor creates a CompositeExpression with a single term, sorted in descending order.
+ ///
+ public CompositeExpression(ExpressionTerm term)
+ {
+ _terms = new SortedSet(DescendingOrderComparer) { term };
+ }
+
+ private CompositeExpression(IEnumerable terms)
+ {
+ _terms = new SortedSet(terms, DescendingOrderComparer);
+ }
+
+ public Fraction Degree => _terms.Min?.Exponent ?? Fraction.Zero;
+
+ public bool IsConstant => Degree == Fraction.Zero;
+
+ public IReadOnlyCollection Terms => _terms;
+
+ public void Add(ExpressionTerm term)
+ {
+ if (_terms.TryGetValue(term, out ExpressionTerm? sameDegreeTerm))
+ {
+ // merge the two terms
+ term = term with { Coefficient = sameDegreeTerm.Coefficient + term.Coefficient };
+ _terms.Remove(sameDegreeTerm);
+ }
+
+ _terms.Add(term);
+ }
+
+ public void AddTerms(IEnumerable expressionTerms)
+ {
+ foreach (ExpressionTerm term in expressionTerms)
+ {
+ Add(term);
+ }
+ }
+
+
+ public static implicit operator CompositeExpression(ExpressionTerm term)
+ {
+ return new CompositeExpression(term);
+ }
+
+ public static explicit operator ExpressionTerm(CompositeExpression term)
+ {
+ return term._terms.Max!;
+ }
+
+ public CompositeExpression Negate()
+ {
+ return new CompositeExpression(_terms.Select(term => term.Negate()));
+ }
+
+ public CompositeExpression Invert()
+ {
+ return new CompositeExpression(_terms.Select(term => term.Invert()));
+ }
+
+ public CompositeExpression SolveForY()
+ {
+ if (_terms.Count == 0)
+ {
+ throw new InvalidOperationException("The expression is empty");
+ }
+
+ if (_terms.Count > 2)
+ {
+ throw new NotImplementedException("Solving is only supported for expressions of first degree");
+ }
+
+ ExpressionTerm degreeTerm = _terms.Min!;
+ if (degreeTerm.Exponent == Fraction.One)
+ {
+ return new CompositeExpression(_terms.Where(x => x.IsConstant).Select(x => x with { Coefficient = x.Coefficient.Negate() / degreeTerm.Coefficient })
+ .Prepend(new ExpressionTerm(degreeTerm.Coefficient.Reciprocal(), 1)));
+ }
+
+ if (degreeTerm.Exponent == Fraction.MinusOne)
+ {
+ return new CompositeExpression(_terms.Where(x => x.IsConstant).Select(x => x with { Coefficient = degreeTerm.Coefficient / x.Coefficient.Negate() })
+ .Prepend(new ExpressionTerm(degreeTerm.Coefficient, -1)));
+ }
+
+ throw new NotImplementedException("Solving is only supported for expressions of first degree");
+ }
+
+ public CompositeExpression Multiply(CompositeExpression other)
+ {
+ var result = new CompositeExpression();
+ foreach (ExpressionTerm otherTerm in other)
+ {
+ result.AddTerms(_terms.Select(x => x.Multiply(otherTerm)));
+ }
+
+ return result;
+ }
+
+ public CompositeExpression Divide(CompositeExpression other)
+ {
+ var result = new CompositeExpression();
+ foreach (ExpressionTerm otherTerm in other)
+ {
+ result.AddTerms(_terms.Select(x => x.Divide(otherTerm)));
+ }
+
+ return result;
+ }
+
+ public static CompositeExpression operator *(CompositeExpression left, CompositeExpression right)
+ {
+ return left.Multiply(right);
+ }
+
+ public static CompositeExpression operator /(CompositeExpression left, CompositeExpression right)
+ {
+ return left.Divide(right);
+ }
+
+ public override string ToString()
+ {
+ return string.Join(" + ", _terms);
+ }
+
+ public CompositeExpression Evaluate(Fraction x)
+ {
+ var result = new CompositeExpression();
+ result.AddTerms(_terms.Select(t => t.Evaluate(x)));
+ return result;
+ }
+
+ public CompositeExpression Evaluate(CompositeExpression expression)
+ {
+ var result = new CompositeExpression();
+ foreach (ExpressionTerm expressionTerm in _terms)
+ {
+ if (expressionTerm.IsConstant)
+ {
+ result.Add(expressionTerm);
+ }
+ else
+ {
+ result.AddTerms(expression.Terms.Select(term => expressionTerm.Evaluate(term)));
+ }
+ }
+
+ return result;
+ }
+
+ #region TermComparer
+
+ private sealed class DescendingOrderTermComparer : IComparer
+ {
+ public int Compare(ExpressionTerm? y, ExpressionTerm? x)
+ {
+ if (ReferenceEquals(x, y)) return 0;
+ if (ReferenceEquals(null, y)) return 1;
+ if (ReferenceEquals(null, x)) return -1;
+ var nestedFunctionComparison = Comparer.Default.Compare(x.NestedFunction, y.NestedFunction);
+ if (nestedFunctionComparison != 0) return nestedFunctionComparison;
+ return x.Exponent.Abs().CompareTo(y.Exponent.Abs());
+ }
+ }
+
+ public static IComparer DescendingOrderComparer { get; } = new DescendingOrderTermComparer();
+
+ #endregion
+
+ #region Implementation of IEnumerable
+
+ public IEnumerator GetEnumerator()
+ {
+ return _terms.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CustomFunction.cs b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CustomFunction.cs
new file mode 100644
index 0000000000..292347f8c7
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CustomFunction.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+
+///
+/// A custom function f(ax^n + bx^m)
+///
+///
+///
+///
+///
+/// These are functions that we don't directly support, such as Sqrt.
+internal record CustomFunction(string Namespace, string Name, CompositeExpression Terms, params string[] AdditionalParameters) : IComparable, IComparable
+{
+ #region Overrides of Object
+
+ public override string ToString()
+ {
+ if(AdditionalParameters.Length == 0)
+ return $"{Name}({Terms})";
+ return $"{Name}({Terms}, {string.Join(", ", AdditionalParameters)})";
+ }
+
+ #endregion
+
+ #region Relational members
+
+ public int CompareTo(CustomFunction? other)
+ {
+ if (ReferenceEquals(this, other)) return 0;
+ if (ReferenceEquals(null, other)) return 1;
+ return string.Compare(Name, other.Name, StringComparison.Ordinal);
+ }
+
+ public int CompareTo(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return 1;
+ if (ReferenceEquals(this, obj)) return 0;
+ return obj is CustomFunction other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(CustomFunction)}");
+ }
+
+ #endregion
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Expressions/ExpressionTerm.cs b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/ExpressionTerm.cs
new file mode 100644
index 0000000000..ecfb25d88c
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/ExpressionTerm.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+
+///
+/// A term of the form "a * f(x)^n".
+///
+/// The constant coefficient (a)
+/// The degree of the term (n)
+/// f(x) if one is available
+/// When there is no nested function f(x) = x
+internal record ExpressionTerm(Fraction Coefficient, Fraction Exponent, CustomFunction? NestedFunction = null) : IComparable, IComparable
+{
+ public bool IsRational => NestedFunction is null && Exponent.Denominator.IsOne;
+
+ public bool IsConstant => NestedFunction is null && Exponent.IsZero;
+
+ public ExpressionTerm Negate()
+ {
+ return this with { Coefficient = Coefficient.Negate() };
+ }
+
+ public ExpressionTerm Invert()
+ {
+ return this with { Exponent = Exponent.Negate(), Coefficient = Coefficient.Reciprocal() };
+ }
+
+ public ExpressionTerm Multiply(ExpressionTerm otherTerm)
+ {
+ if (NestedFunction != null && otherTerm.NestedFunction != null &&
+ NestedFunction != otherTerm.NestedFunction) // there aren't any cases of this in the code-base
+ {
+ throw new NotSupportedException(
+ "Multiplying terms with different functions is currently not supported"); // if we need to, we should use a collection or create some function-composition
+ }
+
+ return new ExpressionTerm(Coefficient * otherTerm.Coefficient, Exponent + otherTerm.Exponent, NestedFunction ?? otherTerm.NestedFunction);
+ }
+
+ public ExpressionTerm Divide(ExpressionTerm otherTerm)
+ {
+ return Multiply(otherTerm.Invert());
+ }
+
+ public static ExpressionTerm Constant(Fraction coefficient)
+ {
+ return new ExpressionTerm(coefficient, Fraction.Zero);
+ }
+
+ #region Overrides of Object
+
+ public override string ToString()
+ {
+ var coefficientFormat = Coefficient == Fraction.One ? "" :
+ Coefficient == Fraction.MinusOne ? "-" : $"{Coefficient.ToDouble()} * ";
+ if (NestedFunction == null)
+ {
+ if (Exponent == Fraction.Zero)
+ {
+ return $"{Coefficient.ToDouble()}";
+ }
+
+ if (Exponent == Fraction.One)
+ {
+ return $"{coefficientFormat}x";
+ }
+
+ return $"{coefficientFormat}x^{Exponent.ToDouble()}";
+ }
+
+ return $"{coefficientFormat}{NestedFunction}";
+ }
+
+ #endregion
+
+ public ExpressionTerm Evaluate(Fraction x)
+ {
+ if (NestedFunction != null || Exponent.IsZero)
+ {
+ return this;
+ }
+
+ return IsRational
+ ? Constant(Coefficient * Fraction.Pow(x, Exponent.ToInt32()))
+ : Constant(Coefficient * FractionExtensions.FromDoubleRounded(Math.Pow(x.ToDouble(), Exponent.ToDouble())));
+ }
+
+ public ExpressionTerm Evaluate(ExpressionTerm term)
+ {
+ if (Exponent.IsZero)
+ {
+ return this;
+ }
+
+ if (NestedFunction == null)
+ {
+ return new ExpressionTerm(Coefficient * term.Coefficient.Pow(Exponent), term.Exponent * Exponent);
+ }
+
+ CompositeExpression nestedTerms = NestedFunction.Terms.Evaluate(term);
+ return this with { NestedFunction = NestedFunction with { Terms = nestedTerms } };
+
+ }
+
+ #region Relational members
+
+ public int CompareTo(ExpressionTerm? other)
+ {
+ if (ReferenceEquals(this, other)) return 0;
+ if (other is null) return 1;
+ var exponentComparison = Exponent.CompareTo(other.Exponent);
+ if (exponentComparison != 0) return exponentComparison;
+ var nestedFunctionComparison = Comparer.Default.Compare(NestedFunction, other.NestedFunction);
+ if (nestedFunctionComparison != 0) return nestedFunctionComparison;
+ return Coefficient.CompareTo(other.Coefficient);
+ }
+
+ public int CompareTo(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return 1;
+ if (ReferenceEquals(this, obj)) return 0;
+ return obj is ExpressionTerm other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(ExpressionTerm)}");
+ }
+
+ #endregion
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/IFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/IFunctionEvaluator.cs
new file mode 100644
index 0000000000..3af9dcd222
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/IFunctionEvaluator.cs
@@ -0,0 +1,26 @@
+using System;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions;
+
+///
+/// Defines the contract for a function evaluator that can parse and create expressions.
+///
+///
+/// Implementations of this interface are used to evaluate specific mathematical functions.
+///
+internal interface IFunctionEvaluator
+{
+ ///
+ /// Gets the name of the function that this evaluator can handle.
+ ///
+ string FunctionName { get; }
+
+ ///
+ /// Parses the given expression and returns a pending term.
+ ///
+ /// The expression to parse.
+ /// Can be used to evaluate the function body expression.
+ /// A that represents the parsed expression.
+ CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, Func expressionResolver);
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/FractionExtensions.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/FractionExtensions.cs
new file mode 100644
index 0000000000..a07ef2d67d
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/FractionExtensions.cs
@@ -0,0 +1,69 @@
+// 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.Numerics;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
+
+///
+/// We should try to push these extensions to the original library (working on the PRs)
+///
+internal static class FractionExtensions
+{
+ public static readonly Fraction OneHalf = new(BigInteger.One, new BigInteger(2));
+ public static readonly BigInteger Ten = new(10);
+
+ public static Fraction Pow(this Fraction x, Fraction power)
+ {
+ if (power == Fraction.One)
+ {
+ return x;
+ }
+
+ if (x == Fraction.One)
+ {
+ return x;
+ }
+
+ power = power.Reduce();
+ if (power.Denominator.IsOne)
+ {
+ return Fraction.Pow(x, (int)power);
+ }
+
+ // return FromDoubleRounded(System.Math.Pow(x.ToDouble(), power.ToDouble()));
+ return PowRational(x, power);
+ }
+
+ private static Fraction PowRational(this Fraction x, Fraction power)
+ {
+ var numeratorRaised = Fraction.Pow(x, (int)power.Numerator);
+ return numeratorRaised.Root((int)power.Denominator, 30);
+ }
+
+ public static Fraction Root(this Fraction number, int n, int nbDigits)
+ {
+ var precision = BigInteger.Pow(10, nbDigits);
+ // Fraction x = number; // Initial guess
+ var initialGuess = System.Math.Pow(number.ToDouble(), 1.0 / n);
+ Fraction x = initialGuess == 0.0 ? number : FromDoubleRounded(initialGuess);
+ Fraction xPrev;
+ var minChange = new Fraction(1, precision);
+ do
+ {
+ xPrev = x;
+ x = ((n - 1) * x + number / Fraction.Pow(x, n - 1)) / n;
+ } while ((x - xPrev).Abs() > minChange);
+
+ return Fraction.Round(x, nbDigits);
+ }
+
+ ///
+ ///
+ ///
+ public static Fraction FromDoubleRounded(double value, int nbSignificantDigits = 15)
+ {
+ return Fraction.FromDoubleRounded(value, nbSignificantDigits);
+ }
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/MathFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/MathFunctionEvaluator.cs
new file mode 100644
index 0000000000..d5df4dacae
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/MathFunctionEvaluator.cs
@@ -0,0 +1,27 @@
+using System;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
+
+internal abstract class MathFunctionEvaluator : IFunctionEvaluator
+{
+ public virtual string Namespace => nameof(System.Math);
+ public abstract string FunctionName { get; }
+
+ public CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, Func expressionResolver)
+ {
+ CompositeExpression functionBody = expressionResolver(expressionToParse with {Exponent = 1});
+ Fraction power = expressionToParse.Exponent;
+ if (functionBody.IsConstant) // constant expression (directly evaluate the function)
+ {
+ var constantTerm = (ExpressionTerm)functionBody;
+ Fraction resultingValue = Evaluate(constantTerm.Coefficient);
+ return ExpressionTerm.Constant(resultingValue.Pow(power));
+ }
+ // we cannot expand a function of x
+ return new ExpressionTerm(1, power, new CustomFunction(Namespace, FunctionName, functionBody));
+ }
+
+ public abstract Fraction Evaluate(Fraction value);
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/PowFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/PowFunctionEvaluator.cs
new file mode 100644
index 0000000000..b1a6bafc7a
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/PowFunctionEvaluator.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Globalization;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
+
+internal class PowFunctionEvaluator : IFunctionEvaluator
+{
+ public string Namespace => nameof(System.Math);
+ public string FunctionName => nameof(System.Math.Pow);
+
+ public bool ExpandNonConstantExpressions { get; set; } = false; // while it's possible to expand the expression (even for non-rational powers)- probably we shouldn't
+
+ public CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse,
+ Func expressionResolver)
+ {
+ var functionParams = expressionToParse.Expression.Split(',');
+ if (functionParams.Length != 2 || !Fraction.TryParse(functionParams[1], out Fraction exponentParsed))
+ {
+ throw new FormatException($"The provided string is not in the correct format for the Pow function {expressionToParse}");
+ }
+
+ CompositeExpression functionBody = expressionResolver(new ExpressionEvaluationTerm(functionParams[0], 1));
+ Fraction power = expressionToParse.Exponent * exponentParsed;
+
+ if (functionBody.IsConstant)
+ {
+ var singleTerm = (ExpressionTerm)functionBody;
+ Fraction coefficient = singleTerm.Coefficient.Pow(power);
+ return ExpressionTerm.Constant(coefficient);
+ }
+
+ if (!ExpandNonConstantExpressions)
+ {
+ return new ExpressionTerm(1, 1, new CustomFunction(Namespace, FunctionName, functionBody, power.ToDecimal().ToString(CultureInfo.InvariantCulture)));
+ }
+
+ // while it's possible to expand the expression (even for non-rational powers)- we shouldn't, as the operation would not be reversible: the result of (x^0.5)^2 may be different from x
+ if (functionBody.Terms.Count == 1)
+ {
+ var singleTerm = (ExpressionTerm)functionBody;
+ Fraction coefficient = singleTerm.Coefficient.Pow(power);
+ return singleTerm with { Coefficient = coefficient, Exponent = singleTerm.Exponent * power };
+ }
+
+ // TODO see about handling the multi-term expansion (at least for integer exponents)
+ return new ExpressionTerm(1, 1, new CustomFunction(Namespace, FunctionName, functionBody, power.ToDecimal().ToString(CultureInfo.InvariantCulture)));
+ }
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/SqrtFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/SqrtFunctionEvaluator.cs
new file mode 100644
index 0000000000..989a63c992
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/SqrtFunctionEvaluator.cs
@@ -0,0 +1,40 @@
+using System;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
+
+internal class SqrtFunctionEvaluator : IFunctionEvaluator
+{
+ public string Namespace => nameof(System.Math); // we could switch this to QuantityValue if we decide to add it later
+ public string FunctionName => nameof(System.Math.Sqrt);
+ public bool ExpandNonConstantExpressions { get; set; } = false; // while it's possible to expand the expression (even for non-rational powers)- probably we shouldn't
+
+ public CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, Func expressionResolver)
+ {
+ CompositeExpression functionBody = expressionResolver(expressionToParse with {Exponent = 1});
+ if (functionBody.IsConstant)
+ {
+ var constantTerm = (ExpressionTerm)functionBody;
+ Fraction coefficient = constantTerm.Coefficient.Pow(expressionToParse.Exponent * FractionExtensions.OneHalf);
+ return ExpressionTerm.Constant(coefficient);
+ }
+
+ if (!ExpandNonConstantExpressions)
+ {
+ return new ExpressionTerm(1, expressionToParse.Exponent, new CustomFunction(Namespace, FunctionName, functionBody));
+ }
+
+ // while it's possible to expand the expression (even for non-rational powers)- we shouldn't, as the operation would not be reversible: the result of (x^0.5)^2 may be different from x
+ Fraction power = expressionToParse.Exponent * FractionExtensions.OneHalf;
+ if (functionBody.Terms.Count == 1)
+ {
+ var constantTerm = (ExpressionTerm)functionBody;
+ Fraction coefficient = constantTerm.Coefficient.Pow(power);
+ return constantTerm with { Coefficient = coefficient, Exponent = constantTerm.Exponent * power };
+ }
+
+ // TODO see about handling the multi-term expansion (at least for integer exponents)
+ return new ExpressionTerm(1, power, new CustomFunction(Namespace, FunctionName, functionBody));
+ }
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/AsinFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/AsinFunctionEvaluator.cs
new file mode 100644
index 0000000000..de7ef7623c
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/AsinFunctionEvaluator.cs
@@ -0,0 +1,13 @@
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math.Trigonometry;
+
+internal class AsinFunctionEvaluator : MathFunctionEvaluator
+{
+ public override string FunctionName => nameof(System.Math.Asin);
+
+ public override Fraction Evaluate(Fraction value)
+ {
+ return FractionExtensions.FromDoubleRounded(System.Math.Asin(value.ToDouble()));
+ }
+}
diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/SinFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/SinFunctionEvaluator.cs
new file mode 100644
index 0000000000..b81d69613c
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/SinFunctionEvaluator.cs
@@ -0,0 +1,13 @@
+using Fractions;
+
+namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math.Trigonometry;
+
+internal class SinFunctionEvaluator : MathFunctionEvaluator
+{
+ public override string FunctionName => nameof(System.Math.Sin);
+
+ public override Fraction Evaluate(Fraction value)
+ {
+ return FractionExtensions.FromDoubleRounded(System.Math.Sin(value.ToDouble()));
+ }
+}
diff --git a/CodeGen/Helpers/ExpressionEvaluationHelpers.cs b/CodeGen/Helpers/ExpressionEvaluationHelpers.cs
new file mode 100644
index 0000000000..a252f4f71e
--- /dev/null
+++ b/CodeGen/Helpers/ExpressionEvaluationHelpers.cs
@@ -0,0 +1,462 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using CodeGen.Helpers.ExpressionAnalyzer;
+using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
+using CodeGen.JsonTypes;
+using Fractions;
+
+namespace CodeGen.Helpers;
+
+internal static class ExpressionEvaluationHelpers
+{
+ private record struct Factor(BigInteger Number, int Power, BigInteger Value)
+ {
+ public static Factor FromNumber(BigInteger number) => new(number, 1, number);
+
+ private sealed class ValueRelationalComparer : IComparer
+ {
+ public int Compare(Factor x, Factor y)
+ {
+ return x.Value.CompareTo(y.Value);
+ }
+ }
+
+ public static IComparer ValueComparer { get; } = new ValueRelationalComparer();
+ };
+
+ private static List ExtractFactors(this BigInteger number)
+ {
+ number = BigInteger.Abs(number);
+ var factors = new List();
+ if (number.IsPowerOfTwo)
+ {
+ var exponent = (int)(number.GetBitLength() - 1);
+ var divisor = BigInteger.Pow(2, exponent);
+ factors.Add(new Factor(2, exponent, divisor));
+ return factors;
+ }
+
+ var factorsToTryFirst = new BigInteger[] {10, 2, 3, 5, 7};
+ foreach (BigInteger divisor in factorsToTryFirst)
+ {
+ if (TryGetFactors(number, divisor, out number, out Factor factor))
+ {
+ factors.Add(factor);
+ if (number.IsOne)
+ {
+ return factors;
+ }
+
+ if (number <= long.MaxValue)
+ {
+ factors.Add(Factor.FromNumber(number));
+ return factors;
+ }
+ }
+ }
+
+ BigInteger currentDivisor = 11;
+ do
+ {
+ if (TryGetFactors(number, currentDivisor, out number, out Factor factor))
+ {
+ factors.Add(factor);
+ }
+
+ currentDivisor++;
+ } while (number > long.MaxValue && number > currentDivisor);
+
+ if (!number.IsOne)
+ {
+ factors.Add(Factor.FromNumber(number));
+ }
+
+ return factors;
+ }
+
+ private static bool TryGetFactors(BigInteger number, BigInteger divisor, out BigInteger quotient, out Factor factor)
+ {
+ quotient = number;
+ var power = 0;
+ while (true)
+ {
+ var nextQuotient = BigInteger.DivRem(quotient, divisor, out BigInteger remainder);
+ if (remainder.IsZero)
+ {
+ quotient = nextQuotient;
+ power++;
+ }
+ else
+ {
+ factor = new Factor(divisor, power, BigInteger.Pow(divisor, power));
+ return power > 0;
+ }
+ }
+ }
+
+ private static SortedSet MergeFactors(this IEnumerable factorsToMerge)
+ {
+ var factors = new SortedSet(factorsToMerge, Factor.ValueComparer);
+ while (factors.Count > 1)
+ {
+ // try to merge the next two factors
+ Factor smallestFactor = factors.First();
+ Factor secondSmallestFactor = factors.Skip(1).First();
+ var mergedFactor = Factor.FromNumber(smallestFactor.Value * secondSmallestFactor.Value);
+ if (mergedFactor.Value > long.MaxValue)
+ {
+ return factors; // we've got the smallest possible set
+ }
+
+ // replace the two factors with their merged version
+ factors.Remove(smallestFactor);
+ factors.Remove(secondSmallestFactor);
+ factors.Add(mergedFactor);
+ }
+
+ return factors;
+ }
+
+ private static string GetConstantFormat(this Factor factor)
+ {
+ if (factor.Power == 1)
+ {
+ return $"new BigInteger({factor.Value})";
+ }
+ else if (factor.Number == 10)
+ {
+ return $"QuantityValue.PowerOfTen({factor.Power})";
+ }
+ else
+ {
+ return $"BigInteger.Pow({factor.Number}, {factor.Power})";
+ }
+ }
+
+ private static string GetConstantMultiplicationFormat(this IEnumerable factors, bool negate = false)
+ {
+ var expression = string.Join(" * ", factors.Select(x => x.GetConstantFormat()));
+ if (negate)
+ {
+ expression = "-" + expression;
+ }
+
+ return expression;
+ }
+
+ private static string GetConstantFormat(this Fraction coefficient)
+ {
+ if (coefficient == Fraction.One)
+ {
+ return "1";
+ }
+
+ return coefficient.Denominator.IsOne
+ ? coefficient.Numerator.ToString()
+ : $"new QuantityValue({coefficient.Numerator}, {coefficient.Denominator})";
+ }
+
+ private static string GetLongConstantFormat(this Fraction coefficient)
+ {
+ var numeratorExpression = coefficient.Numerator > long.MaxValue || coefficient.Numerator < long.MinValue
+ ? coefficient.Numerator.ExtractFactors().MergeFactors().GetConstantMultiplicationFormat(coefficient.IsNegative)
+ : coefficient.Numerator.ToString();
+ var denominatorExpression = coefficient.Denominator > long.MaxValue
+ ? coefficient.Denominator.ExtractFactors().MergeFactors().GetConstantMultiplicationFormat()
+ : coefficient.Denominator.ToString();
+ var expandedExpression = $"new QuantityValue({numeratorExpression}, {denominatorExpression})";
+ return expandedExpression;
+ }
+
+ private static string GetFractionalConstantFormat(this Fraction coefficient)
+ {
+ coefficient = coefficient.Reduce();
+ // making sure that neither the Numerator nor the Denominator contain a value that cannot be represented as a compiler constant
+ if (coefficient.Numerator >= long.MinValue && coefficient.Numerator <= long.MaxValue && coefficient.Denominator <= long.MaxValue)
+ {
+ return coefficient.GetConstantFormat();
+ }
+
+ // need to represent the fraction in terms of two terms: "a * b"
+ return coefficient.GetLongConstantFormat();
+ }
+
+ private static string GetConstantExpression(this Fraction coefficient, string csharpParameter)
+ {
+ if (coefficient == Fraction.One)
+ {
+ return csharpParameter;
+ }
+
+ if (coefficient.Denominator.IsOne)
+ {
+ return $"{csharpParameter} * {coefficient.Numerator}";
+ }
+
+ if (coefficient.Numerator.IsOne)
+ {
+ return $"{csharpParameter} / {coefficient.Denominator}";
+ }
+
+ if (coefficient.Numerator == BigInteger.MinusOne)
+ {
+ return $"{csharpParameter} / -{coefficient.Denominator}";
+ }
+
+ return $"{csharpParameter} * new QuantityValue({coefficient.Numerator}, {coefficient.Denominator})";
+ }
+
+ private static string GetFractionalExpressionFormat(this Fraction coefficient, string csharpParameter)
+ {
+ coefficient = coefficient.Reduce();
+ // making sure that neither the Numerator nor the Denominator contain a value that cannot be represented as a compiler constant
+ if (coefficient.Numerator >= long.MinValue && coefficient.Numerator <= long.MaxValue && coefficient.Denominator <= long.MaxValue)
+ {
+ return coefficient.GetConstantExpression(csharpParameter);
+ }
+
+ // need to represent the fraction in terms of two (or more) terms: "x * a * b"
+ return $"{csharpParameter} * {coefficient.GetLongConstantFormat()}";
+ }
+
+
+ public static string GetExpressionFormat(this CustomFunction customFunction, string csharpParameter)
+ {
+ // TODO see about redirecting these to a static method in the quantity's class which is responsible for handling the required operations (efficiently)
+ var mainArgument = $"({customFunction.Terms.GetExpressionFormat(csharpParameter)}).ToDouble()";
+ var functionArguments = string.Join(", ", customFunction.AdditionalParameters.Prepend(mainArgument));
+ return $"QuantityValue.FromDoubleRounded({customFunction.Namespace}.{customFunction.Name}({functionArguments}))";
+ }
+
+ public static string GetConstantExpressionFormat(this CustomFunction customFunction)
+ {
+ // TODO see about redirecting these to a static method in the quantity's class which is responsible for handling the required operations (efficiently)
+ var functionArguments = string.Join(", ", customFunction.AdditionalParameters);
+ return $"QuantityValue.FromDoubleRounded({customFunction.Namespace}.{customFunction.Name}({functionArguments}))";
+ }
+
+ public static string GetExponentFormat(this Fraction exponent, string csharpParameter)
+ {
+ if (exponent == Fraction.One)
+ {
+ return csharpParameter;
+ }
+
+ // alternatively this could be an operator: e.g. $"({csharpParameter} ^ {exponent.ToInt32()})"
+ return exponent.Denominator.IsOne
+ ? $"QuantityValue.Pow({csharpParameter}, {exponent.ToInt32()})"
+ : $"QuantityValue.FromDoubleRounded(Math.Pow({csharpParameter}.ToDouble(), {exponent.ToDouble()}))";
+ }
+
+ public static string GetExpressionFormat(this ExpressionTerm term, string csharpParameter)
+ {
+ if (term.IsConstant)
+ {
+ return term.Coefficient.GetFractionalConstantFormat();
+ }
+
+ if (term is { NestedFunction: not null, Exponent.IsZero: true })
+ {
+ return term.NestedFunction.GetConstantExpressionFormat();
+ }
+
+ var expressionFormat = term.NestedFunction is null ? csharpParameter : term.NestedFunction.GetExpressionFormat(csharpParameter);
+ return term.Coefficient.GetFractionalExpressionFormat(term.Exponent.GetExponentFormat(expressionFormat));
+ }
+
+ public static string GetExpressionFormat(this CompositeExpression expression, string csharpParameter)
+ {
+ return string.Join(" + ", expression.Terms.Select(x => x.GetExpressionFormat(csharpParameter)));
+ }
+
+ private static string GetStringExpression(string expression, string csharpParameter, string jsonParameter = "{x}")
+ {
+ CompositeExpression compositeExpression = ExpressionEvaluator.Evaluate(expression, jsonParameter);
+ var expectedFormat = compositeExpression.GetExpressionFormat(csharpParameter);
+ return expectedFormat;
+ }
+
+ public static string GetConversionExpressionFormat(this CompositeExpression expression, string csharpParameter = "value")
+ {
+ string? coefficientTermFormat = null;
+ string? exponentFormat = null;
+ string? customConversionFunctionFormat = null;
+ string? constantTermValue = null;
+
+ foreach (ExpressionTerm expressionTerm in expression.Terms)
+ {
+ if (expressionTerm.IsConstant)
+ {
+ constantTermValue = expressionTerm.Coefficient.GetFractionalConstantFormat();
+ }
+ else if (expressionTerm.Exponent == 0)
+ {
+ throw new InvalidOperationException("The ConversionExpression class does not support custom functions as the constant term.");
+ }
+ else
+ {
+ if (coefficientTermFormat is not null || exponentFormat is not null || customConversionFunctionFormat is not null)
+ {
+ throw new InvalidOperationException("The ConversionExpression class does not support more than 2 terms");
+ }
+
+ coefficientTermFormat = expressionTerm.Coefficient.GetFractionalConstantFormat();
+
+ if (expressionTerm.NestedFunction is not null)
+ {
+ customConversionFunctionFormat = expressionTerm.NestedFunction.GetExpressionFormat(csharpParameter);
+ }
+
+ if (expressionTerm.Exponent == Fraction.One)
+ {
+ continue;
+ }
+
+ if (expressionTerm.Exponent.Denominator.IsOne)
+ {
+ exponentFormat = expressionTerm.Exponent.Numerator.ToString();
+ }
+ else if (customConversionFunctionFormat is null)
+ {
+ customConversionFunctionFormat = expressionTerm.Exponent.GetExponentFormat(csharpParameter);
+ }
+ else // create a composition between the two functions
+ {
+ customConversionFunctionFormat = expressionTerm.Exponent.GetExponentFormat(customConversionFunctionFormat);
+ }
+ }
+ }
+
+ coefficientTermFormat ??= "1";
+
+ if (constantTermValue is not null && exponentFormat is null && customConversionFunctionFormat is null)
+ {
+ return $"new ConversionExpression({coefficientTermFormat}, {constantTermValue})";
+ }
+
+ if (customConversionFunctionFormat is not null)
+ {
+ return $"new ConversionExpression({coefficientTermFormat}, {csharpParameter} => {customConversionFunctionFormat}, {exponentFormat ?? "1"}, {constantTermValue ?? "0"})";
+ }
+
+ if (constantTermValue is not null)
+ {
+ return $"new ConversionExpression({coefficientTermFormat}, null, {exponentFormat ?? "1"}, {constantTermValue})";
+ }
+
+ if (exponentFormat is not null)
+ {
+ return $"new ConversionExpression({coefficientTermFormat}, null, {exponentFormat})";
+ }
+
+ // return $"new ConversionExpression({coefficientTermFormat})";
+ return coefficientTermFormat; // using the implicit constructor from QuantityValue
+ }
+
+ private static string GetConversionExpressionFormat(string expression, string csharpParameter = "value", string jsonParameter = "{x}")
+ {
+ CompositeExpression compositeExpression = ExpressionEvaluator.Evaluate(expression, jsonParameter);
+ var expectedFormat = compositeExpression.GetConversionExpressionFormat(csharpParameter);
+ return expectedFormat;
+ }
+
+ ///
+ /// Gets the format of the conversion from the unit to the base unit.
+ ///
+ /// The unit for which to get the conversion format.
+ /// The C# parameter to be used in the conversion expression.
+ /// A string representing the format of the conversion from the unit to the base unit.
+ internal static string GetUnitToBaseConversionFormat(this Unit unit, string csharpParameter = "value")
+ {
+ return GetStringExpression(unit.FromUnitToBaseFunc, csharpParameter);
+ }
+
+ ///
+ /// Gets the format of the conversion from the base unit to the specified unit.
+ ///
+ /// The unit to which the conversion format is to be obtained.
+ /// The C# parameter to be used in the conversion expression.
+ /// A string representing the format of the conversion from the base unit to the specified unit.
+ internal static string GetFromBaseToUnitConversionFormat(this Unit unit, string csharpParameter = "value")
+ {
+ return GetStringExpression(unit.FromBaseToUnitFunc, csharpParameter);
+ }
+
+ ///
+ /// Gets the format of the conversion from the unit to the base unit using a ConversionExpression.
+ ///
+ /// The unit for which to get the conversion format.
+ /// The C# parameter to be used in the conversion expression.
+ /// A string representing the constructor of a ConversionExpression from the unit to the base unit.
+ internal static string GetUnitToBaseConversionExpressionFormat(this Unit unit, string csharpParameter = "value")
+ {
+ return GetConversionExpressionFormat(unit.FromUnitToBaseFunc, csharpParameter);
+ }
+
+ ///
+ /// Gets the format of the conversion from the base unit to the specified unit using a ConversionExpression.
+ ///
+ /// The unit to which the conversion format is to be obtained.
+ /// The C# parameter to be used in the conversion expression.
+ /// A string representing the constructor of a ConversionExpression from the base unit to the specified unit.
+ internal static string GetFromBaseToUnitConversionExpressionFormat(this Unit unit, string csharpParameter = "value")
+ {
+ return GetConversionExpressionFormat(unit.FromBaseToUnitFunc, csharpParameter);
+ }
+
+ ///
+ /// Generates a dictionary of conversion expressions for a given quantity, mapping each unit to its conversion
+ /// expressions with other units.
+ ///
+ /// The quantity for which conversion expressions are generated.
+ ///
+ /// An optional JSON parameter used in the evaluation of conversion expressions. Defaults to
+ /// "{x}".
+ ///
+ ///
+ /// A dictionary where each key is a unit and the value is another dictionary mapping other units to their
+ /// respective conversion expressions.
+ ///
+ ///
+ /// Thrown if the calculated conversion expression does not match the expected
+ /// conversion expression.
+ ///
+ internal static Dictionary> GetConversionExpressions(this Quantity quantity, string jsonParameter = "{x}")
+ {
+ var conversionsFromBase = new Dictionary();
+ var conversionsToBase = new Dictionary();
+ Unit baseUnit = quantity.Units.First(unit => unit.SingularName == quantity.BaseUnit);
+ foreach (Unit unit in quantity.Units)
+ {
+ if (unit == baseUnit) continue;
+ CompositeExpression conversionFromBase = conversionsFromBase[unit] = ExpressionEvaluator.Evaluate(unit.FromBaseToUnitFunc, jsonParameter);
+ if (conversionFromBase.Terms.Count == 1 && conversionFromBase.Degree.Abs() == Fraction.One)
+ {
+ // as long as there aren't any complex functions we can just invert the expression
+ conversionsToBase[unit] = conversionFromBase.SolveForY();
+ }
+ else
+ {
+ // complex conversion functions require an explicit expression in both directions
+ conversionsToBase[unit] = ExpressionEvaluator.Evaluate(unit.FromUnitToBaseFunc, jsonParameter);
+ }
+ }
+
+ var conversionsFrom = new Dictionary> { [baseUnit] = conversionsToBase };
+ foreach ((Unit fromUnit, CompositeExpression expressionFromBase) in conversionsFromBase)
+ {
+ Dictionary fromUnitConversion = conversionsFrom[fromUnit] = new Dictionary();
+ foreach ((Unit otherUnit, CompositeExpression expressionToBase) in conversionsToBase)
+ {
+ if (fromUnit == otherUnit) continue;
+ fromUnitConversion[otherUnit] = expressionFromBase.Evaluate(expressionToBase);
+ }
+
+ fromUnitConversion[baseUnit] = conversionsFromBase[fromUnit];
+ }
+
+ return conversionsFrom;
+ }
+}
diff --git a/CodeGen/JsonTypes/BaseDimensions.cs b/CodeGen/JsonTypes/BaseDimensions.cs
index 59f9389505..87ffc2c272 100644
--- a/CodeGen/JsonTypes/BaseDimensions.cs
+++ b/CodeGen/JsonTypes/BaseDimensions.cs
@@ -54,10 +54,10 @@ private static void AppendDimensionString(StringBuilder sb, string name, int val
case 0:
return;
case 1:
- sb.AppendFormat("[{0}]", name);
+ sb.Append(name);
break;
default:
- sb.AppendFormat("[{0}^{1}]", name, value);
+ sb.Append($"{name}^{value}");
break;
}
}
diff --git a/CodeGen/JsonTypes/Quantity.cs b/CodeGen/JsonTypes/Quantity.cs
index f471225ad8..ac92ed51d8 100644
--- a/CodeGen/JsonTypes/Quantity.cs
+++ b/CodeGen/JsonTypes/Quantity.cs
@@ -1,4 +1,4 @@
-// Licensed under MIT No Attribution, see LICENSE file at the root.
+// 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;
@@ -14,7 +14,10 @@ internal record Quantity
public BaseDimensions BaseDimensions = new(); // Default to empty
public string BaseUnit = null!;
+ [Obsolete]
public bool GenerateArithmetic = true;
+ public string? AffineOffsetType;
+ public bool IsAffine => !string.IsNullOrEmpty(AffineOffsetType);
public bool Logarithmic = false;
public int LogarithmicScalingFactor = 1;
public string Name = null!;
diff --git a/Common/UnitDefinitions/Temperature.json b/Common/UnitDefinitions/Temperature.json
index c782600ae7..625bffa81e 100644
--- a/Common/UnitDefinitions/Temperature.json
+++ b/Common/UnitDefinitions/Temperature.json
@@ -3,6 +3,7 @@
"BaseUnit": "Kelvin",
"XmlDocSummary": "A temperature is a numerical measure of hot or cold. Its measurement is by detection of heat radiation or particle velocity or kinetic energy, or by the bulk behavior of a thermometric material. It may be calibrated in any of various temperature scales, Celsius, Fahrenheit, Kelvin, etc. The fundamental physical definition of temperature is provided by thermodynamics.",
"GenerateArithmetic": false,
+ "AffineOffsetType": "TemperatureDelta",
"BaseDimensions": {
"Θ": 1
},
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 54479369db..5e553c66cf 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,18 +3,23 @@
true
-
+
+
-
+
+
+
-
+
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
\ No newline at end of file
diff --git a/Samples/Directory.Packages.props b/Samples/Directory.Packages.props
index 3f24827b01..e04094e657 100644
--- a/Samples/Directory.Packages.props
+++ b/Samples/Directory.Packages.props
@@ -5,9 +5,10 @@
-
+
+
\ No newline at end of file
diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MvvmSample.Wpf.csproj b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MvvmSample.Wpf.csproj
index cb757e1ceb..6faa73e5d5 100644
--- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MvvmSample.Wpf.csproj
+++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MvvmSample.Wpf.csproj
@@ -19,7 +19,12 @@
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Nuget.config b/Samples/Nuget.config
new file mode 100644
index 0000000000..d92d9fff56
--- /dev/null
+++ b/Samples/Nuget.config
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples.sln b/Samples/Samples.sln
index 84ba089fe6..582e246391 100644
--- a/Samples/Samples.sln
+++ b/Samples/Samples.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27130.2020
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35514.174 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitConverter.Wpf", "UnitConverter.Wpf\UnitConverter.Wpf\UnitConverter.Wpf.csproj", "{D04EE35D-496A-4C83-A369-09B9B2BEAEEC}"
EndProject
@@ -9,12 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvvmSample.Wpf", "MvvmSampl
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitConverter.Console", "UnitConverter.Console\UnitConverter.Console.csproj", "{B3141011-CEF2-46DE-B3DD-7FECD0D6108C}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Files", "_Files", "{D3B39B9C-CE85-4929-A268-1AEBD945127C}"
- ProjectSection(SolutionItems) = preProject
- build.bat = build.bat
- Directory.Packages.props = Directory.Packages.props
- msbuild.cmd = msbuild.cmd
- EndProjectSection
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitsNetSetup.Configuration", "UnitsNetSetup.Configuration\UnitsNetSetup.Configuration.csproj", "{22425924-4277-4528-8ACD-01F2EA677507}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -72,6 +67,22 @@ Global
{B3141011-CEF2-46DE-B3DD-7FECD0D6108C}.Release|x64.Build.0 = Release|Any CPU
{B3141011-CEF2-46DE-B3DD-7FECD0D6108C}.Release|x86.ActiveCfg = Release|Any CPU
{B3141011-CEF2-46DE-B3DD-7FECD0D6108C}.Release|x86.Build.0 = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|ARM.Build.0 = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|x64.Build.0 = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Debug|x86.Build.0 = Debug|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|ARM.ActiveCfg = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|ARM.Build.0 = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|x64.ActiveCfg = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|x64.Build.0 = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|x86.ActiveCfg = Release|Any CPU
+ {22425924-4277-4528-8ACD-01F2EA677507}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Samples/UnitConverter.Console/Program.cs b/Samples/UnitConverter.Console/Program.cs
index 7ba55aef09..86d77a015f 100644
--- a/Samples/UnitConverter.Console/Program.cs
+++ b/Samples/UnitConverter.Console/Program.cs
@@ -1,4 +1,5 @@
using UnitsNet;
+using UnitsNet.Units;
using static System.Console;
using static UnitsNet.Units.LengthUnit;
diff --git a/Samples/UnitConverter.Console/UnitConverter.Console.csproj b/Samples/UnitConverter.Console/UnitConverter.Console.csproj
index 3f3de72140..abaf7891a0 100644
--- a/Samples/UnitConverter.Console/UnitConverter.Console.csproj
+++ b/Samples/UnitConverter.Console/UnitConverter.Console.csproj
@@ -1,14 +1,18 @@
-
+
Exe
- net8.0
+ net9.0
enable
enable
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs
index c2ed86fb67..e696c1fbc9 100644
--- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs
+++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs
@@ -23,8 +23,8 @@ public interface IMainWindowVm : INotifyPropertyChanged
string FromHeader { get; }
string ToHeader { get; }
- double FromValue { get; set; }
- double ToValue { get; }
+ QuantityValue FromValue { get; set; }
+ QuantityValue ToValue { get; }
ICommand SwapCommand { get; }
}
}
diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml
index 81d8491452..fb6d0e18ef 100644
--- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml
+++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml
@@ -7,15 +7,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpf="clr-namespace:UnitsNet.Samples.UnitConverter.Wpf"
mc:Ignorable="d"
- WindowStartupLocation="CenterScreen"
- Title="UnitsNet - WPF unit converter sample app" Height="800" Width="800"
- d:DesignHeight="600" d:DesignWidth="600"
+ Title="UnitsNet - WPF unit converter sample app" Height="600" Width="800"
d:DataContext="{d:DesignInstance wpf:MainWindowDesignVm, IsDesignTimeCreatable=True}">
-
@@ -32,7 +29,7 @@
SelectionChanged="Selector_OnSelectionChanged" />
-
+
-
+
-
+
+
+
+
+
+
+ Text="{Binding FromValue, StringFormat=G35, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
-
+
-
+
+ Text="{Binding ToValue, Mode=OneWay, StringFormat=G35}" IsReadOnly="true" />
diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowDesignVM.cs b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowDesignVM.cs
index 89f47de952..ad0a2cdf78 100644
--- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowDesignVM.cs
+++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowDesignVM.cs
@@ -30,8 +30,8 @@ public MainWindowDesignVm()
public string FromHeader { get; } = "Value [cm]";
public string ToHeader { get; } = "Result [dm]";
- public double FromValue { get; set; } = 14.5;
- public double ToValue { get; } = 1.45;
+ public QuantityValue FromValue { get; set; } = 14.5m;
+ public QuantityValue ToValue { get; } = 1.45m;
public ICommand SwapCommand { get; } = new RoutedCommand();
diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowVM.cs b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowVM.cs
index a959ab87aa..7b1c38978f 100644
--- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowVM.cs
+++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowVM.cs
@@ -17,7 +17,7 @@ namespace UnitsNet.Samples.UnitConverter.Wpf
public sealed class MainWindowVm : IMainWindowVm
{
private readonly ObservableCollection _units;
- private double _fromValue;
+ private QuantityValue _fromValue;
[CanBeNull] private UnitListItem _selectedFromUnit;
@@ -25,7 +25,7 @@ public sealed class MainWindowVm : IMainWindowVm
[CanBeNull] private UnitListItem _selectedToUnit;
- private double _toValue;
+ private QuantityValue _toValue;
public MainWindowVm()
{
@@ -90,22 +90,24 @@ public UnitListItem SelectedToUnit
public string ToHeader => $"Result [{SelectedToUnit?.Abbreviation}]";
- public double FromValue
+ public QuantityValue FromValue
{
get => _fromValue;
set
{
+ if (value == _fromValue) return;
_fromValue = value;
OnPropertyChanged();
UpdateResult();
}
}
- public double ToValue
+ public QuantityValue ToValue
{
get => _toValue;
private set
{
+ if (value == _toValue) return;
_toValue = value;
OnPropertyChanged();
}
@@ -116,7 +118,7 @@ private set
private void Swap()
{
UnitListItem oldToUnit = SelectedToUnit;
- var oldToValue = ToValue;
+ QuantityValue oldToValue = ToValue;
// Setting these will change ToValue
SelectedToUnit = SelectedFromUnit;
@@ -129,6 +131,11 @@ private void UpdateResult()
{
if (SelectedFromUnit == null || SelectedToUnit == null) return;
+ // note: starting from v6 it is possible to store (and invoke here) a conversion expression
+ // ConvertValueDelegate _convertValueToUnit = UnitsNet.UnitConverter.Default.GetConversionFunction(SelectedFromUnit.UnitEnumValue, SelectedToUnit.UnitEnumValue);
+ // ToValue = _convertValueToUnit(FromValue);
+
+ // note: as of v6 this can be optimized by working directly with the IUntInfo (or it's UnitKey).
ToValue = UnitsNet.UnitConverter.Convert(FromValue,
SelectedFromUnit.UnitEnumValue,
SelectedToUnit.UnitEnumValue);
@@ -139,11 +146,12 @@ private void OnSelectedQuantity(string quantityName)
QuantityInfo quantityInfo = Quantity.ByName[quantityName];
_units.Clear();
+ // note: as of v6 approach this approach is not recommended as the unit info no longer stores the boxed version of the unit (as untyped Enum)
foreach (Enum unitValue in quantityInfo.UnitInfos.Select(ui => ui.Value))
{
- _units.Add(new UnitListItem(unitValue));
+ _units.Add(new UnitListItem(unitValue)); // TODO see about simply passing the UnitInfo (or the UnitKey)
}
-
+
SelectedFromUnit = _units.FirstOrDefault();
SelectedToUnit = _units.Skip(1).FirstOrDefault() ?? SelectedFromUnit; // Try to pick a different to-unit
}
diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitConverter.Wpf.csproj b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitConverter.Wpf.csproj
index 29bc3a93e2..cfea6fe0e6 100644
--- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitConverter.Wpf.csproj
+++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitConverter.Wpf.csproj
@@ -22,8 +22,14 @@
+
-
+
+
+
+
+
+
diff --git a/Samples/UnitsNetSetup.Configuration/ConfigureWithConverterCachingOptions.cs b/Samples/UnitsNetSetup.Configuration/ConfigureWithConverterCachingOptions.cs
new file mode 100644
index 0000000000..2640f23e9f
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/ConfigureWithConverterCachingOptions.cs
@@ -0,0 +1,135 @@
+using UnitsNet;
+using UnitsNet.Units;
+
+namespace UnitsNetSetup.Configuration;
+
+internal static class ConfigureWithConverterCachingOptions
+{
+ ///
+ /// Demonstrates the default caching options for the UnitsNet configuration.
+ ///
+ ///
+ /// This method sets up the configuration using the same caching options as the ones which are used by default:
+ /// The configuration uses an empty dynamic cache (backed by a ConcurrentDictionary), with the option to reduce any conversion expression prior to
+ /// caching.
+ ///
+ /// This configuration has a small overhead when it first encounters a particular combination of units, as well
+ /// as some overhead due to locking.
+ ///
+ ///
+ public static void ConfigureDefault()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder => builder
+ .WithConverterOptions(new QuantityConverterBuildOptions(freeze: false, defaultCachingMode: ConversionCachingMode.None, reduceConstants: true)));
+ }
+
+ ///
+ /// Demonstrates the use of a fully-cached immutable (a.k.a. "frozen") caching configuration.
+ ///
+ ///
+ /// This method sets up the configuration using an immutable cache (backed by a FrozenDictionary) and loads up the unit
+ /// conversions for all quantities (~40K).
+ ///
+ /// While offering the best performance for all subsequent operations, this configuration comes with increase
+ /// startup time and memory footprint:
+ ///
+ /// On a modern machine this takes up around ~30 ms, and uses ~12 MB of RAM.
+ ///
+ public static void ConfigureAsFrozen()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder => builder
+ .WithConverterOptions(new QuantityConverterBuildOptions(freeze: true, defaultCachingMode: ConversionCachingMode.All, reduceConstants: true)));
+ }
+
+ ///
+ /// Demonstrates the use of a no-caching configuration.
+ ///
+ ///
+ /// This method sets up a configuration which doesn't support caching.
+ /// While having the smallest memory footprint, this configuration is about 2x slower than the alternatives.
+ ///
+ public static void ConfigureWithoutCaching()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder => builder
+ .WithConverterOptions(new QuantityConverterBuildOptions(freeze: true, defaultCachingMode: ConversionCachingMode.None)));
+ }
+
+ ///
+ /// Demonstrates the use of an immutable (a.k.a. "frozen") caching configuration which caches only a specific selection of quantities.
+ ///
+ ///
+ /// This method sets up the configuration using an immutable cache (backed by a FrozenDictionary) and loads up the unit
+ /// conversions for the Mass, Volume and Density.
+ ///
+ /// The startup time and memory footprint for such a configuration should be fairly very small, while offering the best performance for the selected quantities,
+ /// while still being able to use the remaining quantities, without caching any of their conversions.
+ ///
+ ///
+ public static void ConfigureAsFrozenWithCustomCachingOptions()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder => builder
+ .WithConverterOptions(new QuantityConverterBuildOptions(freeze: true, defaultCachingMode: ConversionCachingMode.None)
+ .WithCustomCachingOptions(new ConversionCacheOptions(cachingMode: ConversionCachingMode.All, reduceConstants: true))
+ .WithCustomCachingOptions(new ConversionCacheOptions(cachingMode: ConversionCachingMode.All, reduceConstants: true))
+ .WithCustomCachingOptions(new ConversionCacheOptions(cachingMode: ConversionCachingMode.All, reduceConstants: true))));
+ }
+
+ ///
+ /// Demonstrates the use of a fully-cached immutable (a.k.a. "frozen") caching configuration with a specific set of
+ /// quantities.
+ ///
+ ///
+ /// This method sets up a configuration containing only "Mass", "Volume" and "Density" and loads up all their unit
+ /// conversions into an immutable cache.
+ ///
+ /// This configuration offers the best performance, by skipping the loading of all but the specified quantities,
+ /// however attempting to use any other quantity is going to result in a .
+ ///
+ ///
+ public static void ConfigureWithSpecificQuantitiesAsFrozen()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder => builder
+ .WithQuantities([Mass.Info, Volume.Info, Density.Info])
+ .WithConverterOptions(new QuantityConverterBuildOptions(freeze: true, defaultCachingMode: ConversionCachingMode.All, reduceConstants: true)));
+ }
+
+ ///
+ /// Demonstrates the use of a fully-cached immutable (a.k.a. "frozen") caching configuration with a specific set of
+ /// quantities and a subset of their units.
+ ///
+ ///
+ /// This method sets up a configuration containing only "Mass", "Volume" and "Density", each configured with a minimum
+ /// set of units, loaded into an immutable cache.
+ ///
+ /// This configuration offers the absolute best performance (startup, operations and memory), by skipping the
+ /// loading of all but the specified quantities,
+ /// configured to contain the minimum set of required units, however attempting to use any other unit or quantity
+ /// is going to result in a or .
+ ///
+ ///
+ /// Note that instead of selecting a particular set of units, you can use the
+ ///
+ /// to remove units that you know to be inapplicable for the given application (e.g.
+ /// and )
+ ///
+ ///
+ public static void ConfigureWithSpecificQuantitiesAndUnitsAsFrozen()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder => builder
+ .WithQuantities(() => [
+ Mass.MassInfo.CreateDefault(units => units.SelectUnits(MassUnit.Kilogram, MassUnit.Gram)),
+ Volume.VolumeInfo.CreateDefault(units => units.SelectUnits(VolumeUnit.CubicMeter, VolumeUnit.Milliliter)),
+ Density.DensityInfo.CreateDefault(units => units.SelectUnits(DensityUnit.KilogramPerCubicMeter, DensityUnit.GramPerMilliliter))
+ ])
+ .WithConverterOptions(new QuantityConverterBuildOptions(freeze: true, defaultCachingMode: ConversionCachingMode.All, reduceConstants: true)));
+ }
+
+ public static void OutputDensity()
+ {
+ var mass = Mass.FromGrams(5);
+ var volume = Volume.FromMilliliters(2);
+ Density density = mass / volume;
+ Console.WriteLine($"Density: {density}"); // outputs "2500 kg/m3"
+ Console.WriteLine($"Density: {density.ToUnit(DensityUnit.GramPerMilliliter)}"); // outputs "2.5 g/ml"
+ }
+}
diff --git a/Samples/UnitsNetSetup.Configuration/ConfigureWithCustomConversions.cs b/Samples/UnitsNetSetup.Configuration/ConfigureWithCustomConversions.cs
new file mode 100644
index 0000000000..1256b1029b
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/ConfigureWithCustomConversions.cs
@@ -0,0 +1,93 @@
+using UnitsNet;
+using UnitsNet.Units;
+
+namespace UnitsNetSetup.Configuration;
+
+internal static class ConfigureWithCustomConversions
+{
+ public static void Configure()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(
+ builder => builder
+ // configure custom conversion coefficients while preserving the default QuantityInfo for the Pressure (which has the "Pascal" as the DefaultBaseUnit)
+ .ConfigureQuantity(() => Pressure.PressureInfo.CreateDefault(ConfigureCustomPressureUnitConversions))
+ // configure a "completely new" configuration for the TemperatureDelta
+ .ConfigureQuantity(CreateCustomTemperatureConfiguration)
+ );
+ }
+
+ private static IEnumerable> ConfigureCustomPressureUnitConversions(IEnumerable> unitDefinitions)
+ {
+ // we customize a subset of the existing unit definitions (preserving the rest)
+ return unitDefinitions.Select(definition => definition.Value switch
+ {
+ // since these conversions don't involve a constant term we can only specify the conversionFromBase (with the inverse conversion assumed to be the inverse: e.g. 1/999)
+ PressureUnit.InchOfWaterColumn => new UnitDefinition(definition.Value, definition.Name, definition.BaseUnits, conversionFromBase: 999),
+ PressureUnit.MeterOfWaterColumn => definition.WithConversionFromBase(1234.5m),
+ PressureUnit.MillimeterOfWaterColumn => definition.WithConversionFromBase(1.2345),
+ PressureUnit.MillimeterOfMercury => new UnitDefinition(definition.Value, definition.Name, definition.BaseUnits, QuantityValue.FromTerms(1, 3)),
+ PressureUnit.Decapascal => new UnitDefinition(definition.Value, definition.Name, definition.PluralName, definition.BaseUnits,
+ definition.ConversionFromBase.Coefficient * 1.2m,
+ definition.ConversionToBase.Coefficient / 1.2m
+ ),
+ // all preceding conversions result in something of the form:
+ PressureUnit.InchOfMercury => new UnitDefinition(PressureUnit.InchOfMercury, definition.Name, definition.PluralName, definition.BaseUnits,
+ // f(x) = 1/3 * x
+ QuantityValue.FromTerms(1, 3),
+ // f(x) = 3 * g(x)^1 + 0 // with g(x) => x in this case
+ new ConversionExpression(
+ 3,
+ null, // a delegate of the form QuantityValue -> QuantityValue (when null, g(x) = x)
+ 1,
+ 0)),
+ // we keep all other unit conversions as they are
+ _ => definition
+ });
+ }
+
+ private static QuantityInfo CreateCustomTemperatureConfiguration()
+ {
+ // the default BaseUnit for the TemperatureDelta is the Kelvin, but for the purposes of this demo, we can change it to DegreeCelsius
+ // note that changing the base unit would normally require us to modify all the conversion coefficients, which isn't a trivial task (a generic extension method could be added in the future)
+ return new QuantityInfo(
+ "MyTemperature",
+ TemperatureDeltaUnit.DegreeCelsius,
+ // since the conversion coefficient for the new base is the same of the last we don't have to modify anything here
+ TemperatureDelta.TemperatureDeltaInfo.GetDefaultMappings(),
+ // these correspond to the SI dimensions for the quantity: no reason to modify them
+ TemperatureDelta.TemperatureDeltaInfo.DefaultBaseDimensions,
+ // we could provide a custom delegate here ((QuantityValue, TemperatureDeltaUnit) => TemperatureDelta), but this is not particularly useful for the default quantities
+ TemperatureDelta.From // while required on netstandard2.0, this parameter is optional in net7+
+ );
+ }
+
+ public static void OutputPressure()
+ {
+ var pressure = Pressure.FromPascals(100);
+ Console.WriteLine(pressure); // outputs: "100 Pa"
+ Console.WriteLine(pressure.As(PressureUnit.Kilopascal)); // outputs: "0.1"
+ Console.WriteLine(pressure.As(PressureUnit.InchOfWaterColumn)); // outputs: "9990"
+ Console.WriteLine(pressure.As(PressureUnit.MeterOfWaterColumn)); // outputs: "123450"
+ Console.WriteLine(pressure.As(PressureUnit.MillimeterOfWaterColumn)); // outputs: "123.45"
+ Console.WriteLine(pressure.As(PressureUnit.Decapascal)); // outputs: "12"
+ Pressure pressureInInchesOfWaterColumn = pressure.ToUnit(PressureUnit.InchOfMercury);
+ Console.WriteLine(pressureInInchesOfWaterColumn); // outputs: "33.33333333333333 inHg"
+ Console.WriteLine(pressureInInchesOfWaterColumn.Value * 3 == 100); // outputs: "True"
+ Pressure conversionBackToPascals = pressureInInchesOfWaterColumn
+ .ToUnit(PressureUnit.Atmosphere)
+ .ToUnit(PressureUnit.MeterOfWaterColumn)
+ .ToUnit(PressureUnit.MillimeterOfWaterColumn)
+ .ToUnit(PressureUnit.Decapascal)
+ .ToUnit(PressureUnit.Pascal);
+ Console.WriteLine(conversionBackToPascals); // outputs: "100 Pa"
+ Console.WriteLine(conversionBackToPascals == pressure); // outputs: "True"
+ Console.WriteLine(conversionBackToPascals == pressureInInchesOfWaterColumn); // outputs: "True"
+ }
+
+ public static void OutputTemperatureDelta()
+ {
+ TemperatureDelta temperatureDelta = default;
+ Console.WriteLine(temperatureDelta.Unit); // outputs: "DegreeCelsius"
+ Console.WriteLine(temperatureDelta.QuantityInfo.Name); // outputs: "MyTemperature"
+ }
+}
diff --git a/Samples/UnitsNetSetup.Configuration/ConfigureWithCustomQuantities.cs b/Samples/UnitsNetSetup.Configuration/ConfigureWithCustomQuantities.cs
new file mode 100644
index 0000000000..e069f29afe
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/ConfigureWithCustomQuantities.cs
@@ -0,0 +1,334 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using UnitsNet;
+using UnitsNet.Debug;
+using UnitsNet.Units;
+
+namespace UnitsNetSetup.Configuration;
+
+internal static class ConfigureWithCustomQuantities
+{
+ ///
+ /// Example of a custom/third-party quantity implementation, for plugging in quantities and units at runtime.
+ ///
+ public enum HowMuchUnit
+ {
+ Some,
+ ATon,
+ AShitTon
+ }
+
+ ///
+ ///
+ /// Example of a custom/third-party quantity implementation, for plugging in quantities and units at runtime.
+ ///
+ [DebuggerDisplay(QuantityDebugProxy.DisplayFormat)]
+ [DebuggerTypeProxy(typeof(QuantityDebugProxy))]
+ public readonly struct HowMuch : IArithmeticQuantity, IEquatable, IComparable
+ {
+ public HowMuchUnit Unit { get; }
+
+ public QuantityValue Value { get; }
+
+ public HowMuch(QuantityValue value, HowMuchUnit unit)
+ {
+ Unit = unit;
+ Value = value;
+ }
+
+ public static HowMuch From(QuantityValue value, HowMuchUnit unit)
+ {
+ return new HowMuch(value, unit);
+ }
+
+ // public QuantityValue As(HowMuchUnit unit)
+ // {
+ // return UnitConverter.Default.ConvertValue(this, unit);
+ // }
+
+ // public HowMuch ToUnit(HowMuchUnit unit)
+ // {
+ // return new HowMuch(As(unit), unit);
+ // }
+
+ public static HowMuch Zero { get; } = new(0, HowMuchUnit.Some);
+
+ public static readonly QuantityInfo Info = new(
+ HowMuchUnit.Some,
+ new UnitDefinition[]
+ {
+ new(HowMuchUnit.Some, "Some", BaseUnits.Undefined),
+ new(HowMuchUnit.ATon, "Tons", new BaseUnits(mass: MassUnit.Tonne), QuantityValue.FromTerms(1, 10)),
+ new(HowMuchUnit.AShitTon, "ShitTons", BaseUnits.Undefined, QuantityValue.FromTerms(1, 100))
+ },
+ new BaseDimensions(0, 1, 0, 0, 0, 0, 0),
+ // providing a resource manager for the unit abbreviations (optional)
+ Properties.CustomQuantities_HowMuch.ResourceManager);
+
+ #region IQuantity
+
+ public BaseDimensions Dimensions
+ {
+ get => BaseDimensions.Dimensionless;
+ }
+
+ // Enum IQuantity.Unit
+ // {
+ // get => Unit;
+ // }
+
+ QuantityInfo IQuantity.QuantityInfo
+ {
+ get => Info;
+ }
+
+ // QuantityInfo IQuantity.QuantityInfo
+ // {
+ // get => Info;
+ // }
+
+ QuantityInfo IQuantity.QuantityInfo
+ {
+ get => Info;
+ }
+
+ // QuantityValue IQuantity.As(Enum unit)
+ // {
+ // if (unit is HowMuchUnit howMuchUnit) return As(howMuchUnit);
+ // throw new ArgumentException("Must be of type HowMuchUnit.", nameof(unit));
+ // }
+ //
+ // IQuantity IQuantity.ToUnit(Enum unit)
+ // {
+ // if (unit is HowMuchUnit howMuchUnit) return ToUnit(howMuchUnit);
+ // throw new ArgumentException("Must be of type HowMuchUnit.", nameof(unit));
+ // }
+
+ // QuantityValue IQuantity.As(UnitSystem unitSystem)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // HowMuch IQuantity.ToUnit(UnitSystem unitSystem)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // IQuantity IQuantity.ToUnit(UnitSystem unitSystem)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // IQuantity IQuantity.ToUnit(UnitSystem unitSystem)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // IQuantity IQuantity.ToUnit(HowMuchUnit unit)
+ // {
+ // return ToUnit(unit);
+ // }
+
+ public override string ToString()
+ {
+ return ToString("G", null);
+ }
+
+ public string ToString(string? format, IFormatProvider? formatProvider)
+ {
+ return QuantityFormatter.Default.Format(this, format, formatProvider);
+ }
+
+ // public string ToString(IFormatProvider? provider)
+ // {
+ // return ToString("G", provider);
+ // }
+
+ UnitKey IQuantity.UnitKey
+ {
+ get => UnitKey.ForUnit(Unit);
+ }
+
+ #endregion
+
+ #region Equality members
+
+ public bool Equals(HowMuch other)
+ {
+ return Value.Equals(other.As(Unit));
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is HowMuch other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(typeof(HowMuch), this.As(Info.BaseUnitInfo.Value));
+ // return HashCode.Combine(typeof(HowMuch), As(Info.BaseUnitInfo.Value));
+ }
+
+ public int CompareTo(HowMuch other)
+ {
+ return Value.CompareTo(other.As(Unit));
+ }
+
+ #endregion
+
+ #region Implementation of IEqualityOperators
+
+ public static bool operator ==(HowMuch left, HowMuch right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(HowMuch left, HowMuch right)
+ {
+ return !left.Equals(right);
+ }
+
+ #endregion
+
+ #region Implementation of IComparisonOperators
+
+ // public static bool operator >(HowMuch left, HowMuch right)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // public static bool operator >=(HowMuch left, HowMuch right)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // public static bool operator <(HowMuch left, HowMuch right)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // public static bool operator <=(HowMuch left, HowMuch right)
+ // {
+ // throw new NotImplementedException();
+ // }
+
+ #endregion
+
+ #region Implementation of IParsable
+
+ // public static HowMuch Parse(string s, IFormatProvider? provider)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //
+ // public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out HowMuch result)
+ // {
+ // throw new NotImplementedException();
+ // }
+
+ #endregion
+
+ #region Implementation of IAdditionOperators
+
+ public static HowMuch operator +(HowMuch left, HowMuch right)
+ {
+ return new HowMuch(left.Value + right.As(left.Unit), left.Unit);
+ }
+
+ #endregion
+
+ #region Implementation of ISubtractionOperators
+
+ public static HowMuch operator -(HowMuch left, HowMuch right)
+ {
+ return new HowMuch(left.Value - right.As(left.Unit), left.Unit);
+ }
+
+ #endregion
+
+ #region Implementation of IMultiplyOperators
+
+ public static HowMuch operator *(HowMuch left, QuantityValue right)
+ {
+ return new HowMuch(left.Value * right, left.Unit);
+ }
+
+ #endregion
+
+ #region Implementation of IDivisionOperators
+
+ public static HowMuch operator /(HowMuch left, QuantityValue right)
+ {
+ return new HowMuch(left.Value / right, left.Unit);
+ }
+
+ #endregion
+
+ #region Implementation of IUnaryNegationOperators
+
+ public static HowMuch operator -(HowMuch value)
+ {
+ return new HowMuch(-value.Value, value.Unit);
+ }
+
+ #endregion
+ }
+
+ public static void Configure()
+ {
+ UnitsNet.UnitsNetSetup.ConfigureDefaults(builder =>
+ // selecting only the required quantities (the rest are not needed for this example)
+ builder.WithQuantities([Mass.Info, Length.Info])
+ // adding the list of our custom quantities
+ .WithAdditionalQuantities([HowMuch.Info])
+ // (optionally) configuring custom conversion options (unit-conversion caching, implicit conversion etc.)
+ .WithConverterOptions(new QuantityConverterBuildOptions(defaultCachingMode:ConversionCachingMode.None)
+ .WithCustomCachingOptions(new ConversionCacheOptions(ConversionCachingMode.All))
+ // configuring an "implicit" conversion function between the "HowMuch" and "Mass":
+ .WithImplicitConversionOptions(options => options.SetCustomConversion()
+ // 1. since the BaseDimensions are compatible the relationship "1 ATon" = "1 Ton" would be inferred automatically (as they both have the same BaseUnits)
+ // alternatively we could specify it manually (using a conversion coefficient or a "ConversionExpression"):
+ // .SetCustomConversion(HowMuchUnit.AShitTon, MassUnit.Tonne, conversionCoefficient: 1)
+ // 2. based on this relationship, all other MassUnit <-> HowMuchUnit conversions can be inferred automatically,
+ // but if we want to: we can customize the default units to use when converting in either directions:
+ .SetConversionUnits(MassUnit.ShortHundredweight, HowMuchUnit.Some, mapBothDirections:false)
+ // .SetConversionUnits(MassUnit.LongHundredweight, HowMuchUnit.Some, mapBothDirections:false)
+ )));
+ }
+
+ [SuppressMessage("ReSharper", "LocalizableElement")]
+ public static void OutputHowMuch()
+ {
+ var some = new HowMuch(1, HowMuchUnit.Some);
+ var aTon = new HowMuch(1, HowMuchUnit.ATon);
+ var aShitTon = new HowMuch(1, HowMuchUnit.AShitTon);
+
+ Console.WriteLine($"1 Some = {some.As(HowMuchUnit.ATon)} Tons");
+ Console.WriteLine($"1 Some = {some.As(HowMuchUnit.AShitTon)} ShitTons");
+ Console.WriteLine($"1 Ton = {aTon.As(HowMuchUnit.Some)} Some");
+ Console.WriteLine($"1 Ton = {aTon.As(HowMuchUnit.AShitTon)} ShitTons");
+ Console.WriteLine($"1 ShitTon = {aShitTon.As(HowMuchUnit.Some)} Some");
+ Console.WriteLine($"1 ShitTon = {aShitTon.As(HowMuchUnit.ATon)} Tons");
+ Console.WriteLine($"1 ShitTon = {aShitTon.ToUnit(HowMuchUnit.ATon)}"); // the abbreviation ("at") is mapped from the resource dictionary
+
+ HowMuch sameAsATon = (some + (aTon / 3).ToUnit(HowMuchUnit.AShitTon) - some) * 3;
+ Console.WriteLine($"sameAsATon == aTon is {sameAsATon == aTon}"); // outputs "True"
+
+ Mass aTonOfMass = UnitConverter.Default.ConvertTo(aTon.Value, aTon.Unit, Mass.Info);
+ Console.WriteLine($"aTonOfMass = {aTonOfMass}");
+
+ Mass shortHundredWeight = aTonOfMass.ToUnit(MassUnit.ShortHundredweight);
+ HowMuch aToneAsSome = UnitConverter.Default.ConvertTo(shortHundredWeight.Value, shortHundredWeight.Unit, HowMuch.Info);
+ Console.Out.WriteLine($"aToneAsSome = {aToneAsSome}");
+ Console.Out.WriteLine($"aTon == aToneAsSome is {aTon == aToneAsSome}"); // outputs "True"
+
+ HowMuch[] someItems = [some, aTon, aShitTon];
+ Console.Out.WriteLine("someItems.Min() = {0}", someItems.Min());
+ Console.Out.WriteLine("someItems.Max() = {0}", someItems.Max());
+ Console.Out.WriteLine("someItems.Sum() = {0}", someItems.Sum());
+ Console.Out.WriteLine("someItems.Sum(HowMuchUnit.ATon) = {0}", someItems.Sum(HowMuchUnit.ATon));
+ Console.Out.WriteLine("someItems.Average() = {0}", someItems.Average());
+ Console.Out.WriteLine("someItems.Average(HowMuchUnit.ATon) = {0}", someItems.Average(HowMuchUnit.ATon));
+ Console.Out.WriteLine("someItems.Average(MassUnit.Kilogram) = {0}", UnitConverter.Default.ConvertTo(someItems.Average(), MassUnit.Kilogram));
+ }
+}
diff --git a/Samples/UnitsNetSetup.Configuration/Program.cs b/Samples/UnitsNetSetup.Configuration/Program.cs
new file mode 100644
index 0000000000..fc2721e51b
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/Program.cs
@@ -0,0 +1,74 @@
+using System.Diagnostics;
+
+namespace UnitsNetSetup.Configuration;
+
+internal class Program
+{
+ private static void Main(string[] args)
+ {
+ Console.WriteLine("UnitsNet Configuration Samples");
+ var activeScenario = args[0];
+
+ var startingTimestamp = Stopwatch.GetTimestamp();
+ switch (activeScenario)
+ {
+ case "CachingOptions":
+ StartWithCachingOptions(args.Length == 1 ? null : args[1]);
+ break;
+ case "CustomConversions":
+ StartWithCustomConversions();
+ break;
+ case "CustomQuantities":
+ StartWithCustomQuantities();
+ break;
+ default:
+ Console.WriteLine("\nInvalid scenario specified.");
+ break;
+ }
+
+ Console.WriteLine($"Finished in {Stopwatch.GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp())}");
+ }
+
+ private static void StartWithCachingOptions(string? configurationToUse)
+ {
+ Console.WriteLine($"Running Caching Options Scenario with configuration: {configurationToUse}");
+ switch (configurationToUse)
+ {
+ case "AsFrozen":
+ ConfigureWithConverterCachingOptions.ConfigureAsFrozen();
+ break;
+ case "WithoutCaching":
+ ConfigureWithConverterCachingOptions.ConfigureWithoutCaching();
+ break;
+ case "AsFrozenWithCustomCachingOptions":
+ ConfigureWithConverterCachingOptions.ConfigureAsFrozenWithCustomCachingOptions();
+ break;
+ case "WithSpecificQuantitiesAsFrozen":
+ ConfigureWithConverterCachingOptions.ConfigureWithSpecificQuantitiesAsFrozen();
+ break;
+ case "WithSpecificQuantitiesAndUnitsAsFrozen":
+ ConfigureWithConverterCachingOptions.ConfigureWithSpecificQuantitiesAndUnitsAsFrozen();
+ break;
+ default:
+ ConfigureWithConverterCachingOptions.ConfigureDefault();
+ break;
+ }
+ ConfigureWithConverterCachingOptions.OutputDensity();
+ }
+
+
+ private static void StartWithCustomConversions()
+ {
+ Console.WriteLine($"{DateTime.Now}: Running the Custom Conversions Scenario");
+ ConfigureWithCustomConversions.Configure();
+ ConfigureWithCustomConversions.OutputPressure();
+ ConfigureWithCustomConversions.OutputTemperatureDelta();
+ }
+
+ private static void StartWithCustomQuantities()
+ {
+ Console.WriteLine("\nRunning the Custom Quantities Scenario");
+ ConfigureWithCustomQuantities.Configure();
+ ConfigureWithCustomQuantities.OutputHowMuch();
+ }
+}
diff --git a/Samples/UnitsNetSetup.Configuration/Properties/CustomQuantities.HowMuch.Designer.cs b/Samples/UnitsNetSetup.Configuration/Properties/CustomQuantities.HowMuch.Designer.cs
new file mode 100644
index 0000000000..bdd4095a83
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/Properties/CustomQuantities.HowMuch.Designer.cs
@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace UnitsNetSetup.Configuration.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class CustomQuantities_HowMuch {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CustomQuantities_HowMuch() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UnitsNetSetup.Configuration.Properties.CustomQuantities.HowMuch", typeof(CustomQuantities_HowMuch).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ast.
+ ///
+ internal static string ShitTons {
+ get {
+ return ResourceManager.GetString("ShitTons", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to sm.
+ ///
+ internal static string Some {
+ get {
+ return ResourceManager.GetString("Some", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to at.
+ ///
+ internal static string Tons {
+ get {
+ return ResourceManager.GetString("Tons", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Samples/UnitsNetSetup.Configuration/Properties/CustomQuantities.HowMuch.resx b/Samples/UnitsNetSetup.Configuration/Properties/CustomQuantities.HowMuch.resx
new file mode 100644
index 0000000000..9e87db2d71
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/Properties/CustomQuantities.HowMuch.resx
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ast
+ HowMuch.AShitTon
+
+
+ at
+ HowMuch.ATon
+
+
+ sm
+ HowMuch.Some
+
+
\ No newline at end of file
diff --git a/Samples/UnitsNetSetup.Configuration/Properties/launchSettings.json b/Samples/UnitsNetSetup.Configuration/Properties/launchSettings.json
new file mode 100644
index 0000000000..be0f3d6337
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/Properties/launchSettings.json
@@ -0,0 +1,16 @@
+{
+ "profiles": {
+ "ConfigureCachingOptions": {
+ "commandName": "Project",
+ "commandLineArgs": "CachingOptions WithSpecificQuantitiesAndUnitsAsFrozen"
+ },
+ "ConfigureCustomQuantities": {
+ "commandName": "Project",
+ "commandLineArgs": "CustomQuantities"
+ },
+ "ConfigureCustomConversions": {
+ "commandName": "Project",
+ "commandLineArgs": "CustomConversions"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/UnitsNetSetup.Configuration/UnitsNetSetup.Configuration.csproj b/Samples/UnitsNetSetup.Configuration/UnitsNetSetup.Configuration.csproj
new file mode 100644
index 0000000000..f7fe09397b
--- /dev/null
+++ b/Samples/UnitsNetSetup.Configuration/UnitsNetSetup.Configuration.csproj
@@ -0,0 +1,34 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ CustomQuantities.HowMuch.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ CustomQuantities.HowMuch.Designer.cs
+
+
+
+
diff --git a/UnitsNet.Benchmark/BenchmarkHelpers.cs b/UnitsNet.Benchmark/BenchmarkHelpers.cs
index 477c1c67e3..2f7d4413b8 100644
--- a/UnitsNet.Benchmark/BenchmarkHelpers.cs
+++ b/UnitsNet.Benchmark/BenchmarkHelpers.cs
@@ -2,7 +2,6 @@
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -15,24 +14,39 @@ public static string[] GetRandomAbbreviations(this Random random, Uni
return random.GetItems(abbreviations.GetAllUnitAbbreviationsForQuantity(typeof(TQuantity)).ToArray(), nbAbbreviations);
}
- public static (TQuantity Quantity, TUnit Unit)[] GetRandomConversions(this Random random, double value, TUnit[] options,
+ public static (TUnit FromUnit, TUnit ToUnit)[] GetRandomConversions(this Random random, IEnumerable options, int nbConversions)
+ {
+ var choices = options.ToArray();
+ return random.GetItems(choices, nbConversions).Zip(random.GetItems(choices, nbConversions), (fromUnit, toUnit) => (fromUnit, toUnit)).ToArray();
+ }
+
+ public static (TQuantity Quantity, TUnit Unit)[] GetRandomConversions(this Random random, QuantityValue value, IEnumerable options,
int nbConversions)
where TQuantity : IQuantity
where TUnit : struct, Enum
{
- var quantities = GetRandomQuantities(random, value, options, nbConversions);
+ return GetRandomConversions(random, value, options.ToArray(), nbConversions);
+ }
+
+ public static (TQuantity Quantity, TUnit Unit)[] GetRandomConversions(this Random random, QuantityValue value, TUnit[] options,
+ int nbConversions)
+ where TQuantity : IQuantity
+ where TUnit : struct, Enum
+ {
+ IEnumerable quantities = random.GetRandomQuantities(value, options, nbConversions);
TUnit[] units = random.GetItems(options, nbConversions);
return quantities.Zip(units, (quantity, unit) => (quantity, unit)).ToArray();
}
-
- public static IEnumerable GetRandomQuantities(this Random random, double value, TUnit[] units, int nbQuantities)
- where TQuantity : IQuantity where TUnit : struct, Enum
+
+ public static IEnumerable GetRandomQuantities(this Random random, QuantityValue value, TUnit[] units, int nbQuantities)
+ where TQuantity : IQuantity
+ where TUnit : struct, Enum
{
IEnumerable quantities = random.GetItems(units, nbQuantities).Select(unit => (TQuantity)Quantity.From(value, unit));
return quantities;
}
-
-#if !NET
+
+ #if !NET
/// Creates an array populated with items chosen at random from the provided set of choices.
/// The random number generator used to select items.
/// The items to use to populate the array.
diff --git a/UnitsNet.Benchmark/Comparisons/ComparisonBenchmarks.cs b/UnitsNet.Benchmark/Comparisons/ComparisonBenchmarks.cs
index 6bbc84741d..8b965fd002 100644
--- a/UnitsNet.Benchmark/Comparisons/ComparisonBenchmarks.cs
+++ b/UnitsNet.Benchmark/Comparisons/ComparisonBenchmarks.cs
@@ -1,68 +1,243 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Order;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+using UnitsNet.Units;
namespace UnitsNet.Benchmark.Comparisons;
[MemoryDiagnoser]
-[ShortRunJob(RuntimeMoniker.Net48)]
-[ShortRunJob(RuntimeMoniker.Net80)]
-public class ComparisonBenchmarks
+// [SimpleJob(RuntimeMoniker.Net48)]
+[SimpleJob(RuntimeMoniker.Net90)]
+// [ShortRunJob(RuntimeMoniker.Net48)]
+// [ShortRunJob(RuntimeMoniker.Net80)]
+// [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod)]
+// [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+// [Config(typeof(Config))]
+public class ComparisonBenchmarksTestingAlloc
{
private static readonly Mass Tolerance = Mass.FromNanograms(1);
- public static IEnumerable