Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
110 changes: 110 additions & 0 deletions UnitsNet.Tests/UnitOfTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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;
using UnitsNet.Units;
using Xunit;

namespace UnitsNet.Tests;

public class UnitOfTests
{
// 1 furlong = 201.168 m
private static readonly UnitOf<Length> Furlong = new("Furlong", "Furlongs", 201.168);

// Same conversions as TemperatureUnit.DegreeFahrenheit, to compare against.
private static readonly UnitOf<Temperature> Fahrenheit = new("Fahrenheit", "Fahrenheits",
new ConversionExpression(coefficient: 1.8, constantTerm: -459.67),
new ConversionExpression(coefficient: QuantityValue.FromTerms(5, 9), constantTerm: QuantityValue.FromTerms(45967, 180)));

[Fact]
public void Constructor_WithValueInBaseUnit_DerivesTheConversionFromBase()
{
Assert.Equal("Furlong", Furlong.Name);
Assert.Equal("Furlongs", Furlong.PluralName);
Assert.Equal<QuantityValue>(201.168m, Furlong.ConversionToBase.Evaluate(QuantityValue.One));
Assert.Equal(QuantityValue.FromTerms(1000, 201168), Furlong.ConversionFromBase.Evaluate(QuantityValue.One));
}

[Fact]
public void Constructor_WithNullArguments_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new UnitOf<Length>(null!, "Furlongs", 1));
Assert.Throws<ArgumentNullException>(() => new UnitOf<Length>("Furlong", null!, 1));
Assert.Throws<ArgumentNullException>(() => new UnitOf<Length>("Furlong", "Furlongs", (BaseUnits)null!, 1));
}

[Fact]
public void BaseUnits_WhenNotGiven_IsUndefined()
{
Assert.Equal(BaseUnits.Undefined, Furlong.BaseUnits);
}

[Fact]
public void Constructor_WithBaseUnits_KeepsThem()
{
var baseUnits = new BaseUnits(length: LengthUnit.Foot, time: DurationUnit.Day);
var footPerDay = new UnitOf<Speed>("FootPerDay", "FeetPerDay", baseUnits, (Length.FromFeet(1) / Duration.FromDays(1)).MetersPerSecond);

Assert.Equal(baseUnits, footPerDay.BaseUnits);
Assert.Equal(baseUnits, ((IUnitDefinition)footPerDay).BaseUnits);
Assert.Equal(new QuantityValue(86400), Speed.FromFeetPerSecond(1).As(footPerDay));
}

[Fact]
public void ToString_ReturnsName()
{
Assert.Equal("Furlong", Furlong.ToString());
}

[Fact]
public void As_ReturnsValueInThatUnit()
{
Assert.Equal(QuantityValue.One, Length.FromFeet(660).As(Furlong));
Assert.Equal(new QuantityValue(8), Length.FromMiles(1).As(Furlong));
}

[Fact]
public void As_AffineUnit_ReturnsValueInThatUnit()
{
Temperature temperature = Temperature.FromDegreesCelsius(37);

Assert.Equal(temperature.DegreesFahrenheit, temperature.As(Fahrenheit));
}

[Fact]
public void As_NullUnit_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => Length.FromMeters(1).As((UnitOf<Length>)null!));
}

[Fact]
public void From_ReturnsQuantityInBaseUnit()
{
Length length = Length.Info.From(2, Furlong);

Assert.Equal(LengthUnit.Meter, length.Unit);
Assert.Equal<QuantityValue>(402.336m, length.Value);
}

[Fact]
public void From_AffineUnit_ReturnsQuantityInBaseUnit()
{
Temperature temperature = Temperature.Info.From(98.6, Fahrenheit);

Assert.Equal(Temperature.BaseUnit, temperature.Unit);
Assert.Equal(Temperature.FromDegreesFahrenheit(98.6).Kelvins, temperature.Value);
}

[Fact]
public void From_RoundTripsWithAs()
{
Assert.Equal(new QuantityValue(42), Length.Info.From(42, Furlong).As(Furlong));
}

[Fact]
public void From_NullUnit_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => Length.Info.From(1, (UnitOf<Length>)null!));
}
}
125 changes: 125 additions & 0 deletions UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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;
using System.Diagnostics;

namespace UnitsNet;

/// <summary>
/// A unit of <typeparamref name="TQuantity" />, described by its names and conversions, such as a furlong of
/// <see cref="Length" />.
/// </summary>
/// <remarks>
/// Quantities can be converted to and from the unit with
/// <see cref="QuantityExtensions.As{TQuantity}(TQuantity, UnitOf{TQuantity})" /> and
/// <see cref="QuantityInfoBase{TQuantity, TUnit, TUnitInfo}.From(QuantityValue, UnitOf{TQuantity})" />.
/// Since the unit is tied to its quantity, using it with another quantity doesn't compile.
/// </remarks>
/// <typeparam name="TQuantity">The quantity this is a unit of, such as <see cref="Length" />.</typeparam>
[DebuggerDisplay("{Name}")]
public sealed class UnitOf<TQuantity> : IUnitDefinition
where TQuantity : IQuantity
{
/// <summary>
/// Initializes a new instance of the <see cref="UnitOf{TQuantity}" /> class.
/// </summary>
/// <param name="singularName">The singular name of the unit, such as "Furlong".</param>
/// <param name="pluralName">The plural name of the unit, such as "Furlongs".</param>
/// <param name="valueInBaseUnit">
/// The value of one of this unit in the base unit of the quantity, such as 201.168 for a furlong, which is 201.168
/// meters.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="singularName" /> or <paramref name="pluralName" /> is <c>null</c>.
/// </exception>
public UnitOf(string singularName, string pluralName, QuantityValue valueInBaseUnit)
: this(singularName, pluralName, BaseUnits.Undefined, valueInBaseUnit)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="UnitOf{TQuantity}" /> class for a unit made of the base units of
/// UnitsNet, such as feet per day of <see cref="Speed" />.
/// </summary>
/// <param name="singularName">The singular name of the unit, such as "FootPerDay".</param>
/// <param name="pluralName">The plural name of the unit, such as "FeetPerDay".</param>
/// <param name="baseUnits">The base units the unit is made of, such as feet and days.</param>
/// <param name="valueInBaseUnit">The value of one of this unit in the base unit of the quantity.</param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="singularName" />, <paramref name="pluralName" />, or <paramref name="baseUnits" /> is
/// <c>null</c>.
/// </exception>
public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, QuantityValue valueInBaseUnit)
: this(singularName, pluralName, baseUnits, QuantityValue.Inverse(valueInBaseUnit), valueInBaseUnit)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="UnitOf{TQuantity}" /> class.
/// </summary>
/// <param name="singularName">The singular name of the unit, such as "Furlong".</param>
/// <param name="pluralName">The plural name of the unit, such as "Furlongs".</param>
/// <param name="conversionFromBase">The conversion expression from the base unit of the quantity to this unit.</param>
/// <param name="conversionToBase">The conversion expression from this unit to the base unit of the quantity.</param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="singularName" /> or <paramref name="pluralName" /> is <c>null</c>.
/// </exception>
public UnitOf(string singularName, string pluralName, ConversionExpression conversionFromBase, ConversionExpression conversionToBase)
: this(singularName, pluralName, BaseUnits.Undefined, conversionFromBase, conversionToBase)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="UnitOf{TQuantity}" /> class for a unit made of the base units of
/// UnitsNet.
/// </summary>
/// <param name="singularName">The singular name of the unit.</param>
/// <param name="pluralName">The plural name of the unit.</param>
/// <param name="baseUnits">The base units the unit is made of.</param>
/// <param name="conversionFromBase">The conversion expression from the base unit of the quantity to this unit.</param>
/// <param name="conversionToBase">The conversion expression from this unit to the base unit of the quantity.</param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="singularName" />, <paramref name="pluralName" />, or <paramref name="baseUnits" /> is
/// <c>null</c>.
/// </exception>
public UnitOf(string singularName, string pluralName, BaseUnits baseUnits,
ConversionExpression conversionFromBase, ConversionExpression conversionToBase)
{
Name = singularName ?? throw new ArgumentNullException(nameof(singularName));
PluralName = pluralName ?? throw new ArgumentNullException(nameof(pluralName));
BaseUnits = baseUnits ?? throw new ArgumentNullException(nameof(baseUnits));
ConversionFromBase = conversionFromBase;
ConversionToBase = conversionToBase;
}

/// <summary>
/// The singular name of the unit, such as "Furlong".
/// </summary>
public string Name { get; }

/// <summary>
/// The plural name of the unit, such as "Furlongs".
/// </summary>
public string PluralName { get; }

/// <inheritdoc />
public ConversionExpression ConversionFromBase { get; }

/// <inheritdoc />
public ConversionExpression ConversionToBase { get; }

/// <summary>
/// The base units the unit is made of, or <see cref="UnitsNet.BaseUnits.Undefined" /> if it isn't made of the base
/// units of UnitsNet, such as a furlong.
/// </summary>
public BaseUnits BaseUnits { get; }

/// <summary>
/// Returns the name of the unit.
/// </summary>
public override string ToString()
{
return Name;
}
}
14 changes: 14 additions & 0 deletions UnitsNet/Extensions/QuantityExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ public static QuantityValue As<TQuantity, TUnit>(this TQuantity quantity, TUnit
return UnitConverter.Default.ConvertValue(quantity, unit);
}

/// <summary>
/// Gets the value of the quantity in a <see cref="UnitOf{TQuantity}" />, such as a furlong of <see cref="Length" />.
/// </summary>
/// <param name="quantity">The quantity to convert.</param>
/// <param name="unit">The unit to get the value in.</param>
/// <returns>The value in <paramref name="unit" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="unit" /> is <c>null</c>.</exception>
public static QuantityValue As<TQuantity>(this TQuantity quantity, UnitOf<TQuantity> unit)
where TQuantity : IQuantity
{
if (unit is null) throw new ArgumentNullException(nameof(unit));
return unit.ConversionFromBase.Evaluate(quantity.GetUnitInfo().ConvertValueToBaseUnit(quantity.Value));
}

/// <inheritdoc cref="UnitConverter.ConvertValue{TQuantity,TUnit}" />
/// <param name="quantity">The quantity to convert.</param>
/// <param name="unit">The target unit.</param>
Expand Down
16 changes: 16 additions & 0 deletions UnitsNet/QuantityInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ protected QuantityInfoBase(string name, TQuantity zero, BaseDimensions baseDimen
return FromDelegate(value, unit);
}

/// <summary>
/// Creates an instance of the quantity from a value in a <see cref="UnitOf{TQuantity}" />, such as a furlong of
/// <see cref="Length" />.
/// </summary>
/// <param name="value">The numerical value in <paramref name="unit" />.</param>
/// <param name="unit">The unit of the value.</param>
/// <returns>
/// The quantity in its base unit, since a quantity can only be in a unit of <typeparamref name="TUnit" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="unit" /> is <c>null</c>.</exception>
public TQuantity From(QuantityValue value, UnitOf<TQuantity> unit)
{
if (unit is null) throw new ArgumentNullException(nameof(unit));
return From(unit.ConversionToBase.Evaluate(value), BaseUnitInfo.Value);
}

/// <inheritdoc />
TQuantity IQuantityInstanceInfo<TQuantity>.Create(QuantityValue value, UnitKey unitKey)
{
Expand Down
Loading