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

[PORT]Add 'any' and 'all' prebuilt function #3325

Merged
merged 4 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
53 changes: 53 additions & 0 deletions libraries/adaptive-expressions/src/builtinFunctions/all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @module adaptive-expressions
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Expression } from '../expression';
import { ExpressionEvaluator, ValueWithError } from '../expressionEvaluator';
import { ExpressionType } from '../expressionType';
import { InternalFunctionUtils } from '../functionUtils.internal';
import { MemoryInterface } from '../memory/memoryInterface';
import { Options } from '../options';
import { ReturnType } from '../returnType';

/**
* Determines whether all elements of a sequence satisfy a condition.
*/
export class All extends ExpressionEvaluator {
/**
* Initializes a new instance of the [All](xref:adaptive-expressions.All) class.
*/
public constructor() {
super(ExpressionType.All, All.evaluator, ReturnType.Boolean, InternalFunctionUtils.ValidateLambdaExpression);
}

/**
* @private
*/
private static evaluator(expression: Expression, state: MemoryInterface, options: Options): ValueWithError {
let result = true;
const { value: instance, error: childrenError } = expression.children[0].tryEvaluate(state, options);
let error = childrenError;
if (!error) {
const list = InternalFunctionUtils.convertToList(instance);
if (!list) {
error = `${expression.children[0]} is not a collection or structure object to run Any`;
} else {
InternalFunctionUtils.lambdaEvaluator(expression, state, options, list, (currentItem, r, e) => {
if (!InternalFunctionUtils.isLogicTrue(r) || e) {
result = false;
return true;
}

return false;
});
}
}

return { value: result, error };
}
}
53 changes: 53 additions & 0 deletions libraries/adaptive-expressions/src/builtinFunctions/any.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @module adaptive-expressions
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { Expression } from '../expression';
import { ExpressionEvaluator, ValueWithError } from '../expressionEvaluator';
import { ExpressionType } from '../expressionType';
import { InternalFunctionUtils } from '../functionUtils.internal';
import { MemoryInterface } from '../memory/memoryInterface';
import { Options } from '../options';
import { ReturnType } from '../returnType';

/**
* Determines whether any element of a sequence satisfies a condition.
*/
export class Any extends ExpressionEvaluator {
/**
* Initializes a new instance of the [Any](xref:adaptive-expressions.Any) class.
*/
public constructor() {
super(ExpressionType.Any, Any.evaluator, ReturnType.Boolean, InternalFunctionUtils.ValidateLambdaExpression);
}

/**
* @private
*/
private static evaluator(expression: Expression, state: MemoryInterface, options: Options): ValueWithError {
let result = false;
const { value: instance, error: childrenError } = expression.children[0].tryEvaluate(state, options);
let error = childrenError;
if (!error) {
const list = InternalFunctionUtils.convertToList(instance);
if (!list) {
error = `${expression.children[0]} is not a collection or structure object to run Any`;
} else {
InternalFunctionUtils.lambdaEvaluator(expression, state, options, list, (currentItem, r, e) => {
if (InternalFunctionUtils.isLogicTrue(r) && !e) {
Danieladu marked this conversation as resolved.
Show resolved Hide resolved
result = true;
return true;
}

return false;
});
}
}

return { value: result, error };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class Foreach extends ExpressionEvaluator {
ExpressionType.Foreach,
InternalFunctionUtils.foreach,
ReturnType.Array,
InternalFunctionUtils.validateForeach
InternalFunctionUtils.ValidateLambdaExpression
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export * from './addOrdinal';
export * from './addProperty';
export * from './addSeconds';
export * from './addToTime';
export * from './all';
export * from './and';
export * from './any';
export * from './average';
export * from './base64';
export * from './base64ToBinary';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class Select extends ExpressionEvaluator {
ExpressionType.Select,
InternalFunctionUtils.foreach,
ReturnType.Array,
InternalFunctionUtils.validateForeach
InternalFunctionUtils.ValidateLambdaExpression
);
}
}
49 changes: 13 additions & 36 deletions libraries/adaptive-expressions/src/builtinFunctions/where.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
* Licensed under the MIT License.
*/

import { Constant } from '../constant';
import { Expression } from '../expression';
import { ExpressionEvaluator, ValueWithError } from '../expressionEvaluator';
import { ExpressionType } from '../expressionType';
import { InternalFunctionUtils } from '../functionUtils.internal';
import { MemoryInterface } from '../memory/memoryInterface';
import { SimpleObjectMemory } from '../memory/simpleObjectMemory';
import { StackedMemory } from '../memory/stackedMemory';
import { Options } from '../options';
import { ReturnType } from '../returnType';

Expand All @@ -25,7 +22,7 @@ export class Where extends ExpressionEvaluator {
* Initializes a new instance of the [Where](xref:adaptive-expressions.Where) class.
*/
public constructor() {
super(ExpressionType.Where, Where.evaluator, ReturnType.Array, InternalFunctionUtils.validateForeach);
super(ExpressionType.Where, Where.evaluator, ReturnType.Array, InternalFunctionUtils.ValidateLambdaExpression);
}

/**
Expand All @@ -36,48 +33,28 @@ export class Where extends ExpressionEvaluator {
const { value: instance, error: childrenError } = expression.children[0].tryEvaluate(state, options);
let error = childrenError;
if (!error) {
const iteratorName = (expression.children[1].children[0] as Constant).value as string;
let arr: any[] = [];
let isInstanceArray = false;
if (Array.isArray(instance)) {
arr = instance;
isInstanceArray = true;
} else if (typeof instance === 'object') {
Object.keys(instance).forEach((u): number => arr.push({ key: u, value: instance[u] }));
const list = InternalFunctionUtils.convertToList(instance);
if (!list) {
error = `${expression.children[0]} is not a collection or structure object to run Where`;
} else {
error = `${expression.children[0]} is not a collection or structure object to run foreach`;
}

if (!error) {
const stackedMemory = StackedMemory.wrap(state);
const arrResult = [];
for (const item of arr) {
const local: Map<string, any> = new Map<string, any>([[iteratorName, item]]);

stackedMemory.push(SimpleObjectMemory.wrap(local));
const newOptions = new Options(options);
newOptions.nullSubstitution = undefined;
const { value: r, error: e } = expression.children[2].tryEvaluate(stackedMemory, newOptions);
stackedMemory.pop();
if (e !== undefined) {
return { value: undefined, error: e };
result = [];
InternalFunctionUtils.lambdaEvaluator(expression, state, options, list, (currentItem, r, e) => {
if (InternalFunctionUtils.isLogicTrue(r) && !e) {
// add if only if it evaluates to true
result.push(currentItem);
}

if (r) {
arrResult.push(local.get(iteratorName));
}
}
return false;
});

//reconstruct object if instance is object, otherwise, return array result
if (!isInstanceArray) {
if (!Array.isArray(instance)) {
const objResult = {};
for (const item of arrResult) {
for (const item of result) {
objResult[item.key] = item.value;
}

result = objResult;
} else {
result = arrResult;
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions libraries/adaptive-expressions/src/expressionFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export class ExpressionFunctions {
new BuiltinFunctions.AddProperty(),
new BuiltinFunctions.AddSeconds(),
new BuiltinFunctions.AddToTime(),
new BuiltinFunctions.All(),
new BuiltinFunctions.And(),
new BuiltinFunctions.Any(),
new BuiltinFunctions.Average(),
new BuiltinFunctions.Base64(),
new BuiltinFunctions.Base64ToBinary(),
Expand Down
2 changes: 2 additions & 0 deletions libraries/adaptive-expressions/src/expressionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export class ExpressionType {
public static readonly Flatten: string = 'flatten';
public static readonly Unique: string = 'unique';
public static readonly Reverse: string = 'reverse';
public static readonly Any: string = 'any';
public static readonly All: string = 'all';

// Misc
public static readonly Constant: string = 'Constant';
Expand Down
84 changes: 60 additions & 24 deletions libraries/adaptive-expressions/src/functionUtils.internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,48 +298,84 @@ export class InternalFunctionUtils {
}

if (!error) {
const iteratorName = (expression.children[1].children[0] as Constant).value as string;
let arr = [];
if (Array.isArray(instance)) {
arr = instance;
} else if (typeof instance === 'object') {
Object.keys(instance).forEach((u): number => arr.push({ key: u, value: instance[u] }));
const list = InternalFunctionUtils.convertToList(instance);
if (!list) {
error = `${expression.children[0]} is not a collection or structure object to run Foreach`;
} else {
error = `${expression.children[0]} is not a collection or structure object to run foreach`;
}

if (!error) {
const stackedMemory = StackedMemory.wrap(state);
result = [];
for (const item of arr) {
const local: Map<string, any> = new Map<string, any>([[iteratorName, item]]);

stackedMemory.push(SimpleObjectMemory.wrap(local));
const { value: r, error: e } = expression.children[2].tryEvaluate(stackedMemory, options);
stackedMemory.pop();
if (e !== undefined) {
return { value: undefined, error: e };
InternalFunctionUtils.lambdaEvaluator(expression, state, options, list, (currentItem, r, e) => {
if (e) {
error = e;
return true;
} else {
result.push(r);
return false;
}
result.push(r);
}
});
}
}

return { value: result, error };
}

/**
* Lambda evaluator.
* @param expression expression.
* @param state memory state.
* @param options options.
* @param list item list.
* @param callback call back. return the should break flag.
*/
public static lambdaEvaluator(expression: Expression, state: MemoryInterface, options: Options, list: unknown[], callback: (currentItem: unknown, result: unknown, error: string) => boolean): void{
Danieladu marked this conversation as resolved.
Show resolved Hide resolved
const iteratorName = (expression.children[1].children[0] as Constant).value as string;
Danieladu marked this conversation as resolved.
Show resolved Hide resolved
const stackedMemory = StackedMemory.wrap(state);
for (const item of list) {
const currentItem = item;
const local: Map<string, any> = new Map<string, any>([[iteratorName, item]]);

// the local iterator is pushed as one memory layer in the memory stack
stackedMemory.push(SimpleObjectMemory.wrap(local));
const { value: r, error: e } = expression.children[2].tryEvaluate(stackedMemory, options);
stackedMemory.pop();

const shouldBreak = callback(currentItem, r, e);
if (shouldBreak) {
break;
}
}
}

/**
* Convert an object into array.
* If the instance is array, return itself.
* If the instance is object, return {key, value} pair list.
* Else return undefined.
* @param instance input instance.
*/
public static convertToList(instance: unknown) : unknown[] | undefined {
joshgummersall marked this conversation as resolved.
Show resolved Hide resolved
let arr: unknown[] | undefined;
if (Array.isArray(instance)) {
arr = instance;
} else if (typeof instance === 'object') {
arr = [];
Object.keys(instance).forEach((u): number => arr.push({ key: u, value: instance[u] }));
}

return arr;
}

/**
* Validator for foreach, select, and where functions.
* @param expression
*/
public static validateForeach(expression: Expression): void {
public static ValidateLambdaExpression(expression: Expression): void {
if (expression.children.length !== 3) {
throw new Error(`foreach expect 3 parameters, found ${expression.children.length}`);
throw new Error(`Lambda expression expect 3 parameters, found ${expression.children.length}`);
}

const second: any = expression.children[1];
if (!(second.type === ExpressionType.Accessor && second.children.length === 1)) {
throw new Error(`Second parameter of foreach is not an identifier : ${second}`);
throw new Error(`Second parameter is not an identifier : ${second}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ const badExpressions = [
['where(items, item, item2, item3)', 'should have three parameters'],
['where(items, add(1), item)', 'Second paramter of where is not an identifier'],
['where(items, 1, item)', 'Second paramter error'],
['where(items, x, sum(x))', 'third paramter error'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being removed?

Copy link
Contributor Author

@Danieladu Danieladu Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently SDK would treat the "error item" in Where iteration as a "false" result and drop it to avoid too much errors.
So where(items, x, sum(x)) is valid, but empty array would be returned.

['indicesAndValues(items, 1)', 'should only have one parameter'],
['indicesAndValues(1)', 'shoud have array param'],
['union(one, two)', 'should have collection param'],
Expand Down
18 changes: 14 additions & 4 deletions libraries/adaptive-expressions/tests/expressionParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,10 +672,10 @@ const testCases = [
['join(foreach(items, item, concat(item, string(count(items)))), \',\')', 'zero3,one3,two3', ['items']],
['join(foreach(doubleNestedItems, items, join(foreach(items, item, item.x), ",")), ",")', '1,2,3'],
['join(foreach(doubleNestedItems, items, join(foreach(items, item, concat(y, string(item.x))), ",")), ",")', 'y1,y2,y3'],
['join(foreach(dialog, item, item.key), ",")', 'instance,options,title,subTitle'],
['join(foreach(dialog, item => item.key), ",")', 'instance,options,title,subTitle'],
['foreach(dialog, item, item.value)[1].xxx', 'options'],
['foreach(dialog, item=>item.value)[1].xxx', 'options'],
['join(foreach(dialog, item, item.key), ",")', 'x,instance,options,title,subTitle'],
['join(foreach(dialog, item => item.key), ",")', 'x,instance,options,title,subTitle'],
['foreach(dialog, item, item.value)[2].xxx', 'options'],
['foreach(dialog, item=>item.value)[2].xxx', 'options'],
['join(foreach(indicesAndValues(items), item, item.value), ",")', 'zero,one,two'],
['join(foreach(indicesAndValues(items), item=>item.value), ",")', 'zero,one,two'],
['count(where(doubleNestedItems, items, count(where(items, item, item.x == 1)) == 1))', 1],
Expand Down Expand Up @@ -714,6 +714,15 @@ const testCases = [
['flatten(createArray(1,createArray(2),createArray(createArray(3, 4), createArray(5,6))))', [1, 2, 3, 4, 5, 6]],
['flatten(createArray(1,createArray(2),createArray(createArray(3, 4), createArray(5,6))), 1)', [1, 2, [3, 4], [5, 6]]],
['unique(createArray(1, 5, 1))', [1, 5]],
['any(createArray(1, "cool"), item, isInteger(item))', true],
['any(createArray("first", "cool"), item => isInteger(item))', false],
['all(createArray(1, "cool"), item, isInteger(item))', false],
['all(createArray(1, 2), item => isInteger(item))', true],
['any(dialog, item, item.key == "title")', true],
['any(dialog, item, isInteger(item.value))', true],
['all(dialog, item, item.key == "title")', false],
['all(dialog, item, isInteger(item.value))', false],

],
},
{
Expand Down Expand Up @@ -978,6 +987,7 @@ const scope = {
},
},
dialog: {
x: 3,
instance: { xxx: 'instance', yyy: { instanceY: 'instanceY' } },
options: { xxx: 'options', yyy: ['optionY1', 'optionY2'] },
title: 'Dialog Title',
Expand Down