Skip to content

Commit 37ca329

Browse files
committed
Replaced the underlying Value type from Double to Fraction:
- updated the CodeGen (desktop only) replacing all occurrences of the double with Fraction - restored the IEquality contract - implemented a custom expression analyzer in order to help pre-process (and simplify) the expressions defined in the json definitions - added new extension methods for generating the conversion expressions (w.r.t to the evaluated expression- may include custom functions) - updated a few conversion coefficients that were a little too precise - refactored the custom code functions (mostly) using the Fractions (should review the log expressions)
1 parent 5572dc2 commit 37ca329

File tree

176 files changed

+10499
-10359
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

176 files changed

+10499
-10359
lines changed

CodeGen/CodeGen.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
@@ -10,6 +10,7 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13+
<PackageReference Include="Fractions" Version="7.3.0" />
1314
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
1415
<PackageReference Include="NuGet.Protocol" Version="6.2.4" />
1516
<PackageReference Include="Serilog" Version="2.11.0" />

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 74 additions & 68 deletions
Large diffs are not rendered by default.

CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public string Generate()
2222
using System.Collections.Generic;
2323
using System.Diagnostics.CodeAnalysis;
2424
using System.Linq;
25+
using Fractions;
2526
2627
#nullable enable
2728
@@ -49,7 +50,7 @@ public partial class Quantity
4950
/// <param name=""quantityInfo"">The <see cref=""QuantityInfo""/> of the quantity to create.</param>
5051
/// <param name=""value"">The value to construct the quantity with.</param>
5152
/// <returns>The created quantity.</returns>
52-
public static IQuantity FromQuantityInfo(QuantityInfo quantityInfo, double value)
53+
public static IQuantity FromQuantityInfo(QuantityInfo quantityInfo, Fraction value)
5354
{
5455
return quantityInfo.Name switch
5556
{");
@@ -72,7 +73,7 @@ public static IQuantity FromQuantityInfo(QuantityInfo quantityInfo, double value
7273
/// <param name=""unit"">Unit enum value.</param>
7374
/// <param name=""quantity"">The resulting quantity if successful, otherwise <c>default</c>.</param>
7475
/// <returns><c>True</c> if successful with <paramref name=""quantity""/> assigned the value, otherwise <c>false</c>.</returns>
75-
public static bool TryFrom(double value, Enum? unit, [NotNullWhen(true)] out IQuantity? quantity)
76+
public static bool TryFrom(Fraction value, Enum? unit, [NotNullWhen(true)] out IQuantity? quantity)
7677
{
7778
quantity = unit switch
7879
{");
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Fractions;
2+
3+
namespace CodeGen.Helpers.ExpressionAnalyzer;
4+
5+
/// <summary>
6+
/// A term of the form "P^n" where P is a term that hasn't been parsed, raised to the given power.
7+
/// </summary>
8+
/// <param name="Expression">The actual expression to parse</param>
9+
/// <param name="Exponent">The exponent to use on the parsed expression (default is 1)</param>
10+
/// <remarks>
11+
/// Since we're tokenizing the expressions from top to bottom, the first step is parsing the exponent of the
12+
/// expression: e.g. Math.Pow(P, 2)
13+
/// </remarks>
14+
public record ExpressionEvaluationTerm(string Expression, Fraction Exponent);
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
using CodeGen.Helpers.ExpressionAnalyzer.Expressions;
8+
using CodeGen.Helpers.ExpressionAnalyzer.Functions;
9+
using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math;
10+
using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math.Trigonometry;
11+
using Fractions;
12+
13+
namespace CodeGen.Helpers.ExpressionAnalyzer;
14+
15+
internal class ExpressionEvaluator // TODO make public (and move out in a separate project)
16+
{
17+
private static readonly Fraction Pi = Fraction.FromDoubleRounded(Math.PI);
18+
private readonly IReadOnlyDictionary<string, Fraction> _constantValues;
19+
private readonly Dictionary<string, CompositeExpression> _expressionsEvaluated = [];
20+
21+
private readonly IReadOnlyDictionary<string, IFunctionEvaluator> _functionEvaluators;
22+
23+
public ExpressionEvaluator(string parameterName, params IFunctionEvaluator[] functionEvaluators)
24+
: this(parameterName, functionEvaluators.ToDictionary(x => x.FunctionName), new Dictionary<string, Fraction>())
25+
{
26+
}
27+
28+
public ExpressionEvaluator(string parameterName, IReadOnlyDictionary<string, Fraction> constantValues, params IFunctionEvaluator[] functionEvaluators)
29+
: this(parameterName, functionEvaluators.ToDictionary(x => x.FunctionName), constantValues)
30+
{
31+
}
32+
33+
public ExpressionEvaluator(string parameterName, IReadOnlyDictionary<string, IFunctionEvaluator> functionEvaluators,
34+
IReadOnlyDictionary<string, Fraction> constantValues)
35+
{
36+
ParameterName = parameterName;
37+
_constantValues = constantValues;
38+
_functionEvaluators = functionEvaluators;
39+
}
40+
41+
public string ParameterName { get; }
42+
43+
protected string Add(CompositeExpression expression)
44+
{
45+
var label = "{" + (char)('a' + _expressionsEvaluated.Count) + "}";
46+
_expressionsEvaluated[label] = expression;
47+
return label;
48+
}
49+
50+
public CompositeExpression Evaluate(ExpressionEvaluationTerm expressionEvaluationTerm) // TODO either replace by string or add a coefficient
51+
{
52+
if (TryParseExpressionTerm(expressionEvaluationTerm.Expression, expressionEvaluationTerm.Exponent, out ExpressionTerm? expressionTerm))
53+
{
54+
return expressionTerm;
55+
}
56+
57+
var expressionToParse = expressionEvaluationTerm.Expression;
58+
Fraction exponent = expressionEvaluationTerm.Exponent;
59+
string previousExpression;
60+
do
61+
{
62+
previousExpression = expressionToParse;
63+
// the regex captures the innermost occurrence of a function group: "Sin(x)", "Pow(x, y)", "(x + 1)" are all valid matches
64+
expressionToParse = Regex.Replace(expressionToParse, @"(\w*)\(([^()]*)\)", match =>
65+
{
66+
var functionName = match.Groups[1].Value;
67+
var functionBodyToParse = match.Groups[2].Value;
68+
if (string.IsNullOrEmpty(functionName)) // standard grouping (technically this is equivalent to f(x) -> x)
69+
{
70+
// all terms within the group are expanded: extract the simplified expression
71+
CompositeExpression expression = ReplaceTokenizedExpressions(new ExpressionEvaluationTerm(functionBodyToParse, exponent));
72+
return Add(expression);
73+
}
74+
75+
if (_functionEvaluators.TryGetValue(functionName, out IFunctionEvaluator? functionEvaluator))
76+
{
77+
// extract the body of the function
78+
ExpressionEvaluationTerm tokenizedExpression = functionEvaluator.GetFunctionBody(functionBodyToParse, exponent);
79+
// simplify the terms of the expression that are used for the function call
80+
CompositeExpression functionBody = ReplaceTokenizedExpressions(tokenizedExpression);
81+
// return the simplified argument expression to the function evaluator in order to construct/evaluate the composed expression
82+
CompositeExpression expression = functionEvaluator.CreateExpression(Fraction.One, exponent, functionBody);
83+
return Add(expression);
84+
}
85+
86+
throw new FormatException($"No function evaluator available for {functionName}({functionBodyToParse})");
87+
});
88+
} while (previousExpression != expressionToParse);
89+
90+
return ReplaceTokenizedExpressions(expressionEvaluationTerm with { Expression = expressionToParse });
91+
}
92+
93+
private CompositeExpression ReplaceTokenizedExpressions(ExpressionEvaluationTerm tokenizedExpression)
94+
{
95+
// all groups and function are expanded: we're left with a standard arithmetic expression such as "4 * a + 2 * b * x - c - d + 5"
96+
// with a, b, c, d representing the previously evaluated expressions
97+
var result = new CompositeExpression();
98+
var stringBuilder = new StringBuilder();
99+
ArithmeticOperationToken lastToken = ArithmeticOperationToken.Addition;
100+
CompositeExpression? runningExpression = null;
101+
foreach (var character in tokenizedExpression.Expression)
102+
{
103+
if (!TryReadToken(character, out ArithmeticOperationToken currentToken)) // TODO use None?
104+
{
105+
continue;
106+
}
107+
108+
switch (currentToken)
109+
{
110+
case ArithmeticOperationToken.Addition or ArithmeticOperationToken.Subtraction:
111+
{
112+
if (stringBuilder.Length == 0) // ignore the leading sign
113+
{
114+
lastToken = currentToken;
115+
continue;
116+
}
117+
118+
// we're at the end of a term expression
119+
CompositeExpression lastTerm = ParseTerm();
120+
if (runningExpression is null)
121+
{
122+
result.AddTerms(lastTerm);
123+
}
124+
else // the last term is part of a running multiplication
125+
{
126+
result.AddTerms(runningExpression * lastTerm);
127+
runningExpression = null;
128+
}
129+
130+
lastToken = currentToken;
131+
break;
132+
}
133+
case ArithmeticOperationToken.Multiplication or ArithmeticOperationToken.Division:
134+
{
135+
CompositeExpression previousTerm = ParseTerm();
136+
if (runningExpression is null)
137+
{
138+
runningExpression = previousTerm;
139+
}
140+
else // the previousTerm term is part of a running multiplication (which is going to be followed by at least one more multiplication/division)
141+
{
142+
runningExpression *= previousTerm;
143+
}
144+
145+
lastToken = currentToken;
146+
break;
147+
}
148+
}
149+
}
150+
151+
CompositeExpression finalTerm = ParseTerm();
152+
if (runningExpression is null)
153+
{
154+
result.AddTerms(finalTerm);
155+
}
156+
else
157+
{
158+
result.AddTerms(runningExpression * finalTerm);
159+
}
160+
161+
return result;
162+
163+
bool TryReadToken(char character, out ArithmeticOperationToken token)
164+
{
165+
switch (character)
166+
{
167+
case '+':
168+
token = ArithmeticOperationToken.Addition;
169+
return true;
170+
case '-':
171+
token = ArithmeticOperationToken.Subtraction;
172+
return true;
173+
case '*':
174+
token = ArithmeticOperationToken.Multiplication;
175+
return true;
176+
case '/':
177+
token = ArithmeticOperationToken.Division;
178+
return true;
179+
case not ' ':
180+
stringBuilder.Append(character);
181+
break;
182+
}
183+
184+
token = default;
185+
return false;
186+
}
187+
188+
CompositeExpression ParseTerm()
189+
{
190+
var previousExpression = stringBuilder.ToString();
191+
stringBuilder.Clear();
192+
if (_expressionsEvaluated.TryGetValue(previousExpression, out CompositeExpression? expression))
193+
{
194+
return lastToken switch
195+
{
196+
ArithmeticOperationToken.Subtraction => expression.Negate(),
197+
ArithmeticOperationToken.Division => expression.Invert(),
198+
_ => expression
199+
};
200+
}
201+
202+
if (TryParseExpressionTerm(previousExpression, tokenizedExpression.Exponent, out ExpressionTerm? expressionTerm))
203+
{
204+
return lastToken switch
205+
{
206+
ArithmeticOperationToken.Subtraction => expressionTerm.Negate(),
207+
ArithmeticOperationToken.Division => expressionTerm.Invert(),
208+
_ => expressionTerm
209+
};
210+
}
211+
212+
throw new FormatException($"Failed to parse the previous token: {previousExpression}");
213+
}
214+
}
215+
216+
217+
private bool TryParseExpressionTerm(string expressionToParse, Fraction exponent, [MaybeNullWhen(false)] out ExpressionTerm expressionTerm)
218+
{
219+
if (expressionToParse == ParameterName)
220+
{
221+
expressionTerm = new ExpressionTerm(Fraction.One, exponent);
222+
return true;
223+
}
224+
225+
if (_constantValues.TryGetValue(expressionToParse, out Fraction constantExpression) || Fraction.TryParse(expressionToParse, out constantExpression))
226+
{
227+
if (exponent.Numerator == exponent.Denominator)
228+
{
229+
expressionTerm = ExpressionTerm.Constant(constantExpression);
230+
return true;
231+
}
232+
233+
if (exponent.Denominator.IsOne)
234+
{
235+
expressionTerm = ExpressionTerm.Constant(Fraction.Pow(constantExpression, (int)exponent.Numerator));
236+
return true;
237+
}
238+
239+
// constant expression using a non-integer power: there is currently no Fraction.Pow(Fraction, Fraction)
240+
expressionTerm = ExpressionTerm.Constant(Fraction.FromDoubleRounded(Math.Pow(constantExpression.ToDouble(), exponent.ToDouble())));
241+
return true;
242+
}
243+
244+
expressionTerm = null;
245+
return false;
246+
}
247+
248+
public static string ReplaceDecimalNotations(string expression, Dictionary<string, Fraction> constantValues)
249+
{
250+
return Regex.Replace(expression, @"\d*(\.\d*)?[eE][-\+]?\d*[dD]?", match =>
251+
{
252+
var tokens = match.Value.ToLower().Replace("d", "").Split('e');
253+
if (tokens.Length != 2 || !Fraction.TryParse(tokens[0], out Fraction mantissa) || !int.TryParse(tokens[1], out var exponent))
254+
{
255+
throw new FormatException($"The expression contains invalid tokens: {expression}");
256+
}
257+
258+
var label = $"{{v{constantValues.Count}}}";
259+
constantValues[label] = mantissa * Fraction.Pow(10, exponent);
260+
return label;
261+
}).Replace("d", string.Empty); // TODO these are force-generated for the BitRate (we should stop doing it)
262+
}
263+
264+
public static string ReplaceMathPi(string expression, Dictionary<string, Fraction> constantValues)
265+
{
266+
return Regex.Replace(expression, @"Math\.PI", _ =>
267+
{
268+
constantValues[nameof(Pi)] = Pi;
269+
return nameof(Pi);
270+
});
271+
}
272+
273+
public static CompositeExpression Evaluate(string expression, string parameter)
274+
{
275+
var constantExpressions = new Dictionary<string, Fraction>();
276+
277+
expression = ReplaceDecimalNotations(expression, constantExpressions); // TODO expose an IPreprocessor (or something)
278+
expression = ReplaceMathPi(expression, constantExpressions);
279+
expression = expression.Replace("Math.", string.Empty);
280+
281+
var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions,
282+
new SqrtFunctionEvaluator(),
283+
new PowFunctionEvaluator(),
284+
new SinFunctionEvaluator(),
285+
new AsinFunctionEvaluator());
286+
287+
return expressionEvaluator.Evaluate(new ExpressionEvaluationTerm(expression, Fraction.One));
288+
}
289+
290+
private enum ArithmeticOperationToken
291+
{
292+
Addition,
293+
Subtraction,
294+
Multiplication,
295+
Division
296+
}
297+
298+
299+
}

0 commit comments

Comments
 (0)