Skip to content

Commit 745b42a

Browse files
committed
Add expressions
1 parent 3f413c9 commit 745b42a

14 files changed

Lines changed: 2263 additions & 0 deletions
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// ExpressionPage.cs
2+
3+
using CommunityToolkit.Maui.Markup;
4+
using Microsoft.Extensions.Logging;
5+
using SQuan.Helpers.Maui;
6+
7+
namespace SQuan.Helpers.Sample;
8+
9+
[System.Diagnostics.CodeAnalysis.SuppressMessage(
10+
"Design",
11+
"CA1001:Types that own disposable fields should be disposable",
12+
Justification = "CTS is created in OnNavigatedTo and cancelled/disposed in OnNavigatedFrom as part of the page navigation lifecycle, so implementing IDisposable on this framework-managed ContentPage is unnecessary.")]
13+
public partial class ExpressionPage : ContentPage
14+
{
15+
public static ILogger? Logger { get; private set; } = IPlatformApplication.Current?.Services.GetService<ILogger<ExpressionPage>>();
16+
public ExpressionManager EM { get; }
17+
CancellationTokenSource? cts;
18+
19+
public ExpressionNode X1 { get; }
20+
public ExpressionNode X2 { get; }
21+
public ExpressionNode Sum { get; }
22+
public ExpressionNode Product { get; }
23+
public ExpressionNode Hypotenuse { get; }
24+
public ExpressionNode SerialCode { get; }
25+
public ExpressionNode BadExpression { get; }
26+
public ExpressionNode RandomNumber { get; }
27+
28+
public ExpressionPage()
29+
{
30+
EM = new();
31+
EM.SetInvokeOnUIThread(Dispatcher);
32+
X1 = EM.SetValue<double>("/survey/x1", 3);
33+
X2 = EM.SetValue<double>("/survey/x2", 4);
34+
Sum = EM.SetExpression<double>("/survey/sum", "/survey/x1 + /survey/x2");
35+
Product = EM.SetExpression<double>("/survey/product", "/survey/x1 * /survey/x2");
36+
Hypotenuse = EM.SetExpression<double>("/survey/hypotenuse", "sqrt(pow(/survey/x1, 2) + pow(/survey/x2, 2))");
37+
SerialCode = EM.SetExpression<string>("/survey/serialcode", "concat(/survey/x1, '-ABC-', /survey/x2)");
38+
BadExpression = EM.SetExpression<double>("/survey/parseerror", "invalid_expression(");
39+
RandomNumber = EM.SetExpression<double>("/survey/random", "random()");
40+
41+
Content = new ScrollView
42+
{
43+
Content = new VerticalStackLayout
44+
{
45+
Spacing = 10,
46+
Padding = new Thickness(10, 10, 10, 250),
47+
Children =
48+
{
49+
new Label { Text = "Enter X1" },
50+
new Entry { }.Bind(Entry.TextProperty, "EM[/survey/x1]", BindingMode.TwoWay, source: this),
51+
new Label { Text = "Enter X2" },
52+
new Entry { }.Bind(Entry.TextProperty, "Value", BindingMode.TwoWay, source: X2),
53+
new Label { }.Bind(Label.TextProperty,
54+
new Binding("[/survey/sum]", BindingMode.OneWay, source: EM),
55+
new Binding("ValueType", BindingMode.OneWay, source: Sum),
56+
new Binding("ValueKind", BindingMode.OneWay, source: Sum),
57+
"Sum: {0} (valueType: {1}, valueKind: {2})"),
58+
new Label { }.Bind(Label.TextProperty,
59+
BindingBase.Create(static (ExpressionNode n) => n.Value, BindingMode.OneWay, source: Product),
60+
BindingBase.Create(static (ExpressionNode n) => n.ValueType, BindingMode.OneWay, source: Product),
61+
BindingBase.Create(static (ExpressionNode n) => n.ValueKind, BindingMode.OneWay, source: Product),
62+
"Product: {0} (valueType: {1}, valueKind: {2})"),
63+
new Label { }.Bind(Label.TextProperty,
64+
BindingBase.Create(static (ExpressionNode n) => n.Value, BindingMode.OneWay, source: Hypotenuse),
65+
BindingBase.Create(static (ExpressionNode n) => n.ValueType, BindingMode.OneWay, source: Hypotenuse),
66+
BindingBase.Create(static (ExpressionNode n) => n.ValueKind, BindingMode.OneWay, source: Hypotenuse),
67+
"Hypotenuse: {0} (valueType: {1}, valueKind: {2})"),
68+
new Label { }.Bind(Label.TextProperty,
69+
BindingBase.Create(static (ExpressionNode n) => n.Value, BindingMode.OneWay, source: SerialCode),
70+
BindingBase.Create(static (ExpressionNode n) => n.ValueType, BindingMode.OneWay, source: SerialCode),
71+
BindingBase.Create(static (ExpressionNode n) => n.ValueKind, BindingMode.OneWay, source: SerialCode),
72+
"Serial Code: {0} (valueType: {1}, valueKind: {2})"),
73+
new Label { }.Bind(Label.TextProperty,
74+
BindingBase.Create(static (ExpressionNode n) => n.Value, BindingMode.OneWay, source: BadExpression),
75+
BindingBase.Create(static (ExpressionNode n) => n.ValueType, BindingMode.OneWay, source: BadExpression),
76+
BindingBase.Create(static (ExpressionNode n) => n.ValueKind, BindingMode.OneWay, source: BadExpression),
77+
"Bad Expression: {0} (valueType: {1}, valueKind: {2})"),
78+
new Label { }.Bind(Label.TextProperty,
79+
BindingBase.Create(static (ExpressionNode n) => n.Value, BindingMode.OneWay, source: RandomNumber),
80+
BindingBase.Create(static (ExpressionNode n) => n.ValueType, BindingMode.OneWay, source: RandomNumber),
81+
BindingBase.Create(static (ExpressionNode n) => n.ValueKind, BindingMode.OneWay, source: RandomNumber),
82+
"Random Number: {0} (valueType: {1}, valueKind: {2})"),
83+
new Button { Text = "Recalculate Random Number" }
84+
.Invoke(b => b.Clicked += (s, e) => EM.RecalculateByNodeRef("/survey/random"))
85+
}
86+
}
87+
};
88+
}
89+
90+
protected override void OnNavigatedTo(NavigatedToEventArgs args)
91+
{
92+
base.OnNavigatedTo(args);
93+
try
94+
{
95+
cts = new();
96+
EM.StartCalculationLoop(cts.Token);
97+
}
98+
catch (Exception ex)
99+
{
100+
Logger?.LogError(ex, "An error occurred while starting the calculation loop in OnNavigatedTo.");
101+
}
102+
}
103+
104+
protected override async void OnNavigatedFrom(NavigatedFromEventArgs args)
105+
{
106+
base.OnNavigatedFrom(args);
107+
try
108+
{
109+
await EM.StopCalculationLoopAsync();
110+
await EM.ClearAsync();
111+
cts?.Cancel();
112+
cts?.Dispose();
113+
cts = null;
114+
EM.Dispose();
115+
}
116+
catch (Exception ex)
117+
{
118+
Logger?.LogError(ex, "An error occurred during cleanup in OnNavigatedFrom.");
119+
}
120+
}
121+
}
122+
123+
public static class BindHelpers
124+
{
125+
public static T Bind<T>(
126+
this T target,
127+
BindableProperty targetProperty,
128+
BindingBase binding1,
129+
BindingBase binding2,
130+
BindingBase binding3,
131+
string stringFormat) where T : BindableObject
132+
{
133+
target.SetBinding(targetProperty, new MultiBinding
134+
{
135+
Bindings =
136+
{
137+
binding1,
138+
binding2,
139+
binding3
140+
},
141+
StringFormat = stringFormat
142+
});
143+
return target;
144+
}
145+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="SQuan.Helpers.Sample.ExpressionPage"
5+
Title="ExpressionPage">
6+
<VerticalStackLayout>
7+
<Label
8+
Text="Welcome to .NET MAUI!"
9+
VerticalOptions="Center"
10+
HorizontalOptions="Center" />
11+
</VerticalStackLayout>
12+
</ContentPage>

samples/SQuan.Helpers.Sample/MauiProgram.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public static MauiApp CreateMauiApp()
3838
SamplesHelper.RegisterSample("Count Demo", nameof(CountPage), typeof(CountPage));
3939
SamplesHelper.RegisterSample("Count Dynamic Demo", nameof(CountDynamicPage), typeof(CountDynamicPage));
4040
SamplesHelper.RegisterSample("Card Demo", nameof(CardPage), typeof(CardPage));
41+
SamplesHelper.RegisterSample("Expression Demo", nameof(ExpressionPage), typeof(ExpressionPage));
4142
SamplesHelper.RegisterSample("Localization Demo", nameof(LocalizePage), typeof(LocalizePage));
4243
SamplesHelper.RegisterSample("Numeric Mask Demo", nameof(NumericMaskPage), typeof(NumericMaskPage));
4344
SamplesHelper.RegisterSample("Theme Demo", nameof(ThemePage), typeof(ThemePage));
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// ExpressionAritySpec.cs
2+
3+
namespace SQuan.Helpers.Maui;
4+
5+
/// <summary>
6+
/// Describes the allowed arity (number of arguments) for a function or operator.
7+
/// </summary>
8+
public readonly record struct AritySpec
9+
{
10+
/// <summary>
11+
/// The minimum number of arguments required.
12+
/// </summary>
13+
public int Min { get; }
14+
15+
/// <summary>
16+
/// The maximum number of arguments allowed, or null if unbounded.
17+
/// </summary>
18+
public int? Max { get; }
19+
20+
/// <summary>
21+
/// Initializes a new <see cref="AritySpec"/> with the specified bounds.
22+
/// </summary>
23+
/// <param name="min">The minimum number of arguments.</param>
24+
/// <param name="max">The maximum number of arguments, or null to indicate no upper bound.</param>
25+
/// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="min"/> is negative, or if <paramref name="max"/> is less than <paramref name="min"/>.</exception>
26+
public AritySpec(int min, int? max)
27+
{
28+
if (min < 0)
29+
{
30+
throw new ArgumentOutOfRangeException(nameof(min));
31+
}
32+
if (max is not null && max < min)
33+
{
34+
throw new ArgumentOutOfRangeException(nameof(max));
35+
}
36+
37+
Min = min;
38+
Max = max;
39+
}
40+
41+
/// <summary>
42+
/// Creates an arity specification that requires exactly <paramref name="n"/> arguments.
43+
/// </summary>
44+
public static AritySpec Exactly(int n) => new(n, n);
45+
46+
/// <summary>
47+
/// Creates an arity specification that allows a range of arguments.
48+
/// </summary>
49+
/// <param name="min">The minimum number of arguments.</param>
50+
/// <param name="max">The maximum number of arguments.</param>
51+
public static AritySpec Range(int min, int max) => new(min, max);
52+
53+
/// <summary>
54+
/// Creates an arity specification that allows at least <paramref name="min"/> arguments.
55+
/// </summary>
56+
public static AritySpec AtLeast(int min) => new(min, null);
57+
58+
/// <summary>Requires zero arguments.</summary>
59+
public static AritySpec Zero => Exactly(0);
60+
61+
/// <summary>Allows zero or more arguments.</summary>
62+
public static AritySpec ZeroOrMore => AtLeast(0);
63+
64+
/// <summary>Requires exactly one argument.</summary>
65+
public static AritySpec One => Exactly(1);
66+
67+
/// <summary>Requires one or two arguments.</summary>
68+
public static AritySpec OneOrTwo => Range(1, 2);
69+
70+
/// <summary>Allows one or more arguments.</summary>
71+
public static AritySpec OneOrMore => AtLeast(1);
72+
73+
/// <summary>Requires exactly two arguments.</summary>
74+
public static AritySpec Two => Exactly(2);
75+
76+
/// <summary>Allows two or three arguments.</summary>
77+
public static AritySpec TwoOrThree => Range(2, 3);
78+
79+
/// <summary>Requires exactly three arguments.</summary>
80+
public static AritySpec Three => Exactly(3);
81+
82+
/// <summary>
83+
/// Determines whether this specification requires exactly <paramref name="n"/> arguments.
84+
/// </summary>
85+
public bool IsExactly(int n) => Min == n && Max == n;
86+
87+
/// <summary>
88+
/// Indicates whether the arity is exactly zero.
89+
/// </summary>
90+
public bool IsZero => IsExactly(0);
91+
92+
/// <summary>
93+
/// Indicates whether the arity is exactly one.
94+
/// </summary>
95+
public bool IsOne => IsExactly(1);
96+
97+
/// <summary>
98+
/// Indicates whether the arity is exactly two.
99+
/// </summary>
100+
public bool IsTwo => IsExactly(2);
101+
102+
/// <summary>
103+
/// Indicates whether the arity is fixed (minimum equals maximum).
104+
/// </summary>
105+
public bool IsFixed => Max is int max && max == Min;
106+
107+
/// <summary>
108+
/// Indicates whether the arity is a range (minimum does not equal maximum).
109+
/// </summary>
110+
public bool IsRange => Max is int max && max != Min;
111+
112+
/// <summary>
113+
/// Indicates whether the arity is unbounded (no maximum).
114+
/// </summary>
115+
public bool IsInfinite => Max is null;
116+
117+
/// <summary>
118+
/// Determines whether the specified argument count is accepted by this arity.
119+
/// </summary>
120+
/// <param name="arity">The argument count to test.</param>
121+
/// <returns>True if the argument count falls within the allowed range; otherwise false.</returns>
122+
public bool Accepts(int arity) =>
123+
arity >= Min && (Max is null || arity <= Max.Value);
124+
125+
/// <summary>
126+
/// Returns a string representation of the arity specification.
127+
/// </summary>
128+
/// <returns>A human-readable representation of the allowed argument range.</returns>
129+
public override string ToString() =>
130+
Max is null ? $"{Min}..Infinity" : (Min == Max ? Min.ToString() : $"{Min}..{Max}");
131+
}

0 commit comments

Comments
 (0)