Skip to content
57 changes: 6 additions & 51 deletions CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,63 +227,18 @@ public sealed class {quantityInfoClassName}: QuantityInfo<{_quantity.Name}, {_un
/// </summary>
/// <returns>An <see cref=""IEnumerable{{T}}""/> of <see cref=""UnitDefinition{{{_unitEnumName}}}""/> representing the default unit mappings for {_quantity.Name}.</returns>
public static IEnumerable<UnitDefinition<{_unitEnumName}>> GetDefaultMappings()
{{");
{{
return new UnitDefinition<{_unitEnumName}>[]
{{");

foreach (Unit unit in _quantity.Units)
{
BaseUnits? baseUnits = unit.BaseUnits;
string baseUnitsFormat;
if (baseUnits == null)
{
baseUnitsFormat = "BaseUnits.Undefined";
}
else
{
baseUnitsFormat = $"new BaseUnits({string.Join(", ",
new[]
{
baseUnits.L != null ? $"length: LengthUnit.{baseUnits.L}" : null,
baseUnits.M != null ? $"mass: MassUnit.{baseUnits.M}" : null,
baseUnits.T != null ? $"time: DurationUnit.{baseUnits.T}" : null,
baseUnits.I != null ? $"current: ElectricCurrentUnit.{baseUnits.I}" : null,
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))})";
}

// the UnitInfo constructor has 3 overloads:
// - one for the base unit without conversion expressions
// - one for units with only FromBaseToUnit conversion expression (with the FromUnitToBase expression assumed to be the inverse)
// - one for units with both FromBaseToUnit and FromUnitToBase conversion expressions (required when the conversion is not a simple inverse, e.g. affine conversions)
if (unit.SingularName == _quantity.BaseUnit)
{
Writer.WL($@"
yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat});");
}
else
{
CompositeExpression expressionFromBaseToUnit = ExpressionEvaluator.Evaluate(unit.FromBaseToUnitFunc, "{x}");
// Check if FromUnitToBase is simply the inverse of FromBaseToUnit
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($@"
new ({_unitEnumName}.{unit.SingularName}, {_quantity.Name}Units.{unit.SingularName}),");
}

Writer.WL($@"
}};
}}
}}
");
Expand Down
108 changes: 108 additions & 0 deletions CodeGen/Generators/UnitsNetGen/UnitsClassGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Linq;
using CodeGen.Helpers;
using CodeGen.Helpers.ExpressionAnalyzer;
using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
using CodeGen.JsonTypes;
using Fractions;

namespace CodeGen.Generators.UnitsNetGen
{
/// <summary>
/// Generates the static class of the units of a quantity as <c>UnitOf&lt;TQuantity&gt;</c>, such as
/// <c>LengthUnits.Meter</c>, with their names, base units and conversions.
/// </summary>
internal class UnitsClassGenerator : GeneratorBase
{
private readonly Quantity _quantity;
private readonly string _unitEnumName;

public UnitsClassGenerator(Quantity quantity)
{
_quantity = quantity;
_unitEnumName = $"{quantity.Name}Unit";
}

public string Generate()
{
Writer.WL(GeneratedFileHeader);
Writer.WL($@"
// ReSharper disable once CheckNamespace
namespace UnitsNet.Units
{{
/// <summary>
/// The units of <see cref=""{_quantity.Name}""/>, such as <see cref=""{_quantity.Name}Units.{_quantity.BaseUnit}""/>.
/// </summary>");
Writer.WLIfText(1, GetObsoleteAttributeOrNull(_quantity.ObsoleteText));
Writer.WL($@"
public static class {_quantity.Name}Units
{{");
var isFirst = true;
foreach (Unit unit in _quantity.Units)
{
if (!isFirst)
{
Writer.WL();
}

isFirst = false;
Writer.WL($@"
/// <summary>
/// The <see cref=""{_unitEnumName}.{unit.SingularName}""/> unit of <see cref=""{_quantity.Name}""/>.
/// </summary>");
Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit.ObsoleteText));
Writer.WL($@"
public static readonly UnitOf<{_quantity.Name}> {unit.SingularName} = new(""{unit.SingularName}"", ""{unit.PluralName}"", {GetBaseUnitsFormat(unit)},{GetConversionFormat(unit)});");
}

Writer.WL($@"
}}
}}");
return Writer.ToString();
}

private string GetConversionFormat(Unit unit)
{
if (unit.SingularName == _quantity.BaseUnit)
{
return " 1";
}

CompositeExpression expressionFromBaseToUnit = ExpressionEvaluator.Evaluate(unit.FromBaseToUnitFunc, "{x}");
// A conversion with a single term is described by the value of the unit in the base unit, the reciprocal of its
// coefficient. Otherwise, such as for units with an offset, it needs both conversion expressions.
if (expressionFromBaseToUnit.Terms.Count == 1 && expressionFromBaseToUnit.Degree == Fraction.One)
{
return " " + expressionFromBaseToUnit.GetReciprocalCoefficientFormat();
}

return $@"
{expressionFromBaseToUnit.GetConversionExpressionFormat()},
{unit.GetUnitToBaseConversionExpressionFormat()}";
}

private static string GetBaseUnitsFormat(Unit unit)
{
BaseUnits? baseUnits = unit.BaseUnits;
if (baseUnits == null)
{
return "BaseUnits.Undefined";
}

return $"new BaseUnits({string.Join(", ",
new[]
{
baseUnits.L != null ? $"length: LengthUnit.{baseUnits.L}" : null,
baseUnits.M != null ? $"mass: MassUnit.{baseUnits.M}" : null,
baseUnits.T != null ? $"time: DurationUnit.{baseUnits.T}" : null,
baseUnits.I != null ? $"current: ElectricCurrentUnit.{baseUnits.I}" : null,
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))})";
}

private static string? GetObsoleteAttributeOrNull(string? obsoleteText) => string.IsNullOrWhiteSpace(obsoleteText)
? null
: $"[System.Obsolete(\"{obsoleteText}\")]";
}
}
7 changes: 7 additions & 0 deletions CodeGen/Generators/UnitsNetGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static void Generate(string rootDir, Quantity[] quantities, QuantityNameT

GenerateQuantity(quantity, $"{outputDir}/Quantities/{quantity.Name}.g.cs");
GenerateUnitType(quantity, $"{outputDir}/Units/{quantity.Name}Unit.g.cs", unitEnumValues);
GenerateUnitsClass(quantity, $"{outputDir}/Units/{quantity.Name}Units.g.cs");
GenerateNumberToExtensions(quantity, $"{extensionsOutputDir}/NumberTo{quantity.Name}Extensions.g.cs");
GenerateNumberToExtensionsTestClass(quantity, $"{extensionsTestOutputDir}/NumberTo{quantity.Name}ExtensionsTest.g.cs");
GenerateNumberToExtensionsCS14(quantity, $"{extensionsCs14OutputDir}/NumberTo{quantity.Name}Extensions.g.cs");
Expand Down Expand Up @@ -126,6 +127,12 @@ private static void GenerateNumberToExtensionsCS14TestClass(Quantity quantity, s
CodeGenFile.WriteAllText(filePath, content);
}

private static void GenerateUnitsClass(Quantity quantity, string filePath)
{
var content = new UnitsClassGenerator(quantity).Generate();
CodeGenFile.WriteAllText(filePath, content);
}

private static void GenerateUnitType(Quantity quantity, string filePath, UnitEnumNameToValue unitEnumValues)
{
var content = new UnitTypeGenerator(quantity, unitEnumValues).Generate();
Expand Down
9 changes: 9 additions & 0 deletions CodeGen/Helpers/ExpressionEvaluationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ private static string GetLongConstantFormat(this Fraction coefficient)
return expandedExpression;
}

/// <summary>
/// Gets the format of the reciprocal of the coefficient of a conversion with a single term, such as the value of a
/// unit in the base unit from the conversion from the base unit to the unit.
/// </summary>
public static string GetReciprocalCoefficientFormat(this CompositeExpression expression)
{
return expression.Terms.Single().Coefficient.Reciprocal().GetFractionalConstantFormat();
}

private static string GetFractionalConstantFormat(this Fraction coefficient)
{
coefficient = coefficient.Reduce();
Expand Down
14 changes: 14 additions & 0 deletions Docs/extending-with-custom-units.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ To add new quantities or units to the `UnitsNet` nuget, please see [Adding a New
You miss out on the statically generated code for members like `Length.FromMeters(1)` and `myLength.Meters`.
Conversion methods like `myLength.As()` and `myLength.ToUnit()` currently only support their respective unit enums, in this case `LengthUnit`.

### Convert to and from your own unit

To convert a quantity to or from a unit that UnitsNet doesn't define, describe the unit with a `UnitOf<TQuantity>`. You give the value of one of the unit in the base unit of the quantity, and UnitsNet uses it directly, without a unit enum value or any setup.

```c#
// 1 furlong = 201.168 m
var furlong = new UnitOf<Length>("Furlong", "Furlongs", 201.168);

QuantityValue furlongs = Length.FromMiles(1).As(furlong); // 8
Length length = Length.Info.From(2, furlong); // 402.336 m, in the base unit
```

The unit is tied to its quantity, so `Mass.FromKilograms(1).As(furlong)` doesn't compile. For units with an offset, such as temperatures, pass the conversion expressions from and to the base unit instead. `From` returns the quantity in its base unit, since a quantity can only be in one of its own units.

### Can I add a custom unit to an existing quantity in UnitsNet?

Currently, no.
Expand Down
Loading
Loading