Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions CodeGen/Generators/QuantityRelationsParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CodeGen.Exceptions;
using CodeGen.JsonTypes;
using Newtonsoft.Json;

namespace CodeGen.Generators
{
/// <summary>
/// Parses the JSON file that defines the relationships (operators) between quantities
/// and applies them to the parsed quantity objects.
/// </summary>
internal static class QuantityRelationsParser
{
/// <summary>
/// Parse and apply relations to quantities.
///
/// The relations are defined in UnitRelations.json
/// Each defined relation can be applied multiple times to one or two quantities depending on the operator and the operands.
///
/// The format of a relation definition is "Quantity.Unit operator Quantity.Unit = Quantity.Unit" (See examples below).
/// "double" can be used as a unitless operand.
/// "1" can be used as the left operand to define inverse relations.
/// </summary>
/// <example>
/// [
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
/// "Speed.MeterPerSecond = Length.Meter / Duration.Second",
/// "ReciprocalLength.InverseMeter = 1 / Length.Meter"
/// ]
/// </example>
/// <param name="rootDir">Repository root directory.</param>
/// <param name="quantities">List of previously parsed Quantity objects.</param>
public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
{
var quantityDictionary = quantities.ToDictionary(q => q.Name, q => q);

// Add double and 1 as pseudo-quantities to validate relations that use them.
var pseudoQuantity = new Quantity { Name = null!, Units = [new Unit { SingularName = null! }] };
quantityDictionary["double"] = pseudoQuantity with { Name = "double" };
quantityDictionary["1"] = pseudoQuantity with { Name = "1" };

var relations = ParseRelations(rootDir, quantityDictionary);

// Because multiplication is commutative, we can infer the other operand order.
relations.AddRange(relations
.Where(r => r.Operator is "*" or "inverse" && r.LeftQuantity != r.RightQuantity)
.Select(r => r with
{
LeftQuantity = r.RightQuantity,
LeftUnit = r.RightUnit,
RightQuantity = r.LeftQuantity,
RightUnit = r.LeftUnit,
})
.ToList());

// We can infer TimeSpan relations from Duration relations.
var timeSpanQuantity = pseudoQuantity with { Name = "TimeSpan" };
relations.AddRange(relations
.Where(r => r.LeftQuantity.Name is "Duration")
.Select(r => r with { LeftQuantity = timeSpanQuantity })
.ToList());
relations.AddRange(relations
.Where(r => r.RightQuantity.Name is "Duration")
.Select(r => r with { RightQuantity = timeSpanQuantity })
.ToList());

// Sort all relations to keep generated operators in a consistent order.
relations.Sort();

var duplicates = relations
.GroupBy(r => r.SortString)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();

if (duplicates.Any())
{
var list = string.Join("\n ", duplicates);
throw new UnitsNetCodeGenException($"Duplicate inferred relations:\n {list}");
}

foreach (var quantity in quantities)
{
var quantityRelations = new List<QuantityRelation>();

foreach (var relation in relations)
{
if (relation.LeftQuantity == quantity)
{
// The left operand of a relation is responsible for generating the operator.
quantityRelations.Add(relation);
}
else if (relation.RightQuantity == quantity && relation.LeftQuantity.Name is "double" or "TimeSpan")
{
// Because we cannot add generated operators to double or TimeSpan, we make the right operand responsible in this case.
quantityRelations.Add(relation);
}
}

quantity.Relations = quantityRelations.ToArray();
}
}

private static List<QuantityRelation> ParseRelations(string rootDir, IReadOnlyDictionary<string, Quantity> quantities)
{
var relationsFileName = Path.Combine(rootDir, "Common/UnitRelations.json");

try
{
var text = File.ReadAllText(relationsFileName);
var relationStrings = JsonConvert.DeserializeObject<SortedSet<string>>(text) ?? [];

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

// File parsed successfully, save it back to disk in the sorted state.
File.WriteAllText(relationsFileName, JsonConvert.SerializeObject(relationStrings, Formatting.Indented));

return parsedRelations;
}
catch (Exception e)
{
throw new UnitsNetCodeGenException($"Error parsing relations file: {relationsFileName}", e);
}
}

private static QuantityRelation ParseRelation(string relationString, IReadOnlyDictionary<string, Quantity> quantities)
{
var segments = relationString.Split(' ');

if (segments is not [_, "=", _, "*" or "/", _])
{
throw new Exception($"Invalid relation string: {relationString}");
}

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

var leftQuantity = GetQuantity(left[0]);
var rightQuantity = GetQuantity(right[0]);
var resultQuantity = GetQuantity(result[0]);

var leftUnit = GetUnit(leftQuantity, left.ElementAtOrDefault(1));
var rightUnit = GetUnit(rightQuantity, right.ElementAtOrDefault(1));
var resultUnit = GetUnit(resultQuantity, result.ElementAtOrDefault(1));

if (leftQuantity.Name == "1")
{
@operator = "inverse";
leftQuantity = resultQuantity;
leftUnit = resultUnit;
}

return new QuantityRelation
{
Operator = @operator,
LeftQuantity = leftQuantity,
LeftUnit = leftUnit,
RightQuantity = rightQuantity,
RightUnit = rightUnit,
ResultQuantity = resultQuantity,
ResultUnit = resultUnit
};

Quantity GetQuantity(string quantityName)
{
if (!quantities.TryGetValue(quantityName, out var quantity))
{
throw new Exception($"Undefined quantity {quantityName} in relation string: {relationString}");
}

return quantity;
}

Unit GetUnit(Quantity quantity, string? unitName)
{
try
{
return quantity.Units.First(u => u.SingularName == unitName);
}
catch (InvalidOperationException)
{
throw new Exception($"Undefined unit {unitName} in relation string: {relationString}");
}
}
}
}
}
124 changes: 122 additions & 2 deletions CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ public string Generate()
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Linq;");
if (_quantity.Relations.Any(r => r.Operator is "*" or "/"))
Writer.WL(@"#if NET7_0_OR_GREATER
using System.Numerics;
#endif");
Writer.WL(@"using System.Runtime.Serialization;
using UnitsNet.InternalHelpers;
using UnitsNet.Units;

Expand All @@ -67,6 +71,35 @@ namespace UnitsNet
public readonly partial struct {_quantity.Name} :
{(_quantity.GenerateArithmetic ? "IArithmeticQuantity" : "IQuantity")}<{_quantity.Name}, {_unitEnumName}, {_quantity.ValueType}>,");

if (_quantity.Relations.Any(r => r.Operator is "*" or "/"))
{
Writer.WL(@$"
#if NET7_0_OR_GREATER");
foreach (var relation in _quantity.Relations)
{
if (relation.LeftQuantity == _quantity)
{
switch (relation.Operator)
{
case "*":
Writer.W(@"
IMultiplyOperators");
break;
case "/":
Writer.W(@"
IDivisionOperators");
break;
default:
continue;
}
Writer.WL($"<{relation.LeftQuantity.Name}, {relation.RightQuantity.Name}, {relation.ResultQuantity.Name}>,");
}
}

Writer.WL(@$"
#endif");
}

if (_quantity.ValueType == "decimal") Writer.WL(@$"
IDecimalQuantity,");

Expand Down Expand Up @@ -100,6 +133,7 @@ namespace UnitsNet
GenerateStaticFactoryMethods();
GenerateStaticParseMethods();
GenerateArithmeticOperators();
GenerateRelationalOperators();
GenerateEqualityAndComparison();
GenerateConversionMethods();
GenerateToString();
Expand Down Expand Up @@ -690,6 +724,92 @@ private void GenerateLogarithmicArithmeticOperators()
" );
}

/// <summary>
/// Generates operators that express relations between quantities as applied by <see cref="QuantityRelationsParser" />.
/// </summary>
private void GenerateRelationalOperators()
{
if (!_quantity.Relations.Any()) return;

Writer.WL($@"
#region Relational Operators
");

foreach (QuantityRelation relation in _quantity.Relations)
{
if (relation.Operator == "inverse")
{
Writer.WL($@"
/// <summary>Calculates the inverse of this quantity.</summary>
/// <returns>The corresponding inverse quantity, <see cref=""{relation.RightQuantity.Name}""/>.</returns>
public {relation.RightQuantity.Name} Inverse()
{{
return {relation.LeftUnit.PluralName} == 0.0 ? {relation.RightQuantity.Name}.Zero : {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(1 / {relation.LeftUnit.PluralName});
}}
");
}
else
{
var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
var leftConversionProperty = relation.LeftUnit.PluralName;
var rightParameter = relation.RightQuantity.Name.ToCamelCase();
var rightConversionProperty = relation.RightUnit.PluralName;

if (relation.LeftQuantity.Name is nameof(TimeSpan))
{
leftConversionProperty = "Total" + leftConversionProperty;
}

if (relation.RightQuantity.Name is nameof(TimeSpan))
{
rightConversionProperty = "Total" + rightConversionProperty;
}

if (leftParameter == rightParameter)
{
leftParameter = "left";
rightParameter = "right";
}

var leftPart = $"{leftParameter}.{leftConversionProperty}";
var rightPart = $"{rightParameter}.{rightConversionProperty}";

if (leftParameter is "double")
{
leftParameter = leftPart = "value";
}

if (rightParameter is "double")
{
rightParameter = rightPart = "value";
}

var leftCast = relation.LeftQuantity.ValueType is "decimal" ? "(double)" : string.Empty;
var rightCast = relation.RightQuantity.ValueType is "decimal" ? "(double)" : string.Empty;

var expression = $"{leftCast}{leftPart} {relation.Operator} {rightCast}{rightPart}";

if (relation.ResultQuantity.Name is not ("double" or "decimal"))
{
expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
}

Writer.WL($@"
/// <summary>Get <see cref=""{relation.ResultQuantity.Name}""/> from <see cref=""{relation.LeftQuantity.Name}""/> {relation.Operator} <see cref=""{relation.RightQuantity.Name}""/>.</summary>
public static {relation.ResultQuantity.Name} operator {relation.Operator}({relation.LeftQuantity.Name} {leftParameter}, {relation.RightQuantity.Name} {rightParameter})
{{
return {expression};
}}
");
}
}

Writer.WL($@"

#endregion
");
}

private void GenerateEqualityAndComparison()
{
Writer.WL($@"
Expand Down
3 changes: 2 additions & 1 deletion CodeGen/JsonTypes/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace CodeGen.JsonTypes
{
internal class Quantity
internal record Quantity
{
// 0649 Field is never assigned to
#pragma warning disable 0649
Expand All @@ -18,6 +18,7 @@ internal class Quantity
public int LogarithmicScalingFactor = 1;
public string Name = null!;
public Unit[] Units = Array.Empty<Unit>();
public QuantityRelation[] Relations = Array.Empty<QuantityRelation>();
public string? XmlDocRemarks;
public string XmlDocSummary = null!;
public string? ObsoleteText;
Expand Down
Loading