diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 1ec0979d4..9eb3b037b 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.Json; using System.Text.RegularExpressions; using Altinn.App.Core.Models.Expressions; using Altinn.App.Core.Models.Layout.Components; @@ -473,54 +474,39 @@ bool ab return a >= b; // Actual implementation } - private static string? ToStringForEquals(object? value) - { - if (value is null) - { - return null; - } - - if (value is bool bvalue) - { - return bvalue ? "true" : "false"; - } - - if (value is string svalue) + internal static string? ToStringForEquals(object? value) => + value switch { + null => null, + bool bValue => bValue ? "true" : "false", // Special case for "TruE" to be equal to true - if ("true".Equals(svalue, StringComparison.OrdinalIgnoreCase)) - { - return "true"; - } - else if ("false".Equals(svalue, StringComparison.OrdinalIgnoreCase)) - { - return "false"; - } - else if ("null".Equals(svalue, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - return svalue; - } - else if (value is decimal decvalue) - { - return decvalue.ToString(CultureInfo.InvariantCulture); - } - else if (value is double doubvalue) - { - return doubvalue.ToString(CultureInfo.InvariantCulture); - } - else if (value is int intvalue) - { - return intvalue.ToString(CultureInfo.InvariantCulture); - } - - //TODO: consider accepting more types that might be used in model (eg Datetime) - throw new NotImplementedException(); - } + string sValue when "true".Equals(sValue, StringComparison.OrdinalIgnoreCase) => "true", + string sValue when "false".Equals(sValue, StringComparison.OrdinalIgnoreCase) => "false", + string sValue when "null".Equals(sValue, StringComparison.OrdinalIgnoreCase) => null, + string sValue => sValue, + decimal decValue => decValue.ToString(CultureInfo.InvariantCulture), + double doubleValue => doubleValue.ToString(CultureInfo.InvariantCulture), + float floatValue => floatValue.ToString(CultureInfo.InvariantCulture), + int intValue => intValue.ToString(CultureInfo.InvariantCulture), + uint uintValue => uintValue.ToString(CultureInfo.InvariantCulture), + short shortValue => shortValue.ToString(CultureInfo.InvariantCulture), + ushort ushortValue => ushortValue.ToString(CultureInfo.InvariantCulture), + long longValue => longValue.ToString(CultureInfo.InvariantCulture), + ulong ulongValue => ulongValue.ToString(CultureInfo.InvariantCulture), + byte byteValue => byteValue.ToString(CultureInfo.InvariantCulture), + sbyte sbyteValue => sbyteValue.ToString(CultureInfo.InvariantCulture), + // BigInteger bigIntValue => bigIntValue.ToString(CultureInfo.InvariantCulture), // Big integer not supported in json + DateTime dtValue => JsonSerializer.Serialize(dtValue), + DateOnly dateValue => JsonSerializer.Serialize(dateValue), + TimeOnly timeValue => JsonSerializer.Serialize(timeValue), + //TODO: Consider having JsonSerializer as a fallback for everything (including arrays and objects) + _ + => throw new NotImplementedException( + $"ToStringForEquals not implemented for type {value.GetType().Name}" + ) + }; - private static bool? EqualsImplementation(object?[] args) + internal static bool? EqualsImplementation(object?[] args) { if (args.Length != 2) { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs new file mode 100644 index 000000000..275bba8d1 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs @@ -0,0 +1,99 @@ +using System.Numerics; +using System.Text.Json; +using Altinn.App.Core.Internal.Expressions; +using Xunit.Abstractions; + +namespace Altinn.App.Core.Tests.LayoutExpressions.ExpressionEvaluatorTests; + +public class EqualTests(ITestOutputHelper outputHelper) +{ + public static TheoryData GetNumericTestData(double value) => + new() + { + value, + (byte)value, + (sbyte)value, + (short)value, + (ushort)value, + (int)value, + (uint)value, + (long)value, + (ulong)value, + (float)value, + (decimal)value, + // (BigInteger)value, // Not supported by JsonSerializer + }; + + public static TheoryData GetExoticTypes => + new() + { + "123", + true, + false, + "", + DateTime.Now, + DateOnly.FromDateTime(DateTime.Now), + TimeOnly.FromDateTime(DateTime.Now), + }; + + [Theory] + [MemberData(nameof(GetNumericTestData), 123.0)] + [MemberData(nameof(GetNumericTestData), 0.5)] + [MemberData(nameof(GetNumericTestData), -123.0)] + [MemberData(nameof(GetExoticTypes))] + public void ToStringForEquals_AgreesWithJsonSerializer(object? value) + { + outputHelper.WriteLine($"Object of type {value?.GetType().FullName ?? "null"}:"); + outputHelper.WriteLine($" value:{value}"); + outputHelper.WriteLine($" json: {JsonSerializer.Serialize(value)}"); + // Verify that the EqualsToString method returns the same value as the JsonSerializer. + var json = value is string ? value : JsonSerializer.Serialize(value); + var toStringForEquals = ExpressionEvaluator.ToStringForEquals(value); + Assert.Equal(json, toStringForEquals); + } + + public static TheoryData GetNonsenseValues => + new() + { + new BigInteger(123), // Not supported by JsonSerializer, but might make sense to support + new object[] { 1, 2, 3 }, + new object(), + new + { + A = 1, + B = 2, + C = 3 + }, + new byte[] { 0x01, 0x02, 0x03 }, + }; + + [Theory] + [MemberData(nameof(GetNonsenseValues))] + public void ToStringForEquals_NonsenseTypes_ThrowsException(object? value) + { + outputHelper.WriteLine($"Object of type {value?.GetType().FullName ?? "null"}:"); + outputHelper.WriteLine($" value:{value}"); + outputHelper.WriteLine($" json: {JsonSerializer.Serialize(value)}"); + // Verify that the EqualsToString method throws an exception for unsupported types. + Assert.Throws(() => ExpressionEvaluator.ToStringForEquals(value)); + } + + [Theory] + [InlineData(null, null)] + [InlineData("null", null)] + [InlineData("Null", null)] + [InlineData("true", "true")] + [InlineData("trUe", "true")] + [InlineData("True", "true")] + [InlineData(true, "true")] + [InlineData("false", "false")] + [InlineData("False", "false")] + [InlineData("falSe", "false")] + [InlineData(false, "false")] + public void ToStringForEquals_SpecialCases(object? value, string? expected) + { + // Verify that the EqualsToString method returns the expected value for special cases. + var toStringForEquals = ExpressionEvaluator.ToStringForEquals(value); + Assert.Equal(expected, toStringForEquals); + } +}