Skip to content

Extending with Custom Units

Andreas Gullberg Larsen edited this page Feb 2, 2023 · 20 revisions

This article is for when you want to add your own custom units and quantities in your code, to represent as IQuantity and to reuse Units.NET for parsing and converting between units.

If you are looking to add new quantities or units to the UnitsNet nuget, please see https://github.com/angularsen/UnitsNet/wiki/Adding-a-New-Unit.

Why add a custom unit?

Good question.

In its current state, the support for custom quantities and units is limited and provides limited integration with the existing units and code. We consider it exploratory, to see what is possible, and we welcome ideas on how it can be improved.

Key benefits

  • Reuse functionality that operates on IQuantity
    • Dynamically convert to unit with .As(Enum)
    • UnitMath with min, max, sum, average etc.
    • .NET generic math support, currently sum and average.
  • Reuse UnitConverter to dynamically convert between units by their enum values
    • Also allows you to dynamically convert between your custom units and the built-in units, such as CustomLengthUnit.ElbowToThumb to LengthUnit.Meter.
  • Reuse QuantityParser and UnitParser to parse quantity strings like "5 cm" and "cm" for your own quantities and units

What could be better

  • Source generators via nuget, if possible #902
  • String-based lookup instead of enum-based, blocks #1181

Got more ideas? Create a discussion or issue.

Units.NET structure

Units.NET roughly consists of these parts:

  • Quantities like Length and Force
  • Unit enum values like LengthUnit.Meter and ForceUnit.Newton
  • UnitAbbreviationsCache, UnitParser, QuantityParser and UnitConverter for parsing and converting quantities and units
  • JSON files for defining units, conversion functions and abbreviations
  • CodeGen.exe to generate C# code based on JSON files

Example: Custom quantity HowMuch with units HowMuchUnit

Sample output

GetDefaultAbbreviation(): sm, lts, tns
Parse<HowMuchUnit>(): Some, Lots, Tons

Convert 10 tons to:
200 sm
100 lts
10 tns

Map unit enum values to unit abbreviations

UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Some, "sm");
UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Lots, "lts");
UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Tons, "tns");

Lookup unit abbrevations from enum values

Console.WriteLine("GetDefaultAbbreviation(): " + string.Join(", ", 
	UnitAbbreviationsCache.Default.GetDefaultAbbreviation(HowMuchUnit.Some), // "sm"
	UnitAbbreviationsCache.Default.GetDefaultAbbreviation(HowMuchUnit.Lots), // "lts"
	UnitAbbreviationsCache.Default.GetDefaultAbbreviation(HowMuchUnit.Tons)	 // "tns"
));

Parse unit abbreviations back to enum values

Console.WriteLine("Parse<HowMuchUnit>(): " + string.Join(", ",
	UnitParser.Default.Parse<HowMuchUnit>("sm"),  // Some
	UnitParser.Default.Parse<HowMuchUnit>("lts"), // Lots
	UnitParser.Default.Parse<HowMuchUnit>("tns")  // Tons
));

Convert between units of custom quantity

var unitConverter = UnitConverter.Default;
unitConverter.SetConversionFunction<HowMuch>(HowMuchUnit.Lots, HowMuchUnit.Some, x => new HowMuch(x.Value * 2, HowMuchUnit.Some));
unitConverter.SetConversionFunction<HowMuch>(HowMuchUnit.Tons, HowMuchUnit.Lots, x => new HowMuch(x.Value * 10, HowMuchUnit.Lots));
unitConverter.SetConversionFunction<HowMuch>(HowMuchUnit.Tons, HowMuchUnit.Some, x => new HowMuch(x.Value * 20, HowMuchUnit.Some));

var from = new HowMuch(10, HowMuchUnit.Tons);
IQuantity Convert(HowMuchUnit toUnit) => unitConverter.GetConversionFunction<HowMuch>(from.Unit, toUnit)(from);

Console.WriteLine($"Convert 10 tons to:");
Console.WriteLine(Convert(HowMuchUnit.Some)); // 200 sm
Console.WriteLine(Convert(HowMuchUnit.Lots)); // 100 lts
Console.WriteLine(Convert(HowMuchUnit.Tons)); // 10 tns

Sample quantity

https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomQuantities/HowMuchUnit.cs
https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomQuantities/HowMuch.cs

public enum HowMuchUnit
{
	Some,
	Lots,
	Tons
}

public struct HowMuch : IQuantity
{
	public HowMuch(double value, HowMuchUnit unit)
	{
		Unit = unit;
		Value = value;
	}

	Enum IQuantity.Unit => Unit;
	public HowMuchUnit Unit { get; }

	public double Value { get; }

	#region IQuantity

	private static readonly HowMuch Zero = new HowMuch(0, HowMuchUnit.Some);

	public QuantityType Type => QuantityType.Undefined;
	public BaseDimensions Dimensions => BaseDimensions.Dimensionless;

	public QuantityInfo QuantityInfo => new QuantityInfo(Type,
		new UnitInfo[]
		{
				new UnitInfo<HowMuchUnit>(HowMuchUnit.Some, BaseUnits.Undefined),
				new UnitInfo<HowMuchUnit>(HowMuchUnit.Lots, BaseUnits.Undefined),
				new UnitInfo<HowMuchUnit>(HowMuchUnit.Tons, BaseUnits.Undefined),
		},
		HowMuchUnit.Some,
		Zero,
		BaseDimensions.Dimensionless);

	public double As(Enum unit) => Convert.ToDouble(unit);

	public double As(UnitSystem unitSystem) => throw new NotImplementedException();

	public IQuantity ToUnit(Enum unit)
	{
		if (unit is HowMuchUnit howMuchUnit) return new HowMuch(As(unit), howMuchUnit);
		throw new ArgumentException("Must be of type HowMuchUnit.", nameof(unit));
	}

	public IQuantity ToUnit(UnitSystem unitSystem) => throw new NotImplementedException();

	public override string ToString() => $"{Value} {UnitAbbreviationsCache.Default.GetDefaultAbbreviation(Unit)}";
	public string ToString(string format, IFormatProvider formatProvider) => $"HowMuch ({format}, {formatProvider})";
	public string ToString(IFormatProvider provider) => $"HowMuch ({provider})";
	public string ToString(IFormatProvider provider, int significantDigitsAfterRadix) => $"HowMuch ({provider}, {significantDigitsAfterRadix})";
	public string ToString(IFormatProvider provider, string format, params object[] args) => $"HowMuch ({provider}, {string.Join(", ", args)})";

	#endregion
}