├── .gitattributes ├── .gitignore ├── LICENSE ├── Nuget README.md ├── README.md ├── ReadmeAssets ├── Banner.svg ├── Boolean to Visibility.gif ├── Custom Function Animation.gif ├── Custom Function.png ├── Evenly-Spaced Rectangles.png ├── Flattened Oval.png ├── Interpolated String Animation.gif ├── No ConverterParameter.png ├── Now Plus Six Hours.png ├── True Rounded Rectangle.png ├── Wide Rounded Rectangle.png └── smalllogo.png └── src ├── CSharp ├── MathConverter │ ├── AssemblyInfo.cs │ ├── CompatibilityExtensions.cs │ ├── ConvertExtension.cs │ ├── CustomFunctionCollection.cs │ ├── CustomFunctions.cs │ ├── EvaluationException.cs │ ├── MathConverter.cs │ ├── Operator.cs │ ├── Parser.cs │ ├── ParsingException.cs │ ├── Scanner.cs │ ├── SyntaxTree.cs │ └── Token.cs └── UnitTests │ └── UnitTests.cs ├── Demos └── WPF │ ├── App.xaml │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── CustomFunctions │ ├── CustomAverageFunction.cs │ └── GetWindowTitle.cs │ ├── Demos │ ├── BooleanToVisibility.xaml │ ├── BooleanToVisibility.xaml.cs │ ├── CountClicks.xaml │ ├── CountClicks.xaml.cs │ ├── CustomAverageFunction.xaml │ ├── CustomAverageFunction.xaml.cs │ ├── DifferentMargins.xaml │ ├── DifferentMargins.xaml.cs │ ├── FlattenedOval.xaml │ ├── FlattenedOval.xaml.cs │ ├── NoConverterParameter.xaml │ ├── NoConverterParameter.xaml.cs │ ├── NowPlusSixHours.xaml │ ├── NowPlusSixHours.xaml.cs │ ├── TrueRoundedRectangle.xaml │ ├── TrueRoundedRectangle.xaml.cs │ ├── WideRoundedRectangle.xaml │ └── WideRoundedRectangle.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ └── MathConverter.Demo.WPF.csproj ├── MathConverter.MacOS.sln ├── MathConverter.sln └── Projects ├── Common ├── App.props ├── Common.props ├── StrongNamedAssemblyKey.snk ├── UnitTests.props └── WPF.props ├── Maui ├── App │ └── MathConverter.Maui.csproj └── UnitTests │ ├── App.xaml │ ├── App.xaml.cs │ ├── MathConverter.UnitTests.Maui.csproj │ ├── MauiProgram.cs │ ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ └── MainApplication.cs │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Package.appxmanifest │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs │ ├── Properties │ └── launchSettings.json │ └── Resources │ ├── AppIcon │ ├── androidicon.svg │ ├── appicon.svg │ └── appiconfg.svg │ └── Splash │ └── splash.svg ├── WPF ├── App │ └── MathConverter.WPF.csproj └── UnitTests │ └── MathConverter.UnitTests.WPF.csproj └── XamarinForms ├── App └── MathConverter.XamarinForms.csproj └── UnitTests ├── Android ├── MainActivity.cs ├── MathConverter.UnitTests.XamarinForms.Android.csproj ├── Properties │ └── AndroidManifest.xml └── Resources │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ └── styles.xml ├── DotNet └── MathConverter.UnitTests.XamarinForms.DotNet.csproj ├── UWP ├── Assets │ ├── LockScreenLogo.scale-200.png │ ├── SplashScreen.scale-200.png │ ├── Square150x150Logo.scale-200.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── StoreLogo.png │ └── Wide310x150Logo.scale-200.png ├── MainPage.xaml ├── MainPage.xaml.cs ├── MathConverter.UnitTests.XamarinForms.UWP.csproj ├── Package.appxmanifest ├── Properties │ └── Default.rd.xml ├── UnitTestApp.xaml └── UnitTestApp.xaml.cs └── iOS ├── AppDelegate.cs ├── Assets.xcassets └── AppIcon.appiconset │ ├── Contents.json │ ├── Icon1024.png │ ├── Icon120.png │ ├── Icon152.png │ ├── Icon167.png │ ├── Icon180.png │ ├── Icon20.png │ ├── Icon29.png │ ├── Icon40.png │ ├── Icon58.png │ ├── Icon60.png │ ├── Icon76.png │ ├── Icon80.png │ └── Icon87.png ├── Entitlements.plist ├── Info.plist ├── Main.cs ├── MathConverter.UnitTests.XamarinForms.iOS.csproj └── Resources └── LaunchScreen.storyboard /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | # See https://github.com/alexkaratarakis/gitattributes 3 | * text=auto 4 | 5 | *.cs text diff=csharp 6 | *.cshtml text diff=html 7 | *.csx text diff=csharp 8 | *.xml text 9 | *.sln text eol=crlf 10 | *.csproj text eol=crlf 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | bin/ 3 | MathConverter.v12.suo 4 | *.csproj.user 5 | .vs/ 6 | /src/Projects/XamarinForms/UnitTests/Android/Resources/Resource.designer.cs 7 | *.DotSettings.user 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2020 Hex Innovation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Nuget README.md: -------------------------------------------------------------------------------- 1 | [![Math Converter: A XAML Converter that does it all.](https://raw.githubusercontent.com/hexinnovation/MathConverter/main/ReadmeAssets/Banner.svg)](https://github.com/hexinnovation/MathConverter) 2 | 3 | What is MathConverter? 4 | ---------------------- 5 | 6 | `MathConverter` allows you to do Math in XAML. 7 | 8 | `MathConverter` is a powerful `Binding` converter that allows you to specify how to perform conversions directly in XAML, without needing to define a new `IValueConverter` in C# for every single conversion. 9 | 10 | Getting Started: 11 | ---------------- 12 | 13 | It's as easy as 1-2-3. 14 | 15 | **1)** Install the Nuget package. 16 | 17 | **2)** Add a `MathConverter` resource. 18 | 19 | ```xaml 20 | 21 | 22 | 23 | ``` 24 | 25 | The `math` namespace is defined as follows: 26 | 27 | ```xaml 28 | xmlns:math="http://hexinnovation.com/math" 29 | ``` 30 | 31 | **3)** Do Math. Now, you can use `MathConverter` on any `Binding`. Specify a `ConverterParameter` to specify the rules of the conversion. 32 | 33 | ```xaml 34 | 35 | ``` 36 | Or, for conversions with multiple bindings. 37 | 38 | ```xaml 39 | 40 | ``` 41 | 42 | See [the GitHub repository](https://github.com/hexinnovation/MathConverter) for documentation and examples. 43 | -------------------------------------------------------------------------------- /ReadmeAssets/Boolean to Visibility.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Boolean to Visibility.gif -------------------------------------------------------------------------------- /ReadmeAssets/Custom Function Animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Custom Function Animation.gif -------------------------------------------------------------------------------- /ReadmeAssets/Custom Function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Custom Function.png -------------------------------------------------------------------------------- /ReadmeAssets/Evenly-Spaced Rectangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Evenly-Spaced Rectangles.png -------------------------------------------------------------------------------- /ReadmeAssets/Flattened Oval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Flattened Oval.png -------------------------------------------------------------------------------- /ReadmeAssets/Interpolated String Animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Interpolated String Animation.gif -------------------------------------------------------------------------------- /ReadmeAssets/No ConverterParameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/No ConverterParameter.png -------------------------------------------------------------------------------- /ReadmeAssets/Now Plus Six Hours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Now Plus Six Hours.png -------------------------------------------------------------------------------- /ReadmeAssets/True Rounded Rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/True Rounded Rectangle.png -------------------------------------------------------------------------------- /ReadmeAssets/Wide Rounded Rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/Wide Rounded Rectangle.png -------------------------------------------------------------------------------- /ReadmeAssets/smalllogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexinnovation/MathConverter/5cb55e38d8ae0719219faef61ab1367d92aae2b6/ReadmeAssets/smalllogo.png -------------------------------------------------------------------------------- /src/CSharp/MathConverter/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | #if XAMARIN 5 | using Xamarin.Forms; 6 | #elif MAUI 7 | using Microsoft.Maui.Controls; 8 | #else 9 | using System.Windows; 10 | using System.Windows.Markup; 11 | #endif 12 | 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | [assembly: ComVisible(false)] 16 | 17 | [assembly: XmlnsPrefix("http://hexinnovation.com/math", "math")] 18 | #if !WINDOWS_UWP && !NETSTANDARD1_0 && !NETSTANDARD1_3 19 | [assembly: XmlnsDefinition("http://hexinnovation.com/math", "HexInnovation")] 20 | #endif 21 | 22 | [assembly: InternalsVisibleTo("MathConverter.UnitTests,PublicKey=" + 23 | "0024000004800000940000000602000000240000525341310004000001000100056bb3f4bc6f27" + 24 | "a583fb5713ddbe24f2dabdf9688b60147eca177159a995ef153b183156c4566b457819661af3a1" + 25 | "b6810a9cae7928ccb10b834de2eaa99c133f2c0540f77cd43040853d166227d6bb252618b95ad3" + 26 | "0e3e5a1487c19bb9854e94edadb6c5fb2d2eaf771edb3d290a655bfc8c9eb852855f8d339ab102" + 27 | "78bf44be")] 28 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/CompatibilityExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace HexInnovation 7 | { 8 | internal static class CompatibilityExtensions 9 | { 10 | #if NET35 11 | public static string[] MyToArray(this IEnumerable objects) 12 | { 13 | return objects.Cast().MyToArray(); 14 | } 15 | public static string[] MyToArray(this IEnumerable objects) 16 | { 17 | return objects.Select(p => $"{p}").ToArray(); 18 | } 19 | public static string[] MyToArray(this IEnumerable strings) 20 | { 21 | return strings.ToArray(); 22 | } 23 | #else 24 | public static IEnumerable MyToArray(this IEnumerable objects) 25 | { 26 | return objects; 27 | } 28 | public static IEnumerable MyToArray(this IEnumerable strings) 29 | { 30 | return strings; 31 | } 32 | #endif 33 | 34 | public static IEnumerable GetCustomAttributes(this Type self) where TAttribute : Attribute 35 | { 36 | return 37 | #if WINDOWS_UWP || NETSTANDARD1_0 || NETSTANDARD1_3 38 | self.GetTypeInfo().GetCustomAttributes() 39 | #else 40 | Attribute.GetCustomAttributes(self) 41 | #endif 42 | .OfType(); 43 | } 44 | 45 | public static bool IsIConvertible(object self) 46 | { 47 | #if NETSTANDARD1_0 48 | // I can't figure out how to see if an object is an IConvertible before .NET Standard 1.3, so we'll just let an 49 | // InvalidCastException occur if it's not an IConvertible. We'll swallow that exception and return an unconverted 50 | // value. This totally sucks, so it'd be nice if we could not do that, but I don't really know a workaround. 51 | return true; 52 | #else 53 | return self is IConvertible; 54 | #endif 55 | } 56 | 57 | public static IEnumerable GetPublicStaticMethods(this Type self) 58 | { 59 | #if NETSTANDARD1_0 || NETSTANDARD1_3 60 | return self.GetRuntimeMethods().Where(method => method.IsPublic && method.IsStatic); 61 | #else 62 | return self.GetMethods(BindingFlags.Public | BindingFlags.Static); 63 | #endif 64 | } 65 | 66 | #if NETSTANDARD1_0 67 | public static char Last(this string str) 68 | { 69 | return str.ToCharArray().Last(); 70 | } 71 | #endif 72 | 73 | #if NETSTANDARD1_0 || NETSTANDARD1_3 74 | public static void ForEach(this IEnumerable enumeration, Action action) 75 | { 76 | Xamarin.Forms.Internals.EnumerableExtensions.ForEach(enumeration, action); 77 | } 78 | public static IEnumerable GetInterfaces(this Type self) 79 | { 80 | return self.GetTypeInfo().ImplementedInterfaces; 81 | } 82 | public static Type[] GetGenericArguments(this Type self) 83 | { 84 | return self.GenericTypeArguments; 85 | } 86 | #endif 87 | 88 | #if WINDOWS_UWP || NETSTANDARD1_0 || NETSTANDARD1_3 89 | public static bool IsInstanceOfType(this Type self, object o) 90 | { 91 | return Xamarin.Forms.Internals.ReflectionExtensions.IsInstanceOfType(self, o); 92 | } 93 | public static bool IsAssignableFrom(this Type self, Type o) 94 | { 95 | return Xamarin.Forms.Internals.ReflectionExtensions.IsAssignableFrom(self, o); 96 | } 97 | #else 98 | public static Type GetTypeInfo(this Type self) 99 | { 100 | return self; 101 | } 102 | #endif 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/ConvertExtension.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | #if WPF 4 | using BindableProperty = System.Windows.DependencyProperty; 5 | using System.Windows; 6 | using System.Windows.Data; 7 | using System.Windows.Markup; 8 | #else 9 | using System; 10 | using System.Collections.Generic; 11 | #endif 12 | 13 | #if MAUI 14 | using Microsoft.Maui.Controls; 15 | using Microsoft.Maui.Controls.Xaml; 16 | #elif XAMARIN 17 | using Xamarin.Forms; 18 | using Xamarin.Forms.Xaml; 19 | #endif 20 | 21 | namespace HexInnovation; 22 | 23 | 24 | /// 25 | /// A wrapper around MultiBinding that simplifies the syntax of creating a multi-input MathConverter binding. 26 | /// 27 | #if WPF 28 | [MarkupExtensionReturnType(typeof(object))] 29 | [Localizability(LocalizationCategory.None, Modifiability = Modifiability.Unmodifiable, Readability = Readability.Unreadable)] 30 | #else 31 | [ContentProperty(nameof(Expression))] 32 | [AcceptEmptyServiceProvider] 33 | #endif 34 | public sealed class ConvertExtension 35 | #if WPF 36 | : MultiBinding 37 | #else 38 | : IMarkupExtension 39 | #endif 40 | { 41 | /// 42 | /// This is the default used by the . 43 | /// 44 | public static MathConverter DefaultConverter { get; } 45 | 46 | static ConvertExtension() 47 | { 48 | #if WPF 49 | if (Application.Current?.TryFindResource("Math") is MathConverter mathConverter) 50 | #else 51 | if (Application.Current?.Resources.TryGetValue("Math", out var obj) == true && obj is MathConverter mathConverter) 52 | #endif 53 | DefaultConverter = mathConverter; 54 | else 55 | DefaultConverter = new(); 56 | } 57 | 58 | /// 59 | /// The of which we are a wrapper. 60 | /// 61 | internal readonly MultiBinding _binding; 62 | 63 | /// 64 | /// A binding to that is used if we skip a value when adding bindings. 65 | /// 66 | private static readonly Binding unsetValueBinding = new Binding { Source = BindableProperty.UnsetValue }; 67 | 68 | /// 69 | /// Creates a new ConvertExtension 70 | /// 71 | public ConvertExtension() 72 | { 73 | #if WPF 74 | Converter = DefaultConverter; 75 | Mode = BindingMode.OneWay; 76 | _binding = this; 77 | #else 78 | _binding = new MultiBinding { Converter = DefaultConverter, Mode = BindingMode.OneWay }; 79 | #endif 80 | } 81 | 82 | 83 | #if WPF 84 | /// 85 | /// Creates a new ConvertExtension 86 | /// 87 | /// The ConverterParameter 88 | public ConvertExtension(string expression) 89 | : this() 90 | { 91 | Expression = expression; 92 | } 93 | #else 94 | /// 95 | /// Returns a MultiBinding with a x to convert the value as specified. 96 | /// 97 | /// The service that provides the value. 98 | /// A MultiBinding that uses a MathConverter 99 | public BindingBase ProvideValue(IServiceProvider serviceProvider) => _binding; 100 | /// 101 | /// Returns a MultiBinding with a x to convert the value as specified. 102 | /// 103 | /// The service that provides the value. 104 | /// A MultiBinding that uses a MathConverter 105 | object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider); 106 | #endif 107 | 108 | 109 | /// 110 | /// The parameter to pass to converter. 111 | /// 112 | [DefaultValue(null)] 113 | public string Expression 114 | { 115 | #if WPF 116 | get => ConverterParameter as string; 117 | set => ConverterParameter = value; 118 | #else 119 | get => _binding.ConverterParameter as string; 120 | set => _binding.ConverterParameter = value; 121 | #endif 122 | } 123 | 124 | #if !WPF 125 | /// 126 | /// Value to use when source cannot provide a value 127 | /// 128 | /// 129 | /// Initialized to DependencyProperty.UnsetValue; if FallbackValue is not set, BindingExpression 130 | /// will return target property's default when Binding cannot get a real value. 131 | /// 132 | public object FallbackValue 133 | { 134 | get => _binding.FallbackValue; 135 | set => _binding.FallbackValue = value; 136 | } 137 | 138 | 139 | /// 140 | /// Value used to represent "null" in the target property. 141 | /// 142 | public object TargetNullValue 143 | { 144 | get => _binding.TargetNullValue; 145 | set => _binding.TargetNullValue = value; 146 | } 147 | #endif 148 | 149 | private void SetBinding(int index, BindingBase binding) 150 | { 151 | while (Bindings.Count < index) 152 | Bindings.Add(unsetValueBinding); 153 | 154 | if (Bindings.Count == index) 155 | Bindings.Add(binding); 156 | else 157 | Bindings[index] = binding; 158 | } 159 | 160 | #if !WPF 161 | private IList Bindings => _binding.Bindings; 162 | #endif 163 | 164 | /// 165 | /// The first variable (accessed by [0] or x) 166 | /// 167 | public BindingBase x 168 | { 169 | get => Bindings[0]; 170 | set => SetBinding(0, value); 171 | } 172 | /// 173 | /// The second variable (accessed by [1] or y) 174 | /// 175 | public BindingBase y 176 | { 177 | get => Bindings[1]; 178 | set => SetBinding(1, value); 179 | } 180 | /// 181 | /// The third variable (accessed by [2] or z) 182 | /// 183 | public BindingBase z 184 | { 185 | get => Bindings[2]; 186 | set => SetBinding(2, value); 187 | } 188 | /// 189 | /// The fourth variable (accessed by [3]) 190 | /// 191 | public BindingBase Var3 192 | { 193 | get => Bindings[3]; 194 | set => SetBinding(3, value); 195 | } 196 | /// 197 | /// The fifth variable (accessed by [4]) 198 | /// 199 | public BindingBase Var4 200 | { 201 | get => Bindings[4]; 202 | set => SetBinding(4, value); 203 | } 204 | /// 205 | /// The sixth variable (accessed by [5]) 206 | /// 207 | public BindingBase Var5 208 | { 209 | get => Bindings[5]; 210 | set => SetBinding(5, value); 211 | } 212 | /// 213 | /// The seventh variable (accessed by [6]) 214 | /// 215 | public BindingBase Var6 216 | { 217 | get => Bindings[6]; 218 | set => SetBinding(6, value); 219 | } 220 | /// 221 | /// The eighth variable (accessed by [7]) 222 | /// 223 | public BindingBase Var7 224 | { 225 | get => Bindings[7]; 226 | set => SetBinding(7, value); 227 | } 228 | /// 229 | /// The ninth variable (accessed by [8]) 230 | /// 231 | public BindingBase Var8 232 | { 233 | get => Bindings[8]; 234 | set => SetBinding(8, value); 235 | } 236 | /// 237 | /// The tenth variable (accessed by [9]) 238 | /// 239 | public BindingBase Var9 240 | { 241 | get => Bindings[9]; 242 | set => SetBinding(9, value); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/CustomFunctionCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | 7 | namespace HexInnovation 8 | { 9 | public class CustomFunctionCollection : ICollection, IList 10 | { 11 | private readonly Dictionary _functions = new Dictionary(); 12 | public int Count => _functions.Count; 13 | public bool IsReadOnly => false; 14 | 15 | public void Add(CustomFunctionDefinition item) 16 | { 17 | if (item == null) 18 | { 19 | throw new NullReferenceException($"The {nameof(CustomFunctionDefinition)} cannot be null."); 20 | } 21 | else if (item.Function == null || !typeof(CustomFunction).IsAssignableFrom(item.Function)) 22 | { 23 | throw new NullReferenceException($"The {nameof(CustomFunctionDefinition.Function)} property must be an instance of {nameof(CustomFunction)}."); 24 | } 25 | else 26 | { 27 | if (item.Name == null) 28 | { 29 | throw new NullReferenceException($"The {nameof(CustomFunctionDefinition.Name)} property must not be null."); 30 | } 31 | else if (_functions.ContainsKey(item.Name)) 32 | { 33 | throw new ArgumentException($"A function with the name \"{item.Name}\" has already been added."); 34 | } 35 | else 36 | { 37 | switch (item.Name) 38 | { 39 | case "e": 40 | case "pi": 41 | case "null": 42 | case "true": 43 | case "x": 44 | case "y": 45 | case "z": 46 | throw new ArgumentException($"\"{item.Name}\" is a reserved keyword. You cannot add a function with that name."); 47 | } 48 | _functions[item.Name] = item.Function; 49 | } 50 | } 51 | } 52 | public void Clear() 53 | { 54 | _functions.Clear(); 55 | 56 | #if WINDOWS_UWP 57 | if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) 58 | #elif !NETSTANDARD1_0 && !NETSTANDARD1_3 59 | if (LicenseManager.UsageMode == LicenseUsageMode.Designtime) 60 | #endif 61 | { 62 | // If we create a MathConverter in XAML where we add a CustomFunctionDefinition to the MathConverter, then the designer calls Clear(), and then 63 | // we get design-time exceptions any time we reference any default function. So if the designer calls Clear(), we will register the default functions again. 64 | #if !NETSTANDARD1_0 && !NETSTANDARD1_3 65 | 66 | RegisterDefaultFunctions(); 67 | #endif 68 | } 69 | } 70 | public void RegisterDefaultFunctions() 71 | { 72 | Add(CustomFunctionDefinition.Create("Now")); 73 | Add(CustomFunctionDefinition.Create("Cos")); 74 | Add(CustomFunctionDefinition.Create("Sin")); 75 | Add(CustomFunctionDefinition.Create("Tan")); 76 | Add(CustomFunctionDefinition.Create("Abs")); 77 | Add(CustomFunctionDefinition.Create("Acos")); 78 | Add(CustomFunctionDefinition.Create("ArcCos")); 79 | Add(CustomFunctionDefinition.Create("Asin")); 80 | Add(CustomFunctionDefinition.Create("ArcSin")); 81 | Add(CustomFunctionDefinition.Create("Atan")); 82 | Add(CustomFunctionDefinition.Create("ArcTan")); 83 | Add(CustomFunctionDefinition.Create("Ceil")); 84 | Add(CustomFunctionDefinition.Create("Ceiling")); 85 | Add(CustomFunctionDefinition.Create("Floor")); 86 | Add(CustomFunctionDefinition.Create("Sqrt")); 87 | Add(CustomFunctionDefinition.Create("Deg")); 88 | Add(CustomFunctionDefinition.Create("Degrees")); 89 | Add(CustomFunctionDefinition.Create("Rad")); 90 | Add(CustomFunctionDefinition.Create("Radians")); 91 | Add(CustomFunctionDefinition.Create("ToLower")); 92 | Add(CustomFunctionDefinition.Create("LCase")); 93 | Add(CustomFunctionDefinition.Create("ToUpper")); 94 | Add(CustomFunctionDefinition.Create("UCase")); 95 | #if WPF 96 | Add(CustomFunctionDefinition.Create("VisibleOrCollapsed")); 97 | Add(CustomFunctionDefinition.Create("VisibleOrHidden")); 98 | #endif 99 | Add(CustomFunctionDefinition.Create("TryParseDouble")); 100 | Add(CustomFunctionDefinition.Create("GetType")); 101 | Add(CustomFunctionDefinition.Create("StartsWith")); 102 | Add(CustomFunctionDefinition.Create("ConvertType")); 103 | Add(CustomFunctionDefinition.Create("EnumEquals")); 104 | Add(CustomFunctionDefinition.Create("Contains")); 105 | Add(CustomFunctionDefinition.Create("EndsWith")); 106 | Add(CustomFunctionDefinition.Create("Log")); 107 | Add(CustomFunctionDefinition.Create("Atan2")); 108 | Add(CustomFunctionDefinition.Create("ArcTan2")); 109 | Add(CustomFunctionDefinition.Create("IsNull")); 110 | Add(CustomFunctionDefinition.Create("IfNull")); 111 | Add(CustomFunctionDefinition.Create("Round")); 112 | Add(CustomFunctionDefinition.Create("And")); 113 | Add(CustomFunctionDefinition.Create("Nor")); 114 | Add(CustomFunctionDefinition.Create("Or")); 115 | Add(CustomFunctionDefinition.Create("Max")); 116 | Add(CustomFunctionDefinition.Create("Min")); 117 | Add(CustomFunctionDefinition.Create("Avg")); 118 | Add(CustomFunctionDefinition.Create("Average")); 119 | Add(CustomFunctionDefinition.Create("Format")); 120 | Add(CustomFunctionDefinition.Create("Concat")); 121 | Add(CustomFunctionDefinition.Create("Join")); 122 | Add(CustomFunctionDefinition.Create("Throw")); 123 | Add(CustomFunctionDefinition.Create("UnsetValue")); 124 | Add(CustomFunctionDefinition.Create("TryCatch")); 125 | } 126 | public bool Contains(CustomFunctionDefinition item) 127 | { 128 | if (item == null) 129 | throw new NullReferenceException($"The {nameof(CustomFunctionDefinition)} must not be null."); 130 | return _functions.TryGetValue(item.Name, out var @type) && type == item.Function; 131 | } 132 | private IEnumerable ToIEnumerable() 133 | { 134 | return _functions.Select(x => new CustomFunctionDefinition { Name = x.Key, Function = x.Value }); 135 | } 136 | public void CopyTo(CustomFunctionDefinition[] array, int arrayIndex) 137 | { 138 | CopyTo((Array)array, arrayIndex); 139 | } 140 | public IEnumerator GetEnumerator() 141 | { 142 | return ToIEnumerable().GetEnumerator(); 143 | } 144 | public bool Remove(CustomFunctionDefinition item) 145 | { 146 | if (Contains(item)) 147 | { 148 | // ReSharper disable once PossibleNullReferenceException => if item is null, Contains(item) will throw. 149 | return _functions.Remove(item.Name); 150 | } 151 | else 152 | { 153 | return false; 154 | } 155 | } 156 | public bool Remove(string functionName) 157 | { 158 | return _functions.Remove(functionName); 159 | } 160 | IEnumerator IEnumerable.GetEnumerator() 161 | { 162 | return ToIEnumerable().GetEnumerator(); 163 | } 164 | 165 | public bool TryGetFunction(string functionName, out CustomFunction function) 166 | { 167 | if (_functions.TryGetValue(functionName, out var type)) 168 | { 169 | function = Activator.CreateInstance(type) as CustomFunction; 170 | 171 | if (function != null) 172 | { 173 | function.FunctionName = functionName; 174 | } 175 | } 176 | else 177 | { 178 | function = null; 179 | } 180 | 181 | return function != null; 182 | } 183 | 184 | public object SyncRoot => this; 185 | public bool IsSynchronized => false; 186 | public bool IsFixedSize => false; 187 | 188 | public object this[int index] 189 | { 190 | get => ToIEnumerable().Skip(index).First(); 191 | set => throw new NotSupportedException(); 192 | } 193 | 194 | public void CopyTo(Array array, int index) 195 | { 196 | Array.Copy(ToIEnumerable().ToArray(), 0, array, index, Count); 197 | } 198 | 199 | public int Add(object value) 200 | { 201 | if (value is CustomFunctionDefinition x) 202 | { 203 | Add(x); 204 | return Count - 1; 205 | } 206 | else 207 | throw new ArgumentException("You can only add {CustomFunctionDefinition} objects.", nameof(value)); 208 | } 209 | 210 | public bool Contains(object value) 211 | { 212 | if (value is CustomFunctionDefinition x) 213 | return Contains(x); 214 | else 215 | return false; 216 | } 217 | 218 | public int IndexOf(object value) 219 | { 220 | if (value is CustomFunctionDefinition x) 221 | return IndexOf(x); 222 | else 223 | return -1; 224 | } 225 | 226 | public void Insert(int index, object value) 227 | { 228 | Add(value); 229 | } 230 | 231 | public void Remove(object value) 232 | { 233 | if (value is CustomFunctionDefinition x) 234 | Remove(x); 235 | } 236 | 237 | public void RemoveAt(int index) 238 | { 239 | throw new NotSupportedException(); 240 | } 241 | } 242 | public class CustomFunctionDefinition 243 | { 244 | /// 245 | /// The name of the function. For example, if we choose "MyCustomFunction", 246 | /// you can invoke the function like "MyCustomFunction(x)" 247 | /// 248 | public string Name { get; set; } 249 | /// 250 | /// The type of the function. This type must extend 251 | /// 252 | public Type Function { get; set; } 253 | 254 | public static CustomFunctionDefinition Create(string name) 255 | where T : CustomFunction 256 | { 257 | return new CustomFunctionDefinition { Name = name, Function = typeof(T) }; 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/EvaluationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace HexInnovation 5 | { 6 | public class EvaluationException : Exception 7 | { 8 | private static string ComputeMessage(Exception innerException, string converterParameter, object[] bindingValues) 9 | { 10 | return $"MathConverter threw an exception while performing a conversion.{Environment.NewLine}{Environment.NewLine}{nameof(ConverterParameter)}:{Environment.NewLine}{converterParameter}{Environment.NewLine}{Environment.NewLine}{nameof(BindingValues)}:{string.Concat(bindingValues.Select((p, i) => $"{Environment.NewLine}[{i}]: {(p == null ? "null" : $"({p.GetType().FullName}): {p}")}").MyToArray())}"; 11 | } 12 | public EvaluationException(string converterParameter, object[] bindingValues, NodeEvaluationException inner) : base(ComputeMessage(inner, converterParameter, bindingValues), inner) 13 | { 14 | ConverterParameter = converterParameter; 15 | BindingValues = bindingValues; 16 | } 17 | public string ConverterParameter { get; } 18 | public object[] BindingValues { get; } 19 | } 20 | public class NodeEvaluationException : Exception 21 | { 22 | private static string ComputeMessage(Exception innerException, AbstractSyntaxTree node) 23 | { 24 | return $"A {innerException.GetType().FullName} was thrown while evaluating the {node.GetType().Name}:{Environment.NewLine}{node}"; 25 | } 26 | 27 | internal NodeEvaluationException(AbstractSyntaxTree node, Exception inner) : base(ComputeMessage(inner, node), inner) 28 | { 29 | Node = node; 30 | } 31 | 32 | /// 33 | /// The abstract syntax tree that threw an exception while being evaluated. 34 | /// 35 | internal AbstractSyntaxTree Node { get; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/MathConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | #if XAMARIN 10 | using Xamarin.Forms; 11 | using TypeConverterAttribute = Xamarin.Forms.TypeConverterAttribute; 12 | using PlatformTypeConverter = Xamarin.Forms.TypeConverter; 13 | #elif MAUI 14 | using Microsoft.Maui; 15 | using Microsoft.Maui.Controls; 16 | using PlatformTypeConverter = System.ComponentModel.TypeConverter; 17 | #elif WPF 18 | using BindableProperty = System.Windows.DependencyProperty; 19 | using System.Windows; 20 | using System.Windows.Data; 21 | using System.Windows.Markup; 22 | #endif 23 | 24 | namespace HexInnovation 25 | { 26 | /// 27 | /// MathConverter is a WPF Converter class that does it all. 28 | /// 29 | [ContentProperty(nameof(CustomFunctions))] 30 | public class MathConverter : IValueConverter, IMultiValueConverter 31 | { 32 | /// 33 | /// Computes the ordinal number for an integer. 34 | /// For example, turns 12 to "12th" or 1 to "1st" 35 | /// 36 | /// The number for which we want to compute the ordinal value. 37 | /// A string that indicates to a human what position a number is in (1st, 2nd, 3rd, etc.) 38 | internal static string ComputeOrdinal(int number) 39 | { 40 | if (number % 100 < 11 || number % 100 > 13) 41 | { 42 | switch (number % 10) 43 | { 44 | case 1: 45 | return $"{number}st"; 46 | case 2: 47 | return $"{number}nd"; 48 | case 3: 49 | return $"{number}rd"; 50 | } 51 | } 52 | return $"{number}th"; 53 | } 54 | /// 55 | /// Sanitizes an argument as specified by a Binding. 56 | /// Converts DependencyProperty.UnsetValue with a warning to identify to a developer which Binding might not be correctly configured. 57 | /// 58 | /// The argument to sanitize 59 | /// Which argument (starting with zero) is this binding in the ? 60 | /// How many arguments are there total? If there is only one, we're assuming this converter is being used on a , not a 61 | /// The ConverterParameter being used for this conversion. This helps identify the (possibly faulty) binding. 62 | /// The type we're trying to convert to. This helps identify the (possibly faulty) binding. 63 | /// The passed in, or null if the is equal to 64 | private object SanitizeBinding(object arg, int argIndex, int totalBinding, object parameter, Type targetType) 65 | { 66 | if (arg == BindableProperty.UnsetValue && !AllowUnsetValue) 67 | { 68 | Debug.WriteLine($"Encountered {nameof(BindableProperty.UnsetValue)} in the {(totalBinding > 1 ? $"{ComputeOrdinal(argIndex + 1)} " : "")}argument while trying to convert to type \"{targetType.FullName}\" using the ConverterParameter {(parameter == null ? "'null'" : $"\"{parameter}\"")}. Double-check that your binding is correct."); 69 | return null; 70 | } 71 | 72 | return arg; 73 | } 74 | 75 | /// 76 | /// The custom functions used by MathConverter. 77 | /// Allows you to extend MathConverter with custom functions. 78 | /// 79 | public CustomFunctionCollection CustomFunctions { get; } 80 | 81 | 82 | 83 | /// 84 | /// Creates a new MathConverter object. 85 | /// 86 | public MathConverter() 87 | { 88 | CustomFunctions = new CustomFunctionCollection(); 89 | CustomFunctions.RegisterDefaultFunctions(); 90 | } 91 | 92 | /// 93 | /// If is set to true, clears the cache of this MathConverter object; If is false, this method does nothing. 94 | /// 95 | public void ClearCache() 96 | { 97 | if (UseCache) 98 | { 99 | _cachedResults.Clear(); 100 | } 101 | } 102 | 103 | /// 104 | /// True to use a cache, false to parse every expression every time. 105 | /// 106 | [DefaultValue(true)] 107 | public bool UseCache 108 | { 109 | get => _cachedResults != null; 110 | set 111 | { 112 | if (value && _cachedResults == null) 113 | _cachedResults = new Dictionary(); 114 | else if (!value) 115 | _cachedResults = null; 116 | } 117 | } 118 | 119 | /// 120 | /// Defaults to false, which implicitly converts to null with a debug warning. 121 | /// Set to true to actually allow UnsetValue to be used to convert. 122 | /// 123 | public bool AllowUnsetValue { get; set; } = false; 124 | 125 | /// 126 | /// A dictionary which stores a cache of AbstractSyntaxTrees for given ConverterParameter strings. 127 | /// This eliminates the need to parse the same statement over and over. 128 | /// 129 | private Dictionary _cachedResults = new Dictionary(); 130 | #if !WPF 131 | private static readonly Dictionary PlatformTypeConverters = new() 132 | #if MAUI 133 | { { typeof(GridLength), new GridLengthTypeConverter() } } 134 | #endif 135 | ; 136 | #endif 137 | 138 | /// 139 | /// The conversion for a single value. 140 | /// 141 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 142 | { 143 | return Convert(new[] { value }, targetType, parameter, culture); 144 | } 145 | /// 146 | /// The actual convert method, for zero or more parameters. 147 | /// 148 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 149 | { 150 | var sanitizedValues = values?.Select((v, i) => SanitizeBinding(v, i, values.Length, parameter, targetType)).ToArray(); 151 | 152 | List evaluatedValues; 153 | 154 | if (parameter is string param) 155 | { 156 | // We start by evaluating the parameter passed in. For certain types (e.g. Rect), we allow multiple values to be specified in the parameter, separated by either commas or semicolons. 157 | // So the parameter "x,2x,x+y,2y" has four parts, which each parse to their own AbstractSyntaxTree: "x", "2x", "x+y", and "2y". 158 | var parameterParts = ParseParameter(param); 159 | 160 | // We now compute the evaluated values. In the above example, the parts would evaluate to four doubles: values[0], 2*values[0], values[0]+values[1], and 2*values[1]. 161 | try 162 | { 163 | evaluatedValues = parameterParts.Select(p => p.Evaluate(culture, sanitizedValues)).ToList(); 164 | } 165 | catch (NodeEvaluationException ex) 166 | { 167 | throw new EvaluationException(param, values, ex); 168 | } 169 | } 170 | else if (parameter == null) 171 | { 172 | // If there is no parameter, we'll just use the value(s) specified by the (Multi)Binding. 173 | // In this case, MathConverter is merely used for type conversion (e.g. turning 4 doubles into a Rect). 174 | evaluatedValues = sanitizedValues?.ToList() ?? new List(); 175 | } 176 | else 177 | { 178 | throw new ArgumentException("The Converter Parameter must be a string.", nameof(parameter)); 179 | } 180 | 181 | // Now if there are more than one value, we will simply merge the values with commas, and use TypeConverter to handle the conversion to the appropriate type. 182 | // We do this in invariant culture to ensure that any type conversion (which must happen in InvariantCulture) succeeds. 183 | var stringJoinCulture = targetType == typeof(string) ? culture : CultureInfo.InvariantCulture; 184 | var finalAnswerToConvert = evaluatedValues?.Count == 1 ? evaluatedValues[0] : string.Join(",", evaluatedValues.Select(p => string.Format(stringJoinCulture, "{0}", p)).MyToArray()); 185 | 186 | return ConvertType(finalAnswerToConvert, targetType); 187 | } 188 | 189 | /// 190 | /// Converts a value to a given type. Returns the input value if the type conversion fails. 191 | /// This function tries the following things: 192 | /// - TypeConverters 193 | /// - Coersion (Implicit conversions: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/casting-and-type-conversions#implicit-conversions) 194 | /// - IConvertible (System.Convert.ChangeType) 195 | /// 196 | /// The value to convert 197 | /// The type to convert to 198 | public static object ConvertType(object value, Type targetType) 199 | { 200 | // At this point, we have now computed our final answer. 201 | // However, we might need to do standard type conversion to convert it to a different type. 202 | 203 | // We don't need to convert null, and we can't convert if there's no type specified that we need to convert to. 204 | if (value == null || targetType == null) 205 | return value; 206 | 207 | // We might not need to convert. 208 | if (targetType.IsInstanceOfType(value)) 209 | { 210 | return value; 211 | } 212 | 213 | // We need to convert the answer to the appropriate type. Let's start with the default TypeConverter. 214 | var converter = TypeDescriptor.GetConverter(targetType); 215 | 216 | if (converter.CanConvertFrom(value.GetType())) 217 | { 218 | // We don't want to use the CultureInfo here when converting, because Rect conversion is broken in some cultures. 219 | // We'll keep these conversions working in InvariantCulture. 220 | return converter.ConvertFrom(null, CultureInfo.InvariantCulture, value); 221 | } 222 | 223 | // We know we're not returning null... If we're trying to convert to a Nullable, let's just convert to SomeStruct instead. 224 | var newTarget = Nullable.GetUnderlyingType(targetType); 225 | if (newTarget != null) 226 | { 227 | targetType = newTarget; 228 | } 229 | 230 | #if !WPF 231 | // Let's try PlatformTypeConverter 232 | var typeConverter = GetPlatformTypeConverter(targetType); 233 | 234 | if (typeConverter != null) 235 | { 236 | // PlatformTypeConverters only convert from Invariant Strings. All other conversions are deprecated. 237 | string convertFrom = value as string ?? $"{value}"; 238 | return typeConverter.ConvertFromInvariantString(convertFrom); 239 | } 240 | #endif 241 | 242 | try 243 | { 244 | if (Operator.DoesImplicitConversionExist(value.GetType(), targetType, true)) 245 | { 246 | // The default TypeConverter doesn't support this conversion. Let's try an implicit conversion. 247 | return Operator.DoImplicitConversion(value, targetType); 248 | } 249 | else if (CompatibilityExtensions.IsIConvertible(value)) 250 | { 251 | if (targetType == typeof(char)) 252 | { 253 | // We'll add a special cast for conversions to char, where we'll convert to int first. 254 | return System.Convert.ToChar((int)System.Convert.ChangeType(value, typeof(int))); 255 | } 256 | else 257 | { 258 | // Let's try System.Convert. This might throw an exception. 259 | return System.Convert.ChangeType(value, targetType); 260 | } 261 | } 262 | } 263 | catch (InvalidCastException) { } 264 | 265 | try 266 | { 267 | if (targetType.GetTypeInfo().IsEnum) 268 | { 269 | return Enum.ToObject(targetType, value); 270 | } 271 | } 272 | catch (ArgumentException) { } 273 | 274 | // Welp, we can't convert this value... Oh well. 275 | return value; 276 | } 277 | 278 | /// 279 | /// Don't call this method, as it is not supported. 280 | /// 281 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 282 | { 283 | // WE CAN'T CONVERT BACK 284 | throw new NotSupportedException(); 285 | } 286 | /// 287 | /// Don't call this method, as it is not supported. 288 | /// 289 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 290 | { 291 | // WE CAN'T CONVERT BACK 292 | throw new NotSupportedException(); 293 | } 294 | #if !WPF 295 | private static PlatformTypeConverter GetPlatformTypeConverter(Type targetType) 296 | { 297 | if (PlatformTypeConverters.ContainsKey(targetType)) 298 | { 299 | return PlatformTypeConverters[targetType]; 300 | } 301 | 302 | foreach (var attribute in targetType.GetCustomAttributes()) 303 | { 304 | if (Type.GetType(attribute.ConverterTypeName, false) is { } converterType) 305 | { 306 | return PlatformTypeConverters[targetType] = (PlatformTypeConverter)Activator.CreateInstance(converterType); 307 | } 308 | } 309 | 310 | return PlatformTypeConverters[targetType] = null; 311 | } 312 | #endif 313 | 314 | 315 | /// 316 | /// Parses an expression into a syntax tree that can be evaluated later. 317 | /// This method keeps a cache of parsed results, so it doesn't have to parse the same expression twice. 318 | /// 319 | /// The parameter that we're parsing 320 | /// A syntax tree that can be evaluated later. 321 | internal AbstractSyntaxTree[] ParseParameter(string parameter) 322 | { 323 | if (_cachedResults == null) 324 | return Parser.Parse(CustomFunctions, parameter); 325 | 326 | return _cachedResults.ContainsKey(parameter) ? _cachedResults[parameter] : (_cachedResults[parameter] = Parser.Parse(CustomFunctions, parameter)); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/ParsingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HexInnovation 4 | { 5 | public class ParsingException : Exception 6 | { 7 | internal ParsingException(Scanner scanner) 8 | { 9 | Position = scanner.Position; 10 | Expression = scanner.Expression; 11 | } 12 | internal ParsingException(Scanner scanner, string message) : base(message) 13 | { 14 | Position = scanner.Position; 15 | Expression = scanner.Expression; 16 | } 17 | internal ParsingException(Scanner scanner, string message, Exception inner) : base(message, inner) 18 | { 19 | Position = scanner.Position; 20 | Expression = scanner.Expression; 21 | } 22 | /// 23 | /// The position in the string at which an exception was thrown. 24 | /// 25 | public int Position { get; } 26 | public string Expression { get; } 27 | 28 | public override string Message => $"The parser threw an exception at the {MathConverter.ComputeOrdinal(Position)} character:\r\n{base.Message}\r\n\r\nExpression: \"{Expression}\""; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/SyntaxTree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace HexInnovation 9 | { 10 | public abstract class AbstractSyntaxTree 11 | { 12 | public object Evaluate(CultureInfo cultureInfo, object[] bindingValues) 13 | { 14 | try 15 | { 16 | return DoEvaluate(cultureInfo, bindingValues); 17 | } 18 | catch (Exception ex) when (ex is not NodeEvaluationException) 19 | { 20 | throw new NodeEvaluationException(this, ex); 21 | } 22 | } 23 | public abstract object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues); 24 | public abstract override string ToString(); 25 | } 26 | abstract class BinaryNode : AbstractSyntaxTree 27 | { 28 | protected BinaryNode(BinaryOperator @operator, AbstractSyntaxTree left, AbstractSyntaxTree right) 29 | { 30 | _operator = @operator; 31 | _left = left; 32 | _right = right; 33 | } 34 | private readonly AbstractSyntaxTree _left, _right; 35 | private readonly BinaryOperator _operator; 36 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 37 | { 38 | return _operator.Evaluate(_left, _right, cultureInfo, bindingValues); 39 | } 40 | public sealed override string ToString() 41 | { 42 | return $"({_left} {_operator} {_right})"; 43 | } 44 | } 45 | sealed class ExponentNode : BinaryNode 46 | { 47 | public ExponentNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 48 | : base(Operator.Exponentiation, left, right) { } 49 | } 50 | sealed class AddNode : BinaryNode 51 | { 52 | public AddNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 53 | : base(Operator.Addition, left, right) { } 54 | } 55 | sealed class SubtractNode : BinaryNode 56 | { 57 | public SubtractNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 58 | : base(Operator.Subtraction, left, right) { } 59 | } 60 | sealed class MultiplyNode : BinaryNode 61 | { 62 | public MultiplyNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 63 | : base(Operator.Multiply, left, right) { } 64 | } 65 | sealed class ModuloNode : BinaryNode 66 | { 67 | public ModuloNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 68 | : base(Operator.Remainder, left, right) { } 69 | } 70 | sealed class AndNode : BinaryNode 71 | { 72 | public AndNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 73 | : base(Operator.And, left, right) { } 74 | } 75 | sealed class NullCoalescingNode : BinaryNode 76 | { 77 | public NullCoalescingNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 78 | : base(Operator.NullCoalescing, left, right) { } 79 | } 80 | sealed class OrNode : BinaryNode 81 | { 82 | public OrNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 83 | : base(Operator.Or, left, right) { } 84 | } 85 | sealed class DivideNode : BinaryNode 86 | { 87 | public DivideNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 88 | : base(Operator.Division, left, right) { } 89 | } 90 | sealed class NotEqualNode : BinaryNode 91 | { 92 | public NotEqualNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 93 | : base(Operator.Inequality, left, right) { } 94 | } 95 | sealed class EqualNode : BinaryNode 96 | { 97 | public EqualNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 98 | : base(Operator.Equality, left, right) { } 99 | } 100 | sealed class LessThanNode : BinaryNode 101 | { 102 | public LessThanNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 103 | : base(Operator.LessThan, left, right) { } 104 | } 105 | sealed class LessThanEqualNode : BinaryNode 106 | { 107 | public LessThanEqualNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 108 | : base(Operator.LessThanOrEqual, left, right) { } 109 | } 110 | sealed class GreaterThanNode : BinaryNode 111 | { 112 | public GreaterThanNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 113 | : base(Operator.GreaterThan, left, right) { } 114 | } 115 | sealed class GreaterThanEqualNode : BinaryNode 116 | { 117 | public GreaterThanEqualNode(AbstractSyntaxTree left, AbstractSyntaxTree right) 118 | : base(Operator.GreaterThanOrEqual, left, right) { } 119 | } 120 | sealed class TernaryNode : AbstractSyntaxTree 121 | { 122 | public TernaryNode(AbstractSyntaxTree condition, AbstractSyntaxTree positive, AbstractSyntaxTree negative) 123 | { 124 | _condition = condition; 125 | _positive = positive; 126 | _negative = negative; 127 | } 128 | private readonly AbstractSyntaxTree _condition; 129 | private readonly AbstractSyntaxTree _positive; 130 | private readonly AbstractSyntaxTree _negative; 131 | 132 | public override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 133 | { 134 | return TernaryOperator.Evaluate(_condition, _positive, _negative, cultureInfo, bindingValues); 135 | } 136 | public override string ToString() 137 | { 138 | return $"({_condition} ? {_positive} : {_negative})"; 139 | } 140 | } 141 | sealed class NullNode : ValueNode 142 | { 143 | public NullNode() : base(null) { } 144 | public override string ToString() => "null"; 145 | } 146 | abstract class UnaryNode : AbstractSyntaxTree 147 | { 148 | protected UnaryNode(UnaryOperator @operator, AbstractSyntaxTree node) 149 | { 150 | _operator = @operator; 151 | _node = node; 152 | } 153 | private readonly UnaryOperator _operator; 154 | private readonly AbstractSyntaxTree _node; 155 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 156 | { 157 | return _operator.Evaluate(_node.Evaluate(cultureInfo, bindingValues)); 158 | } 159 | public sealed override string ToString() => $"{_operator}({_node})"; 160 | } 161 | sealed class NotNode : UnaryNode 162 | { 163 | public NotNode(AbstractSyntaxTree node) 164 | : base(Operator.LogicalNot, node) { } 165 | } 166 | sealed class NegativeNode : UnaryNode 167 | { 168 | public NegativeNode(AbstractSyntaxTree node) 169 | : base(Operator.UnaryNegation, node) { } 170 | } 171 | class ValueNode : AbstractSyntaxTree 172 | { 173 | public ValueNode(object value) 174 | { 175 | Value = value; 176 | } 177 | protected object Value { get; } 178 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) => Value; 179 | public override string ToString() => $"{Value}"; 180 | } 181 | sealed class StringNode : ValueNode 182 | { 183 | public StringNode(string value) 184 | : base(value) { } 185 | public override string ToString() => $"\"{Value}\""; 186 | } 187 | sealed class VariableNode : AbstractSyntaxTree 188 | { 189 | public VariableNode(int index) 190 | { 191 | _index = index; 192 | } 193 | /// 194 | /// The index of the variable we want to get. 195 | /// 196 | private readonly int _index; 197 | public override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 198 | { 199 | if (bindingValues.Length <= _index) 200 | { 201 | var error = new StringBuilder("Error accessing binding value ").Append(this).Append(". "); 202 | 203 | if (bindingValues.Length == 0) 204 | error.Append("No"); 205 | else 206 | error.Append("Only ").Append(bindingValues.Length); 207 | 208 | throw new IndexOutOfRangeException(error.Append(" value").Append(bindingValues.Length == 1 ? " was" : "s were") 209 | .Append(" specified.").ToString()); 210 | } 211 | 212 | return bindingValues[_index]; 213 | } 214 | public override string ToString() 215 | { 216 | switch (_index) 217 | { 218 | case 0: 219 | return "x"; 220 | case 1: 221 | return "y"; 222 | case 2: 223 | return "z"; 224 | default: 225 | return $"[{_index}]"; 226 | } 227 | } 228 | } 229 | 230 | /// 231 | /// A custom function used by MathConverter. 232 | /// Register the function with the to use it. 233 | /// 234 | /// 235 | /// 236 | /// 237 | /// 238 | /// 239 | public abstract class CustomFunction : AbstractSyntaxTree 240 | { 241 | /// 242 | /// The name of the function. 243 | /// There could potentially be multiple names for same function. 244 | /// 245 | public string FunctionName { get; internal set; } 246 | /// 247 | /// Converts an object to a specified type. Returns true if the conversion was successful; otherwise false. 248 | /// 249 | /// The type to convert the specified value to. 250 | /// The value to convert. 251 | /// The value, casted to the specified type, or the default value, if the conversion was unsuccessful. 252 | /// True if the conversion was successful; otherwise false. 253 | protected bool TryConvert(object value, out T convertedValue) 254 | { 255 | var convertToType = typeof(T); 256 | 257 | if (Operator.DoesImplicitConversionExist(value?.GetType(), convertToType, true) && Operator.DoImplicitConversion(value, convertToType.GetTypeInfo().IsValueType ? typeof(Nullable<>).MakeGenericType(convertToType) : convertToType) is T a) 258 | { 259 | convertedValue = a; 260 | return true; 261 | } 262 | else 263 | { 264 | convertedValue = default; 265 | return false; 266 | } 267 | } 268 | 269 | /// 270 | /// The actual parameters passed to this function. 271 | /// 272 | internal List Parameters { get; set; } 273 | /// 274 | /// Gets the number of parameters passed to the function. 275 | /// 276 | protected int NumParameters => Parameters.Count; 277 | /// 278 | /// Evaluates a specific parameter passed into the function. 279 | /// 280 | /// The zero-based index of the argument to evaluate. 281 | /// The CultureInfo to use when evaluating the parameter. 282 | /// The values being converted by MathConverter. 283 | /// 284 | protected object EvaluateParameter(int whichParameter, CultureInfo cultureInfo, object[] bindingValues) 285 | { 286 | return Parameters[whichParameter].Evaluate(cultureInfo, bindingValues); 287 | } 288 | /// 289 | /// A method that can be overridden in base classes that specifies if a ParsingException should be thrown while parsing the parameters to this function. 290 | /// 291 | /// The number of parameters parsed. 292 | /// True if the number of parameters is valid; otherwise false. 293 | public virtual bool IsValidNumberOfParameters(int numParams) 294 | { 295 | return true; 296 | } 297 | public override sealed string ToString() 298 | { 299 | return $"{FunctionName}({string.Join(", ", Parameters.MyToArray())})"; 300 | } 301 | } 302 | 303 | /// 304 | /// A function that takes no arguments and returns an object. 305 | /// 306 | public abstract class ZeroArgFunction : CustomFunction 307 | { 308 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 309 | { 310 | return Evaluate(cultureInfo); 311 | } 312 | /// 313 | /// The actual function. 314 | /// 315 | /// The culture to evaluate with. 316 | public abstract object Evaluate(CultureInfo cultureInfo); 317 | 318 | /// 319 | public sealed override bool IsValidNumberOfParameters(int numParams) => numParams == 0; 320 | } 321 | 322 | 323 | /// 324 | /// A function that takes a single parameter of type object that returns an object. 325 | /// 326 | public abstract class OneArgFunction : CustomFunction 327 | { 328 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 329 | { 330 | return Evaluate(cultureInfo, Parameters[0].Evaluate(cultureInfo, bindingValues)); 331 | } 332 | /// 333 | /// The actual function. 334 | /// 335 | /// The culture to evaluate with. 336 | /// The argument passed to the function. 337 | public abstract object Evaluate(CultureInfo cultureInfo, object argument); 338 | /// 339 | public override bool IsValidNumberOfParameters(int numParams) => numParams == 1; 340 | } 341 | /// 342 | /// A function that takes a single parameter of type double that returns a double. 343 | /// 344 | public abstract class OneDoubleFunction : OneArgFunction 345 | { 346 | /// 347 | public sealed override object Evaluate(CultureInfo cultureInfo, object argument) 348 | { 349 | if (TryConvert(argument, out var x)) 350 | return Evaluate(cultureInfo, x); 351 | else if (argument == null) 352 | return EvaluateNullArgument(cultureInfo); 353 | else 354 | throw new ArgumentException($"{FunctionName} accepts only a numeric input or null."); 355 | } 356 | /// 357 | /// The actual function. 358 | /// 359 | /// The culture to evaluate with. 360 | /// The argument passed to the function. 361 | public abstract double? Evaluate(CultureInfo cultureInfo, double argument); 362 | /// 363 | /// What the function should return when the argument is null: defaults to null. 364 | /// 365 | /// The culture to evaluate with. 366 | public virtual double? EvaluateNullArgument(CultureInfo cultureInfo) => null; 367 | } 368 | 369 | /// 370 | /// A function that takes two parameters. 371 | /// 372 | public abstract class TwoArgFunction : CustomFunction 373 | { 374 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 375 | { 376 | return Evaluate(cultureInfo, Parameters[0].Evaluate(cultureInfo, bindingValues), Parameters[1].Evaluate(cultureInfo, bindingValues)); 377 | } 378 | /// 379 | /// The actual function. 380 | /// 381 | /// The culture to evaluate with. 382 | /// The first argument passed to the function. 383 | /// The second argument passed to the function. 384 | public abstract object Evaluate(CultureInfo cultureInfo, object x, object y); 385 | /// 386 | public sealed override bool IsValidNumberOfParameters(int numParams) => numParams == 2; 387 | } 388 | /// 389 | /// A formula that takes anywhere from zero to infinity arguments. 390 | /// 391 | public abstract class ArbitraryArgFunction : CustomFunction 392 | { 393 | public sealed override object DoEvaluate(CultureInfo cultureInfo, object[] bindingValues) 394 | { 395 | return Evaluate(cultureInfo, Enumerable.Range(0, NumParameters).Select(i => new Func(() => EvaluateParameter(i, cultureInfo, bindingValues))).ToArray()); 396 | } 397 | /// 398 | /// The actual function. 399 | /// 400 | /// The culture to evaluate with. 401 | /// A function that can be used to get arbitrary arguments passed to the function. 402 | public abstract object Evaluate(CultureInfo cultureInfo, Func[] getArgument); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/CSharp/MathConverter/Token.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace HexInnovation 4 | { 5 | class Token 6 | { 7 | public Token(TokenType tokenType) 8 | { 9 | TokenType = tokenType; 10 | } 11 | public TokenType TokenType { get; } 12 | 13 | public override string ToString() => $"{TokenType} token"; 14 | } 15 | class LexicalToken : Token 16 | { 17 | public LexicalToken(TokenType tokenType, string lex) 18 | : base(tokenType) 19 | { 20 | Lex = lex; 21 | } 22 | public string Lex { get; } 23 | 24 | public override string ToString() 25 | { 26 | return $"Lexical ({TokenType}) Token (\"{Lex.Replace("\"", "\\\"")}\")"; 27 | } 28 | } 29 | class InterpolatedStringToken : LexicalToken 30 | { 31 | public InterpolatedStringToken(string lex, List arguments) 32 | : base(TokenType.InterpolatedString, lex) 33 | { 34 | Arguments = arguments; 35 | } 36 | public List Arguments { get; } 37 | } 38 | enum TokenType 39 | { 40 | X, 41 | Y, 42 | Z, 43 | Number, 44 | Plus, 45 | Minus, 46 | Times, 47 | Divide, 48 | LBracket, 49 | RBracket, 50 | LParen, 51 | RParen, 52 | EOF, 53 | Semicolon, 54 | Caret, 55 | Lexical, 56 | Not, 57 | DoubleEqual, 58 | NotEqual, 59 | LessThan, 60 | GreaterThan, 61 | LessThanEqual, 62 | GreaterThanEqual, 63 | QuestionMark, 64 | DoubleQuestionMark, 65 | Colon, 66 | String, 67 | Or, 68 | And, 69 | Modulo, 70 | InterpolatedString, 71 | RCurlyBracket, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Demos/WPF/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Demos/WPF/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace MathConverterDemo 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Demos/WPF/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTrademark("")] 9 | [assembly: AssemblyCulture("")] 10 | 11 | // Setting ComVisible to false makes the types in this assembly not visible 12 | // to COM components. If you need to access a type in this assembly from 13 | // COM, set the ComVisible attribute to true on that type. 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: ThemeInfo( 17 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 18 | //(used if a resource is not found in the page, 19 | // or application resource dictionaries) 20 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 21 | //(used if a resource is not found in the page, 22 | // app, or any theme specific resource dictionaries) 23 | )] 24 | -------------------------------------------------------------------------------- /src/Demos/WPF/CustomFunctions/CustomAverageFunction.cs: -------------------------------------------------------------------------------- 1 | using HexInnovation; 2 | using System; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace MathConverter.Demo.CustomFunctions; 7 | 8 | sealed class MyCustomAverageFunction : ArbitraryArgFunction 9 | { 10 | public override object Evaluate(CultureInfo cultureInfo, Func[] arguments) 11 | { 12 | var args = arguments.Select(x => TryConvert(x(), out var d) ? Math.Round(d) : new double?()) 13 | .Where(x => x.HasValue).Select(x => x.Value).ToList(); 14 | 15 | return args.Count == 0 ? new double?() : args.Average(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Demos/WPF/CustomFunctions/GetWindowTitle.cs: -------------------------------------------------------------------------------- 1 | using HexInnovation; 2 | using System; 3 | using System.Globalization; 4 | using System.Windows; 5 | 6 | namespace MathConverter.Demo.CustomFunctions; 7 | 8 | public class GetWindowTitleFunction : OneArgFunction 9 | { 10 | public override object Evaluate(CultureInfo cultureInfo, object argument) 11 | { 12 | return argument is Type t && t.IsAssignableTo(typeof(Window)) ? ((Window)Activator.CreateInstance(t)).Title : null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Demos/WPF/Demos/BooleanToVisibility.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Demos/WPF/Demos/BooleanToVisibility.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace MathConverter.Demo.Demos 4 | { 5 | /// 6 | /// Interaction logic for BooleanToVisibility.xaml 7 | /// 8 | public partial class BooleanToVisibility : Window 9 | { 10 | public BooleanToVisibility() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Demos/WPF/Demos/CountClicks.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 |