Closed BrandonLWhite closed 7 years ago
This is a topic that regularly comes up, and I have wanted it myself at various times, but then I also realized that converting between arbitrary, possibly incompatible, units is a slippery slope. There are very few inter-unit conversions, such as Mass.FromGravitationalForce(Force f)
and operator overloads for common arithmetic, such as Speed * TimeSpan = Length
. These are the exceptions.
If we were to represent units generically, I fear we would promote use where type safety is out the door. Type safety is one of the most valuable benefits to me in this library. Sure, giving it up would be optional, but it's something about helping people fall into the pit of success, so to speak.
These problems can typically be solved while staying type-safe. I don't know your application, but I would assume your user would have to choose what type of unit is coming in, so I would think it was natural to first select Force category, then Newton or Kilogram next. This allows you to work on Force measurements generically, while also knowing what specific ForceUnit the user chose, so you can easily present texts of readings using myForce.ToString(userSelectedForceUnit)
.
If you need to convert it, you know it is a Force measurement, so you list all the other ForceUnit enum values for him to convert to, then use myForce.As(newForceUnit)
. It would not make sense to list Volume units in this case anyway.
My philosophy is that an application doesn't work with universal units if there are conversions involved, you have your set of types of units (Length, Mass, Volume) you care about, and you conditionally handle these separately. For reports/graphs, you usually fall back to numbers and unit abbreviation strings, but you don't really need to know more than that just to present it. If you need to convert them, you need to know a lot more.
If you really, really want universal units and conversions, you can probably implement this in your application by enumerating all ForceUnit, LengthUnit etc. values and converting them to objects like { Unit: "ForceUnit.Newton", Value: 1.0 }
. Then say you want to convert this to ForceUnit.Kilogram
, you do some string pattern matching and figure out the class name Force
and unit ForceUnit.Newton
, then use reflection to call Force.From(1.0, ForceUnit.Newton).Kilograms
, but there are some issues like using the correct pluralization of property names etc. As I said, slippery.
If you believe I have misunderstood, or this approach would not work at all for you, I would like to learn more. I am open to facilitate this usecase if there is a real need for it and if it would truly be better handled in the core lib, rather in the application.
Certainly we wouldn't want to treat all units as being in the same physical quantity class. It would be a runtime error (exception) if an attempt is made to convert from one unit class to another.
For the subsystem of our product that is the focus here, be aware that there is no business logic that is statically assuming any kind of physical quantity. Thus, there is no appropriate place in our code that would be operating on a ForceUnit.
However, there are other parts of our system that implement "baked in" scientific calculations for our application domain, and indeed we are incrementally adopting Units.NET for the compile-time safety it provides as well as other conveniences. Ultimately, user configured sensors or field devices would be able to feed in to some of these quantities, in which case we would certainly do the necessary runtime checks before accepting a quantity that came from the runtime configured module into the statically typed modules.
As for inter-unit conversions, I think it would suffice to simply not support such conversions via the dyanmic/runtime mechanism.
One additional point of clarification. Addressing your point of "while also knowing what specific ForceUnit the user chose, so you can easily present texts of readings using myForce.ToString(userSelectedForceUnit)".
This would require a bunch of conditionals in our code to essentially convert from a runtime configuration data value (ie the PhysicalQuantity) to the appropriate static Units.NET type, so that we could then perform the conversion and get the display label (as per your example). This logic would be extremely coupled to Units.NET.
Certainly I will be implementing such a lookup method in my code as a temporary solution. But to me that is almost as bad as forking Units.NET.
You have a very comprehensive units of measurement library for .NET. It addresses a very important problem of mismatching units in static calculations in code. It reminds me very much of boost::units that I used years ago. This aspect is great when you are performing known scientific calculations in your own code. Compile time errors are always better than runtime errors.
But runtime errors are at least better than spaceships crashing and cancer patients getting nuked with incorrect radiation doses. I don't agree that you give up all of the value and safety of such a units management library by allowing runtime conversions within the same PhysicalQuantity class.
Thank you for the details. Could you please provide me a pseudo code implementation? I'm not 100% sure how you propose we go about this in Units.NET.
It would be awesome if UnitsNet could support this directly. What is needed:
- Enum for UnitClasses to support runtime configuration.
- A runtime conversion function such as `double Convert(UnitClass class, Unit from, Unit to, double value)'
My current understanding is something like this:
// Generated
enum UnitClass {
Force,
Length
//...more
}
// Generated
enum Unit {
Force_Newton,
Force_Kilogramforce,
Length_Meter,
Length_Centimeter
//...more
}
static double Convert(UnitClass class, Unit from, Unit to, double value)
{
// Generated
if (UnitClass == UnitClass.Force) {
ForceUnit fromForceUnit = GetForceUnit(from);
ForceUnit toForceUnit = GetForceUnit(to);
return Force.From(fromForceUnit, value).As(toForceUnit);
}
// ... other unit classes
}
static double? TryConvert(UnitClass class, Unit from, Unit to, double value)
{
// Generated, returns null if units were incompatible instead of throwing exception
}
static ForceUnit GetForceUnit(Unit unit)
{
// Generated, map from Unit enum to ForceUnit enum, throw if not compatible
}
Please note that, as with existing enums, these enums would also not be safe to serialize due to how we generated code and cannot guarantee the enum integer values don't change when adding more units later. See Serialization section for details.
Thanks for continuing to entertain this.
Perhaps we do not need to generate the master "Unit" enum. Maybe something like this:
static double Convert(UnitClass class, Enum from, Enum to, double value)
{
// Throws if the Enums are not of the same type. Needs further exploration for performance.
// However, the downcasts below in the generated code may suffice.
EnsureSameUnitClass(from, to);
// Generated
if (UnitClass == UnitClass.Force) {
return Force.From((ForceUnit)from, (ForceUnit)value).As(to);
}
// ... other unit classes
}
I like your TryConvert. Perhaps that could use ForceUnit fromForceUnit = from as ForceUnit
with a null check along with the same for toForceUnit.
Regarding the enums not being safe to persist. I understand. Currently we are using strings in our database and configuration files. Perhaps UnitsNet could make this reasonably guaranteed that the enum names will not change. Otherwise this business of DB/config file persistence is another issue that would need to be addressed, presumably separately from this issue.
I gave this a spin in LINQPad, and this piece of code seems to work:
static double? TryConvert(Enum from, Enum to, double value)
{
// Generated
LengthUnit? fromLengthUnit = from is LengthUnit ? (LengthUnit?)from : null;
LengthUnit? toLengthUnit = to is LengthUnit ? (LengthUnit?)to : null;
if (fromLengthUnit == null || toLengthUnit == null)
return null;
return Length.From(value, fromLengthUnit.Value).As(toLengthUnit.Value);
// ... other unit classes
}
void Main()
{
// Outputs: 100
Console.WriteLine(TryConvert(LengthUnit.Meter, LengthUnit.Centimeter, 1));
}
Ah, right you are, nullables are needed. You can omit the ternary and safe-cast to nullable:
static double? TryConvert(Enum from, Enum to, double value)
{
// Generated
LengthUnit? fromLengthUnit = from as LengthUnit?;
LengthUnit? toLengthUnit = to as LengthUnit?;
if (fromLengthUnit == null || toLengthUnit == null)
return null;
return Length.From(value, fromLengthUnit.Value).As(toLengthUnit.Value);
// ... other unit classes
}
[Test]
public void RuntimeConversion()
{
Assert.AreEqual(100, TryConvert(LengthUnit.Meter, LengthUnit.Centimeter, 1));
Assert.IsNull(TryConvert(LengthUnit.Meter, ForceUnit.PoundForce, 1));
}
Alright, well I think this looks like straight forward implementation, that adds value by generating all the boiler plate code. Do you want to take a stab at the PR? I don't have a whole lot of time these days, might take me a while.
The TryConvert methods should probably go into the static UnitSystem
class, and powershell scripts must be modified to generate the new code.
Great I will give it a shot.
Just an idea, it would be convenient to have a IList
Just checking in, any progress here?
Sorry, no. I still intend to do this but I haven't made any further progress since the previous PR.
Just checking in on old issues, do you still intend to get around to this?
Our feature that would leverage this Units.Net capability has been moved down the priority list. It is still something we need, just not yet. I still plan to implement this but if someone else wants to then certainly add a comment here indicating so.
I will comment here before I actually do begin coding something and have a firm idea of when there would be a subsequent pull request.
Thanks for the update
On Tue, Jan 31, 2017, 18:22 Brandon White notifications@github.com wrote:
Our feature that would leverage this Units.Net capability has been moved down the priority list. It is still something we need, just not yet. I still plan to implement this but if someone else wants to then certainly add a comment here indicating so.
I will comment here before I actually do begin coding something and have a firm idea of when there would be a subsequent pull request.
— You are receiving this because you commented.
Reply to this email directly, view it on GitHub https://github.com/anjdreas/UnitsNet/issues/186#issuecomment-276430406, or mute the thread https://github.com/notifications/unsubscribe-auth/AAwFaI87nmDSbN3tpJ17mewU7GKu2r-7ks5rX23egaJpZM4KD9Mx .
I think this is more or less covered by UnitConverter.ConvertByName()
introduced in d57c7edba5ecdfb69236c873bd18130f14d6d424.
You can't convert by enum values yet, but you can by using ToString() on the enums. We could add methods that take enums too, but closing this for now until someone wants to add that.
In our industrial/scientific application, we have a need for allowing a user to configure an arbitrary floating point value acquired via digital acquisition (DAQ) into a unit of measure that they specify. Thus, the UnitClass must be specified in addition to the *Unit type.
We must then be able to convert this value to some other arbitrary *Unit within the same unit class in various calculations, views, reports, logging/trending and what not, again dictated by user configuration.
It would be awesome if UnitsNet could support this directly. What is needed: