Description
Following the many lengthy exchanges concerning the BaseUnits (default) and the UnitSystem in #646 here is a summary of limitations that I think should be addressed at some point.
First let me start by stating the general use cases (the way that I see them- feel free to add/correct me here):
- Domain separation
- Stock Screen works with quantities in kg | liters (UnitSystem)
- Recipe Screen works with quantities in mg | ml
- Persistence- use a well defined UnitSystem for storing values- forget about Quantity.To/FromXX- As(..), new (..) instead
- Reporting- I want my report to use this and that measure unit (ideally UnitSystem would be serializable)
- Presentation customization
- default unit, default abbreviation- ideally it is also possible to associate those with a range- [1e, 1e3] -> [Gram], [1e3,1e6] -> [Kilogram]
- list of common units- what do you put in the combo-box
Currently, the default (abbreviation for) BaseType for a UnitSystem is determined by taking the first matching unit, that satisfies the equality comparison with the UnitSystem.BaseUnits. The following two operations depend on the success of the previous lookup:
- Constructing quantities using the (double, UnitSystem) constructor
- Converting using the As(UnitSystem) operator
This approach has the following limitations:
- Possible run-time exceptions due to some combination of quantity types in a UnitSystem not having a matching base type definition- it makes sense to throw an exception for custom UnitSystems- but at least constructing/converting using SI should be safe
- BaseUnits cannot be specified for prefixed units: [Kilo]Gram is not matched for SI as the BaseUnits apply to the Gram only
- Ambiguous BaseUnits definitions: using the "dimensions ignored" conversion may sometimes lead to ambiguous conversions such as with the Liter -> CubicMeter derivatives. Consider the following mapping:
- 1 l = 1 dm3 -> Decimeter
- 1 dl = 1 ? (0.1 dm3 | 100 cm3)
- 1 ml = 1 cm3 -> Centimeter
- Overlapping BaseUnits- having the find-first approach makes it easy to both override/have your default unit definition overridden by mistake
- Mapping BaseUnits definitions for UnitSystems in run-time: there is currently no way to define missing or override existing BaseUnits for a UnitSystem
I think the last point is the key to solving most of the limitations on the list. My proposition would basically consist of storing a dictionary representing the default unit for a given quantity type inside the UnitSystem:
private readonly Dictionary<QuantityType, UnitInfo> _defaultUnits; // could be Lazy<..>
This dictionary could be constructed eagerly or lazily (as is currently the case)- using the existing convention (to start with):
public UnitSystem(BaseUnits baseUnits)
{
...
// default implementation (does not fix the prefixed entities matching issue)
_defaultUnits = Quantity.Infos.ToDictionary(x => x.QuantityType,
i => i.GetUnitInfosFor(baseUnits).FirstOrDefault());
}
The Constructors/As(UnitSystem) methods would no longer have to go about finding the "first matching base type" every time- instead they would get it directly from the UnitSystem:
public double As(UnitSystem unitSystem) { .. var defaultUnitInfo = unitSystem.GetDefaultUnitInfo(QuantityType); // casting omitted for simplicity if(defaultUnitInfo == null) throw new ArgumentException("No default unit was defined for the given UnitSystem.", nameof(unitSystem)); return As(firstUnitInfo.Value); }
There would of course be helper methods for the user to register his preferred defaults for a given quantity type like:
public void RegisterDefaultUnit(QuantityType quantityType, UnitInfo unitInfo)
{
_defaultUnits[quantityType] = unitInfo;
}
Some tweaking of the JSON schema/parsing would probably be required for solving the issue with the base types for the prefixed quantities.
// TODO discuss the usefulness of the points presented in the Presentation Customization section and/or see if any (/other) customizations might be of interest for a given UnitSystem