-
Notifications
You must be signed in to change notification settings - Fork 382
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
Preserve value and unit #389
Conversation
391be21
to
7dc80c9
Compare
Some compile errors still to fix, maybe hold off reviewing until fixed. |
6c64e82
to
687c714
Compare
@angularsen 💯 This is an awesome feature. Though, I'm curious to why the need to remove the constructors using just a number without the Unit? It's intuitive enough to make the BaseUnit be the default Unit if nothing is specified. |
@gojanpaolo I guess you spotted a breaking change 😇 However, I really don't like constructors taking a value and assuming the consumer just knows what the base unit is or reads it from the xmldoc. We have even changed base units for quantities at some point, so I would prefer to have the consumer explicitly state what unit to construct with - just as we do with the FromMeters() methods. It is very clear what you are creating. Going with that thought, instead of removing the ctor it should be obsoleted and removed later. Update: Ok, seems I already did obsolete and not remove any ctors just yet. I'm still open to keep them if people find it intuitive to specify just the numeric value. |
876aed1
to
9be7770
Compare
It seems to compile now and is ready for review. |
@angularsen Here's a couple that might seem to be an unexpected change in behavior compared to current release.
|
@gojanpaolo Thanks!
I don't consider this a breaking change. GetHashCode() is intended for hash sorts such as when using a quantity as the key in a dictionary entry. This should not be a behavior people depend on being fixed between versions, the main thing is that it should be reliable and consistent at runtime (not be based on mutable values for instance) - and it is.
Sigh.. not sure exactly how to work around this. Struct don't allow us to specify the default values when using the default ctor. Another argument for Another option is to rethink this design and only store the base unit value as before, and store the constructed Unit for the purpose of using that as the default for |
For the default constructor, I'm also thinking of a fallback logic. something like:
Another approach is to change the default Unit enum values to the BaseUnit instead of Undefined.
|
Right, the reason I'm on the fence of using nullables as fallback is that then we're back to storing things on the heap and adding pressure to the garbage collector. I think at that point we could just as well go for Changing default from Undefined is interesting, a breaking change though. How about instead treating |
Should an exception be thrown if
Looking back at the suggestions, I think this (below) is the better solution.
...that is until/unless we move from |
Great work on this library! I have a need for maintaining the initial value and not necessarily the unit, although it makes sense to do that as well. I don't think it makes sense for using Undefined as BaseUnit for conversions. Undefined is just that, Undefined, and should not guarantee a conversion. If you are going to use Undefined as the BaseUnit you might as well get rid of Undefined. I think it would be OK to throw an error for Undefined Units, but currently that seems like a breaking change if a User was using the default behavior of the struct. I personally think you should construct your Unit of measure explicitly. |
@dmf07 I fully agree with all you said. I would not want anyone explicitly setting However, the key problem is that we cannot implement code for the default ctor of structs. So, what should happen here in these examles? My intuitive answers in comments beside each, since
Assuming my intuition is valid here, we can achieve this by making the backend field of Then
since we know it is ONLY the default struct ctor that could ever result in unit not being set. We also know it will never be Thoughts? NullableSo I assumed |
@dmf07 Just curious, what is your motivation for preserving the original value? |
My motivation for preserving the original value is primarily for user input without having a separate value to keep track of. As far as using Nullable for the backing value of the Unit, that could fix the issue of initializing the struct without context, but I am under the opinion that you shouldn't use a measurement without knowing explicitly what unit of measure it is. The Length example 0 length typically works in all cases. |
Good point about temperature, where 0 Kelvins does not equal 0 Fahrenheit. In that case, yes, I would intuitively assume 0 Kelvins. If other users share that intuition in all scenarios? Probably not. I get your point about staying on the conservative path and throw instead of trying to assume some meaningful unit when none was specified. I think our options regarding default ctor boil down to this:
I agree with you that option 2 is probably the better choice. This PR is currently implemented pretty much like this already, but not the Thoughts? Any other options? |
9be7770
to
bc3ee4d
Compare
Pushed more commits that update serialization to serialize using the constructed value+unit pair, as well as supporting the new |
88e91cf
to
fd391d1
Compare
Some cleanup |
Thanks for the heads up @JKSnd, no problem! |
I'm terribly sorry for not checking this in detail at an earlier stage. Things I think are worth mentioning (so that they aren't overlooked but conscious choices):
I will try to run some of our performance test/benchmarks with this code and see what the difference is. I think that my use case is quite far from the typical so I'm not sure if that really matters for the change. Impressed by the code quality for such a large commit. |
Thanks for taking a look, your insight is always appreciated especially in light of any performance/memory impact. Benchmarks would be awesome, I have done very little of that at least to make an informed decision.
|
After running the code with our internal projects I have the following notes:
I believe this is on the limit of what could be seen as a minor version, the underlying changes are large. Again I want to stress that this is a great effort and that I'm sorry that I'm not contributing more. |
Based on the performance tests I don't think we have to worry about most of my points. After discussing in Gitter we agreed to merge. |
* Store Value/Unit upon construction with From() methods or ctor() * Move conversion functions into As() and AsBaseUnit() methods * Add QuantityValueDecimal to avoid precision-loss going via double
As fallback if no culture is specified. This allows the user to specify a culture other than CurrentUICulture and fixes tests where rely on that to test default behavior.
This is to avoid a breaking change for this PR. We can later change this behavior on major version bump.
Access unit property instead of base unit value field.
Except in WinRTC, which does not support IFormatProvider type.
Store decimal values internally and use that when explicitly casting to decimal, to preserve its precision when constructing quantities such as Power, Information and BitRate.
Test cases that covers the new behavior for using value/unit by default and how the ToString() overloads with additional parameters affect the output. Some of these tests probably overlap with existing tests in UnitSystemTests, but will address that when moving those tests here.
Serialize with constructed Unit and _value instead of base unit and base unit value. By using _value we serialize using the original value type (decimal vs double), since UnitsNet public API is primarily using double. Cast to QuantityValueDecimal for types that use decimal internally. NOTE: This mix of double and decimal representations is a bit messy right now and needs a better fix. We already discuss this topic in #285 about either moving everything to decimal or to better support multiple numeric types (float, double, long, decimal etc).
Fixes broken test due to previous change
As per @gojanpaolo 's comment: #389 (comment) This will not throw when converting to double, since double has a big enough range to contain all other numeric types in .NET.
d469707
to
13bb546
Compare
Nuget 3.92 on the way out. |
# 4.0.0 Release This PR will serve as the list of items to complete and it will be updated to show the progress before finally merged into `master` when completed. We all have busy schedules, so **if you want to help move this work forward then that is much appreciated!** ## The main theme is to reduce binary size In two years it has grown from 280 kB to 1.4 MB and a lot of it is due to unnecessary syntactic sugar with many method overloads for various number types and nullable types - for every of our 800+ units! It simply adds up to a big total. These items are chosen from #180, trying to include as many of the low hanging fruits as possible, while still keeping the list short and realistic to complete in a reasonably short time - we are all busy in our daily lives. We can always have more major version bumps later than trying to perfect it all now. ## Feature complete before November, merged before mid-December I would like to aim for a "beta" pre-release nuget sometime in October with all the items completed, so we can have some time to test it before releasing the final, stable 4.0.0 version before Christmas holidays. We should have the work items list more or less final before Monday, October 8th. ## Changes #### Added - [x] UnitSystem with a different meaning, now defines the base units for a unit system (#524) #### Removed - [x] Replace netstandard1.0 target with netstandard2.0, to avoid the extra dependencies (#477) - [x] Remove code marked as `[Obsolete]`, such as `VolumeUnit.Teaspoon` (#490) - [x] Remove nullable `From` factory methods (#483) - [x] Remove extension methods on _nullable_ number types (#483) - [x] Remove all number extension methods (#497) - [x] Remove `Length2d`, replaced by `Area` (#501) - [x] Remove static methods on `UnitSystem`, should use `UnitSystem.Default` instead (#496) - [x] Remove unit parameter from `ToString()` methods (#546) #### Changed - [x] Throw exception on NaN values in static constructor methods, like `FromMeters()` (#502, see #176 (comment)) - [x] Throw if unit was not specified when constructing quantities, (#499 - #389 (comment)) - [x] Stricter parsing (#343 and #180 (comment)) - [x] Remove unit parameter from `ToString()` methods (#546) - [x] Change Temperature arithmetic back to use base unit Kelvin (#550, see #518) #### Renamed - [x] Correct SingularName for some Flow unit definitions (#494, see #360) - [x] Split/rename UnitSystem into UnitParser, GlobalConfiguration, UnitAbbreviationsCache (#511) #### Fixed - [x] Search for any TODO comments in the code to address, remove comment and either fix or create issue (5d24432) - [x] Update README with v4 changes #498 - [x] Do not try/catch in UnitConverter.Try-methods (#506) - [x] Do not try/catch in `Length.TryParse()` and for other quantities (#507) - [x] Add/update changelog in wiki for v4 with some summary copied from this issue, some v3 to v4 migration instructions and briefly explain why some stuff were removed ## Milestones - [x] Finalize work items list (Monday, October 8) - [x] Release 4.0.0-beta1, feature complete (Wednesday, October 31) - [ ] ~Show release notes and upgrade guide when upgrading to 4.x nuget, if possible~ - [x] Release 4.0.0 (Monday, December 17)
Motivation
Preserve the original value and unit and add getter properties
Value
andUnit
to expose them.Use
Unit
inToString()
instead of the base unit representation, to better match the units the user is working with.This is a significant rewrite of how values are stored and how conversions are performed in quantity types. It is also the first baby steps of introducing some common values in base types #371 .
There should be no breaking changes in neither main lib nor JSON serialization.
Changes
Using example of
Length
, but this applies to all quantities:_meters
to_value
(stilldouble
ordecimal
, as before)LengthUnit? _unit
Value
getter prop (double
ordecimal
)LengthUnit Unit
getter prop, with fallback to base unitLengthUnit.Meter
(1)BaseUnit
, backwards compatibleToString()
to useUnit
instead ofDefaultToStringUnit
, new test casesDefaultToStringUnit
as obsoletedecimal
precision inQuantityValue
(2).Centimeters
(3)Value
+Unit
instead of base unit and base unit value, backwards compatible, update test casesstruct
) and ctors without unit param assumes base unit, to be backwards compatible. We might want to change this in the future to require explicitly specifying the unit.Unit
to base unit, then to target unit. Before this change, the first conversion was initially made when constructing the quantity. However, there are optimizations in-place to avoid converting ifUnit
already matches either base unit or the target unit.Gotchas