-
Notifications
You must be signed in to change notification settings - Fork 405
QuantityValue implemented as a fractional number 🐲 #1544
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
base: master
Are you sure you want to change the base?
Conversation
- IQuantity interfaces optimized (some methods refactored as extensions) - QuantityInfo/UnitInfo hierachy re-implemented (same properties, different constructors) - QuantityInfoLookup is now public - UntAbbreviationsCache, UnitParser, QuantityParser optimized - UnitConverter: re-implemented (multiple versions) - removed the IConvertible interface - updated the JsonNet converters - introducing the SystemTextJson project - added a new UnitsNetConfiguration to the Samples project showcasing the new configuration options - many more tests and benchmarks (perhaps too many)
|
@angularsen Clearly, I don't expect this to get merged in the Gitty up! fashion, but at least we have the whole picture, with sources that I can reference. If you want, send me an e-mail, we could do a quick walk-through / discussion. |
…lection constructors with IEnumerable - `UnitAbbreviationsCacheInitializationBenchmarks`: replaced some obsolete usages
I tried to create this PR twice before (many months ago), while the changes to the unit definitions were still not merged- and the web interface was giving me an error when trying to browse the files changed.. Something like "Too many files to display" 😄 |
|
Ok, I'm not going to get through a review of this many files anytime soon. On the surface though, it seems like this could be split up into chunks. I know it's tedious and extra work, but it will be way faster to review. Do you see any chunks of changes to easily split off into separate PRs? |
Sofia (GMT+3), but time zones are not relevant to my sleep schedule - so basically any time you want.
Yes, I do have some ideas:
Hopefully by the time we get to 5) you'd be up to speed (and fed up with PRs) and we can turn back to reviewing / working on the rest of it as a whole 😄 |
|
Ok, sounds good. Just send PRs my way and I'll try to get to them. I have a little bit of extra time this weekend. |
…ions into their own folder
- UnitAbbreviationsCache: removing the obsolete overload - adding a few more tests
|
This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days. |
|
This PR was automatically closed due to inactivity. |
…ramework (net8.0) with 9.0 on the QuantityInfoBenchmarks
|
Initial review by Claude. PR Review: QuantityValue as Fractional Number 🐲PR: #1544 OverviewThis is a massive, fundamental rewrite of UnitsNet's core architecture:
Key Changes1. QuantityValue → Fractional Representation
// Internal representation
private readonly BigInteger _numerator;
private readonly BigInteger? _denominator;
// Example: Exact fractions instead of floating-point
new QuantityValue(5000, 127) // Exact: 5000/127 for inchesBenefits:
2. API Breaking Changes
3. New Features
4. Architecture Improvements
Migration ImpactBreaking Changes ExamplesEvery consumer will need code changes: // ❌ Breaking - no longer compiles
double meters = length.Value;
double ratio = length1 / length2;
var newLength = new Length(5.0, LengthUnit.Meter);
void ProcessDistance(double meters) { }
ProcessDistance(length.Meters);
// ✅ Migration needed
QuantityValue meters = length.Value;
double metersAsDouble = length.Value.ToDouble();
QuantityValue ratio = length1 / length2;
var newLength = new Length(new QuantityValue(5), LengthUnit.Meter);
void ProcessDistance(double meters) { }
ProcessDistance(length.Meters.ToDouble());Interface Changes// ❌ Removed from IQuantity
double As(Enum unit);
double As(UnitKey unitKey);
IQuantity ToUnit(Enum unit);
IQuantity<T>.ToUnit(UnitSystem unitSystem);
// ✅ Now available as extension methods
QuantityExtensions.As(this IQuantity quantity, UnitKey unitKey);
QuantityExtensions.ToUnit(this IQuantity quantity, UnitKey unitKey);
QuantityExtensions.ToUnit<T>(this IQuantity<T> quantity, UnitSystem unitSystem);Serialization CompatibilityGood News 🎉JSON serialization is mostly backwards compatible with the right configuration: // Old format (v5): {"Value":10.0,"Unit":"m"}
// New format (v6): {"Value":10,"Unit":"m"}
// Deserialize old data with RoundedDouble option:
var converter = new AbbreviatedUnitsConverter(
new QuantityValueFormatOptions(
SerializationFormat: QuantityValueSerializationFormat.DoublePrecision,
DeserializationFormat: QuantityValueDeserializationFormat.RoundedDouble
));
Length length = JsonConvert.DeserializeObject<Length>(oldJson, converter);
// ✅ Can read old JSON!Default Behavior Change
|
On Rider, this was needed to actually run the samples with 'Official' configuration. Only Debug/Release were shown.
This reverts commit 896f765.
angularsen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Posting review so far, I got down through SystemTextJson.
| /// </summary> | ||
| [DataMember(Name = ""Value"", Order = 1)] | ||
| private readonly double _value; | ||
| [DataMember(Name = ""Value"", Order = 1, EmitDefaultValue = false)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean Lenght.Zero would exclude the value in the JSON/XML?
I think I prefer both the value and unit to always be included. Optional values can make sense to omit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My philosophy regarding data DataContractSerializer and the DataContractJsonSerializer is that these are only used/useful in machine-to-machine communications (such as WCF or the GRPC gateway thing, which AFAIK builds up the protos based off the data contracts)- as such I've dropped all constraints regarding the human-readability of the output and prioritized the reduction of the payload size.
The difference isn't huge, but given how bloated the xml output is, and how rarely one (i.e. a person) actually reads it, I figured it wouldn't hurt to shave off a few bytes when possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@angularsen your call here
| } | ||
|
|
||
| Writer.WL($"<{relation.LeftQuantity.Name}, {relation.RightQuantity.Name}, {relation.ResultQuantity.Name}>,"); | ||
| Writer.WL($"<{relation.LeftQuantity.Name}, {relation.RightQuantity.Name}, {relation.ResultQuantity.Name.Replace("double", "QuantityValue")}>,"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of scope, but double here seems like it could be renamed to avoid this string replace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we should probably replace it in the UnitRelations.json (marking it as a potential breaking change).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@angularsen This is an easy fix (renaming type in UnitRelations.json), but a potential breaking change for other source generators. If you're ok with it.. (alternatively I could potentially replace the current workaround with an extension / property).
| /// <summary> | ||
| /// Construct a converter using the default list of quantities (case insensitive) and unit abbreviation provider | ||
| /// Initializes a new instance of the <see cref="AbbreviatedUnitsConverter" /> class using the default | ||
| /// case-insensitive comparer and the specified <see cref="QuantityValueFormatOptions" />. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default ctor xmldoc could be more clear about what the defaults are.
Same with QuantityValueFormatOptions, it could also more plainly state the defaults in its xmldoc.
|
|
||
| namespace UnitsNet.Serialization.JsonNet; | ||
|
|
||
| internal static class QuantityValueExtensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, but should this not be renamed to something like XxxJsonWriterExtensions?
It also has Reader stuff, so maybe suffix XxxJsonExtensions or split into reader/writer files.
UnitsNet.Serialization.JsonNet/UnitsNet.Serialization.JsonNet.csproj
Outdated
Show resolved
Hide resolved
| [RequiresUnreferencedCode( | ||
| "If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] | ||
| #endif | ||
| public class InterfaceQuantityWithUnitTypeConverter : JsonConverter<IQuantity> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm only skimming all the serialization stuff. At first glance I don't see why we have both InterfaceQuantityWithUnitTypeConverter and InterfaceQuantityConverterBase.
It seems InterfaceQuantityWithUnitTypeConverter is unused/untested?
UnitsNet.Serialization.SystemTextJson/UnitsNet.Serialization.SystemTextJson.csproj
Show resolved
Hide resolved
…rseInvariant Specify invariant culture for Fraction and `int` parsing.
This is typically not a problem with integers, but let's be explicit. Integer parsing is done for quantity string formatting to control number formats, such as `a2` for the 2nd unit abbreviation.
angularsen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another batch of comments.
I got down to: ConversionExpressionTests.cs
| namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; | ||
|
|
||
| /// <summary> | ||
| /// We should try to push these extensions to the original library (working on the PRs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update xmldoc
| // { | ||
| // return Value.ToString("G", CultureInfo.InvariantCulture).Length < 16; | ||
| // } | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove commmented code or add clear TODO
UnitsNet.Serialization.JsonNet.Tests/Value/QuantityValueFractionalNotationConverterTests.cs
Show resolved
Hide resolved
| .ToList(); | ||
| var customMeterDefinition = new UnitDefinition<LengthUnit>(LengthUnit.Meter, "UpdatedMeter", "UpdatedMeters", BaseUnits.Undefined); | ||
|
|
||
| var result = unitDefinitions.Configure(LengthUnit.Meter, _ => customMeterDefinition).ToList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does look a bit weird to have extension method Configure on a collection of UnitDefinition items.
I think it would be better to either create a wrapper type to hold the collection and offer these methods, or convert the extension methods to a named static helper class.
UnitsNet.Tests/CustomCode/QuantityInfos/Builder/QuantityInfoBuilderExtensionsTest.cs
Outdated
Show resolved
Hide resolved
UnitsNet.Tests/CustomCode/ValueTests/QuantityValueTests.ConvertToString.cs
Show resolved
Hide resolved
| // | ||
| // Assert.Equal(0.0508, inSI.Value); | ||
| // Assert.Equal(LengthUnit.Meter, inSI.Unit); | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either remove commented tests or add clear TODO on how to add them back
| { | ||
| var expectedUnit = MassUnit.Milligram; | ||
| var unitInt = (int)expectedUnit; | ||
| var json = $"{{\"Value\":1.2,\"Unit\":{unitInt}}}"; | ||
| var json = $$$"""{"Value":{"N":{"_bits":null,"_sign":12},"D":{"_bits":null,"_sign":10}},"Unit":{{{unitInt}}}}"""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we ensuring backwards compatibility for deserializing JSON like {"Value": 1.2, "Unit": 5} elsewhere?
Also, the _bits stuff looks very internal. Is it feasible to have this match the output of SystemTextJson?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DataContractJsonSerializer is a lost cause- not even microsoft bothers with it any more.
I did try to customize it (trying to preserve the original output) - but there was a bug (I've left a link to the github issues in a comment somewhere) which prevented me.
| var expectedXml = $"<Mass {Namespace} {XmlSchema}><Value>1.2</Value><Unit>Milligram</Unit></Mass>"; | ||
| var numeratorFormat = $"<N xmlns:a={NumericsNamespace}><a:_bits i:nil=\"true\" xmlns:b={ArraysNamespace}/><a:_sign>12</a:_sign></N>"; | ||
| var denominatorFormat = $"<D xmlns:a={NumericsNamespace}><a:_bits i:nil=\"true\" xmlns:b={ArraysNamespace}/><a:_sign>10</a:_sign></D>"; | ||
| var expectedXml = $"<Mass {Namespace} {XmlSchema}><Value>{numeratorFormat}{denominatorFormat}</Value><Unit>Milligram</Unit></Mass>"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar for xml, what is the plan for backwards compat and migration steps here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I believe there is an example which uses a data-contract-surrogate.
PS I originally tried implementing the IXmlSerializable interface, but then I encountered another bug in the proxy-generation stack (not sure if I left the link to that issue or not).
|
I saw quite a bit of commented code here and there, so I asked Claude to identify all the commented out parts. Something to consider going over. PR #1544 - Commented Code ReviewPR Title: QuantityValue implemented as a fractional number 🐲 SummaryThis review identifies commented-out code in PR #1544 that should either be removed or have TODO comments explaining why it's commented out and what the plan is for uncommenting later. 🔴 High Priority - Remove Immediately1. QuantitiesSelector.cs - Duplicate Commented Class (112 lines!)File: // /// <summary>
// /// Provides functionality to select and configure quantities for use within the UnitsNet library.
// /// </summary>
// ... [full duplicate class implementation - 112 lines]
// }Recommendation: DELETE - This appears to be an old version of the same class that should have been removed. Git history preserves this for reference if needed. 2. ExpressionEvaluator.cs - "No Longer Necessary" CodeFile: // these are no longer necessary
// var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions,
// new SqrtFunctionEvaluator(),
// new PowFunctionEvaluator(),
// new SinFunctionEvaluator(),
// new AsinFunctionEvaluator());
var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions);Recommendation: DELETE - Explicitly marked as "no longer necessary". Git history preserves the old constructor call pattern. 🟡 Medium Priority - Remove or Add TODO3. QuantitiesSelector.cs - Unused PropertyFile: // internal Lazy<IEnumerable<QuantityInfo>> QuantitiesSelected { get; }Recommendation: DELETE - No explanation provided; appears to be leftover from refactoring. 4. QuantitiesSelector.cs - ToList() AlternativeFile: return enumeration;
// return enumeration.ToList();Recommendation: Either:
5. DynamicQuantityConverter.cs - Old Implementation (2 instances)File: // TryGetUnitInfo(conversionKey.FromUnitKey with { UnitValue = conversionKey.ToUnitValue }, out UnitInfo? toUnitInfo))Line 347: // return targetQuantityInfo.Create(conversionFunction.Convert(value), conversionFunction.TargetUnit);
return targetQuantityInfo.From(conversionFunction.Convert(value), conversionFunction.TargetUnit.ToUnit<TTargetUnit>());Recommendation: Either:
6. FrozenQuantityConverter.cs - Old ImplementationFile: // TryGetUnitInfo(conversionKey.FromUnitKey with { UnitValue = conversionKey.ToUnitValue }, out UnitInfo? toUnitInfo))Recommendation: Same as DynamicQuantityConverter - either DELETE or add TODO with explanation. 7. FractionExtensions.cs - Old Double-Based ApproachFile: // return FromDoubleRounded(System.Math.Pow(x.ToDouble(), power.ToDouble()));
return PowRational(x, power);Recommendation: Either:
✅ Keep (Already Have Adequate Explanation)ConversionExpression.cs - Inline DocumentationFile: These comments show simplified lambda forms that enhance readability. Keep as-is. // scaleFunction = value => value;
// scaleFunction = value => value * coefficient;UnitsNetSetup.cs - Future Work TODOsFile: // TODO see about allowing eager loading
// private AbbreviationsCachingMode AbbreviationsCaching { get; set; } = AbbreviationsCachingMode.Lazy;
// TODO see about caching the regex associated with the UnitParser
// private UnitsCachingMode UnitParserCaching { get; set; } = UnitsCachingMode.Lazy;Status: ✅ Properly documented future work. Keep as-is. UnitTestBaseClassGenerator.cs - Documented LimitationFile: // note: it's currently not possible to test this due to the rounding error from (quantity - otherQuantity)
// Assert.True(quantity.Equals(otherQuantity, maxTolerance));Status: ✅ Well-documented limitation. Keep as-is. MainWindowVM.cs - Sample Code Alternative ApproachFile: // note: starting from v6 it is possible to store (and invoke here) a conversion expression
// ConvertValueDelegate _convertValueToUnit = UnitsNet.UnitConverter.Default.GetConversionFunction(...);
// ToValue = _convertValueToUnit(FromValue);Status: ✅ Sample/demo code showing users an alternative v6 feature. Keep as-is. ExpressionEvaluationHelpers.cs - Implicit Constructor NoteFile: // return $"new ConversionExpression({coefficientTermFormat})";
return coefficientTermFormat; // using the implicit constructor from QuantityValueStatus: ✅ Has explanation, though could be slightly improved: Optional improvement: // Old explicit approach: return $"new ConversionExpression({coefficientTermFormat})";
return coefficientTermFormat; // using the implicit constructor from QuantityValueSummary Statistics
Recommended Actions
This will remove ~125+ lines of dead code and improve code maintainability. |
- updating all packages to their latest versions - UnitsNet version bumped to 6.0.0-pre100
…Sharper debugger failure Move the configuration lookup out of the QuantityDebugProxy constructor into the Configuration property getter in UnitsNet/CustomCode/Debug/QuantityDebugProxy.cs so the ReSharper inline evaluator no longer shows "Cannot evaluate expression" when expanding the proxy in the debugger. Also replace debug-formatting static fields with properties (DefaultFormatSpecifier, DefaultFormatProvider) and tidy namespaces/XML docs. Mitigates ReSharper issues RSRP-499956 and RSRP-502262.
…constructor logic and improving the xml comments regarding the equality contract
|
@angularsen I expected the build would fail, but regardless went on and pushed the target framework to I ran a couple of the benchmarks - not everything is faster, but there is a good amount of improvement around the Parse/Format stuff. Anyway, I've already updated the package references in my own solutions (multi targeting PS I'm still experiencing issues with the resharper extension not being able to properly evaluate some expressions in the debugger (no issues with the native VS tooltips)- I've got 2 issues open on their tracker, please check that the debug-proxies are all still working fine for you in raider. |
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <TargetFramework>net10.0</TargetFramework> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might potentially want to keep the generator at net9.0 for a while longer, until VS2026 becomes more widely adopted (I haven't actually tried it but I think VS2022 should still be able to build this with Use previews of the .NET SDK option enabled).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do net10.0 as a separate PR, but we'll definitely bump
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was kinda hoping you would bump the build images before, or at the same time as the PR is created (targeting the main branch).
|
Just to give a life sign, I'm still very busy with work and I expect this to continue well into the next year. However, I'm motivated to get this PR merged in the upcoming Christmas season, bump v6 to beta and start stabilizing and collecting feedback on the rather big changes for a little while before we fully release it. I see some of my comments haven't been addressed yet, so I assume you are still working on this? I realize I'm becoming a bottleneck in this project, we should discuss options to improve that. Maybe onboard more contributors, and I think you should have more autonomy to make decisions without having to wait on me. I'm open to ideas here. |
- added AoT compatibility to the NumberExtensions.CS14 project

QuantityValueimplemented as a fractional numberIQuantityinterfaces optimized (some methods refactored as extensions)UnitInfo: introduced two new properties:ConversionFromBaseandConversionToBasewhich are used instead of theswitch(Unit)conversionUnitsNetSetup: introduced helper methods for adding external quantities, or re-configuring one or more of the existing onesUntAbbreviationsCache: introduced additional factory methods (using a configuration delegate)UnitParser: introduced additional factory methods (using a configuration delegate)UnitConverter: re-implemented (multiple versions)Inverserelationship mapping implemented as a type of implicit conversion