Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync backend expression functions with frontend #277

Merged
merged 3 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 143 additions & 14 deletions src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;

using Altinn.App.Core.Models.Expressions;
Expand Down Expand Up @@ -76,6 +75,15 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone
ExpressionFunction.and => And(args),
ExpressionFunction.or => Or(args),
ExpressionFunction.not => Not(args),
ExpressionFunction.contains => Contains(args),
ExpressionFunction.notContains => !Contains(args),
ExpressionFunction.commaContains => CommaContains(args),
ExpressionFunction.endsWith => EndsWith(args),
ExpressionFunction.startsWith => StartsWith(args),
ExpressionFunction.stringLength => StringLength(args),
ExpressionFunction.round => Round(args),
ExpressionFunction.upperCase => UpperCase(args),
ExpressionFunction.lowerCase => LowerCase(args),
_ => throw new ExpressionEvaluatorTypeErrorException($"Function \"{expr.Function}\" not implemented"),
};
return ret;
Expand Down Expand Up @@ -119,6 +127,127 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone
return string.Join("", args.Select(a => a switch { string s => s, _ => ToStringForEquals(a) }));
}

private static bool Contains(object?[] args)
{
if (args.Length != 2)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 2 argument(s), got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
string? stringTwo = ToStringForEquals(args[1]);

if (stringOne is null || stringTwo is null)
{
return false;
}

return stringOne.Contains(stringTwo, StringComparison.InvariantCulture);
}

private static bool EndsWith(object?[] args)
{
if (args.Length != 2)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 2 argument(s), got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
string? stringTwo = ToStringForEquals(args[1]);

if (stringOne is null || stringTwo is null)
{
return false;
}

return stringOne.EndsWith(stringTwo, StringComparison.InvariantCulture);
}

private static bool StartsWith(object?[] args)
{
if (args.Length != 2)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 2 argument(s), got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
string? stringTwo = ToStringForEquals(args[1]);

if (stringOne is null || stringTwo is null)
{
return false;
}

return stringOne.StartsWith(stringTwo, StringComparison.InvariantCulture);
}

private static bool CommaContains(object?[] args)
{
if (args.Length != 2)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 2 arguments, got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
string? stringTwo = ToStringForEquals(args[1]);

if (stringOne is null || stringTwo is null)
{
return false;
}

return stringOne.Split(",").Select(s => s.Trim()).Contains(stringTwo, StringComparer.InvariantCulture);
}

private static int StringLength(object?[] args)
{
if (args.Length != 1)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument, got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
return stringOne?.Length ?? 0;
}

private static string Round(object?[] args)
{
if (args.Length < 1 || args.Length> 2)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 1-2 argument(s), got {args.Length}");
}

var number = PrepareNumericArg(args[0]);

if(number is null)
{
number = 0;
}

int precision = 0;
if (args.Length == 2 && args[1] is not null)
{
precision = Convert.ToInt32(args[1]);
}

return number.Value.ToString($"N{precision}", CultureInfo.InvariantCulture);
}

private static string? UpperCase(object?[] args)
{
if (args.Length != 1)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument, got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
return stringOne?.ToUpperInvariant();
}

private static string? LowerCase(object?[] args)
{
if (args.Length != 1)
{
throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument, got {args.Length}");
}
string? stringOne = ToStringForEquals(args[0]);
return stringOne?.ToLowerInvariant();
}

private static bool PrepareBooleanArg(object? arg)
{
return arg switch
Expand Down Expand Up @@ -187,23 +316,23 @@ private static (double?, double?) PrepareNumericArgs(object?[] args)
{
throw new ExpressionEvaluatorTypeErrorException("Invalid number of args for compare");
}
var a = args[0] switch
{
bool ab => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(ab ? "true" : "false")}"),
string s => parseNumber(s),
object o => o as double?, // assume all relevant numers are representable as double (as in frontend)
_ => null,
};

var b = args[1] switch
var a = PrepareNumericArg(args[0]);

var b = PrepareNumericArg(args[1]);

return (a, b);
}

private static double? PrepareNumericArg(object? arg)
{
return arg switch
{
bool bb => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(bb ? "true" : "false")}"),
bool ab => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(ab ? "true" : "false")}"),
string s => parseNumber(s),
object o => o as double?, // assume all relevant numers are representable as double (as in frontend)
_ => null,
object o => o as double?, // assume all relevant numbers are representable as double (as in frontend)
_ => null
};

return (a, b);
}

private static object? IfImpl(object?[] args)
Expand Down
36 changes: 36 additions & 0 deletions src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ public enum ExpressionFunction
/// </summary>
concat,
/// <summary>
/// Turn characters to upper case
/// </summary>
upperCase,
/// <summary>
/// Turn characters to lower case
/// </summary>
lowerCase,
/// <summary>
/// Check if a string contains another string
/// </summary>
contains,
/// <summary>
/// Check if a string does not contain another string
/// </summary>
notContains,
/// <summary>
/// Check if a comma separated string contains a value
/// </summary>
commaContains,
/// <summary>
/// Check if a string ends with another string
/// </summary>
endsWith,
/// <summary>
/// Check if a string starts with another string
/// </summary>
startsWith,
/// <summary>
/// Check if values are equal
/// </summary>
equals,
Expand All @@ -58,6 +86,14 @@ public enum ExpressionFunction
/// </summary>
greaterThan,
/// <summary>
/// Return the length of a string
/// </summary>
stringLength,
/// <summary>
/// Rounds a number to an integer, or optionally a decimal with a configurable amount of decimal points
/// </summary>
round,
/// <summary>
/// Return true if all the expressions evaluate to true
/// </summary>
and,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,27 @@ public TestFunctions(ITestOutputHelper output)
[Theory]
[SharedTest("component")]
public void Component_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("commaContains")]
public void CommaContains_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("concat")]
public void Concat_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("contains")]
public void Contains_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("dataModel")]
public void DataModel_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("endsWith")]
public void EndsWith_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("equals")]
public void Equals_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);
Expand All @@ -59,6 +71,10 @@ public TestFunctions(ITestOutputHelper output)
[Theory]
[SharedTest("not")]
public void Not_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("notContains")]
public void NotContains_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("instanceContext")]
Expand All @@ -83,6 +99,26 @@ public TestFunctions(ITestOutputHelper output)
[Theory]
[SharedTest("unknown")]
public void Unknown_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("upperCase")]
public void UpperCase_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("lowerCase")]
public void LowerCase_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("startsWith")]
public void StartsWith_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("stringLength")]
public void StringLength_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

[Theory]
[SharedTest("round")]
public void Round_Theory(ExpressionTestCaseRoot test) => RunTestCase(test);

private void RunTestCase(ExpressionTestCaseRoot test)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"layout": [
{
"id": "comp1",
"type": "Heading"
"type": "Header"
},
{
"id": "group0",
Expand Down
Loading