Skip to content

Commit 6dddf9f

Browse files
authored
Improve working dynamically with units and quantities (#576)
* R#: Don't set namespace for CustomCode folder * README: Add pseudo code for converter app * Add QuantityInfo to IQuantity * Add static and instance props for QuantityInfo * Add tests * Add features to make sample apps not have to use reflection - Add As() and ToUnit() on IQuantity with Enum parameters - Add UnitConverter.Convert() and .TryConvert() with Enum parameters * Update sample apps to not use reflection * Rename UnitsHelper to Quantity, add dynamic TryParse() * Adding BaseDimensions to QuantityInfo * Add TypeWrapper to help with reflection code It was not a good idea to use extension methods with similar names as some of the targets. A wrapper type avoids this problem. * Adding BaseUnit to QuantityInfo. Adding GetQuantitiesWithBaseDimensions helper to Quantity class * Adding Unit to IQuantity
1 parent 1aaad81 commit 6dddf9f

File tree

210 files changed

+4972
-487
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

210 files changed

+4972
-487
lines changed

README.md

Lines changed: 117 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ See [Upgrading from 3.x to 4.x](https://github.com/angularsen/UnitsNet/wiki/Upgr
2929
* [Statically typed quantities and units](#static-typing) to avoid mistakes and communicate intent
3030
* [Operator overloads](#operator-overloads) for arithmetic on quantities
3131
* [Parse and ToString()](#culture) supports cultures and localization
32-
* [Dynamically parsing and converting](#dynamic-parsing) quantities and units
32+
* [Dynamically parse and convert](#dynamic-parsing) quantities and units
3333
* [Example: Creating a unit converter app](#example-app)
3434
* [Example: WPF app using IValueConverter to parse quantities from input](#example-wpf-app-using-ivalueconverter-to-parse-quantities-from-input)
3535
* [Precision and accuracy](#precision)
@@ -125,29 +125,105 @@ Unfortunately there is no built-in way to avoid this, either you need to ensure
125125
Example:
126126
`Length.Parse("1 pt")` throws `AmbiguousUnitParseException` with message `Cannot parse "pt" since it could be either of these: DtpPoint, PrinterPoint`.
127127

128-
### <a name="dynamic-parsing"></a>Dynamically Parsing and Converting Quantities
128+
### <a name="dynamic-parsing"></a>Dynamically Parse Quantities and Convert to Units
129129
Sometimes you need to work with quantities and units at runtime, such as parsing user input.
130-
There are three classes to help with this:
131-
- [UnitParser](UnitsNet/CustomCode/UnitParser.cs) for parsing unit abbreviation strings like `cm` to `LengthUnit.Centimeter`
132-
- [UnitAbbreviationsCache](UnitsNet/CustomCode/UnitAbbreviationsCache.cs) for looking up unit abbreviations like `cm` given type `LengthUnit` and value `1` (`Centimeter`)
133-
- [UnitConverter](UnitsNet/UnitConverter.cs) for converting values given a quantity name `Length`, a value `1` and from/to unit names `Centimeter` and `Meter`
130+
131+
There are a handful of classes to help with this:
132+
133+
- [Quantity](UnitsNet/CustomCode/Quantity.cs) for parsing and constructing quantities as well as looking up units, names and quantity information dynamically
134+
- [UnitConverter](UnitsNet/UnitConverter.cs) for converting values to a different unit, with only strings or enum values
135+
- [UnitParser](UnitsNet/CustomCode/UnitParser.cs) for parsing unit abbreviation strings, such as `"cm"` to `LengthUnit.Centimeter`
136+
137+
#### Enumerate quantities and units
138+
`Quantity` is the go-to class for looking up information about quantities at runtime.
139+
```c#
140+
string[] Quantity.Names; // ["Length", "Mass", ...]
141+
QuantityType[] Quantity.Types; // [QuantityType.Length, QuantityType.Mass, ...]
142+
QuantityInfo[] Quantity.Infos; // Information about all quantities and their units, types, values etc., see more below
143+
144+
QuantityInfo Quantity.GetInfo(QuantityType.Length); // Get information about Length
145+
```
146+
147+
#### Information about quantity type
148+
`QuantityInfo` makes it easy to enumerate names, units, types and values for the quantity type.
149+
This is useful for populating lists of quantities and units for the user to choose.
150+
151+
```c#
152+
QuantityInfo lengthInfo = Quantity.GetInfo(QuantityType.Length); // You can get it statically here
153+
lengthInfo = Length.Info; // or statically per quantity
154+
lengthInfo = Length.Zero.QuantityInfo; // or dynamically from quantity instances
155+
156+
lengthInfo.Name; // "Length"
157+
lengthInfo.QuantityType; // QuantityType.Length
158+
lengthInfo.UnitNames; // ["Centimeter", "Meter", ...]
159+
lengthInfo.Units; // [LengthUnit.Centimeter, LengthUnit.Meter, ...]
160+
lengthInfo.UnitType; // typeof(LengthUnit)
161+
lengthInfo.ValueType; // typeof(Length)
162+
lengthInfo.Zero; // Length.Zero
163+
```
164+
165+
#### Construct quantity
166+
All you need is the value and the unit enum value.
167+
168+
```c#
169+
IQuantity quantity = Quantity.From(3, LengthUnit.Centimeter); // Length
170+
171+
if (Quantity.TryFrom(3, LengthUnit.Centimeter, out IQuantity quantity2))
172+
{
173+
}
174+
```
175+
#### Parse quantity
176+
Parse any string to a quantity instance of the given the quantity type.
177+
178+
```c#
179+
IQuantity quantity = Quantity.Parse(typeof(Length), "3 cm"); // Length
180+
181+
if (Quantity.TryParse(typeof(Length), "3cm", out IQuantity quantity2)
182+
{
183+
}
184+
```
185+
186+
#### Parse unit
187+
[UnitParser](UnitsNet/CustomCode/UnitParser.cs) parses unit abbreviation strings to unit enum values.
134188

135189
```c#
136-
// This type was perhaps selected by the user in GUI from a list of units
137-
Type lengthUnitType = typeof(LengthUnit); // Selected by user in GUI from a list of units
190+
Enum unit = UnitParser.Default.Parse("cm", typeof(LengthUnit)); // LengthUnit.Centimeter
138191
139-
// Parse units dynamically
140-
UnitParser parser = UnitParser.Default;
141-
int fromUnitValue = (int)parser.Parse("cm", lengthUnitType); // LengthUnit.Centimeter == 1
192+
if (UnitParser.Default.TryParse("cm", typeof(LengthUnit), out Enum unit2))
193+
{
194+
// Use unit2 as LengthUnit.Centimeter
195+
}
196+
```
142197

143-
// Get unit abbreviations dynamically
144-
var cache = UnitAbbreviationsCache.Default;
145-
string fromUnitAbbreviation = cache.GetDefaultAbbreviation(lengthUnitType, 1); // "cm"
198+
#### Convert quantity to unit - IQuantity and Enum
199+
Convert any `IQuantity` instance to a different unit by providing a target unit enum value.
200+
```c#
201+
// Assume these are passed in at runtime, we don't know their values or type
202+
Enum userSelectedUnit = LengthUnit.Millimeter;
203+
IQuantity quantity = Length.FromCentimeters(3);
204+
205+
// Later we convert to a unit
206+
quantity.ToUnit(userSelectedUnit).Value; // 30
207+
quantity.ToUnit(userSelectedUnit).Unit; // LengthUnit.Millimeter
208+
quantity.ToUnit(userSelectedUnit).ToString(); // "30 mm"
209+
quantity.ToUnit(PressureUnit.Pascal); // Throws exception, not compatible
210+
quantity.As(userSelectedUnit); // 30
211+
```
146212

147-
double centimeters = UnitConverter.ConvertByName(1, "Length", "Meter", "Centimeter"); // 100
213+
#### Convert quantity to unit - From/To Enums
214+
Useful when populating lists with unit enum values for the user to choose.
215+
216+
```c#
217+
UnitConverter.Convert(1, LengthUnit.Centimeter, LengthUnit.Millimeter); // 10 mm
148218
```
149219

150-
For more examples on dynamic parsing and conversion, see the unit conversion applications below.
220+
#### Convert quantity to unit - Names or abbreviation strings
221+
Sometimes you only have strings to work with, that works too!
222+
223+
```c#
224+
UnitConverter.ConvertByName(1, "Length", "Centimeter", "Millimeter"); // 10 mm
225+
UnitConverter.ConvertByAbbreviation(1, "Length", "cm", "mm"); // 10 mm
226+
```
151227

152228
### <a name="example-app"></a>Example: Creating a dynamic unit converter app
153229
[Source code](https://github.com/angularsen/UnitsNet/tree/master/Samples/UnitConverter.Wpf) for `Samples/UnitConverter.Wpf`<br/>
@@ -156,25 +232,37 @@ For more examples on dynamic parsing and conversion, see the unit conversion app
156232
![image](https://user-images.githubusercontent.com/787816/34920961-9b697004-f97b-11e7-9e9a-51ff7142969b.png)
157233
158234

159-
This example shows how you can create a dynamic unit converter, where the user selects the quantity to convert, such as `Length` or `Mass`, then selects to convert from `Meter` to `Centimeter` and types in a value for how many meters.
235+
This example shows how you can create a dynamic unit converter, where the user selects the quantity to convert, such as `Temperature`, then selects to convert from `DegreeCelsius` to `DegreeFahrenheit` and types in a numeric value for how many degrees Celsius to convert.
236+
The quantity list box contains `QuantityType` values such as `QuantityType.Length` and the two unit list boxes contain `Enum` values, such as `LengthUnit.Meter`.
160237

161-
NOTE: There are still some limitations in the library that requires reflection to enumerate units for quantity and getting the abbreviation for a unit, when we want to dynamically enumerate and convert between units.
238+
#### Populate quantity selector
239+
Use `Quantity` to enumerate all quantity type enum values, such as `QuantityType.Length` and `QuantityType.Mass`.
162240

163-
### <a name="example-app-hardcoded"></a>Example: Creating a unit converter app with hard coded quantities
241+
```c#
242+
this.Quantities = Quantity.Types; // QuantityType[]
243+
```
164244

165-
If you can live with hard coding what quantities to convert between, then the following code snippet shows you one way to go about it:
245+
#### Update unit lists when selecting new quantity
246+
So user can only choose from/to units compatible with the quantity type.
166247

167-
```C#
168-
// Get quantities for populating quantity UI selector
169-
QuantityType[] quantityTypes = Enum.GetValues(typeof(QuantityType)).Cast<QuantityType>().ToArray();
248+
```c#
249+
QuantityInfo quantityInfo = Quantity.GetInfo(quantityType);
170250

171-
// If Length is selected, get length units for populating from/to UI selectors
172-
LengthUnit[] lengthUnits = Length.Units;
251+
_units.Clear();
252+
foreach (Enum unitValue in quantityInfo.Units)
253+
{
254+
_units.Add(unitValue);
255+
}
256+
```
257+
258+
#### Update calculation on unit selection changed
259+
Using `UnitConverter` to convert by unit enum values as given by the list selection `"Length"` and unit names like `"Centimeter"` and `"Meter"`.
173260

174-
// Perform conversion using input value and selected from/to units
175-
double inputValue; // Obtain from textbox
176-
LengthUnit fromUnit, toUnit; // Obtain from ListBox selections
177-
double resultValue = Length.From(inputValue, fromUnit).As(toUnit);
261+
```c#
262+
double convertedValue = UnitConverter.Convert(
263+
FromValue, // numeric value
264+
SelectedFromUnit.UnitEnumValue, // Enum, such as LengthUnit.Meter
265+
SelectedToUnit.UnitEnumValue); // Enum, such as LengthUnit.Centimeter
178266
```
179267

180268
### Example: WPF app using IValueConverter to parse quantities from input
@@ -183,11 +271,8 @@ Src: [Samples/WpfMVVMSample](https://github.com/angularsen/UnitsNet/tree/master/
183271
184272
![wpfmvvmsample_219w](https://user-images.githubusercontent.com/787816/34913417-094332e2-f8fd-11e7-9d8a-92db105fbbc9.png)
185273
186-
187274
The purpose of this app is to show how to create an `IValueConverter` in order to bind XAML to quantities.
188275

189-
NOTE: A lot of reflection and complexity were introduced due to not having a base type. See #371 for discussion on adding base types.
190-
191276
### <a name="precision"></a>Precision and Accuracy
192277

193278
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.

Samples/UnitConverter.Wpf/UnitConverter.Wpf/DelegateCommand.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public void Execute(object parameter)
2626
_commandDelegate.Invoke();
2727
}
2828

29+
// Is never used
30+
#pragma warning disable CS0067
2931
public event EventHandler CanExecuteChanged;
32+
#pragma warning restore CS0067
3033
}
31-
}
34+
}

Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ public interface IMainWindowVm : INotifyPropertyChanged
2727
decimal ToValue { get; }
2828
ICommand SwapCommand { get; }
2929
}
30-
}
30+
}

Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowDesignVM.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.Collections.ObjectModel;
43
using System.ComponentModel;
54
using System.Linq;
5+
using System.Runtime.CompilerServices;
66
using System.Windows.Input;
77

88
namespace UnitsNet.Samples.UnitConverter.Wpf
@@ -15,13 +15,14 @@ public sealed class MainWindowDesignVm : IMainWindowVm
1515
{
1616
public MainWindowDesignVm()
1717
{
18-
Quantities = ToReadOnly(Enum.GetValues(typeof(QuantityType)).Cast<QuantityType>().Skip(1));
18+
Quantities = ToReadOnly(Quantity.Types);
1919
Units = ToReadOnly(Length.Units.Select(u => new UnitListItem(u)));
2020
SelectedQuantity = QuantityType.Length;
2121
SelectedFromUnit = Units[1];
2222
SelectedToUnit = Units[2];
2323
}
2424

25+
2526
public ReadOnlyObservableCollection<QuantityType> Quantities { get; }
2627
public ReadOnlyObservableCollection<UnitListItem> Units { get; }
2728
public QuantityType SelectedQuantity { get; set; }
@@ -35,11 +36,14 @@ public MainWindowDesignVm()
3536

3637
public ICommand SwapCommand { get; } = new RoutedCommand();
3738

39+
// Is never used
40+
#pragma warning disable CS0067
3841
public event PropertyChangedEventHandler PropertyChanged;
42+
#pragma warning restore CS0067
3943

4044
private static ReadOnlyObservableCollection<T> ToReadOnly<T>(IEnumerable<T> items)
4145
{
4246
return new ReadOnlyObservableCollection<T>(new ObservableCollection<T>(items));
4347
}
4448
}
45-
}
49+
}

Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowVM.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public sealed class MainWindowVm : IMainWindowVm
2929

3030
public MainWindowVm()
3131
{
32-
Quantities = ToReadOnly(Enum.GetValues(typeof(QuantityType)).Cast<QuantityType>().Skip(1));
32+
Quantities = ToReadOnly(Quantity.Types);
3333

3434
_units = new ObservableCollection<UnitListItem>();
3535
Units = new ReadOnlyObservableCollection<UnitListItem>(_units);
@@ -131,21 +131,25 @@ private void UpdateResult()
131131
{
132132
if (SelectedFromUnit == null || SelectedToUnit == null) return;
133133

134-
ToValue = Convert.ToDecimal(UnitsNet.UnitConverter.ConvertByName(FromValue,
135-
SelectedQuantity.ToString(),
136-
SelectedFromUnit.UnitEnumValue.ToString(),
137-
SelectedToUnit.UnitEnumValue.ToString()));
134+
double convertedValue = UnitsNet.UnitConverter.Convert(FromValue,
135+
SelectedFromUnit.UnitEnumValue,
136+
SelectedToUnit.UnitEnumValue);
137+
138+
ToValue = Convert.ToDecimal(convertedValue);
138139
}
139140

140-
private void OnSelectedQuantity(QuantityType quantity)
141+
private void OnSelectedQuantity(QuantityType quantityType)
141142
{
143+
QuantityInfo quantityInfo = Quantity.GetInfo(quantityType);
144+
142145
_units.Clear();
143-
IEnumerable<object> unitValues = UnitHelper.GetUnits(quantity);
144-
foreach (object unitValue in unitValues) _units.Add(new UnitListItem(unitValue));
146+
foreach (Enum unitValue in quantityInfo.Units)
147+
{
148+
_units.Add(new UnitListItem(unitValue));
149+
}
145150

146-
SelectedQuantity = quantity;
147-
SelectedFromUnit = Units.FirstOrDefault();
148-
SelectedToUnit = Units.Skip(1).FirstOrDefault() ?? SelectedFromUnit; // Try to pick a different to-unit
151+
SelectedFromUnit = _units.FirstOrDefault();
152+
SelectedToUnit = _units.Skip(1).FirstOrDefault() ?? SelectedFromUnit; // Try to pick a different to-unit
149153
}
150154

151155
private static ReadOnlyObservableCollection<T> ToReadOnly<T>(IEnumerable<T> items)
@@ -159,4 +163,4 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
159163
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
160164
}
161165
}
162-
}
166+
}

Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitConverter.Wpf.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
<Generator>MSBuild:Compile</Generator>
7474
<SubType>Designer</SubType>
7575
</ApplicationDefinition>
76-
<Compile Include="UnitHelper.cs" />
7776
<Compile Include="UnitListItem.cs" />
7877
<Page Include="MainWindow.xaml">
7978
<Generator>MSBuild:Compile</Generator>

Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitHelper.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitListItem.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ namespace UnitsNet.Samples.UnitConverter.Wpf
99
/// </summary>
1010
public sealed class UnitListItem
1111
{
12-
public UnitListItem(object val)
12+
public UnitListItem(Enum val)
1313
{
1414
UnitEnumValue = val;
15-
UnitEnumValueInt = (int) val;
15+
UnitEnumValueInt = Convert.ToInt32(val);
1616
UnitEnumType = val.GetType();
1717
Abbreviation = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(UnitEnumType, UnitEnumValueInt);
1818

1919
Text = $"{val} [{Abbreviation}]";
2020
}
2121

2222
public string Text { get; }
23-
public object UnitEnumValue { get; }
23+
public Enum UnitEnumValue { get; }
2424
public int UnitEnumValueInt { get; }
2525
public Type UnitEnumType { get; }
2626
public string Abbreviation { get; }

0 commit comments

Comments
 (0)