Skip to content

Improve working dynamically with units and quantities #576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Feb 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
39ee5d5
R#: Don't set namespace for CustomCode folder
angularsen Jan 27, 2019
857bd7e
Add UnitsHelper with tests
angularsen Dec 16, 2018
6498904
README: Add pseudo code for converter app
angularsen Dec 16, 2018
5cf8424
Add QuantityInfo to IQuantity
angularsen Jan 27, 2019
4ca4ee5
Add static and instance props for QuantityInfo
angularsen Jan 27, 2019
d8e73dd
Add tests
angularsen Jan 27, 2019
51c39c8
TEMP: Samples: Use UnitsNet instead of nuget
angularsen Jan 27, 2019
0d0c1db
Revert "TEMP: Samples: Use UnitsNet instead of nuget"
angularsen Jan 27, 2019
063bfa1
Add features to make sample apps not have to use reflection
angularsen Jan 27, 2019
1a51fd0
Update sample apps to not use reflection
angularsen Jan 27, 2019
5e67b74
Rename UnitsHelper to Quantity, add dynamic TryParse()
angularsen Jan 27, 2019
d876908
Update sample apps again
angularsen Jan 27, 2019
1460bc8
Fix test case
angularsen Jan 27, 2019
022b4d7
Update README
angularsen Jan 27, 2019
626c8c2
Fix static racing condition
angularsen Jan 28, 2019
d323bd1
Fix missing code generation
angularsen Jan 28, 2019
0e87df1
Mute warnings in sample app
angularsen Jan 28, 2019
46eea13
Minor cleanup
angularsen Jan 28, 2019
78cf0e5
Fix code generation
angularsen Jan 28, 2019
5a4af8e
Rename code generator script
angularsen Jan 28, 2019
67b5e69
Remove IQuantity<T> typed member in WRC
angularsen Jan 28, 2019
e30ea07
WIF Fix WRC compile errors, not done
angularsen Jan 28, 2019
6eced4b
Fix remaining test cases
angularsen Jan 28, 2019
3989958
Merge branch 'fix-parsing-ambiguous-lowercase-units' into add-unitshe…
angularsen Jan 28, 2019
6c419e9
Return Enum in UnitParser.Parse(Type)
angularsen Jan 28, 2019
a16a78d
Generated code: Manually add xmldoc
angularsen Jan 28, 2019
372a098
Update README with more sections
angularsen Jan 28, 2019
b84219b
Update README with sample app sections
angularsen Jan 28, 2019
504fbad
Fix README
angularsen Jan 28, 2019
c170ec1
Yet more README
angularsen Jan 28, 2019
e9875fb
Fix code generation
angularsen Jan 28, 2019
34a5ee0
tmilnthorp Jan 29, 2019
ef5665f
Add TypeWrapper to help with reflection code
angularsen Jan 30, 2019
c274223
WRC and .NET now compiles
angularsen Jan 30, 2019
abf873c
Fix broken tests
angularsen Jan 31, 2019
a42ffe3
Merge remote-tracking branch 'origin/master' into add-unitshelper
angularsen Jan 31, 2019
4274899
tmilnthorp Jan 31, 2019
861fb0d
tmilnthorp Jan 31, 2019
f4cf15f
Merge remote-tracking branch 'origin/master' into add-unitshelper
angularsen Feb 1, 2019
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
149 changes: 117 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ See [Upgrading from 3.x to 4.x](https://github.com/angularsen/UnitsNet/wiki/Upgr
* [Statically typed quantities and units](#static-typing) to avoid mistakes and communicate intent
* [Operator overloads](#operator-overloads) for arithmetic on quantities
* [Parse and ToString()](#culture) supports cultures and localization
* [Dynamically parsing and converting](#dynamic-parsing) quantities and units
* [Dynamically parse and convert](#dynamic-parsing) quantities and units
* [Example: Creating a unit converter app](#example-app)
* [Example: WPF app using IValueConverter to parse quantities from input](#example-wpf-app-using-ivalueconverter-to-parse-quantities-from-input)
* [Precision and accuracy](#precision)
Expand Down Expand Up @@ -125,29 +125,105 @@ Unfortunately there is no built-in way to avoid this, either you need to ensure
Example:
`Length.Parse("1 pt")` throws `AmbiguousUnitParseException` with message `Cannot parse "pt" since it could be either of these: DtpPoint, PrinterPoint`.

### <a name="dynamic-parsing"></a>Dynamically Parsing and Converting Quantities
### <a name="dynamic-parsing"></a>Dynamically Parse Quantities and Convert to Units
Sometimes you need to work with quantities and units at runtime, such as parsing user input.
There are three classes to help with this:
- [UnitParser](UnitsNet/CustomCode/UnitParser.cs) for parsing unit abbreviation strings like `cm` to `LengthUnit.Centimeter`
- [UnitAbbreviationsCache](UnitsNet/CustomCode/UnitAbbreviationsCache.cs) for looking up unit abbreviations like `cm` given type `LengthUnit` and value `1` (`Centimeter`)
- [UnitConverter](UnitsNet/UnitConverter.cs) for converting values given a quantity name `Length`, a value `1` and from/to unit names `Centimeter` and `Meter`

There are a handful of classes to help with this:

- [Quantity](UnitsNet/CustomCode/Quantity.cs) for parsing and constructing quantities as well as looking up units, names and quantity information dynamically
- [UnitConverter](UnitsNet/UnitConverter.cs) for converting values to a different unit, with only strings or enum values
- [UnitParser](UnitsNet/CustomCode/UnitParser.cs) for parsing unit abbreviation strings, such as `"cm"` to `LengthUnit.Centimeter`

#### Enumerate quantities and units
`Quantity` is the go-to class for looking up information about quantities at runtime.
```c#
string[] Quantity.Names; // ["Length", "Mass", ...]
QuantityType[] Quantity.Types; // [QuantityType.Length, QuantityType.Mass, ...]
QuantityInfo[] Quantity.Infos; // Information about all quantities and their units, types, values etc., see more below

QuantityInfo Quantity.GetInfo(QuantityType.Length); // Get information about Length
```

#### Information about quantity type
`QuantityInfo` makes it easy to enumerate names, units, types and values for the quantity type.
This is useful for populating lists of quantities and units for the user to choose.

```c#
QuantityInfo lengthInfo = Quantity.GetInfo(QuantityType.Length); // You can get it statically here
lengthInfo = Length.Info; // or statically per quantity
lengthInfo = Length.Zero.QuantityInfo; // or dynamically from quantity instances

lengthInfo.Name; // "Length"
lengthInfo.QuantityType; // QuantityType.Length
lengthInfo.UnitNames; // ["Centimeter", "Meter", ...]
lengthInfo.Units; // [LengthUnit.Centimeter, LengthUnit.Meter, ...]
lengthInfo.UnitType; // typeof(LengthUnit)
lengthInfo.ValueType; // typeof(Length)
lengthInfo.Zero; // Length.Zero
```

#### Construct quantity
All you need is the value and the unit enum value.

```c#
IQuantity quantity = Quantity.From(3, LengthUnit.Centimeter); // Length

if (Quantity.TryFrom(3, LengthUnit.Centimeter, out IQuantity quantity2))
{
}
```
#### Parse quantity
Parse any string to a quantity instance of the given the quantity type.

```c#
IQuantity quantity = Quantity.Parse(typeof(Length), "3 cm"); // Length

if (Quantity.TryParse(typeof(Length), "3cm", out IQuantity quantity2)
{
}
```

#### Parse unit
[UnitParser](UnitsNet/CustomCode/UnitParser.cs) parses unit abbreviation strings to unit enum values.

```c#
// This type was perhaps selected by the user in GUI from a list of units
Type lengthUnitType = typeof(LengthUnit); // Selected by user in GUI from a list of units
Enum unit = UnitParser.Default.Parse("cm", typeof(LengthUnit)); // LengthUnit.Centimeter

// Parse units dynamically
UnitParser parser = UnitParser.Default;
int fromUnitValue = (int)parser.Parse("cm", lengthUnitType); // LengthUnit.Centimeter == 1
if (UnitParser.Default.TryParse("cm", typeof(LengthUnit), out Enum unit2))
{
// Use unit2 as LengthUnit.Centimeter
}
```

// Get unit abbreviations dynamically
var cache = UnitAbbreviationsCache.Default;
string fromUnitAbbreviation = cache.GetDefaultAbbreviation(lengthUnitType, 1); // "cm"
#### Convert quantity to unit - IQuantity and Enum
Convert any `IQuantity` instance to a different unit by providing a target unit enum value.
```c#
// Assume these are passed in at runtime, we don't know their values or type
Enum userSelectedUnit = LengthUnit.Millimeter;
IQuantity quantity = Length.FromCentimeters(3);

// Later we convert to a unit
quantity.ToUnit(userSelectedUnit).Value; // 30
quantity.ToUnit(userSelectedUnit).Unit; // LengthUnit.Millimeter
quantity.ToUnit(userSelectedUnit).ToString(); // "30 mm"
quantity.ToUnit(PressureUnit.Pascal); // Throws exception, not compatible
quantity.As(userSelectedUnit); // 30
```

double centimeters = UnitConverter.ConvertByName(1, "Length", "Meter", "Centimeter"); // 100
#### Convert quantity to unit - From/To Enums
Useful when populating lists with unit enum values for the user to choose.

```c#
UnitConverter.Convert(1, LengthUnit.Centimeter, LengthUnit.Millimeter); // 10 mm
```

For more examples on dynamic parsing and conversion, see the unit conversion applications below.
#### Convert quantity to unit - Names or abbreviation strings
Sometimes you only have strings to work with, that works too!

```c#
UnitConverter.ConvertByName(1, "Length", "Centimeter", "Millimeter"); // 10 mm
UnitConverter.ConvertByAbbreviation(1, "Length", "cm", "mm"); // 10 mm
```

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


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.
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.
The quantity list box contains `QuantityType` values such as `QuantityType.Length` and the two unit list boxes contain `Enum` values, such as `LengthUnit.Meter`.

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.
#### Populate quantity selector
Use `Quantity` to enumerate all quantity type enum values, such as `QuantityType.Length` and `QuantityType.Mass`.

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

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:
#### Update unit lists when selecting new quantity
So user can only choose from/to units compatible with the quantity type.

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

// If Length is selected, get length units for populating from/to UI selectors
LengthUnit[] lengthUnits = Length.Units;
_units.Clear();
foreach (Enum unitValue in quantityInfo.Units)
{
_units.Add(unitValue);
}
```

#### Update calculation on unit selection changed
Using `UnitConverter` to convert by unit enum values as given by the list selection `"Length"` and unit names like `"Centimeter"` and `"Meter"`.

// Perform conversion using input value and selected from/to units
double inputValue; // Obtain from textbox
LengthUnit fromUnit, toUnit; // Obtain from ListBox selections
double resultValue = Length.From(inputValue, fromUnit).As(toUnit);
```c#
double convertedValue = UnitConverter.Convert(
FromValue, // numeric value
SelectedFromUnit.UnitEnumValue, // Enum, such as LengthUnit.Meter
SelectedToUnit.UnitEnumValue); // Enum, such as LengthUnit.Centimeter
```

### Example: WPF app using IValueConverter to parse quantities from input
Expand All @@ -183,11 +271,8 @@ Src: [Samples/WpfMVVMSample](https://github.com/angularsen/UnitsNet/tree/master/

![wpfmvvmsample_219w](https://user-images.githubusercontent.com/787816/34913417-094332e2-f8fd-11e7-9d8a-92db105fbbc9.png)


The purpose of this app is to show how to create an `IValueConverter` in order to bind XAML to quantities.

NOTE: A lot of reflection and complexity were introduced due to not having a base type. See #371 for discussion on adding base types.

### <a name="precision"></a>Precision and Accuracy

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public void Execute(object parameter)
_commandDelegate.Invoke();
}

// Is never used
#pragma warning disable CS0067
public event EventHandler CanExecuteChanged;
#pragma warning restore CS0067
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ public interface IMainWindowVm : INotifyPropertyChanged
decimal ToValue { get; }
ICommand SwapCommand { get; }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace UnitsNet.Samples.UnitConverter.Wpf
Expand All @@ -15,13 +15,14 @@ public sealed class MainWindowDesignVm : IMainWindowVm
{
public MainWindowDesignVm()
{
Quantities = ToReadOnly(Enum.GetValues(typeof(QuantityType)).Cast<QuantityType>().Skip(1));
Quantities = ToReadOnly(Quantity.Types);
Units = ToReadOnly(Length.Units.Select(u => new UnitListItem(u)));
SelectedQuantity = QuantityType.Length;
SelectedFromUnit = Units[1];
SelectedToUnit = Units[2];
}


public ReadOnlyObservableCollection<QuantityType> Quantities { get; }
public ReadOnlyObservableCollection<UnitListItem> Units { get; }
public QuantityType SelectedQuantity { get; set; }
Expand All @@ -35,11 +36,14 @@ public MainWindowDesignVm()

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

// Is never used
#pragma warning disable CS0067
public event PropertyChangedEventHandler PropertyChanged;
#pragma warning restore CS0067

private static ReadOnlyObservableCollection<T> ToReadOnly<T>(IEnumerable<T> items)
{
return new ReadOnlyObservableCollection<T>(new ObservableCollection<T>(items));
}
}
}
}
28 changes: 16 additions & 12 deletions Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindowVM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed class MainWindowVm : IMainWindowVm

public MainWindowVm()
{
Quantities = ToReadOnly(Enum.GetValues(typeof(QuantityType)).Cast<QuantityType>().Skip(1));
Quantities = ToReadOnly(Quantity.Types);

_units = new ObservableCollection<UnitListItem>();
Units = new ReadOnlyObservableCollection<UnitListItem>(_units);
Expand Down Expand Up @@ -131,21 +131,25 @@ private void UpdateResult()
{
if (SelectedFromUnit == null || SelectedToUnit == null) return;

ToValue = Convert.ToDecimal(UnitsNet.UnitConverter.ConvertByName(FromValue,
SelectedQuantity.ToString(),
SelectedFromUnit.UnitEnumValue.ToString(),
SelectedToUnit.UnitEnumValue.ToString()));
double convertedValue = UnitsNet.UnitConverter.Convert(FromValue,
SelectedFromUnit.UnitEnumValue,
SelectedToUnit.UnitEnumValue);

ToValue = Convert.ToDecimal(convertedValue);
}

private void OnSelectedQuantity(QuantityType quantity)
private void OnSelectedQuantity(QuantityType quantityType)
{
QuantityInfo quantityInfo = Quantity.GetInfo(quantityType);

_units.Clear();
IEnumerable<object> unitValues = UnitHelper.GetUnits(quantity);
foreach (object unitValue in unitValues) _units.Add(new UnitListItem(unitValue));
foreach (Enum unitValue in quantityInfo.Units)
{
_units.Add(new UnitListItem(unitValue));
}

SelectedQuantity = quantity;
SelectedFromUnit = Units.FirstOrDefault();
SelectedToUnit = Units.Skip(1).FirstOrDefault() ?? SelectedFromUnit; // Try to pick a different to-unit
SelectedFromUnit = _units.FirstOrDefault();
SelectedToUnit = _units.Skip(1).FirstOrDefault() ?? SelectedFromUnit; // Try to pick a different to-unit
}

private static ReadOnlyObservableCollection<T> ToReadOnly<T>(IEnumerable<T> items)
Expand All @@ -159,4 +163,4 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="UnitHelper.cs" />
<Compile Include="UnitListItem.cs" />
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
Expand Down
26 changes: 0 additions & 26 deletions Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitHelper.cs

This file was deleted.

6 changes: 3 additions & 3 deletions Samples/UnitConverter.Wpf/UnitConverter.Wpf/UnitListItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ namespace UnitsNet.Samples.UnitConverter.Wpf
/// </summary>
public sealed class UnitListItem
{
public UnitListItem(object val)
public UnitListItem(Enum val)
{
UnitEnumValue = val;
UnitEnumValueInt = (int) val;
UnitEnumValueInt = Convert.ToInt32(val);
UnitEnumType = val.GetType();
Abbreviation = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(UnitEnumType, UnitEnumValueInt);

Text = $"{val} [{Abbreviation}]";
}

public string Text { get; }
public object UnitEnumValue { get; }
public Enum UnitEnumValue { get; }
public int UnitEnumValueInt { get; }
public Type UnitEnumType { get; }
public string Abbreviation { get; }
Expand Down
Loading