diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..81b71c5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,112 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+ - main
+ - "codex/**"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ CONFIGURATION: Release
+ CORE_PROJECT: Evaluant.Calculator/NCalc.csproj
+ TEST_PROJECT: Evaluant.Calculator.Tests/NCalc.Tests.csproj
+ DOTNET_NOLOGO: true
+ NCALC_EXPECTED_PUBLIC_KEY_TOKEN: f59bbff0fd43f598
+
+jobs:
+ quality-gates:
+ name: Local quality gates and compatibility
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Install Mono tooling
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes mono-devel mono-utils
+
+ - name: Prepare strong-name key
+ shell: bash
+ env:
+ NCALC_STRONG_NAME_KEY_BASE64: ${{ secrets.NCALC_STRONG_NAME_KEY_BASE64 }}
+ run: |
+ set -euo pipefail
+
+ key_path="$RUNNER_TEMP/NCalc.snk"
+
+ if [ -n "${NCALC_STRONG_NAME_KEY_BASE64:-}" ]; then
+ printf '%s' "$NCALC_STRONG_NAME_KEY_BASE64" | base64 --decode > "$key_path"
+ else
+ sn -k "$key_path"
+ sn -p "$key_path" "$RUNNER_TEMP/NCalc.public.snk" >/dev/null
+ token="$(sn -tp "$RUNNER_TEMP/NCalc.public.snk" | awk '/Public Key Token/ { print $4 }')"
+ echo "NCALC_EXPECTED_PUBLIC_KEY_TOKEN=$token" >> "$GITHUB_ENV"
+ echo "Using an ephemeral strong-name key for non-release CI validation."
+ fi
+
+ echo "NCalcStrongNameKeyFile=$key_path" >> "$GITHUB_ENV"
+
+ - name: Run local validation gates
+ run: scripts/validate.sh
+
+ build-test:
+ name: Restore, build, and test (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Show .NET information
+ run: dotnet --info
+
+ - name: Restore
+ run: |
+ dotnet restore ${{ env.CORE_PROJECT }}
+ dotnet restore ${{ env.TEST_PROJECT }}
+
+ - name: Build
+ run: |
+ dotnet build ${{ env.CORE_PROJECT }} --configuration ${{ env.CONFIGURATION }} --no-restore
+ dotnet build ${{ env.TEST_PROJECT }} --configuration ${{ env.CONFIGURATION }} --no-restore
+
+ - name: Test
+ run: >
+ dotnet test ${{ env.TEST_PROJECT }}
+ --configuration ${{ env.CONFIGURATION }}
+ --no-build
+ --logger "trx;LogFileName=test-results.trx"
+ --results-directory TestResults
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results-${{ matrix.os }}
+ path: TestResults
+ if-no-files-found: ignore
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..5a6ea37
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,192 @@
+name: Release Package
+
+on:
+ release:
+ types:
+ - published
+ workflow_dispatch:
+ inputs:
+ package_version:
+ description: NuGet package version to build, without a leading v.
+ required: true
+ type: string
+ publish:
+ description: Publish the package to NuGet.org instead of running a dry pack.
+ required: true
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+
+env:
+ CONFIGURATION: Release
+ CORE_PROJECT: Evaluant.Calculator/NCalc.csproj
+ TEST_PROJECT: Evaluant.Calculator.Tests/NCalc.Tests.csproj
+ DOTNET_NOLOGO: true
+ NCALC_EXPECTED_PUBLIC_KEY_TOKEN: f59bbff0fd43f598
+
+jobs:
+ package:
+ name: Build, test, and pack
+ runs-on: ubuntu-latest
+ outputs:
+ package-version: ${{ steps.resolve-package-version.outputs.package-version }}
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Resolve package version
+ id: resolve-package-version
+ shell: bash
+ run: |
+ if [ "$GITHUB_EVENT_NAME" = "release" ]; then
+ package_version="${GITHUB_REF_NAME#v}"
+ else
+ package_version="${{ inputs.package_version }}"
+ fi
+
+ if [ -z "$package_version" ]; then
+ echo "Package version could not be resolved." >&2
+ exit 1
+ fi
+
+ if [[ ! "$package_version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)([-+][0-9A-Za-z.-]+)?$ ]]; then
+ echo "Unsupported package version format: $package_version" >&2
+ exit 2
+ fi
+
+ package_major_version="${BASH_REMATCH[1]}"
+ if [ "$package_major_version" -lt 2 ]; then
+ echo "NCalc uses a new strong-name key, so release package versions must be 2.0.0 or later." >&2
+ exit 1
+ fi
+
+ echo "PACKAGE_VERSION=$package_version" >> "$GITHUB_ENV"
+ echo "package-version=$package_version" >> "$GITHUB_OUTPUT"
+
+ - name: Install Mono tooling
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes mono-devel mono-utils
+
+ - name: Prepare release strong-name key
+ shell: bash
+ env:
+ NCALC_STRONG_NAME_KEY_BASE64: ${{ secrets.NCALC_STRONG_NAME_KEY_BASE64 }}
+ run: |
+ set -euo pipefail
+
+ if [ -z "${NCALC_STRONG_NAME_KEY_BASE64:-}" ]; then
+ echo "NCALC_STRONG_NAME_KEY_BASE64 is required for release packaging." >&2
+ exit 2
+ fi
+
+ key_path="$RUNNER_TEMP/NCalc.snk"
+ public_key_path="$RUNNER_TEMP/NCalc.public.snk"
+ printf '%s' "$NCALC_STRONG_NAME_KEY_BASE64" | base64 --decode > "$key_path"
+ sn -p "$key_path" "$public_key_path" >/dev/null
+
+ actual_token="$(sn -tp "$public_key_path" | awk '/Public Key Token/ { print $4 }')"
+ if [ "$actual_token" != "$NCALC_EXPECTED_PUBLIC_KEY_TOKEN" ]; then
+ echo "Release signing key token mismatch: expected $NCALC_EXPECTED_PUBLIC_KEY_TOKEN, got $actual_token." >&2
+ exit 1
+ fi
+
+ echo "NCalcStrongNameKeyFile=$key_path" >> "$GITHUB_ENV"
+
+ - name: Run release validation gates
+ run: scripts/validate.sh
+
+ - name: Upload NuGet package
+ uses: actions/upload-artifact@v4
+ with:
+ name: ncalc-${{ steps.resolve-package-version.outputs.package-version }}
+ path: |
+ artifacts/packages/*.nupkg
+ artifacts/packages/*.snupkg
+ if-no-files-found: error
+
+ windows-framework-smoke:
+ name: Windows .NET Framework package smoke
+ needs: package
+ runs-on: windows-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Download NuGet package
+ uses: actions/download-artifact@v4
+ with:
+ name: ncalc-${{ needs.package.outputs.package-version }}
+ path: artifacts/packages
+
+ - name: Run .NET Framework consumer smoke
+ shell: pwsh
+ run: >
+ scripts/windows-framework-consumer-smoke.ps1
+ -PackageSource artifacts/packages
+ -PackageVersion "${{ needs.package.outputs.package-version }}"
+
+ publish:
+ name: Publish to NuGet.org
+ needs:
+ - package
+ - windows-framework-smoke
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Set up .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Download NuGet package
+ uses: actions/download-artifact@v4
+ with:
+ name: ncalc-${{ needs.package.outputs.package-version }}
+ path: artifacts/packages
+
+ - name: Publish to NuGet.org
+ shell: bash
+ run: |
+ should_publish=false
+
+ if [ "$GITHUB_EVENT_NAME" = "release" ]; then
+ should_publish=true
+ elif [ "${{ inputs.publish }}" = "true" ]; then
+ should_publish=true
+ fi
+
+ if [ "$should_publish" != "true" ]; then
+ echo "Dry run complete. Package was built but not published."
+ exit 0
+ fi
+
+ for package in artifacts/packages/*.nupkg; do
+ dotnet nuget push "$package" \
+ --api-key "${{ secrets.NUGET_API_KEY }}" \
+ --source "https://api.nuget.org/v3/index.json" \
+ --skip-duplicate
+ done
+
+ for symbols_package in artifacts/packages/*.snupkg; do
+ if [ -e "$symbols_package" ]; then
+ dotnet nuget push "$symbols_package" \
+ --api-key "${{ secrets.NUGET_API_KEY }}" \
+ --source "https://api.nuget.org/v3/index.json" \
+ --skip-duplicate
+ fi
+ done
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2498ff2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,51 @@
+# Build outputs
+[Bb]in/
+[Oo]bj/
+[Bb]uild/
+!build/
+!build/NCalc.BuildTools/
+!build/NCalc.BuildTools/*.cs
+!build/NCalc.BuildTools/*.csproj
+build/NCalc.BuildTools/[Bb]in/
+build/NCalc.BuildTools/[Oo]bj/
+artifacts/
+TestResults/
+_Package/
+
+# User and IDE state
+.vs/
+.idea/
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
+*.sln.cache
+*.rsuser
+*ReSharper*/
+
+# Test and coverage output
+*.trx
+*.coverage
+*.coveragexml
+*.dotCover
+*.itrace.csdef
+
+# NuGet outputs
+*.nupkg
+*.snupkg
+packages/
+
+# Private signing material
+*.snk
+*.pfx
+*.p12
+*.pem
+
+# Patch and merge leftovers
+*.patch
+*.orig
+*.rej
+
+# OS files
+.DS_Store
+desktop.ini
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..d53f9c4
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,29 @@
+
+
+ true
+ true
+
+
+
+ NCalc
+ NCalc
+ Sebastien Ros
+ NCalc
+ Mathematical expressions evaluator for .NET.
+ ncalc;expression;expressions;calculator;formula;evaluator
+ https://github.com/sheetsync/NCalc
+ MIT
+ https://github.com/sheetsync/NCalc.git
+ git
+ README.md
+ false
+ true
+ snupkg
+ true
+ true
+
+
+
+
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..5f112c9
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Evaluant.Calculator.FrameworkCompatibility.Tests/NCalc.FrameworkCompatibility.Tests.csproj b/Evaluant.Calculator.FrameworkCompatibility.Tests/NCalc.FrameworkCompatibility.Tests.csproj
new file mode 100644
index 0000000..4e7e651
--- /dev/null
+++ b/Evaluant.Calculator.FrameworkCompatibility.Tests/NCalc.FrameworkCompatibility.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+ Exe
+ net35;net10.0
+ NCalc.FrameworkCompatibility.Tests
+ NCalc.FrameworkCompatibility.Tests
+ disable
+ disable
+ latest
+
+
+
+
+
+
+
+
+
+
diff --git a/Evaluant.Calculator.FrameworkCompatibility.Tests/Program.cs b/Evaluant.Calculator.FrameworkCompatibility.Tests/Program.cs
new file mode 100644
index 0000000..0af1621
--- /dev/null
+++ b/Evaluant.Calculator.FrameworkCompatibility.Tests/Program.cs
@@ -0,0 +1,310 @@
+using System;
+using System.Globalization;
+using System.Threading;
+using Antlr.Runtime;
+using Antlr.Runtime.Tree;
+using NCalc.Domain;
+using DomainValueType = NCalc.Domain.ValueType;
+
+namespace NCalc.FrameworkCompatibility.Tests
+{
+ internal delegate object BinaryOperation(object left, object right);
+
+ internal static class Program
+ {
+ private static int _passed;
+ private static int _failed;
+
+ private static int Main()
+ {
+ CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture;
+ CultureInfo originalUserInterfaceCulture = Thread.CurrentThread.CurrentUICulture;
+
+ try
+ {
+ Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
+ Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-FR");
+
+ RunExpressionCases();
+ RunParserSurfaceCases();
+ RunNumbersCases();
+ RunDomainCases();
+ RunCallbackCases();
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = originalCulture;
+ Thread.CurrentThread.CurrentUICulture = originalUserInterfaceCulture;
+ }
+
+ if (_failed > 0)
+ {
+ Console.Error.WriteLine("NCalc framework compatibility failed: " + _failed + " failed, " + _passed + " passed.");
+ return 1;
+ }
+
+ Console.WriteLine("NCalc framework compatibility passed: " + _passed + " assertions.");
+ return 0;
+ }
+
+ private static void RunExpressionCases()
+ {
+ Run("ExpressionEvaluate_WithInvariantFloatLiteralUnderFrenchCulture_DoesReturnDouble", delegate
+ {
+ AssertExpression("1.5 + 2.25", 3.75d, typeof(double));
+ });
+
+ Run("ExpressionEvaluate_WithLargeIntegerLiteral_DoesPreserveSingleOverflowPath", delegate
+ {
+ AssertExpression("40000000000", 40000000000f, typeof(float));
+ });
+
+ Run("ExpressionEvaluate_WithDateLiteralUnderFrenchCulture_DoesReturnDateTime", delegate
+ {
+ AssertExpression("#01/01/2001#", new DateTime(2001, 1, 1), typeof(DateTime));
+ });
+
+ Run("ExpressionEvaluate_WithUnicodeAndNewlineEscapes_DoesUnescapeString", delegate
+ {
+ AssertExpression("'\\u0041\\n'", "A\n", typeof(string));
+ });
+
+ Run("ExpressionEvaluate_WithIgnoreCaseBuiltInFunction_DoesResolveFunctionName", delegate
+ {
+ AssertObject(1m, typeof(decimal), new Expression("aBs(-1)", EvaluateOptions.IgnoreCase).Evaluate());
+ });
+
+ Run("ExpressionEvaluate_WithDecimalDoubleMix_DoesReturnDecimal", delegate
+ {
+ Expression expression = new Expression("1.8 + Abs([var1])");
+ expression.Parameters["var1"] = 9.2d;
+
+ AssertObject(11.0m, typeof(decimal), expression.Evaluate());
+ });
+ }
+
+ private static void RunParserSurfaceCases()
+ {
+ Run("ParserNcalcExpression_WithLegacyReturnScopeVariable_DoesExposeAstTree", delegate
+ {
+ NCalcLexer lexer = new NCalcLexer(new ANTLRStringStream("1 + 2"));
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ NCalcParser parser = new NCalcParser(tokens);
+
+ ParserRuleReturnScope parserResult = parser.ncalcExpression();
+ IAstRuleReturnScope astResult = (IAstRuleReturnScope)parserResult;
+ CommonTree tree = astResult.Tree as CommonTree;
+
+ if (tree == null)
+ {
+ throw new InvalidOperationException("Expected legacy parser return scope to expose a CommonTree.");
+ }
+
+ if (parserResult.Start == null || parserResult.Stop == null)
+ {
+ throw new InvalidOperationException("Expected legacy parser return scope to expose token boundaries.");
+ }
+ });
+ }
+
+ private static void RunNumbersCases()
+ {
+ Run("NumbersAdd_WithStringCommaDecimalUnderFrenchCulture_DoesParseUsingCurrentCulture", delegate
+ {
+ AssertOperation(Numbers.Add, "4,5", 1, 5.5m, typeof(decimal));
+ });
+
+ Run("NumbersAdd_WithDoubleAndDecimal_DoesReturnDecimal", delegate
+ {
+ AssertOperation(Numbers.Add, 2.5d, 3.5m, 6.0m, typeof(decimal));
+ });
+
+ Run("NumbersSoustract_WithByteAndByte_DoesPreserveNullFallthrough", delegate
+ {
+ AssertNull(Numbers.Soustract((byte)2, (byte)2));
+ });
+
+ Run("NumbersSoustract_WithUnsignedOperands_DoesPreserveUncheckedWraparound", delegate
+ {
+ AssertOperation(Numbers.Soustract, (uint)6, (ulong)8, UInt64.MaxValue - 1, typeof(ulong));
+ });
+
+ Run("NumbersDivide_WithUlongAndBoolean_DoesPreserveWrongOperatorMessage", delegate
+ {
+ AssertThrows(
+ delegate { Numbers.Divide((ulong)8, true); },
+ typeof(InvalidOperationException),
+ "Operator '-'");
+ });
+
+ Run("NumbersModulo_WithDecimalAndDouble_DoesPreserveDecimalMessageTypo", delegate
+ {
+ AssertThrows(
+ delegate { Numbers.Modulo(3.5m, 2.5d); },
+ typeof(InvalidOperationException),
+ "decimal' and 'decimal");
+ });
+
+ Run("NumbersMax_WithByteAndDecimal_DoesUseLeftOperandConversion", delegate
+ {
+ AssertOperation(Numbers.Max, (byte)2, 3.5m, (byte)4, typeof(byte));
+ });
+
+ Run("NumbersMin_WithUshortAndDouble_DoesUseLeftOperandConversion", delegate
+ {
+ AssertOperation(Numbers.Min, (ushort)4, 2.5d, (ushort)2, typeof(ushort));
+ });
+ }
+
+ private static void RunDomainCases()
+ {
+ Run("ValueExpression_WithFloatUnderFrenchCulture_DoesSerializeWithInvariantDecimalSeparator", delegate
+ {
+ AssertEqual("1.5", new ValueExpression(1.5f).ToString());
+ });
+
+ Run("ValueExpression_WithUnsupportedObject_DoesThrowEvaluationException", delegate
+ {
+ AssertThrows(
+ delegate { new ValueExpression(new object()); },
+ typeof(EvaluationException),
+ "This value could not be handled");
+ });
+
+ Run("ExpressionEvaluate_WithManualAst_DoesUseSameArithmeticPipeline", delegate
+ {
+ Expression expression = new Expression(
+ new BinaryExpression(
+ BinaryExpressionType.Plus,
+ new ValueExpression(2),
+ new ValueExpression(3)));
+
+ AssertObject(5, typeof(int), expression.Evaluate());
+ });
+
+ Run("ValueExpression_WithDateTime_DoesInferDateTimeType", delegate
+ {
+ DateTime dateTime = new DateTime(2026, 6, 20);
+ ValueExpression expression = new ValueExpression(dateTime);
+
+ AssertEqual(DomainValueType.DateTime, expression.Type);
+ AssertEqual(dateTime, expression.Value);
+ });
+ }
+
+ private static void RunCallbackCases()
+ {
+ Run("ExpressionEvaluate_WithLazyTernary_DoesNotResolveUnusedBranch", delegate
+ {
+ Expression expression = new Expression("if([flag], [value], [missing])");
+ expression.Parameters["flag"] = true;
+ expression.Parameters["value"] = 42;
+ expression.EvaluateParameter += delegate(string name, ParameterArgs args)
+ {
+ if (name == "missing")
+ {
+ throw new InvalidOperationException("Unused branch was evaluated.");
+ }
+ };
+
+ AssertObject(42, typeof(int), expression.Evaluate());
+ });
+
+ Run("ExpressionEvaluate_WithCustomFunction_DoesPreserveLazyParameterEvaluation", delegate
+ {
+ Expression expression = new Expression("twice([value] + 1)");
+ expression.Parameters["value"] = 20;
+ expression.EvaluateFunction += delegate(string name, FunctionArgs args)
+ {
+ if (name == "twice")
+ {
+ args.Result = Convert.ToInt32(args.Parameters[0].Evaluate()) * 2;
+ }
+ };
+
+ AssertObject(42, typeof(int), expression.Evaluate());
+ });
+ }
+
+ private static void AssertExpression(string expression, object expectedValue, Type expectedType)
+ {
+ AssertObject(expectedValue, expectedType, new Expression(expression).Evaluate());
+ }
+
+ private static void AssertOperation(BinaryOperation operation, object left, object right, object expectedValue, Type expectedType)
+ {
+ AssertObject(expectedValue, expectedType, operation(left, right));
+ }
+
+ private static void AssertObject(object expectedValue, Type expectedType, object actualValue)
+ {
+ if (actualValue == null)
+ {
+ throw new InvalidOperationException("Expected value <" + expectedValue + "> but got null.");
+ }
+
+ if (actualValue.GetType() != expectedType)
+ {
+ throw new InvalidOperationException("Expected type <" + expectedType.FullName + "> but got <" + actualValue.GetType().FullName + ">.");
+ }
+
+ AssertEqual(expectedValue, actualValue);
+ }
+
+ private static void AssertNull(object actualValue)
+ {
+ if (actualValue != null)
+ {
+ throw new InvalidOperationException("Expected null but got <" + actualValue + ">.");
+ }
+ }
+
+ private static void AssertEqual(object expectedValue, object actualValue)
+ {
+ if (!object.Equals(expectedValue, actualValue))
+ {
+ throw new InvalidOperationException("Expected <" + expectedValue + "> but got <" + actualValue + ">.");
+ }
+ }
+
+ private static void AssertThrows(Action action, Type expectedExceptionType, string expectedMessageFragment)
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception exception)
+ {
+ if (exception.GetType() != expectedExceptionType)
+ {
+ throw new InvalidOperationException("Expected exception <" + expectedExceptionType.FullName + "> but got <" + exception.GetType().FullName + ">.", exception);
+ }
+
+ if (exception.Message.IndexOf(expectedMessageFragment, StringComparison.Ordinal) < 0)
+ {
+ throw new InvalidOperationException("Expected message <" + exception.Message + "> to contain <" + expectedMessageFragment + ">.");
+ }
+
+ return;
+ }
+
+ throw new InvalidOperationException("Expected exception <" + expectedExceptionType.FullName + ">.");
+ }
+
+ private static void Run(string name, Action action)
+ {
+ try
+ {
+ action();
+ _passed++;
+ Console.WriteLine("PASS " + name);
+ }
+ catch (Exception exception)
+ {
+ _failed++;
+ Console.Error.WriteLine("FAIL " + name);
+ Console.Error.WriteLine(exception);
+ }
+ }
+ }
+}
diff --git a/Evaluant.Calculator.Play/NCalc.Play.csproj b/Evaluant.Calculator.Play/NCalc.Play.csproj
index 4823ad6..b623a3b 100644
--- a/Evaluant.Calculator.Play/NCalc.Play.csproj
+++ b/Evaluant.Calculator.Play/NCalc.Play.csproj
@@ -1,95 +1,15 @@
-
-
-
- Debug
- AnyCPU
- 9.0.30729
- 2.0
- {4183699B-5B7E-4294-9807-9709BAD86477}
- Exe
- Properties
- NCalc.Play
- Play
-
-
- 3.5
-
-
- v3.5
- publish\
- true
- Disk
- false
- Foreground
- 7
- Days
- false
- false
- true
- 0
- 1.0.0.%2a
- false
- false
- true
-
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
- AllRules.ruleset
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
- AllRules.ruleset
-
-
-
-
-
-
-
-
-
-
-
-
- {5F014003-50D8-49E0-8AFE-91D38DCCC97C}
- NCalc
-
-
-
-
- False
- .NET Framework 3.5 SP1 Client Profile
- false
-
-
- False
- .NET Framework 3.5 SP1
- true
-
-
- False
- Windows Installer 3.1
- true
-
-
-
-
-
\ No newline at end of file
+
+
+ Exe
+ net10.0
+ NCalc.Play
+ NCalc.Play
+ disable
+ disable
+ latest
+
+
+
+
+
+
diff --git a/Evaluant.Calculator.Play/Program.cs b/Evaluant.Calculator.Play/Program.cs
index a4a847f..4441fd4 100644
--- a/Evaluant.Calculator.Play/Program.cs
+++ b/Evaluant.Calculator.Play/Program.cs
@@ -1,33 +1,29 @@
-using System;
-
-namespace NCalc.Play
-{
- ///
- /// Summary description for Program.
- ///
- public class Program
- {
- public static void Main(string[] args)
- {
-
-
- var expressions = new[]
- {
- "2 * (3 + 5)",
- "2 * (2*(2*(2+1)))",
- "10 % 3",
- "false || not (false and true)",
- "3 > 2 and 1 <= (3-2)",
- "3 % 2 != 10 % 3",
- "if( [age] >= 18, 'majeur', 'mineur')",
- "CalculateBenefits([user]) * [Taxes]"
- };
-
- foreach (string expression in expressions)
- Console.WriteLine("{0} = {1}",
- expression,
- new Expression(expression).Evaluate());
-
- }
- }
-}
+using System;
+
+namespace NCalc.Play
+{
+ public static class Program
+ {
+ public static void Main()
+ {
+ string[] expressions =
+ {
+ "2 * (3 + 5)",
+ "2 * (2*(2*(2+1)))",
+ "10 % 3",
+ "false || not (false and true)",
+ "3 > 2 and 1 <= (3-2)",
+ "3 % 2 != 10 % 3",
+ "if([age] >= 18, 'adult', 'minor')",
+ };
+
+ foreach (string expressionText in expressions)
+ {
+ Expression expression = new Expression(expressionText);
+ expression.Parameters["age"] = 42;
+
+ Console.WriteLine("{0} = {1}", expressionText, expression.Evaluate());
+ }
+ }
+ }
+}
diff --git a/Evaluant.Calculator.Play/Properties/AssemblyInfo.cs b/Evaluant.Calculator.Play/Properties/AssemblyInfo.cs
deleted file mode 100644
index b00a2e2..0000000
--- a/Evaluant.Calculator.Play/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Evaluant.Calculator.Play")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Evaluant.Calculator.Play")]
-[assembly: AssemblyCopyright("Copyright © 2007")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("a49c6a87-4568-4ec3-a162-374db1fccd80")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Evaluant.Calculator.Tests/AntlrCompatibilityTests.cs b/Evaluant.Calculator.Tests/AntlrCompatibilityTests.cs
new file mode 100644
index 0000000..82bd143
--- /dev/null
+++ b/Evaluant.Calculator.Tests/AntlrCompatibilityTests.cs
@@ -0,0 +1,268 @@
+using System;
+using System.Collections;
+using System.IO;
+using System.Text;
+using Antlr.Runtime;
+using Antlr.Runtime.Tree;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace NCalc.Tests
+{
+ [TestClass]
+ public class AntlrCompatibilityTests
+ {
+ [TestMethod]
+ public void ParserReturnScope_WithLegacyObjectProperties_DoesStoreTokensAndTree()
+ {
+ CommonToken start = new CommonToken(1, "start");
+ CommonToken stop = new CommonToken(2, "stop");
+ CommonTree tree = new CommonTree(start);
+ RuleReturnScope ruleReturnScope = new RuleReturnScope();
+ ParserRuleReturnScope parserRuleReturnScope = new ParserRuleReturnScope();
+ TreeRuleReturnScope treeRuleReturnScope = new TreeRuleReturnScope();
+
+ ruleReturnScope.Start = start;
+ ruleReturnScope.Stop = stop;
+ ruleReturnScope.Tree = tree;
+ parserRuleReturnScope.Start = start;
+ treeRuleReturnScope.Start = tree;
+
+ Assert.AreSame(start, ruleReturnScope.Start);
+ Assert.AreSame(stop, ruleReturnScope.Stop);
+ Assert.AreSame(tree, ruleReturnScope.Tree);
+ Assert.IsNull(ruleReturnScope.Template);
+ Assert.AreSame(start, parserRuleReturnScope.Start);
+ Assert.AreSame(tree, treeRuleReturnScope.Start);
+ }
+
+ [TestMethod]
+ public void Token_WithLegacyConstants_DoesExposeCurrentRuntimeValues()
+ {
+ AssertLegacyTokenValue(-1, Token.EOF);
+ AssertLegacyTokenValue(0, Token.INVALID_TOKEN_TYPE);
+ AssertLegacyTokenValue(1, Token.EOR_TOKEN_TYPE);
+ AssertLegacyTokenValue(2, Token.DOWN);
+ AssertLegacyTokenValue(3, Token.UP);
+ AssertLegacyTokenValue(0, Token.DEFAULT_CHANNEL);
+ AssertLegacyTokenValue(99, Token.HIDDEN_CHANNEL);
+ AssertLegacyTokenValue(4, Token.MIN_TOKEN_TYPE);
+ Assert.IsNotNull(Token.EOF_TOKEN);
+ Assert.IsNotNull(Token.INVALID_TOKEN);
+ Assert.IsNotNull(Token.SKIP_TOKEN);
+ }
+
+ [TestMethod]
+ public void AntlrFileStream_WithUtf8File_DoesLoadTextAndSourceName()
+ {
+ string filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".txt");
+
+ try
+ {
+ File.WriteAllText(filePath, "abc", Encoding.UTF8);
+
+ ANTLRFileStream stream = new ANTLRFileStream(filePath, Encoding.UTF8);
+
+ Assert.AreEqual(filePath, stream.SourceName);
+ Assert.AreEqual(3, stream.Count);
+ Assert.AreEqual("abc", stream.Substring(0, 3));
+ LegacyAntlrFileStream derivedStream = new LegacyAntlrFileStream();
+
+ derivedStream.Load(filePath, Encoding.UTF8);
+ Assert.IsNull(derivedStream.SourceName);
+ Assert.IsNull(derivedStream.FileName);
+ Assert.AreEqual(3, derivedStream.Count);
+ }
+ finally
+ {
+ if (File.Exists(filePath))
+ {
+ File.Delete(filePath);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void CollectionUtils_WithLegacyInputs_DoesFormatCollections()
+ {
+ Hashtable dictionary = new Hashtable();
+ dictionary["one"] = 1;
+ ArrayList list = new ArrayList();
+ list.Add("a");
+ list.Add("b");
+
+ string dictionaryText = Antlr.Runtime.Collections.CollectionUtils.DictionaryToString(dictionary);
+ string listText = Antlr.Runtime.Collections.CollectionUtils.ListToString(list);
+
+ StringAssert.Contains(dictionaryText, "one=1");
+ Assert.AreEqual("[a, b]", listText);
+ Assert.AreEqual("null", Antlr.Runtime.Collections.CollectionUtils.DictionaryToString(null));
+ Assert.AreEqual("null", Antlr.Runtime.Collections.CollectionUtils.ListToString(null));
+ }
+
+ [TestMethod]
+ public void HashList_WithDictionaryOperations_DoesPreserveInsertionAndLookup()
+ {
+ Antlr.Runtime.Collections.HashList hashList = new Antlr.Runtime.Collections.HashList(2);
+ hashList.Add("first", 1);
+ hashList["second"] = 2;
+
+ Assert.AreEqual(2, hashList.Count);
+ Assert.IsTrue(hashList.Contains("first"));
+ Assert.AreEqual(2, hashList["second"]);
+ Assert.IsFalse(hashList.IsReadOnly);
+ Assert.IsFalse(hashList.IsFixedSize);
+ Assert.IsFalse(hashList.IsSynchronized);
+ Assert.IsNotNull(hashList.SyncRoot);
+ CollectionAssert.Contains((ICollection)hashList.Keys, "first");
+ CollectionAssert.Contains((ICollection)hashList.Values, 2);
+
+ DictionaryEntry[] entries = new DictionaryEntry[2];
+ hashList.CopyTo(entries, 0);
+ CollectionAssert.Contains(new[] { entries[0].Key, entries[1].Key }, "first");
+ CollectionAssert.Contains(new[] { entries[0].Key, entries[1].Key }, "second");
+
+ IDictionaryEnumerator enumerator = hashList.GetEnumerator();
+ Assert.IsTrue(enumerator.MoveNext());
+
+ hashList.Remove("first");
+ Assert.IsFalse(hashList.Contains("first"));
+ hashList.Clear();
+ Assert.AreEqual(0, hashList.Count);
+ }
+
+ [TestMethod]
+ public void StackList_WithPushPopOperations_DoesBehaveLikeStack()
+ {
+ Antlr.Runtime.Collections.StackList stackList = new Antlr.Runtime.Collections.StackList();
+ stackList.Push("first");
+ stackList.Push("second");
+
+ Assert.AreEqual("second", stackList.Peek());
+ Assert.AreEqual("second", stackList.Pop());
+ Assert.AreEqual("first", stackList.Pop());
+ Assert.Throws(delegate { stackList.Peek(); });
+ }
+
+ [TestMethod]
+ public void Stats_WithLegacyHelpers_DoesCalculateValuesAndWriteReport()
+ {
+ int[] values = { 1, 2, 3 };
+ string reportPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".txt");
+
+ try
+ {
+ Assert.AreEqual(2d, Antlr.Runtime.Misc.Stats.Avg(values));
+ Assert.AreEqual(3, Antlr.Runtime.Misc.Stats.Max(values));
+ Assert.AreEqual(1, Antlr.Runtime.Misc.Stats.Min(values));
+ Assert.AreEqual(6, Antlr.Runtime.Misc.Stats.Sum(values));
+ Assert.AreEqual(1d, Antlr.Runtime.Misc.Stats.Stddev(values));
+ Assert.AreEqual(0d, Antlr.Runtime.Misc.Stats.Avg(new int[0]));
+ Assert.AreEqual(0, Antlr.Runtime.Misc.Stats.Max(new int[0]));
+ Assert.AreEqual(0, Antlr.Runtime.Misc.Stats.Min(new int[0]));
+ Assert.AreEqual(0d, Antlr.Runtime.Misc.Stats.Stddev(new[] { 1 }));
+
+ Antlr.Runtime.Misc.Stats.WriteReport(reportPath, "report");
+ Assert.AreEqual("report", File.ReadAllText(Antlr.Runtime.Misc.Stats.GetAbsoluteFileName(reportPath)));
+ }
+ finally
+ {
+ if (File.Exists(reportPath))
+ {
+ File.Delete(reportPath);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void ErrorManager_WithLegacyErrors_DoesThrowInvalidOperationException()
+ {
+ Assert.Throws(delegate { Antlr.Runtime.Misc.ErrorManager.Error("error"); });
+ Assert.Throws(delegate { Antlr.Runtime.Misc.ErrorManager.InternalError("error"); });
+
+ InvalidOperationException exception = Assert.Throws(
+ delegate { Antlr.Runtime.Misc.ErrorManager.InternalError("error", new ApplicationException("inner")); });
+
+ Assert.IsInstanceOfType(exception.InnerException);
+ }
+
+ [TestMethod]
+ public void TreeCompatibility_WithLegacyTypes_DoesExposeTreeHelpers()
+ {
+ CommonTree tree = new CommonTree(new CommonToken(1, "root"));
+ CommonTokenStream tokens = new CommonTokenStream(new NCalcLexer(new ANTLRStringStream("1 + 2")));
+ IToken start = new CommonToken(1, "start");
+ IToken stop = new CommonToken(2, "stop");
+ Antlr.Runtime.CommonErrorNode errorNode = new Antlr.Runtime.CommonErrorNode(
+ tokens,
+ start,
+ stop,
+ new RecognitionException(tokens));
+ Antlr.Runtime.Tree.UnBufferedTreeNodeStream stream = new Antlr.Runtime.Tree.UnBufferedTreeNodeStream(tree);
+
+ stream.HasUniqueNavigationNodes = true;
+
+ Assert.IsNotNull(Antlr.Runtime.Tree.Tree.INVALID_NODE);
+ Assert.IsNotNull(errorNode);
+ Assert.IsTrue(stream.HasUniqueNavigationNodes);
+ Assert.IsGreaterThanOrEqualTo(0, stream.Size());
+ Assert.AreEqual(-1, stream.Index());
+ Assert.Throws(delegate { stream.Get(0); });
+ Assert.IsNull(stream.Current);
+ Assert.IsTrue(stream.MoveNext());
+ Assert.AreSame(tree, stream.Current);
+ Assert.IsFalse(stream.MoveNext());
+ Assert.IsNull(stream.Current);
+ stream.Reset();
+ Assert.IsNull(stream.Current);
+ }
+
+ [TestMethod]
+ public void RewriteRuleElementStream_WithLegacyGenericType_DoesExposeStreamMembers()
+ {
+ LegacyRewriteRuleElementStream stream = new LegacyRewriteRuleElementStream(new CommonTreeAdaptor());
+
+ stream.Add("value");
+
+ Assert.IsTrue(stream.HasNext());
+ Assert.AreEqual(1, stream.Size());
+ Assert.AreEqual("value", stream.ReadNext());
+ }
+
+ [TestMethod]
+ public void SynPredPointer_WithLegacyDelegate_DoesInvoke()
+ {
+ bool invoked = false;
+ SynPredPointer synPredPointer = delegate { invoked = true; };
+
+ synPredPointer();
+
+ Assert.IsTrue(invoked);
+ }
+
+ private sealed class LegacyRewriteRuleElementStream : RewriteRuleElementStream