Skip to content

Commit

Permalink
JS translations: Add Enumerable.Sum() and .Sum(x => ...)
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Sep 11, 2023
1 parent 5872c82 commit 3c76626
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,6 @@ private bool EnsureIsComparableInJavascript(MethodInfo method, Type type)
private void AddDefaultEnumerableTranslations()
{
var returnTrueFunc = new JsArrowFunctionExpression(Enumerable.Empty<JsIdentifier>(), 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;
Expand Down Expand Up @@ -562,19 +560,6 @@ string GetDelegateReturnTypeHash(Type type)
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().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<Generic.T>().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())));
Expand All @@ -592,6 +577,69 @@ string GetDelegateReturnTypeHash(Type type)
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().ToList(), new GenericMethodCompiler(args => args[1]));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<dot:Button Text="Get maximum number of finished transactions" Click="{staticCommand: OperationResult = SourceCustomers.Max((CustomerData c) => c.FinishedTransactions)}" />
<dot:Button Text="Get minimum number of finished transactions" Click="{staticCommand: OperationResult = SourceCustomers.Min((CustomerData c) => c.FinishedTransactions)}" />
<dot:Button Text="Is there green in the list" Click="{staticCommand: OperationResult = SelectedCategories.Contains(StaticCommandsViewModel.Category.Green)}" />
<dot:Button Text="Sum of name lengths" Click="{staticCommand: OperationResult = FilteredCustomers.Sum(c => c.Name.Length)}" />
</div>

<div>
Expand Down
15 changes: 15 additions & 0 deletions src/Samples/Tests/Tests/Feature/StaticCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
26 changes: 26 additions & 0 deletions src/Tests/Binding/JavascriptCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down

0 comments on commit 3c76626

Please sign in to comment.