diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
index 8f4d5d8735..73c4a478db 100644
--- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
+++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Globalization;
+using System.Numerics;
using System.Text.Json;
using System.Text.RegularExpressions;
using Altinn.App.Core.Models;
@@ -11,7 +12,7 @@ namespace Altinn.App.Core.Internal.Expressions;
///
/// Static class used to evaluate expressions. Holds the implementation for all expression functions.
///
-public static class ExpressionEvaluator
+public static partial class ExpressionEvaluator
{
///
/// Shortcut for evaluating a boolean expression on a given property on a
@@ -127,6 +128,10 @@ internal static async Task EvaluateExpression_internal(
ExpressionFunction.argv => Argv(args, positionalArguments),
ExpressionFunction.gatewayAction => state.GetGatewayAction(),
ExpressionFunction.language => state.GetLanguage(),
+ ExpressionFunction.plus => Plus(args),
+ ExpressionFunction.minus => Minus(args),
+ ExpressionFunction.multiply => Multiply(args),
+ ExpressionFunction.divide => Divide(args),
ExpressionFunction.INVALID => throw new ExpressionEvaluatorTypeErrorException(
$"Function {expr.Args.FirstOrDefault()} not implemented in backend {expr}"
),
@@ -610,8 +615,8 @@ private static int StringLength(ExpressionValue[] args)
throw new ExpressionEvaluatorTypeErrorException($"Expected 2-3 arguments, got {args.Length}");
}
string? subject = args[0].ToStringForEquals();
- double? start = PrepareNumericArg(args[1]);
- double? end = args.Length == 3 ? PrepareNumericArg(args[2]) : null;
+ double? start = PrepareNumericArg(args[1]);
+ double? end = args.Length == 3 ? PrepareNumericArg(args[2]) : null;
bool hasEnd = args.Length == 3;
if (start == null || (hasEnd && end == null))
@@ -659,13 +664,13 @@ private static string Round(ExpressionValue[] args)
);
}
- var number = PrepareNumericArg(args[0]) ?? 0;
+ var number = PrepareNumericArg(args[0]) ?? 0;
int precision = 0;
if (args.Length == 2)
{
- precision = (int)(PrepareNumericArg(args[1]) ?? 0);
+ precision = (int)(PrepareNumericArg(args[1]) ?? 0);
}
return number.ToString($"N{precision}", CultureInfo.InvariantCulture);
@@ -806,33 +811,51 @@ ExpressionValue[] args
return !PrepareBooleanArg(args[0]);
}
- private static (double?, double?) PrepareNumericArgs(ExpressionValue[] args)
+ private static (T?, T?) PrepareNumericArgs(ExpressionValue[] args)
+ where T : struct, INumber
{
if (args.Length != 2)
{
- throw new ExpressionEvaluatorTypeErrorException("Invalid number of args for compare");
+ throw new ExpressionEvaluatorTypeErrorException("Invalid number of args");
}
- var a = PrepareNumericArg(args[0]);
+ var a = PrepareNumericArg(args[0]);
- var b = PrepareNumericArg(args[1]);
+ var b = PrepareNumericArg(args[1]);
return (a, b);
}
- private static double? PrepareNumericArg(ExpressionValue arg)
+ private static T? PrepareNumericArg(ExpressionValue arg)
+ where T : struct, INumber
{
return arg.ValueKind switch
{
JsonValueKind.True or JsonValueKind.False or JsonValueKind.Array or JsonValueKind.Object =>
throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {arg}"),
- JsonValueKind.String => ParseNumber(arg.String, throwException: true),
- JsonValueKind.Number => arg.Number,
+ JsonValueKind.String => ParseNumber(arg.String, throwException: true),
+ JsonValueKind.Number => CastNumber(arg.Number),
_ => null,
};
}
+ private static T? CastNumber(double? number)
+ where T : struct, INumber
+ {
+ if (typeof(T) != typeof(decimal))
+ {
+ return number.HasValue ? T.CreateChecked(number.Value) : null;
+ }
+
+ if (number.HasValue && ValidFloatingPoint(number.Value))
+ {
+ return T.CreateChecked(number.Value);
+ }
+
+ return null;
+ }
+
private static ExpressionValue IfImpl(ExpressionValue[] args)
{
if (args.Length == 2)
@@ -861,11 +884,13 @@ private static ExpressionValue IfImpl(ExpressionValue[] args)
);
}
- private static readonly Regex _numberRegex = new Regex(@"^-?\d+(\.\d+)?$");
-
- internal static double? ParseNumber(string s, bool throwException = true)
+ ///
+ /// Parses a number from a string representation.
+ ///
+ internal static T? ParseNumber(string s, bool throwException = true)
+ where T : struct, INumber
{
- if (_numberRegex.IsMatch(s) && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
+ if (NumberRegex().IsMatch(s) && T.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
{
return d;
}
@@ -879,7 +904,7 @@ private static ExpressionValue IfImpl(ExpressionValue[] args)
private static bool LessThan(ExpressionValue[] args)
{
- var (a, b) = PrepareNumericArgs(args);
+ var (a, b) = PrepareNumericArgs(args);
if (a is null || b is null)
{
@@ -888,9 +913,37 @@ private static bool LessThan(ExpressionValue[] args)
return a < b; // Actual implementation
}
+ private static double? Plus(ExpressionValue[] args)
+ {
+ var (a, b) = PrepareNumericArgs(args);
+ return PerformArithmetic(a, b, (x, y) => x + y);
+ }
+
+ private static double? Minus(ExpressionValue[] args)
+ {
+ var (a, b) = PrepareNumericArgs(args);
+ return PerformArithmetic(a, b, (x, y) => x - y);
+ }
+
+ private static double? Multiply(ExpressionValue[] args)
+ {
+ var (a, b) = PrepareNumericArgs(args);
+ return PerformArithmetic(a, b, (x, y) => x * y);
+ }
+
+ private static double? Divide(ExpressionValue[] args)
+ {
+ var (a, b) = PrepareNumericArgs(args);
+ if (a != null && b == 0)
+ {
+ throw new ExpressionEvaluatorTypeErrorException("The second argument is 0, cannot divide by 0");
+ }
+ return PerformArithmetic(a, b, (x, y) => x / y);
+ }
+
private static bool LessThanEq(ExpressionValue[] args)
{
- var (a, b) = PrepareNumericArgs(args);
+ var (a, b) = PrepareNumericArgs(args);
if (a is null || b is null)
{
@@ -901,7 +954,7 @@ private static bool LessThanEq(ExpressionValue[] args)
private static bool GreaterThan(ExpressionValue[] args)
{
- var (a, b) = PrepareNumericArgs(args);
+ var (a, b) = PrepareNumericArgs(args);
if (a is null || b is null)
{
@@ -912,7 +965,7 @@ private static bool GreaterThan(ExpressionValue[] args)
private static bool GreaterThanEq(ExpressionValue[] args)
{
- var (a, b) = PrepareNumericArgs(args);
+ var (a, b) = PrepareNumericArgs(args);
if (a is null || b is null)
{
@@ -943,7 +996,7 @@ private static ExpressionValue Argv(ExpressionValue[] args, ExpressionValue[]? p
throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument(s), got {args.Length}");
}
- var index = (int?)PrepareNumericArg(args[0]);
+ var index = (int?)PrepareNumericArg(args[0]);
if (!index.HasValue)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value \"{args[0]}\"");
@@ -960,4 +1013,47 @@ private static ExpressionValue Argv(ExpressionValue[] args, ExpressionValue[]? p
return positionalArguments[index.Value];
}
+
+ ///
+ /// Performs arithmetic operation using decimal precision to avoid floating point precision issues.
+ /// Converts doubles to decimal, performs the operation, and converts back to double.
+ ///
+ /// First operand
+ /// Second operand
+ /// Function that performs the arithmetic operation on two decimals
+ /// Result of the operation as double, or null if any operand is null
+ private static double? PerformArithmetic(
+ decimal? aDecimal,
+ decimal? bDecimal,
+ Func operation
+ )
+ {
+ if (aDecimal.HasValue is false || bDecimal.HasValue is false)
+ {
+ return null;
+ }
+
+ var result = operation(aDecimal.Value, bDecimal.Value);
+
+ return (double)result;
+ }
+
+ private static bool ValidFloatingPoint(double value)
+ {
+ if (
+ double.IsNaN(value)
+ || double.IsInfinity(value)
+ || value > (double)decimal.MaxValue
+ || value < (double)decimal.MinValue
+ )
+ {
+ throw new ExpressionEvaluatorTypeErrorException(
+ $"Cannot convert non-finite or out-of-range number to decimal: {value}"
+ );
+ }
+ return true;
+ }
+
+ [GeneratedRegex(@"^-?\d+(\.\d+)?$")]
+ private static partial Regex NumberRegex();
}
diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
index be4e2d351c..ce5e9b0fa0 100644
--- a/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
+++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
@@ -141,9 +141,7 @@ public static ExpressionValue FromObject(object? value)
uint numberValue => numberValue,
long numberValue => numberValue,
ulong numberValue => numberValue,
- decimal numberValue =>
- (double?)numberValue // expressions uses double which needs an explicit cast
- ,
+ decimal numberValue => (double?)numberValue, // expressions uses double which needs an explicit cast
DateTime dateTimeValue => JsonSerializer
.Serialize(dateTimeValue, _unsafeSerializerOptionsForSerializingDates)
.Trim(
@@ -422,7 +420,7 @@ public override int GetHashCode()
"0" => false,
{ } sValue when sValue.Equals("true", StringComparison.OrdinalIgnoreCase) => true,
{ } sValue when sValue.Equals("false", StringComparison.OrdinalIgnoreCase) => false,
- _ => ExpressionEvaluator.ParseNumber(String, throwException: false) switch
+ _ => ExpressionEvaluator.ParseNumber(String, throwException: false) switch
{
1 => true,
0 => false,
@@ -525,7 +523,7 @@ public bool TryDeserialize(Type type, out object? result)
// Support parsing numbers from strings for numeric types
case JsonValueKind.String when IsSupportedNumericType(underlyingType):
{
- var parsedNumber = ExpressionEvaluator.ParseNumber(String, throwException: false);
+ var parsedNumber = ExpressionEvaluator.ParseNumber(String, throwException: false);
if (parsedNumber.HasValue)
{
result = Convert.ChangeType(parsedNumber.Value, underlyingType, CultureInfo.InvariantCulture);
@@ -560,7 +558,7 @@ public bool TryDeserialize(Type type, out object? result)
try
{
var json = ToString();
- result = JsonSerializer.Deserialize(json, type, _unsafeSerializerOptionsForSerializingDates);
+ result = JsonSerializer.Deserialize(json, type);
return true;
}
catch (JsonException)
diff --git a/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs b/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs
index f799c95a9f..57ecfe5d26 100644
--- a/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs
+++ b/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs
@@ -205,4 +205,24 @@ public enum ExpressionFunction
/// If no translations exist for the current language, we will use the resources for "nb"
///
text,
+
+ ///
+ /// Adding numbers. Use a period (.) as the decimals separator. Must be numeric values.
+ ///
+ plus,
+
+ ///
+ /// Subtracting all preceding values from the first. Use a period (.) as the decimals separator. Must be numeric values.
+ ///
+ minus,
+
+ ///
+ /// Multiplying numbers. Use a period (.) as the decimals separator. Must be numeric values.
+ ///
+ multiply,
+
+ ///
+ /// Divide numbers. Use a period (.) as the decimals separator. Must be numeric values.
+ ///
+ divide,
}
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs
index d1a0d9de3b..a45da8a56f 100644
--- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs
@@ -10,6 +10,16 @@ namespace Altinn.App.Core.Tests.LayoutExpressions.CommonTests;
public class ExpressionTestCaseRoot
{
+ public ExpressionTestCaseRoot(TestCaseItem testCaseItem)
+ {
+ Name = testCaseItem.Name;
+ Expression = testCaseItem.Expression;
+ Expects = testCaseItem.Expects;
+ ExpectsFailure = testCaseItem.ExpectsFailure;
+ }
+
+ public ExpressionTestCaseRoot() { }
+
[JsonIgnore]
public string? Filename { get; set; }
@@ -42,6 +52,9 @@ public class ExpressionTestCaseRoot
public class TestCaseItem
{
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
[JsonPropertyName("expression")]
public required Expression Expression { get; set; }
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs
index 5ce7e4a049..7d2ddbfd8f 100644
--- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using Altinn.App.Core.Internal.Expressions;
+using Altinn.App.Core.Models.Expressions;
using Altinn.App.Core.Models.Layout;
using Altinn.App.Core.Tests.LayoutExpressions.TestUtilities;
using Altinn.App.Core.Tests.TestUtils;
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs
index ec7c206c8e..212b823635 100644
--- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs
@@ -203,6 +203,26 @@ public TestFunctions(ITestOutputHelper output)
[SharedTest("stringLength")]
public async Task StringLength_Theory(string testName, string folder) => await RunTestCase(testName, folder);
+ [Theory]
+ [SharedTestCases("plus")]
+ public async Task Plus_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) =>
+ await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem));
+
+ [Theory]
+ [SharedTestCases("minus")]
+ public async Task Minus_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) =>
+ await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem));
+
+ [Theory]
+ [SharedTestCases("multiply")]
+ public async Task Multiply_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) =>
+ await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem));
+
+ [Theory]
+ [SharedTestCases("divide")]
+ public async Task Divide_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) =>
+ await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem));
+
[Theory]
[SharedTest("round")]
public async Task Round_Theory(string testName, string folder) => await RunTestCase(testName, folder);
@@ -237,10 +257,15 @@ private static async Task LoadTestCase(string file, stri
private async Task RunTestCase(string testName, string folder)
{
var test = await LoadTestCase(testName, folder);
- _output.WriteLine(test.Name);
+ await RunTestCase(testName, test);
+ }
+
+ private async Task RunTestCase(string testName, ExpressionTestCaseRoot test)
+ {
+ _output.WriteLine(testName);
_output.WriteLine($"{test.Folder}{Path.DirectorySeparatorChar}{test.Filename}");
- _output.WriteLine(test.RawJson);
- _output.WriteLine(test.FullPath);
+ _output.WriteLine(test.RawJson ?? "");
+ _output.WriteLine(test.FullPath ?? "");
IInstanceDataAccessor dataAccessor;
List dataTypes = new();
@@ -316,16 +341,16 @@ private async Task RunTestCase(string testName, string folder)
componentModel = new LayoutModel([layout], null);
}
- var appRewourcesMock = new Mock(MockBehavior.Strict);
+ var appResourcesMock = new Mock(MockBehavior.Strict);
var language = test.ProfileSettings?.Language ?? "nb";
- appRewourcesMock
+ appResourcesMock
.Setup(ar => ar.GetTexts(It.IsAny(), It.IsAny(), language))
.ReturnsAsync(new TextResource() { Resources = test.TextResources ?? [] });
var translationService = new TranslationService(
new Core.Models.AppIdentifier("org", "app"),
- appRewourcesMock.Object,
+ appResourcesMock.Object,
FakeLoggerXunit.Get(_output)
);
@@ -385,6 +410,7 @@ private async Task RunTestCaseItem(
object?[]? positionalArguments
)
{
+ _output.WriteLine(test.Name ?? "");
if (test.ExpectsFailure is not null)
{
_output.WriteLine($"Expecting failure: {test.ExpectsFailure}");
@@ -457,7 +483,10 @@ public void Ensure_tests_For_All_Folders()
var testMethods = this.GetType()
.GetMethods()
.Select(m =>
- m.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(SharedTestAttribute))
+ m.CustomAttributes.FirstOrDefault(ca =>
+ ca.AttributeType == typeof(SharedTestAttribute)
+ || ca.AttributeType == typeof(SharedTestCasesAttribute)
+ )
?.ConstructorArguments.FirstOrDefault()
.Value
)
@@ -473,3 +502,7 @@ public class SharedTestAttribute(string folder)
: FileNamesInFolderDataAttribute(
Path.Join("LayoutExpressions", "CommonTests", "shared-tests", "functions", folder)
) { }
+
+// Can be used when you only want to run the tests listed in the testCases array in the json file
+public class SharedTestCasesAttribute(string folder)
+ : TestCasesAttribute(Path.Join("LayoutExpressions", "CommonTests", "shared-tests", "functions", folder)) { }
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
index c935f44554..d8c47ca911 100644
--- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using Altinn.App.Core.Internal.Expressions;
+using Altinn.App.Core.Models.Expressions;
using Altinn.App.Core.Models.Layout;
using Altinn.App.Core.Tests.LayoutExpressions.TestUtilities;
using Altinn.App.Core.Tests.TestUtils;
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/divide/divide.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/divide/divide.json
new file mode 100644
index 0000000000..83f4f37025
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/divide/divide.json
@@ -0,0 +1,150 @@
+{
+ "name": "Divide tests",
+ "testCases": [
+ {
+ "name": "Should divide the first argument with the second",
+ "expression": ["divide", 0.1, 0.2],
+ "expects": 0.5
+ },
+ {
+ "name": "Should divide the first argument with the second",
+ "expression": ["divide", -0.1, -0.2],
+ "expects": 0.5
+ },
+ {
+ "name": "Should return null when both of the arguments are null",
+ "expression": ["divide", null, null],
+ "expects": null
+ },
+ {
+ "name": "Should return null when one of the arguments is null",
+ "expression": ["divide", null, 2.1],
+ "expects": null
+ },
+ {
+ "name": "Should divide two integers",
+ "expression": ["divide", 30, 6],
+ "expects": 5
+ },
+ {
+ "name": "Should divide [float, integer]",
+ "expression": ["divide", 30.0, 6],
+ "expects": 5.0
+ },
+ {
+ "name": "Should divide [string, integer] when string is valid number",
+ "expression": ["divide", "30", 6],
+ "expects": 5
+ },
+ {
+ "name": "Should divide [string, float] when string is valid number",
+ "expression": ["divide", "22.0", 4.0],
+ "expects": 5.5
+ },
+ {
+ "name": "Should handle dividing zero by a number",
+ "expression": ["divide", 0, 42],
+ "expects": 0
+ },
+ {
+ "name": "Should divide negative by positive",
+ "expression": ["divide", -30, 6],
+ "expects": -5
+ },
+ {
+ "name": "Should divide two negative numbers",
+ "expression": ["divide", -30, -6],
+ "expects": 5
+ },
+ {
+ "name": "Should divide numbers in scientific notation",
+ "expression": ["divide", 6e3, 2e3],
+ "expects": 3
+ },
+ {
+ "name": "Should divide numbers with negative exponents",
+ "expression": ["divide", 6e-3, 2e-3],
+ "expects": 3
+ },
+ {
+ "name": "Should divide scientific notation by regular number",
+ "expression": ["divide", 5e2, 10],
+ "expects": 50
+ },
+ {
+ "name": "Should fail with invalid string that cannot be parsed as number",
+ "expression": ["divide", 100, "not a number"],
+ "expectsFailure": "Expected number, got value \"not a number\""
+ },
+ {
+ "name": "Should fail with string with no decimals after dot",
+ "expression": ["divide", "55.", 5],
+ "expectsFailure": "Expected number, got value \"55.\""
+ },
+ {
+ "name": "Should handle very large numbers",
+ "expression": ["divide", 1000000000000, 1000000],
+ "expects": 1000000
+ },
+ {
+ "name": "Should handle very small numbers",
+ "expression": ["divide", 0.00000001, 0.0001],
+ "expects": 0.0001
+ },
+ {
+ "name": "Should divide string scientific notation by number",
+ "expression": ["divide", "5e2", 10],
+ "expectsFailure": "Expected number, got value \"5e2\""
+ },
+ {
+ "name": "Should throw exception when less than two arguments are provided",
+ "expression": ["divide", 2.1],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should throw exception when more than two arguments are present",
+ "expression": ["divide", 0.1, 0.2, 15, 1.50],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should divide negative by positive decimal",
+ "expression": ["divide", -0.1, 0.2],
+ "expects": -0.5
+ },
+ {
+ "name": "Should return null when dividend is null and divisor is zero",
+ "expression": ["divide", null, 0],
+ "expects": null
+ },
+ {
+ "name": "Should throw exception when trying to divide by zero",
+ "expression": ["divide", 15, 0],
+ "expectsFailure": "The second argument is 0, cannot divide by 0"
+ },
+ {
+ "name": "Should fail with [boolean, integer]",
+ "expression": ["divide", true, 100],
+ "expectsFailure": "Expected number, got value true"
+ },
+ {
+ "name": "Should throw exception when any argument is double max value",
+ "expression": ["divide", 0, 1.7976931348623157E+308],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is double min value",
+ "expression": ["divide", -1.7976931348623157E+308, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float max value",
+ "expression": ["divide", 3.4028235E+38, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float min value",
+ "expression": ["divide", 0, -3.402823e+38],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ }
+ ]
+}
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/minus/minus.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/minus/minus.json
new file mode 100644
index 0000000000..86a92d9c68
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/minus/minus.json
@@ -0,0 +1,140 @@
+{
+ "name": "Minus tests",
+ "testCases": [
+ {
+ "name": "Should subtract the second argument from the first",
+ "expression": ["minus", 0.1, 0.2],
+ "expects": -0.1
+ },
+ {
+ "name": "Should subtract the second negative argument from the first",
+ "expression": ["minus", -0.1, -0.2],
+ "expects": 0.1
+ },
+ {
+ "name": "Should return null when both of the arguments are null",
+ "expression": ["minus", null, null],
+ "expects": null
+ },
+ {
+ "name": "Should return null when one of the arguments is null",
+ "expression": ["minus", null, 2.1],
+ "expects": null
+ },
+ {
+ "name": "Should subtract two integers",
+ "expression": ["minus", 50, 20],
+ "expects": 30
+ },
+ {
+ "name": "Should subtract [float, integer]",
+ "expression": ["minus", 50.5, 20],
+ "expects": 30.5
+ },
+ {
+ "name": "Should subtract [string, integer] when string is valid number",
+ "expression": ["minus", "50", 20],
+ "expects": 30
+ },
+ {
+ "name": "Should subtract [string, float] when string is valid number",
+ "expression": ["minus", "50.8", 20.3],
+ "expects": 30.5
+ },
+ {
+ "name": "Should handle subtracting zero",
+ "expression": ["minus", 42, 0],
+ "expects": 42
+ },
+ {
+ "name": "Should handle subtracting from zero",
+ "expression": ["minus", 0, 42],
+ "expects": -42
+ },
+ {
+ "name": "Should subtract negative from positive",
+ "expression": ["minus", 50, -30],
+ "expects": 80
+ },
+ {
+ "name": "Should subtract numbers in scientific notation",
+ "expression": ["minus", 5e3, 2e3],
+ "expects": 3000
+ },
+ {
+ "name": "Should subtract numbers with negative exponents",
+ "expression": ["minus", 5e-3, 2e-3],
+ "expects": 0.003
+ },
+ {
+ "name": "Should subtract scientific notation from regular number",
+ "expression": ["minus", 150, 1e2],
+ "expects": 50
+ },
+ {
+ "name": "Should fail with invalid string that cannot be parsed as number",
+ "expression": ["minus", 50, "not a number"],
+ "expectsFailure": "Expected number, got value \"not a number\""
+ },
+ {
+ "name": "Should fail with [boolean, integer]",
+ "expression": ["minus", false, 10],
+ "expectsFailure": "Expected number, got value false"
+ },
+ {
+ "name": "Should fail with string with no decimals after dot",
+ "expression": ["minus", "55.", 10],
+ "expectsFailure": "Expected number, got value \"55.\""
+ },
+ {
+ "name": "Should handle very large numbers",
+ "expression": ["minus", 9007199254740992, 1],
+ "expects": 9007199254740989
+ },
+ {
+ "name": "Should handle very small numbers",
+ "expression": ["minus", 0.0000003, 0.0000001],
+ "expects": 0.0000002
+ },
+ {
+ "name": "Should throw exception when less than two arguments are provided",
+ "expression": ["minus", 0.2],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should subtract string scientific notation from number",
+ "expression": ["minus", 150, "1e2"],
+ "expectsFailure": "Expected number, got value \"1e2\""
+ },
+ {
+ "name": "Should throw exception when more than two arguments are present",
+ "expression": ["minus", 0.1, 0.2, 15, 1.50],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should subtract negative from positive decimal",
+ "expression": ["minus", 50.55, -30.77],
+ "expects": 81.32
+ },
+ {
+ "name": "Should throw exception when any argument is double max value",
+ "expression": ["minus", 0, 1.7976931348623157E+308],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is double min value",
+ "expression": ["minus", -1.7976931348623157E+308, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float max value",
+ "expression": ["minus", 3.4028235E+38, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float min value",
+ "expression": ["minus", 0, -3.402823e+38],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ }
+ ]
+}
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/multiply/multiply.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/multiply/multiply.json
new file mode 100644
index 0000000000..ca3b0794ff
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/multiply/multiply.json
@@ -0,0 +1,140 @@
+{
+ "name": "Mutiply tests",
+ "testCases": [
+ {
+ "name": "Should multiply the two arguments",
+ "expression": ["multiply", 0.1, 0.2],
+ "expects": 0.02
+ },
+ {
+ "name": "Should return null when both of the arguments are null",
+ "expression": ["multiply", null, null],
+ "expects": null
+ },
+ {
+ "name": "Should multiply the two negative arguments",
+ "expression": ["multiply", -0.1, -0.2],
+ "expects": 0.02
+ },
+ {
+ "name": "Should return null when one of the arguments is null",
+ "expression": ["multiply", null, 2.1],
+ "expects": null
+ },
+ {
+ "name": "Should multiply two integers",
+ "expression": ["multiply", 5, 6],
+ "expects": 30
+ },
+ {
+ "name": "Should multiply [float, integer]",
+ "expression": ["multiply", 7.5, 4],
+ "expects": 30.0
+ },
+ {
+ "name": "Should multiply [string, integer] when string is valid number",
+ "expression": ["multiply", "5", 6],
+ "expects": 30
+ },
+ {
+ "name": "Should multiply [string, float] when string is valid number",
+ "expression": ["multiply", "5.5", 4.0],
+ "expects": 22.0
+ },
+ {
+ "name": "Should handle multiplying by zero",
+ "expression": ["multiply", 42, 0],
+ "expects": 0
+ },
+ {
+ "name": "Should multiply negative and positive numbers",
+ "expression": ["multiply", -5, 6],
+ "expects": -30
+ },
+ {
+ "name": "Should multiply two negative numbers",
+ "expression": ["multiply", -5, -6],
+ "expects": 30
+ },
+ {
+ "name": "Should multiply numbers in scientific notation",
+ "expression": ["multiply", 2e3, 3e2],
+ "expects": 600000
+ },
+ {
+ "name": "Should multiply numbers with negative exponents",
+ "expression": ["multiply", 2e-3, 3e-2],
+ "expects": 0.00006
+ },
+ {
+ "name": "Should multiply scientific notation with regular number",
+ "expression": ["multiply", 1e2, 5],
+ "expects": 500
+ },
+ {
+ "name": "Should fail with invalid string that cannot be parsed as number",
+ "expression": ["multiply", "not a number", 5],
+ "expectsFailure": "Expected number, got value \"not a number\""
+ },
+ {
+ "name": "Should fail with [boolean, integer]",
+ "expression": ["multiply", true, 5],
+ "expectsFailure": "Expected number, got value true"
+ },
+ {
+ "name": "Should fail with string with no decimals after dot",
+ "expression": ["multiply", "55.", 5],
+ "expectsFailure": "Expected number, got value \"55.\""
+ },
+ {
+ "name": "Should handle very large numbers",
+ "expression": ["multiply", 1000000, 1000000],
+ "expects": 1000000000000
+ },
+ {
+ "name": "Should handle very small numbers",
+ "expression": ["multiply", 0.0001, 0.0001],
+ "expects": 0.00000001
+ },
+ {
+ "name": "Should multiply string scientific notation with number",
+ "expression": ["multiply", "1e2", 5],
+ "expectsFailure": "Expected number, got value \"1e2\""
+ },
+ {
+ "name": "Should throw exception when less than two arguments are provided",
+ "expression": ["multiply", 0.1],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should throw exception when more than two arguments are present",
+ "expression": ["multiply", 0.1, 0.2, 15, 1.50],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should multiply negative and positive decimal numbers",
+ "expression": ["multiply", -5.55, 6.66],
+ "expects": -36.963
+ },
+ {
+ "name": "Should throw exception when any argument is double max value",
+ "expression": ["multiply", 0, 1.7976931348623157E+308],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is double min value",
+ "expression": ["multiply", -1.7976931348623157E+308, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float max value",
+ "expression": ["multiply", 3.4028235E+38, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float min value",
+ "expression": ["multiply", 0, -3.402823e+38],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ }
+ ]
+}
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/plus/plus.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/plus/plus.json
new file mode 100644
index 0000000000..21d32e9005
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/plus/plus.json
@@ -0,0 +1,130 @@
+{
+ "name": "Plus tests",
+ "testCases": [
+ {
+ "name": "Should add the two given numbers together",
+ "expression": ["plus", 0.1, 0.2],
+ "expects": 0.3
+ },
+ {
+ "name": "Should add two negative numbers together",
+ "expression": ["plus", -4, -0.2],
+ "expects": -4.2
+ },
+ {
+ "name": "Should return null when both of the arguments are null",
+ "expression": ["plus", null, null],
+ "expects": null
+ },
+ {
+ "name": "Should return null when one of the arguments is null",
+ "expression": ["plus", 4.3, null],
+ "expects": null
+ },
+ {
+ "name": "Should add two integers",
+ "expression": ["plus", 10, 20],
+ "expects": 30
+ },
+ {
+ "name": "Should add [float, integer]",
+ "expression": ["plus", 10.5, 20],
+ "expects": 30.5
+ },
+ {
+ "name": "Should add [string, integer] when string is valid number",
+ "expression": ["plus", "10", 20],
+ "expects": 30
+ },
+ {
+ "name": "Should add [string, float] when string is valid number",
+ "expression": ["plus", "10.5", 20.3],
+ "expects": 30.8
+ },
+ {
+ "name": "Should add negative and positive numbers",
+ "expression": ["plus", 50, -30],
+ "expects": 20
+ },
+ {
+ "name": "Should add numbers in scientific notation",
+ "expression": ["plus", 1e3, 2e3],
+ "expects": 3000
+ },
+ {
+ "name": "Should add numbers with negative exponents",
+ "expression": ["plus", 1e-3, 2e-3],
+ "expects": 0.003
+ },
+ {
+ "name": "Should add scientific notation with regular number",
+ "expression": ["plus", 1e2, 50],
+ "expects": 150
+ },
+ {
+ "name": "Should fail with invalid string that cannot be parsed as number",
+ "expression": ["plus", "not a number", 10],
+ "expectsFailure": "Expected number, got value \"not a number\""
+ },
+ {
+ "name": "Should fail with [boolean, integer]",
+ "expression": ["plus", true, 10],
+ "expectsFailure": "Expected number, got value true"
+ },
+ {
+ "name": "Should fail with string with no decimals after dot",
+ "expression": ["plus", "55.", 10],
+ "expectsFailure": "Expected number, got value \"55.\""
+ },
+ {
+ "name": "Should handle very large numbers",
+ "expression": ["plus", 9007199254740991, 1],
+ "expects": 9007199254740991
+ },
+ {
+ "name": "Should handle very small numbers",
+ "expression": ["plus", 0.0000001, 0.0000002],
+ "expects": 0.0000003
+ },
+ {
+ "name": "Should add string scientific notation with number",
+ "expression": ["plus", "1e2", 50],
+ "expectsFailure": "Expected number, got value \"1e2\""
+ },
+ {
+ "name": "Should throw exception when less than two arguments are provided",
+ "expression": ["plus", 0.2],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should throw exception when more than two arguments are present",
+ "expression": ["plus", 0.1, 0.2, 15, 1.50],
+ "expectsFailure": "Invalid"
+ },
+ {
+ "name": "Should add negative and positive decimal numbers",
+ "expression": ["plus", 50.55, -30.66],
+ "expects": 19.89
+ },
+ {
+ "name": "Should throw exception when any argument is double max value",
+ "expression": ["plus", 0, 1.7976931348623157E+308],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is double min value",
+ "expression": ["plus", -1.7976931348623157E+308, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float max value",
+ "expression": ["plus", 3.4028235E+38, 0],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ },
+ {
+ "name": "Should throw exception when any argument is float min value",
+ "expression": ["plus", 0, -3.402823e+38],
+ "expectsFailure": "Cannot convert non-finite or out-of-range number to decimal:"
+ }
+ ]
+}
diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs
index b34947d406..938eb0373d 100644
--- a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs
+++ b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs
@@ -50,10 +50,10 @@ public static TheoryData