Skip to content
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

Generate operators from unit relations defined in JSON #1329

Merged
merged 13 commits into from
Feb 4, 2024

Conversation

Muximize
Copy link
Contributor

@Muximize Muximize commented Nov 13, 2023

Related #1200

In the PR adding generic math (#1164) @AndreasLeeb states:

Regarding the operators in the *.extra.cs files, that could be tackled easily by describing the dependencies (operations) between different quantities in the quantity JSON files, and then the operator overloads and the generic math interfaces for the quantity structs could also be automatically generated. But that's a topic for another time 😄

I decided to give this a shot.

UnitRelations.json contains relations extracted from the existing *.extra.cs files. I decided on a new file because multiplication is commutative and I didn't want to duplicate these in the individual quantity JSON files, or risk missing one or the other, so it's best to define them once in one place. The generator handles this by generating two operators for a single multiplication relation.

The relations format uses the quantities method names. This is a bit unfortunate, but it's the best I could come up with without making the CodeGen project depend on UnitsNet, which would create a bit of a chicken/egg problem. This is not unheard of (self-hosted compilers) but I wanted to keep it simple for now.

The generated code enables the removal of 44 *.extra.cs files, and the 17 remaining contain much less code.

@Muximize Muximize changed the title [WIP] Generate operators from defined unit relations in JSON [WIP] Generate operators from unit relations defined in JSON Nov 13, 2023
@Muximize
Copy link
Contributor Author

@angularsen What's your opinion about this? Is this a worthwhile endeavor in the right direction?

@angularsen
Copy link
Owner

Apologies, I thought this was still being worked on so I haven't looked at it yet.
The PR description sounds very useful, I'm just curious if there are any challenges to this approach or not.

Let me read more through the code and get back to you 👍 Will try to find time the next couple of days.

@Muximize
Copy link
Contributor Author

No worries, I should have been more clear that this is a proposal in need of feedback. God jul! 😄

@Muximize Muximize changed the title [WIP] Generate operators from unit relations defined in JSON Generate operators from unit relations defined in JSON Dec 22, 2023
Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks very promising and a big improvement on the existing approach.

A few ideas below.

CodeGen/JsonTypes/Relation.cs Outdated Show resolved Hide resolved
Common/UnitRelations.json Outdated Show resolved Hide resolved
UnitsNet/CustomCode/Quantities/Force.extra.cs Show resolved Hide resolved
angularsen pushed a commit that referenced this pull request Jan 2, 2024
Came across this while working on #1329. I'm afraid it's an API-breaking
change, so maybe it should wait for v6?

Related:
#1200
Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks solid! This is some impressive work 👏

Some minor suggestions for you to consider.

The biggest one for me is to get some peace of mind that we are not breaking anyone as it is hard to verify by review.

I added a suggestion to maybe use reflection to compare before/after of all quantities and their public members, by outputting to text and diffing that.

@@ -119,7 +112,7 @@ private static QuantityRelation ParseRelation(string relationString, IReadOnlyDi
{
var segments = relationString.Split(' ');

if (segments.Length != 5 || segments[1] != "=")
if (segments is not [_, "=", _, "*" or "/", _])
{
throw new Exception($"Invalid relation string: {relationString}");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception can give an example of a valid format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but isn't UnitRelations.json full of valid examples?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, but I still think it improves the developer experience a bit.

var relationStrings = JsonConvert.DeserializeObject<List<string>>(text) ?? [];
relationStrings.Sort();

var parsedRelations = relationStrings.Select(relationString => ParseRelation(relationString, quantities)).ToList();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we throw on duplicates here, with a helpful explanation?

It probably gives compile errors in the generated code anyway, but it would be helpful if codegen failed early on invalid input to make it easier for contributors to find out what they did wrong.

If we eventually do #1354 , then duplicates could also occur implicitly by one explicit definition conflicting with the implicit definition for division operators.

Another option is to remove duplicates with a HashSet or similar, then fix the document when writing it back. But it may be complicated to know what to remove, in particular with the magic division operators, so it is probably better to just throw and have the author fix their mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I added a check after all relations are inferred.

Duplicate definitions in UnitRelations.json are automatically removed with a SortedSet.

/// "double" can be used as a unitless operand.
/// "1" can be used as the left operand to define inverse relations.
/// </summary>
/// <param name="rootDir">Repository root directory.</param>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few examples with some actual quantities and units in a <example></example> tag could be helpful for the reader

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

var @operator = segments[3];
var left = segments[2].Split('.');
var right = segments[4].Split('.');
var result = segments[0].Split('.');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, I was just thinking maybe regex was a good fit for this parsing?
Not important at all, just a tip to consider.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that but it wasn't a big improvement, the regex is quite ugly and less comprehensible than the list pattern:

^(\w+)\.?(\w*) = (\w+)\.?(\w*) (\*|\/) (\w+)\.?(\w*)$

[_, "=", _, "*" or "/", _]

@@ -17,6 +17,8 @@ internal class QuantityGenerator : GeneratorBase
private readonly string _valueType;
private readonly Unit _baseUnit;

private readonly string[] _decimalTypes = { "BitRate", "Information", "Power" };
Copy link
Owner

@angularsen angularsen Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is not too much hassle, I would rather take a list of all quantities in the ctor to determine the value type from their Quantity type, or a list of all decimal quantities determined from the original list of quantities.

We are not likely to get more decimal types anytime soon, rather changing them to double I think, but it may take some time and this is one less place to maintain when that changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't needed anymore since I reworked the codegen, so I removed it 👍

var rightParameter = relation.RightQuantity.Name.ToCamelCase();
var rightConversionProperty = relation.RightUnit.PluralName;

if (relation.LeftQuantity.Name == "TimeSpan")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe reuse some constants for all these repeating strings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to tidy up a bit, but code generation is always a bit messy and throwing around consts called _double and _value didn't make things better 😅

UnitsNet/CustomCode/Quantities/Area.extra.cs Show resolved Hide resolved
@angularsen angularsen changed the base branch from master to release/v6 February 4, 2024 14:10
@angularsen angularsen mentioned this pull request Feb 4, 2024
15 tasks
@angularsen angularsen merged commit 2424307 into angularsen:release/v6 Feb 4, 2024
1 check passed
@angularsen angularsen added this to the vNext milestone Feb 4, 2024
angularsen pushed a commit that referenced this pull request Mar 1, 2024
In
[#1329](#1329 (comment))
this proposal came up:

> Another idea: generate division operators based on multiplication.
Right now we define:
> ```
> ElectricPotential.Volt = ElectricCurrent.Ampere *
ElectricResistance.Ohm (and generate the reverse)
> ElectricCurrent.Ampere = ElectricPotential.Volt /
ElectricResistance.Ohm
> ElectricResistance.Ohm = ElectricPotential.Volt /
ElectricCurrent.Ampere
> ```
> But those last two could also be generated based on the first.

This PR is an experiment implementing this.

### Breaking changes:

- `TimeSpan = Volume / VolumeFlow` => `Duration = Volume / VolumeFlow`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants