Skip to content

Commit 63040c3

Browse files
authored
✨ Add Quantity.From/TryFrom quantity name and unit name (#1258)
Make it easier to construct quantities from strings describing the quantity name and unit name, if the unit enum value is not readily available. This is useful for custom serialization of quantities, such as mapping to simple custom quantity DTO types instead of relying on JSON serialization converters. ### Changes - Add `Quantity.From` and `TryFrom` - Refactor `UnitConverter` to reuse code for constructing quantity from strings - Add tests for `From` and `TryFrom` - Add test `CustomSerializationTests.CanMapToJsonAndBackViaCustomDto` to showcase serialization ### Example ```cs public record QuantityDto(double Value, string QuantityName, string UnitName); // The original quantity. IQuantity q = Length.FromCentimeters(5); // Map to DTO. QuantityDto dto = new( Value: (double)q.Value, QuantityName: q.QuantityInfo.Name, UnitName: q.Unit.ToString()); /* Serialize to JSON: { "Value": 5, "QuantityName": "Length", "UnitName": "Centimeter" } */ var json = System.Text.Json.JsonSerializer.Serialize(dto); // Deserialize from JSON. QuantityDto deserialized = System.Text.Json.JsonSerializer.Deserialize<QuantityDto>(json)!; // Map to IQuantity. Quantity.TryFrom(deserialized.Value, deserialized.QuantityName, deserialized.UnitName, out IQuantity? deserializedQuantity); ```
1 parent 4d92cc9 commit 63040c3

File tree

6 files changed

+237
-144
lines changed

6 files changed

+237
-144
lines changed

README.md

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,23 @@ No more magic constants found on Stack Overflow, no more second-guessing the uni
1212

1313
### Overview
1414

15-
* [How to install](#how-to-install)
16-
* [100+ quantities with 1400+ units](UnitsNet/GeneratedCode/Units) generated from [JSON](Common/UnitDefinitions/) by [C# CLI app](CodeGen)
17-
* [30k unit tests](https://dev.azure.com/unitsnet/Units.NET/_build?definitionId=1)
18-
* [Statically typed quantities and units](#static-typing) to avoid mistakes and communicate intent
19-
* Immutable structs
20-
* [Operator overloads](#operator-overloads) for arithmetic
21-
* [Parse and ToString()](#culture) supports localization
22-
* [Dynamically parse and convert](#dynamic-parsing) quantities and units
23-
* [Extensible with custom units](#custom-units)
24-
* [Example: Creating a unit converter app](#example-app)
25-
* [Example: WPF app using IValueConverter to parse quantities from input](#example-wpf-app-using-ivalueconverter-to-parse-quantities-from-input)
26-
* [Precision and accuracy](#precision)
27-
* [Serialize to JSON, XML and more](#serialization)
28-
* [Contribute](#contribute) if you are missing some units
29-
* [Continuous integration](#ci) posts status reports to pull requests and commits
30-
* [Who are using this?](#who-are-using)
31-
32-
### <a name="how-to-install"></a>Installing via NuGet
15+
* [Overview](#overview)
16+
* [Installing via NuGet](#installing-via-nuget)
17+
* [Static Typing](#static-typing)
18+
* [Operator Overloads](#operator-overloads)
19+
* [Culture and Localization](#culture-and-localization)
20+
* [Dynamically Parse Quantities and Convert to Units](#dynamically-parse-quantities-and-convert-to-units)
21+
* [Custom units](#custom-units)
22+
* [Example: Unit converter app](#example-unit-converter-app)
23+
* [Example: WPF app using IValueConverter to parse input](#example-wpf-app-using-ivalueconverter-to-parse-input)
24+
* [Precision and Accuracy](#precision-and-accuracy)
25+
* [Serialize to JSON, XML and more](#serialize-to-json-xml-and-more)
26+
* [Want To Contribute?](#want-to-contribute)
27+
* [Continuous Integration](#continuous-integration)
28+
* [Who are Using This?](#who-are-using-this)
29+
* [Units.NET on other platforms](#unitsnet-on-other-platforms)
30+
31+
### Installing via NuGet
3332

3433
Add it via CLI
3534

@@ -43,7 +42,7 @@ or go to [NuGet Gallery | UnitsNet](https://www.nuget.org/packages/UnitsNet) for
4342
* .NET Standard 2.0
4443
* [.NET nanoFramework](https://www.nanoframework.net/)
4544

46-
### <a name="static-typing"></a>Static Typing
45+
### Static Typing
4746

4847
```C#
4948
// Construct
@@ -67,7 +66,7 @@ string PrintPersonWeight(Mass weight)
6766
}
6867
```
6968

70-
### <a name="operator-overloads"></a>Operator Overloads
69+
### Operator Overloads
7170

7271
```C#
7372
// Arithmetic
@@ -82,7 +81,7 @@ Acceleration a2 = Force.FromNewtons(100) / Mass.FromKilograms(20);
8281
RotationalSpeed r = Angle.FromDegrees(90) / TimeSpan.FromSeconds(2);
8382
```
8483

85-
### <a name="culture"></a>Culture and Localization
84+
### Culture and Localization
8685

8786
The culture for abbreviations defaults to Thread.CurrentCulture and falls back to US English if not defined. Thread.CurrentCulture affects number formatting unless a custom culture is specified. The relevant methods are:
8887

@@ -122,7 +121,7 @@ Unfortunately there is no built-in way to avoid this, either you need to ensure
122121
Example:
123122
`Length.Parse("1 pt")` throws `AmbiguousUnitParseException` with message `Cannot parse "pt" since it could be either of these: DtpPoint, PrinterPoint`.
124123

125-
### <a name="dynamic-parsing"></a>Dynamically Parse Quantities and Convert to Units
124+
### Dynamically Parse Quantities and Convert to Units
126125
Sometimes you need to work with quantities and units at runtime, such as parsing user input.
127126

128127
There are a handful of classes to help with this:
@@ -169,6 +168,16 @@ if (Quantity.TryFrom(3, LengthUnit.Centimeter, out IQuantity quantity2))
169168
{
170169
}
171170
```
171+
172+
You can also construct from strings, such as mapping between DTO types in an API:
173+
```c#
174+
IQuantity quantity = Quantity.From(value: 3, quantityName: "Length", unitName: "Centimeter");
175+
176+
if (Quantity.TryFrom(value: 3, quantityName: "Length", unitName: "Centimeter", out IQuantity? quantity2))
177+
{
178+
}
179+
```
180+
172181
#### Parse quantity
173182
Parse any string to a quantity instance of the given the quantity type.
174183

@@ -222,7 +231,7 @@ UnitConverter.ConvertByName(1, "Length", "Centimeter", "Millimeter"); // 10 mm
222231
UnitConverter.ConvertByAbbreviation(1, "Length", "cm", "mm"); // 10 mm
223232
```
224233

225-
### <a name="custom-units"></a>Custom units
234+
### Custom units
226235

227236
Units.NET allows you to add your own units and quantities at runtime, to represent as `IQuantity` and reusing Units.NET for parsing and converting between units.
228237

@@ -252,7 +261,7 @@ Console.WriteLine(Convert(HowMuchUnit.Lots)); // 100 lts
252261
Console.WriteLine(Convert(HowMuchUnit.Tons)); // 10 tns
253262
```
254263

255-
### <a name="example-app"></a>Example: Unit converter app
264+
### Example: Unit converter app
256265
[Source code](https://github.com/angularsen/UnitsNet/tree/master/Samples/UnitConverter.Wpf) for `Samples/UnitConverter.Wpf`<br/>
257266
[Download](https://github.com/angularsen/UnitsNet/releases/tag/UnitConverterWpf%2F2018-11-09) (release 2018-11-09 for Windows)
258267
@@ -291,15 +300,15 @@ double convertedValue = UnitConverter.Convert(
291300
SelectedToUnit.UnitEnumValue); // Enum, such as LengthUnit.Centimeter
292301
```
293302

294-
### Example: WPF app using IValueConverter to parse quantities from input
303+
### Example: WPF app using IValueConverter to parse input
295304

296305
Src: [Samples/WpfMVVMSample](https://github.com/angularsen/UnitsNet/tree/master/Samples/WpfMVVMSample)
297306
298307
![wpfmvvmsample_219w](https://user-images.githubusercontent.com/787816/34913417-094332e2-f8fd-11e7-9d8a-92db105fbbc9.png)
299308
300309
The purpose of this app is to show how to create an `IValueConverter` in order to bind XAML to quantities.
301310

302-
### <a name="precision"></a>Precision and Accuracy
311+
### Precision and Accuracy
303312

304313
A base unit is chosen for each unit class, represented by a double value (64-bit), and all conversions go via this unit. This means that there will always be a small error in both representing other units than the base unit as well as converting between units.
305314

@@ -309,28 +318,24 @@ The tests accept an error up to 1E-5 for most units added so far. Exceptions inc
309318

310319
For more details, see [Precision](https://github.com/angularsen/UnitsNet/wiki/Precision).
311320
312-
### <a name="serialization"></a>Serialize to JSON, XML and more
313-
314-
* [UnitsNet.Serialization.JsonNet](https://www.nuget.org/packages/UnitsNet.Serialization.JsonNet) with Json.NET (Newtonsoft)
315-
* [DataContractSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer) XML
316-
* [DataContractJsonSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.json.datacontractjsonserializer) JSON (not recommended*)
321+
### Serialize to JSON, XML and more
317322

318-
Read more at [Serializing to JSON, XML and more](https://github.com/angularsen/UnitsNet/wiki/Serializing-to-JSON,-XML-and-more).
323+
Read the wiki on [Serializing to JSON, XML and more](https://github.com/angularsen/UnitsNet/wiki/Serializing-to-JSON,-XML-and-more).
319324
320325

321-
### <a name="contribute"></a>Want To Contribute?
326+
### Want To Contribute?
322327

323328
- [Adding a New Unit](https://github.com/angularsen/UnitsNet/wiki/Adding-a-New-Unit) is fairly easy to do and we are happy to help.
324329
- Want a new feature or to report a bug? [Create an issue](https://github.com/angularsen/UnitsNet/issues/new/choose) or start a [discussion](https://github.com/angularsen/UnitsNet/discussions).
325330
326-
### <a name="ci"></a>Continuous Integration
331+
### Continuous Integration
327332

328333
[AppVeyor](https://ci.appveyor.com/project/angularsen/unitsnet) performs the following:
329334
* Build and test all branches
330335
* Build and test pull requests, notifies on success or error
331336
* Deploy nugets on master branch, if nuspec versions changed
332337

333-
### <a name="who-are-using"></a>Who are Using This?
338+
### Who are Using This?
334339

335340
It would be awesome to know who are using this library. If you would like your project listed here, [create an issue](https://github.com/angularsen/UnitsNet/issues) or edit the [README.md](https://github.com/angularsen/UnitsNet/edit/master/README.md) and send a pull request. Max logo size is `300x35 pixels` and should be in `.png`, `.gif` or `.jpg` formats.
336341
@@ -411,7 +416,7 @@ https://github.com/StructuralAnalysisFormat/StructuralAnalysisFormat-SDK
411416
412417
*- Dirk Schuermans, Software Engineer for [SCIA nv](https://www.scia.net)*
413418
414-
## Units.NET on other platforms
419+
### Units.NET on other platforms
415420

416421
Get the same strongly typed units on other platforms, based on the same [unit definitions](/Common/UnitDefinitions).
417422

UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public abstract class UnitsNetBaseJsonConverter<T> : JsonConverter<T>
2121

2222
/// <summary>
2323
/// Register custom types so that the converter can instantiate these quantities.
24-
/// Instead of calling <see cref="Quantity.From"/>, the <see cref="Activator"/> will be used to instantiate the object.
24+
/// Instead of calling <see cref="Quantity.From(UnitsNet.QuantityValue,System.Enum)"/>, the <see cref="Activator"/> will be used to instantiate the object.
2525
/// It is therefore assumed that the constructor of <paramref name="quantity"/> is specified with <c>new T(double value, typeof(<paramref name="unit"/>) unit)</c>.
2626
/// Registering the same <paramref name="unit"/> multiple times, it will overwrite the one registered.
2727
/// </summary>

UnitsNet.Tests/QuantityTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,56 @@ public void Equals_GenericEquatableIQuantity_OtherIsNull_ReturnsFalse()
9393
Assert.False(q1.Equals(q2, tolerance));
9494
}
9595

96+
[Fact]
97+
public void TryFrom_ValidQuantityNameAndUnitName_ReturnsTrueAndQuantity()
98+
{
99+
void AssertFrom(string quantityName, string unitName, Enum expectedUnit)
100+
{
101+
Assert.True(Quantity.TryFrom(5, quantityName, unitName, out IQuantity? quantity));
102+
Assert.NotNull(quantity);
103+
Assert.Equal(5, quantity!.Value);
104+
Assert.Equal(expectedUnit, quantity.Unit);
105+
}
106+
107+
AssertFrom("Length", "Centimeter", LengthUnit.Centimeter);
108+
AssertFrom("Mass", "Kilogram", MassUnit.Kilogram);
109+
}
110+
111+
[Fact]
112+
public void TryFrom_InvalidQuantityNameAndUnitName_ReturnsFalseAndNull()
113+
{
114+
void AssertInvalid(string quantityName, string unitName)
115+
{
116+
Assert.False(Quantity.TryFrom(5, quantityName, unitName, out IQuantity? quantity));
117+
Assert.Null(quantity);
118+
}
119+
120+
AssertInvalid("Length", "InvalidUnit");
121+
AssertInvalid("InvalidQuantity", "Kilogram");
122+
}
123+
124+
[Fact]
125+
public void From_ValidQuantityNameAndUnitName_ReturnsQuantity()
126+
{
127+
void AssertFrom(string quantityName, string unitName, Enum expectedUnit)
128+
{
129+
IQuantity quantity = Quantity.From(5, quantityName, unitName);
130+
Assert.NotNull(quantity);
131+
Assert.Equal(5, quantity.Value);
132+
Assert.Equal(expectedUnit, quantity.Unit);
133+
}
134+
135+
AssertFrom("Length", "Centimeter", LengthUnit.Centimeter);
136+
AssertFrom("Mass", "Kilogram", MassUnit.Kilogram);
137+
}
138+
139+
[Fact]
140+
public void From_InvalidQuantityNameOrUnitName_ThrowsUnitNotFoundException()
141+
{
142+
Assert.Throws<UnitNotFoundException>(() => Quantity.From(5, "Length", "InvalidUnit"));
143+
Assert.Throws<UnitNotFoundException>(() => Quantity.From(5, "InvalidQuantity", "Kilogram"));
144+
}
145+
96146
private static Length ParseLength(string str)
97147
{
98148
return Length.Parse(str, CultureInfo.InvariantCulture);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using UnitsNet.Units;
5+
using Xunit;
6+
7+
namespace UnitsNet.Tests.Serialization;
8+
9+
public class CustomSerializationTests
10+
{
11+
#if NET7_0_OR_GREATER
12+
private record QuantityDto(double Value, string QuantityName, string UnitName);
13+
14+
/// <summary>
15+
/// This showcases how to serialize and deserialize a quantity to and from JSON using a custom DTO, as described in the wiki:<br />
16+
/// https://github.com/angularsen/UnitsNet/wiki/Serializing-to-JSON,-XML-and-more#-recommended-map-to-your-own-custom-dto-types
17+
/// </summary>
18+
[Fact]
19+
public void CanMapToJsonAndBackViaCustomDto()
20+
{
21+
// The original quantity.
22+
IQuantity q = Length.FromCentimeters(5);
23+
24+
// Map to DTO.
25+
QuantityDto dto = new(
26+
Value: (double)q.Value,
27+
QuantityName: q.QuantityInfo.Name,
28+
UnitName: q.Unit.ToString());
29+
30+
/* Serialize to JSON:
31+
{
32+
"Value": 5,
33+
"QuantityName": "Length",
34+
"UnitName": "Centimeter"
35+
}
36+
*/
37+
string json = System.Text.Json.JsonSerializer.Serialize(dto);
38+
39+
// Deserialize from JSON.
40+
QuantityDto deserialized = System.Text.Json.JsonSerializer.Deserialize<QuantityDto>(json)!;
41+
42+
// Map to IQuantity.
43+
bool tryFromResult = Quantity.TryFrom(deserialized.Value, deserialized.QuantityName, deserialized.UnitName, out IQuantity? deserializedQuantity);
44+
45+
// Assert
46+
Assert.True(tryFromResult);
47+
Assert.NotNull(deserializedQuantity);
48+
Assert.Equal(5, deserializedQuantity!.Value);
49+
Assert.Equal(LengthUnit.Centimeter, deserializedQuantity.Unit);
50+
}
51+
#endif
52+
}

UnitsNet/CustomCode/Quantity.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,53 @@ public static bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInf
6161
/// <exception cref="ArgumentException">Unit value is not a know unit enum type.</exception>
6262
public static IQuantity From(QuantityValue value, Enum unit)
6363
{
64-
if (TryFrom(value, unit, out IQuantity? quantity))
65-
return quantity;
64+
return TryFrom(value, unit, out IQuantity? quantity)
65+
? quantity
66+
: throw new ArgumentException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a third-party enum type defined outside UnitsNet library?");
67+
}
6668

67-
throw new ArgumentException(
68-
$"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a third-party enum type defined outside UnitsNet library?");
69+
/// <summary>
70+
/// Dynamically construct a quantity from a value, the quantity name and the unit name.
71+
/// </summary>
72+
/// <param name="value">Numeric value.</param>
73+
/// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
74+
/// <param name="unitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
75+
/// <returns>An <see cref="IQuantity"/> object.</returns>
76+
/// <exception cref="ArgumentException">Unit value is not a know unit enum type.</exception>
77+
public static IQuantity From(QuantityValue value, string quantityName, string unitName)
78+
{
79+
// Get enum value for this unit, f.ex. LengthUnit.Meter for unit name "Meter".
80+
return UnitConverter.TryParseUnit(quantityName, unitName, out Enum? unitValue)
81+
? From(value, unitValue)
82+
: throw new UnitNotFoundException($"Unit [{unitName}] not found for quantity [{quantityName}].");
6983
}
7084

7185
/// <inheritdoc cref="TryFrom(QuantityValue,System.Enum,out UnitsNet.IQuantity)"/>
7286
public static bool TryFrom(double value, Enum unit, [NotNullWhen(true)] out IQuantity? quantity)
7387
{
88+
quantity = default;
89+
7490
// Implicit cast to QuantityValue would prevent TryFrom from being called,
7591
// so we need to explicitly check this here for double arguments.
76-
if (double.IsNaN(value) || double.IsInfinity(value))
77-
{
78-
quantity = default(IQuantity);
79-
return false;
80-
}
92+
return !double.IsNaN(value) &&
93+
!double.IsInfinity(value) &&
94+
TryFrom((QuantityValue)value, unit, out quantity);
95+
}
96+
97+
/// <summary>
98+
/// Try to dynamically construct a quantity from a value, the quantity name and the unit name.
99+
/// </summary>
100+
/// <param name="value">Numeric value.</param>
101+
/// <param name="unitName">The invariant unit enum name, such as "Meter". Does not support localization.</param>
102+
/// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
103+
/// <param name="quantity">The constructed quantity, if successful, otherwise null.</param>
104+
/// <returns><c>True</c> if successful with <paramref name="quantity"/> assigned the value, otherwise <c>false</c>.</returns>
105+
public static bool TryFrom(double value, string quantityName, string unitName, [NotNullWhen(true)] out IQuantity? quantity)
106+
{
107+
quantity = default;
81108

82-
return TryFrom((QuantityValue)value, unit, out quantity);
109+
return UnitConverter.TryParseUnit(quantityName, unitName, out Enum? unitValue) &&
110+
TryFrom(value, unitValue, out quantity);
83111
}
84112

85113
/// <inheritdoc cref="Parse(IFormatProvider, System.Type,string)"/>

0 commit comments

Comments
 (0)