From 3c766269107acc375ed88a690e0603d490bc2cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 11 Sep 2023 11:58:26 +0200 Subject: [PATCH] JS translations: Add Enumerable.Sum() and .Sum(x => ...) --- .../JavascriptTranslatableMethodCollection.cs | 78 +++++++++++++++---- .../LambdaExpressions/StaticCommands.dothtml | 1 + .../Tests/Tests/Feature/StaticCommandTests.cs | 15 ++++ .../Binding/JavascriptCompilationTests.cs | 26 +++++++ 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 3afbfe1b7f..ad843b4789 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -531,8 +531,6 @@ private bool EnsureIsComparableInJavascript(MethodInfo method, Type type) private void AddDefaultEnumerableTranslations() { var returnTrueFunc = new JsArrowFunctionExpression(Enumerable.Empty(), new JsLiteral(true)); - var selectIdentityFunc = new JsArrowFunctionExpression(new[] { new JsIdentifier("arg") }, - new JsIdentifierExpression("ko").Member("unwrap").Invoke(new JsIdentifierExpression("arg"))); bool IsDelegateReturnTypeEnum(Type type) => type.GetGenericArguments().Last().IsEnum; @@ -562,19 +560,6 @@ string GetDelegateReturnTypeHash(Type type) AddMethodTranslator(() => Enumerable.Empty().LastOrDefault(_ => false), new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("lastOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance))); - foreach (var type in new[] { typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal), typeof(int?), typeof(long?), typeof(float?), typeof(double?), typeof(decimal?) }) - { - AddMethodTranslator(typeof(Enumerable), nameof(Enumerable.Max), parameters: new[] { typeof(IEnumerable<>).MakeGenericType(type) }, translator: new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("max").Invoke(args[1], selectIdentityFunc.Clone(), new JsLiteral(!type.IsNullable())))); - AddMethodTranslator(typeof(Enumerable), nameof(Enumerable.Max), parameterCount: 2, parameterFilter: p => p[1].ParameterType.GetGenericArguments().Last() == type, translator: new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("max").Invoke(args[1], args[2], new JsLiteral(!type.IsNullable())))); - - AddMethodTranslator(typeof(Enumerable), nameof(Enumerable.Min), parameters: new[] { typeof(IEnumerable<>).MakeGenericType(type) }, translator: new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("min").Invoke(args[1], selectIdentityFunc.Clone(), new JsLiteral(!type.IsNullable())))); - AddMethodTranslator(typeof(Enumerable), nameof(Enumerable.Min), parameterCount: 2, parameterFilter: p => p[1].ParameterType.GetGenericArguments().Last() == type, translator: new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("min").Invoke(args[1], args[2], new JsLiteral(!type.IsNullable())))); - } - AddMethodTranslator(() => Enumerable.Empty().OrderBy(_ => Generic.Enum.Something), new GenericMethodCompiler((jArgs, dArgs) => new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("orderBy") .Invoke(jArgs[1], jArgs[2], new JsLiteral((IsDelegateReturnTypeEnum(dArgs.Last().Type)) ? GetDelegateReturnTypeHash(dArgs.Last().Type) : null)), check: (method, _, arguments) => EnsureIsComparableInJavascript(method, arguments.Last().Type.GetGenericArguments().Last()))); @@ -592,6 +577,69 @@ string GetDelegateReturnTypeHash(Type type) AddMethodTranslator(() => Enumerable.Empty().ToList(), new GenericMethodCompiler(args => args[1])); AddMethodTranslator(() => Enumerable.Empty().Where(_ => true), new GenericMethodCompiler(args => args[1].Member("filter").Invoke(args[2]))); + + AddDefaultNumericEnumerableTranslations(); + } + + private void AddDefaultNumericEnumerableTranslations() + { + var methods = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public); + + var selectIdentityFunc = new JsArrowFunctionExpression(new[] { new JsIdentifier("arg") }, + new JsIdentifierExpression("ko").Member("unwrap").Invoke(new JsIdentifierExpression("arg"))); + + foreach (var m in methods) + { + if (m.Name is "Max" or "Min" or "Sum") + { + var parameters = m.GetParameters(); + if (parameters.Length == 0) continue; + var itemType = ReflectionUtils.GetEnumerableType(parameters[0].ParameterType); + if (itemType is null) continue; + var selectorResultType = parameters.ElementAtOrDefault(1)?.ParameterType.GetGenericArguments().LastOrDefault(); + + if (m.Name is "Max" or "Min" && parameters.Length == 1 && itemType.UnwrapNullableType().IsNumericType()) + { + AddMethodTranslator(m, new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member(m.Name is "Min" ? "min" : "max").Invoke(args[1], selectIdentityFunc.Clone(), new JsLiteral(!itemType.IsNullable())))); + } + else if (m.Name is "Max" or "Min" && parameters.Length == 2 && selectorResultType?.UnwrapNullableType().IsNumericType() == true) + { + AddMethodTranslator(m, new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member(m.Name is "Min" ? "min" : "max").Invoke(args[1], args[2], new JsLiteral(!selectorResultType.IsNullable())))); + } + + else if (m.Name is "Sum" && parameters.Length == 1 && itemType.UnwrapNullableType().IsNumericType()) + { + AddMethodTranslator(m, new GenericMethodCompiler(args => args[1].Member("reduce").Invoke( + new JsArrowFunctionExpression( + new[] { new JsIdentifier("acc"), new JsIdentifier("x") }, + new JsIdentifierExpression("acc").Binary(BinaryOperatorType.Plus, + new JsIdentifierExpression("ko").Member("unwrap").Invoke(new JsIdentifierExpression("x")) + .Binary(BinaryOperatorType.NullishCoalescing, new JsLiteral(0)) + ) + ), + new JsLiteral(0)) + )); + } + else if (m.Name is "Sum" && parameters.Length == 2 && selectorResultType?.UnwrapNullableType().IsNumericType() == true) + { + AddMethodTranslator(m, new GenericMethodCompiler(args => args[1] + .Member("map").Invoke(args[2]) + .Member("reduce").Invoke( + new JsArrowFunctionExpression( + new[] { new JsIdentifier("acc"), new JsIdentifier("x") }, + new JsIdentifierExpression("acc").Binary(BinaryOperatorType.Plus, + new JsIdentifierExpression("x").Binary(BinaryOperatorType.NullishCoalescing, new JsLiteral(0)) + ) + ), + new JsLiteral(0) + ) + )); + } + } + + } } private void AddDefaultDictionaryTranslations() diff --git a/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/StaticCommands.dothtml b/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/StaticCommands.dothtml index 2d7054b01b..24939a182c 100644 --- a/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/StaticCommands.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/StaticCommands.dothtml @@ -72,6 +72,7 @@ +
diff --git a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs index c98a819d7b..164bf1fa71 100644 --- a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs +++ b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs @@ -672,6 +672,21 @@ public void Feature_Lambda_Expression_Static_Command_List_Contains() }); } [Fact] + public void Feature_Lambda_Expression_Static_Command_List_Sum() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_LambdaExpressions_StaticCommands); + var textbox = browser.First("[data-ui=textbox]"); + + browser.First($"//input[@value='OrderBy Id']", By.XPath).Click(); + browser.First($"//input[@value='Sum of name lengths']", By.XPath).Click(); + AssertUI.InnerTextEquals(textbox, "51"); + browser.First($"//input[@value='Skip 5 customers']", By.XPath).Click(); + browser.First($"//input[@value='Sum of name lengths']", By.XPath).Click(); + AssertUI.InnerTextEquals(textbox, "26"); + }); + } + [Fact] public void Feature_List_Translation_Add_Item() { RunInAllBrowsers(browser => { diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index b458be74ca..0a563bcadf 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -850,6 +850,32 @@ public void JsTranslator_EnumerableMin_WithSelector(string binding, string prope Assert.AreEqual($"dotvvm.translations.array.min({property}(),(item)=>-ko.unwrap(item),{(!nullable).ToString().ToLowerInvariant()})", result); } + [DataRow("Int32Array")] + [DataRow("NullableInt32Array")] + [DataRow("Int64Array")] + [DataRow("NullableInt64Array")] + [DataRow("SingleArray")] + [DataRow("NullableSingleArray")] + [DataRow("DoubleArray")] + [DataRow("NullableDoubleArray")] + [DataRow("DecimalArray")] + [DataRow("NullableDecimalArray")] + [DataTestMethod] + public void JsTranslator_EnumerableSum(string property) + { + var result = CompileBinding($"{property}.Sum()", new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestArraysViewModel) }); + XAssert.Equal($"{property}().reduce((acc,x)=>acc+(ko.unwrap(x)??0),0)", result); + var resultSelector = CompileBinding($"{property}.Sum(x => -x)", new[] { new NamespaceImport("System.Linq"), new NamespaceImport("Math") }, new[] { typeof(TestArraysViewModel) }); + XAssert.Equal($"{property}().map((x)=>-ko.unwrap(x)).reduce((acc,x)=>acc+(x??0),0)", resultSelector); + } + + [TestMethod] + public void JsTranslator_EnumerableSum_Selector() + { + var result = CompileBinding($"ObjectArray.Sum(x => x.Int)", new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestArraysViewModel) }); + XAssert.Equal($"ObjectArray().map((x)=>ko.unwrap(x).Int()).reduce((acc,x)=>acc+(x??0),0)", result); + } + [TestMethod] [DataRow("Enumerable.OrderBy(ObjectArray, (TestComparisonType item) => item.Int)", "Int", typeof(int), DisplayName = "Regular call of Enumerable.OrderBy")] [DataRow("Enumerable.OrderBy(ObjectArray, (TestComparisonType item) => item.Bool)", "Bool", typeof(bool), DisplayName = "Regular call of Enumerable.OrderBy")]