feat:支持condition-builder条件运算 (#6430)

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算

* feat:支持condition-builder条件运算
This commit is contained in:
hsm-lv 2023-03-21 19:35:36 +08:00 committed by GitHub
parent dc18814fb5
commit 388a59d06d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2735 additions and 133 deletions

View File

@ -2062,7 +2062,7 @@ registerAction('my-action', new MyAction());
## 条件
通过配置`expression: 表达式`来实现条件逻辑。
通过配置`expression: 表达式或ConditionBuilder组合条件`来实现条件逻辑。
```schema
{
@ -2071,7 +2071,15 @@ registerAction('my-action', new MyAction());
type: 'form',
wrapWithPanel: false,
data: {
expression: 'okk'
expression: 'okk',
name: 'amis',
features: ['flexible', 'powerful'],
tool: 'amis-editor',
platform: 'aisuda',
detail: {
version: '2.8.0',
github: 'https://github.com/baidu/amis'
}
},
body: [
{
@ -2085,7 +2093,7 @@ registerAction('my-action', new MyAction());
actionType: 'toast',
args: {
msgType: 'success',
msg: '我okk~'
msg: 'expression表达式 ok~'
},
expression: 'expression === "okk"'
},
@ -2100,9 +2108,32 @@ registerAction('my-action', new MyAction());
actionType: 'toast',
args: {
msgType: 'success',
msg: '我也okk~'
msg: 'conditin-builder条件组合 也ok~'
},
expression: 'expression === "okk"'
expression: {
id: 'b6434ead40cc',
conjunction: 'and',
children: [
{
id: 'e92b93840f37',
left: {
type: 'field',
field: 'name'
},
op: 'equal',
right: 'amis'
},
{
id: '3779845521db',
left: {
type: 'field',
field: 'features'
},
op: 'select_any_in',
right: '${[LAST(features)]}'
}
]
}
}
]
}
@ -2825,13 +2856,13 @@ http 请求动作执行结束后,后面的动作可以通过 `${responseResult
# 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| actionType | `string` | - | 动作名称 |
| args | `object` | - | 动作属性`{key:value}`,支持数据映射 |
| data | `object` | - | 追加数据`{key:value}`,支持数据映射,如果是触发其他组件的动作,则该数据会传递给目标组件,`> 2.3.2 及以上版本` |
| dataMergeMode | `string` | 'merge' | 当配置了 data 的时候,可以控制数据追加方式,支持合并(`merge`)和覆盖(`override`)两种模式,`> 2.3.2 及以上版本` |
| preventDefault | `boolean`\|[表达式](../concepts/expression) | false | 阻止事件默认行为,`> 1.10.0 及以上版本支持表达式` |
| stopPropagation | `boolean`\|[表达式](../concepts/expression) | false | 停止后续动作执行,`> 1.10.0 及以上版本支持表达式` |
| expression | `boolean`\|[表达式](../concepts/expression) | - | 执行条件,不设置表示默认执行 |
| outputVar | `string` | - | 输出数据变量名 |
| 属性名 | 类型 | 默认值 | 说明 |
| --------------- | -------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| actionType | `string` | - | 动作名称 |
| args | `object` | - | 动作属性`{key:value}`,支持数据映射 |
| data | `object` | - | 追加数据`{key:value}`,支持数据映射,如果是触发其他组件的动作,则该数据会传递给目标组件,`> 2.3.2 及以上版本` |
| dataMergeMode | `string` | 'merge' | 当配置了 data 的时候,可以控制数据追加方式,支持合并(`merge`)和覆盖(`override`)两种模式,`> 2.3.2 及以上版本` |
| preventDefault | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | false | 阻止事件默认行为,`> 1.10.0 及以上版本支持表达式> 2.9.0 及以上版本支持ConditionBuilder` |
| stopPropagation | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | false | 停止后续动作执行,`> 1.10.0 及以上版本支持表达式> 2.9.0 及以上版本支持ConditionBuilder` |
| expression | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | - | 执行条件,不设置表示默认执行`> 1.10.0 及以上版本支持表达式,> 2.9.0 及以上版本支持ConditionBuilder` |
| outputVar | `string` | - | 输出数据变量名 |

View File

@ -0,0 +1,506 @@
import moment from 'moment';
import {resolveCondition, guid, registerConditionComputer} from '../src/utils/';
const data = {
name: 'amis',
feature: 'flexible',
features: ['flexible', 'powerful'],
tool: 'amis-editor',
platform: 'aisuda',
version: '2.9.0',
num: 1,
num2: 0,
num3: undefined,
str: '',
obj: {},
arr: [],
bool: true,
detail: {
version: '2.8.0',
github: 'https://github.com/baidu/amis'
},
range: 1678723200,
date: '2023-03-19',
datetime: '2023-03-20T04:55:00+08:00',
time: '00:05'
};
const equal1 = {
id: guid(),
left: {
type: 'field',
field: 'name'
},
op: 'equal',
right: '${name}'
};
const equal2 = {
id: guid(),
left: {
type: 'field',
field: 'detail'
},
op: 'equal',
right: '${detail}'
};
const equal3 = {
id: guid(),
left: {
type: 'field',
field: 'bool'
},
op: 'equal',
right: '${!num2}'
};
const not_equal = {
id: guid(),
left: {
type: 'field',
field: 'version'
},
op: 'not_equal',
right: '${version}'
};
const greater = {
id: guid(),
left: {
type: 'field',
field: 'num'
},
op: 'greater',
right: '${num + 0.5}'
};
const less = {
id: guid(),
left: {
type: 'field',
field: 'num'
},
op: 'less',
right: '${num - 0.5}'
};
const greater_or_equal = {
id: guid(),
left: {
type: 'field',
field: 'num'
},
op: 'greater_or_equal',
right: '${num}'
};
const less_or_equal = {
id: guid(),
left: {
type: 'field',
field: 'num'
},
op: 'less_or_equal',
right: '${num}'
};
const starts_with = {
id: guid(),
left: {
type: 'field',
field: 'tool'
},
op: 'starts_with',
right: 'amis-'
};
const ends_with = {
id: guid(),
left: {
type: 'field',
field: 'tool'
},
op: 'ends_with',
right: '-editor'
};
const like = {
id: guid(),
left: {
type: 'field',
field: 'tool'
},
op: 'like',
right: '${tool}-core'
};
const not_like = {
id: guid(),
left: {
type: 'field',
field: 'tool'
},
op: 'not_like',
right: '${tool}r'
};
const is_empty1 = {
id: guid(),
left: {
type: 'field',
field: 'num2'
},
op: 'is_empty'
};
const is_empty2 = {
id: guid(),
left: {
type: 'field',
field: 'str'
},
op: 'is_empty'
};
const is_empty3 = {
id: guid(),
left: {
type: 'field',
field: 'obj'
},
op: 'is_empty'
};
const is_not_empty1 = {
id: guid(),
left: {
type: 'field',
field: 'num3'
},
op: 'is_not_empty'
};
const is_not_empty2 = {
id: guid(),
left: {
type: 'field',
field: 'name'
},
op: 'is_not_empty'
};
const is_not_empty3 = {
id: guid(),
left: {
type: 'field',
field: 'detail'
},
op: 'is_not_empty'
};
const is_not_empty4 = {
id: guid(),
left: {
type: 'field',
field: 'arr'
},
op: 'is_not_empty'
};
const between = {
id: guid(),
left: {
type: 'field',
field: 'num'
},
op: 'between',
right: '${[0.5,1]}'
};
const not_between = {
id: guid(),
left: {
type: 'field',
field: 'range'
},
op: 'not_between',
right: '${[1678636800,1678895999]}'
};
const select_any_in1 = {
id: guid(),
left: {
type: 'field',
field: 'features'
},
op: 'select_any_in',
right: '${[LAST(features)]}'
};
const select_any_in2 = {
id: guid(),
left: {
type: 'field',
field: 'feature'
},
op: 'select_any_in',
right: '${features}'
};
const select_not_any_in = {
id: guid(),
left: {
type: 'field',
field: 'features'
},
op: 'select_not_any_in',
right: "${['powerful']}"
};
const conditions1 = {
id: guid(),
conjunction: 'and',
children: [equal1, equal2, equal3, not_equal]
};
const conditions2 = {
id: guid(),
conjunction: 'or',
children: [greater, less, greater_or_equal, less_or_equal]
};
const conditions3 = {
id: guid(),
conjunction: 'and',
children: [starts_with, ends_with, like, not_like]
};
const conditions4 = {
id: guid(),
conjunction: 'and',
children: [is_empty1, is_empty2, is_empty3]
};
const conditions5 = {
id: guid(),
conjunction: 'and',
children: [is_not_empty1, is_not_empty2, is_not_empty3, is_not_empty4]
};
const conditions6 = {
id: guid(),
conjunction: 'and',
children: [between, not_between]
};
const conditions7 = {
id: guid(),
conjunction: 'and',
children: [select_any_in1, select_any_in2, select_not_any_in]
};
test(`condition`, async () => {
expect(await resolveCondition(conditions1, data)).toBe(false);
expect(await resolveCondition(conditions2, data)).toBe(true);
expect(await resolveCondition(conditions3, data)).toBe(false);
expect(await resolveCondition(conditions4, data)).toBe(false);
expect(await resolveCondition(conditions5, data)).toBe(false);
expect(await resolveCondition(conditions6, data)).toBe(false);
expect(await resolveCondition(conditions7, data)).toBe(false);
});
test(`condition date`, async () => {
const conditions = {
id: guid(),
conjunction: 'and',
children: [
{
id: guid(),
left: {
type: 'date',
field: 'date'
},
op: 'less',
right: '2023-03-20'
},
{
id: guid(),
left: {
type: 'date',
field: 'datetime'
},
op: 'less',
right: '2023-03-21T04:55:00+08:00'
},
{
id: guid(),
left: {
type: 'date',
field: 'time'
},
op: 'less',
right: '00:08'
},
{
id: guid(),
left: {
type: 'date',
field: 'date'
},
op: 'less_or_equal',
right: '2023-03-19'
},
{
id: guid(),
left: {
type: 'date',
field: 'datetime'
},
op: 'less_or_equal',
right: '2023-03-20T04:55:00+08:00'
},
{
id: guid(),
left: {
type: 'date',
field: 'time'
},
op: 'less_or_equal',
right: '00:05'
},
{
id: guid(),
left: {
type: 'date',
field: 'date'
},
op: 'between',
right: '${["2023-03-19","2023-03-20"]}'
},
{
id: guid(),
left: {
type: 'date',
field: 'datetime'
},
op: 'between',
right: '${["2023-03-20T04:55:00+08:00","2023-03-21T04:55:00+08:00"]}'
},
{
id: guid(),
left: {
type: 'date',
field: 'time'
},
op: 'between',
right: '${["00:05","00:06"]}'
}
]
};
expect(await resolveCondition(conditions, data)).toBe(true);
});
test(`condition tree`, async () => {
const conditions8 = {
id: guid(),
conjunction: 'and',
children: [
select_any_in1,
{
id: guid(),
conjunction: 'and',
children: [
equal1,
{
id: guid(),
conjunction: 'or',
children: [between, not_between]
},
between,
{
id: guid(),
conjunction: 'and',
children: [between, select_any_in2, conditions5]
}
]
}
]
};
const conditions9 = {
id: guid(),
conjunction: 'or',
children: [
is_empty1,
{
id: guid(),
conjunction: 'or',
children: [
is_empty1,
conditions6,
between,
{
id: guid(),
conjunction: 'or',
children: [is_empty1, select_any_in2, conditions5]
}
]
}
]
};
const conditions10 = {
id: guid(),
conjunction: 'or',
children: [
{
id: guid(),
conjunction: 'or',
children: [
conditions6,
is_empty1,
{
id: guid(),
conjunction: 'or',
children: [
is_empty1,
{
id: guid(),
conjunction: 'or',
children: [
is_not_empty1,
is_not_empty2,
is_not_empty3,
is_not_empty4
]
}
]
}
]
},
is_empty1
]
};
expect(await resolveCondition(conditions8, data)).toBe(false);
expect(await resolveCondition(conditions9, data)).toBe(true);
expect(await resolveCondition(conditions10, data)).toBe(true);
});
test(`condition register`, async () => {
registerConditionComputer(
'customless',
(left: any, right?: any, fieldType?: string) => {
if (fieldType === 'date') {
return moment(left).isBefore(moment(right), 'day');
}
return left < right;
}
);
const conditions = {
id: guid(),
conjunction: 'and',
children: [
{
id: guid(),
left: {
type: 'date',
field: 'date'
},
op: 'customless',
right: '2023-03-20'
},
{
id: guid(),
left: {
type: 'field',
field: 'num'
},
op: 'customless',
right: '5'
}
]
};
expect(await resolveCondition(conditions, data)).toBe(true);
});

View File

@ -1,8 +1,9 @@
import omit from 'lodash/omit';
import {RendererProps} from '../factory';
import {ConditionGroupValue} from '../types';
import {createObject} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpression} from '../utils/tpl';
import {evalExpressionWithConditionBuilder} from '../utils/tpl';
import {dataMapping} from '../utils/tpl-builtin';
import {IBreakAction} from './BreakAction';
import {IContinueAction} from './ContinueAction';
@ -28,7 +29,7 @@ export interface ListenerAction {
outputVar?: string; // 输出数据变量名
preventDefault?: boolean; // 阻止原有组件的动作行为
stopPropagation?: boolean; // 阻止后续的事件处理器执行
expression?: string; // 执行条件
expression?: string | ConditionGroupValue; // 执行条件
execOn?: string; // 执行条件1.9.0废弃
}
@ -207,18 +208,38 @@ export const runAction = async (
);
// 兼容一下1.9.0之前的版本
const expression = actionConfig.expression ?? actionConfig.execOn;
// 执行条件
let isStop = false;
if (expression && !evalExpression(expression, mergeData)) {
if (expression) {
isStop = !(await evalExpressionWithConditionBuilder(
expression,
mergeData,
true
));
}
if (isStop) {
return;
}
// 支持表达式 >=1.10.0
const preventDefault =
actionConfig.preventDefault &&
evalExpression(String(actionConfig.preventDefault), mergeData);
const stopPropagation =
actionConfig.stopPropagation &&
evalExpression(String(actionConfig.stopPropagation), mergeData);
let preventDefault = false;
if (actionConfig.preventDefault) {
preventDefault = await evalExpressionWithConditionBuilder(
String(actionConfig.preventDefault),
mergeData,
false
);
}
let stopPropagation = false;
if (actionConfig.stopPropagation) {
stopPropagation = await evalExpressionWithConditionBuilder(
String(actionConfig.stopPropagation),
mergeData,
false
);
}
// 动作配置
const args = dataMapping(actionConfig.args, mergeData, key =>

View File

@ -1,5 +1,5 @@
import { RendererEvent } from '../utils/renderer-event';
import { evalExpression } from '../utils/tpl';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpressionWithConditionBuilder} from '../utils/tpl';
import {
RendererAction,
ListenerContext,
@ -27,7 +27,12 @@ export class SwitchAction implements RendererAction {
continue;
}
if (evalExpression(branch.expression, mergeData)) {
const isPass = await evalExpressionWithConditionBuilder(
branch.expression,
mergeData
);
if (isPass) {
await runActions(branch, renderer, event);
// 去掉runAllMatch这里只做排他多个可以直接通过expression
break;

View File

@ -79,7 +79,9 @@ import type {RendererEnv} from './env';
import React from 'react';
import {
evaluate,
evaluateForAsync,
Evaluator,
AsyncEvaluator,
extendsFilters,
filters,
getFilters,
@ -152,6 +154,7 @@ export {
parse,
lexer,
Evaluator,
AsyncEvaluator,
FilterContext,
filters,
getFilters,
@ -159,6 +162,7 @@ export {
extendsFilters,
registerFunction,
evaluate,
evaluateForAsync,
// 其他
LazyComponent,
Overlay,

View File

@ -626,3 +626,70 @@ export interface BaseSchemaWithoutType {
staticInputClassName?: SchemaClassName;
staticSchema?: any;
}
export type OperatorType =
| 'equal'
| 'not_equal'
| 'is_empty'
| 'is_not_empty'
| 'like'
| 'not_like'
| 'starts_with'
| 'ends_with'
| 'less'
| 'less_or_equal'
| 'greater'
| 'greater_or_equal'
| 'between'
| 'not_between'
| 'select_equals'
| 'select_not_equals'
| 'select_any_in'
| 'select_not_any_in'
| {
label: string;
value: string;
};
export type ExpressionSimple = string | number | object | undefined;
export type ExpressionValue =
| ExpressionSimple
| {
type: 'value';
value: ExpressionSimple;
};
export type ExpressionFunc = {
type: 'func';
func: string;
args: Array<ExpressionComplex>;
};
export type ExpressionField = {
type: 'field';
field: string;
};
export type ExpressionFormula = {
type: 'formula';
value: string;
};
export type ExpressionComplex =
| ExpressionValue
| ExpressionFunc
| ExpressionField
| ExpressionFormula;
export interface ConditionRule {
id: any;
left?: ExpressionComplex;
op?: OperatorType;
right?: ExpressionComplex | Array<ExpressionComplex>;
}
export interface ConditionGroupValue {
id: string;
conjunction: 'and' | 'or';
not?: boolean;
children?: Array<ConditionRule | ConditionGroupValue>;
}
export interface ConditionValue extends ConditionGroupValue {}

View File

@ -39,6 +39,7 @@ export * from './replaceText';
export * from './resize-sensor';
export * from './resolveVariable';
export * from './resolveVariableAndFilter';
export * from './resolveVariableAndFilterForAsync';
export * from './RootClose';
export * from './scrollPosition';
export * from './SimpleMap';
@ -52,6 +53,7 @@ export * from './validations';
export * from './toNumber';
export * from './decodeEntity';
export * from './style-helper';
export * from './resolveCondition';
import animation from './Animation';

View File

@ -0,0 +1,341 @@
import get from 'lodash/get';
import endsWith from 'lodash/endsWith';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import startsWith from 'lodash/startsWith';
import {resolveVariableAndFilterForAsync} from './resolveVariableAndFilterForAsync';
import moment from 'moment';
import capitalize from 'lodash/capitalize';
const conditionResolverMap: {
[op: string]: (left: any, right: any, fieldType?: string) => boolean;
} = {};
const DEFAULT_RESULT = true;
export async function resolveCondition(
conditions: any,
data: any,
defaultResult: boolean = true
) {
if (
!conditions ||
!conditions.conjunction ||
!Array.isArray(conditions.children) ||
!conditions.children.length
) {
return defaultResult;
}
return await computeConditions(
conditions.children,
conditions.conjunction,
data
);
}
async function computeConditions(
conditions: any[],
conjunction: 'or' | 'and' = 'and',
data: any
): Promise<boolean> {
let computeResult = true;
for (let index = 0, len = conditions.length; index < len; index++) {
const item = conditions[index];
const result =
item.conjunction && Array.isArray(item.children) && item.children.length
? await computeConditions(item.children, item.conjunction, data)
: await computeCondition(item, index, data);
computeResult = !!result;
if (
(result && conjunction === 'or') ||
(!result && conjunction === 'and')
) {
break;
}
}
return computeResult;
}
async function computeCondition(
rule: {
op: string;
left: {
type: string;
field: string;
};
right: any;
},
index: number,
data: any
) {
const leftValue = get(data, rule.left.field);
const rightValue: any = await resolveVariableAndFilterForAsync(
rule.right,
data
);
const func =
conditionResolverMap[`${rule.op}For${capitalize(rule.left.type)}`] ??
conditionResolverMap[rule.op];
return func ? func(leftValue, rightValue, rule.left.type) : DEFAULT_RESULT;
}
function startsWithFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return startsWith(left, right);
}
function endsWithFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return endsWith(left, right);
}
function equalFunc(left: any, right: any) {
return isEqual(left, right);
}
function notEqualFunc(left: any, right: any) {
return !isEqual(left, right);
}
function isEmptyFunc(left: any) {
if (typeof left === 'string') {
return !left;
} else if (typeof left === 'number') {
return left === undefined;
} else if (Array.isArray(left)) {
return !left.length;
} else if (typeof left === 'object') {
return isEmpty(left);
}
return DEFAULT_RESULT;
}
function isNotEmptyFunc(left: any) {
if (typeof left === 'string') {
return !left;
} else if (typeof left === 'number') {
return left !== undefined;
} else if (Array.isArray(left)) {
return !!left.length;
} else if (typeof left === 'object') {
return !isEmpty(left);
}
return DEFAULT_RESULT;
}
function greaterFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return parseFloat(left as any) > parseFloat(right as any);
}
function normalizeDate(raw: any): Date {
if (typeof raw === 'string' || typeof raw === 'number') {
let formats = ['', 'YYYY-MM-DD HH:mm:ss', 'X'];
if (/^\d{10}((\.\d+)*)$/.test(raw.toString())) {
formats = ['X', 'x', 'YYYY-MM-DD HH:mm:ss', ''];
} else if (/^\d{13}((\.\d+)*)$/.test(raw.toString())) {
formats = ['x', 'X', 'YYYY-MM-DD HH:mm:ss', ''];
}
while (formats.length) {
const format = formats.shift()!;
const date = moment(raw, format);
if (date.isValid()) {
return date.toDate();
}
}
}
return raw;
}
function normalizeDateRange(raw: string | Date[]): Date[] {
return (Array.isArray(raw) ? raw : raw.split(',')).map((item: any) =>
normalizeDate(String(item).trim())
);
}
function greaterForDateFunc(left: any, right: any) {
left = normalizeDate(left);
right = normalizeDate(right);
return moment(left).isAfter(moment(right), 's');
}
function greaterOrEqualForDateFunc(left: any, right: any) {
left = normalizeDate(left);
right = normalizeDate(right);
return moment(left).isSameOrAfter(moment(right), 's');
}
function greaterOrEqualFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return parseFloat(left as any) >= parseFloat(right as any);
}
function lessFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return parseFloat(left as any) < parseFloat(right as any);
}
function lessForDateFunc(left: any, right: any) {
left = normalizeDate(left);
right = normalizeDate(right);
return moment(left).isBefore(moment(right), 's');
}
function lessOrEqualForDateFunc(left: any, right: any) {
left = normalizeDate(left);
right = normalizeDate(right);
return moment(left).isSameOrBefore(moment(right), 's');
}
function lessOrEqualFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return parseFloat(left as any) <= parseFloat(right as any);
}
function likeFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return !!~left.indexOf(right);
}
function notLikeFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;
}
return !~left.indexOf(right);
}
function betweenFunc(left: any, right: any) {
if (typeof left === 'number' && right !== undefined) {
const [min, max] = right.sort();
return left >= parseFloat(min) && left <= parseFloat(max);
}
return DEFAULT_RESULT;
}
function betweenForDateFunc(left: any, right: any) {
if (right !== undefined) {
const [min, max] = normalizeDateRange(right);
return moment(normalizeDate(left)).isBetween(min, max, 's', '[]');
}
return DEFAULT_RESULT;
}
function notBetweenFunc(left: any, right: any) {
if (typeof left === 'number' && right !== undefined) {
const [min, max] = right.sort();
return left < parseFloat(min) && left > parseFloat(max);
}
return DEFAULT_RESULT;
}
function notBetweenForDateFunc(left: any, right: any) {
if (right !== undefined) {
const [min, max] = normalizeDateRange(right);
return !moment(normalizeDate(left)).isBetween(min, max, 's', '[]');
}
return DEFAULT_RESULT;
}
function selectAnyInFunc(left: any, right: any) {
if (!Array.isArray(right)) {
return DEFAULT_RESULT;
}
if (Array.isArray(left)) {
return right.every((item: any) => left.includes(item));
}
return right.includes(left);
}
function selectNotAnyInFunc(left: any, right: any) {
if (!Array.isArray(right)) {
return DEFAULT_RESULT;
}
if (Array.isArray(left)) {
return !right.every((item: any) => left.includes(item));
}
return !right.includes(left);
}
export function registerConditionComputer(
op: string,
func: (left: any, right: any, fieldType?: string) => boolean,
fieldType?: string
) {
conditionResolverMap[
`${op}${fieldType ? 'For' + capitalize(fieldType) : ''}`
] = func;
}
export function getConditionComputers() {
return conditionResolverMap;
}
registerConditionComputer('greater', greaterFunc);
registerConditionComputer('greater', greaterForDateFunc, 'date');
registerConditionComputer('greater', greaterForDateFunc, 'time');
registerConditionComputer('greater', greaterForDateFunc, 'datetime');
registerConditionComputer('greater_or_equal', greaterOrEqualFunc);
registerConditionComputer(
'greater_or_equal',
greaterOrEqualForDateFunc,
'date'
);
registerConditionComputer(
'greater_or_equal',
greaterOrEqualForDateFunc,
'time'
);
registerConditionComputer(
'greater_or_equal',
greaterOrEqualForDateFunc,
'datetime'
);
registerConditionComputer('less', lessFunc);
registerConditionComputer('less', lessForDateFunc, 'date');
registerConditionComputer('less', lessForDateFunc, 'time');
registerConditionComputer('less', lessForDateFunc, 'datetime');
registerConditionComputer('less_or_equal', lessOrEqualFunc);
registerConditionComputer('less_or_equal', lessOrEqualForDateFunc, 'date');
registerConditionComputer('less_or_equal', lessOrEqualForDateFunc, 'time');
registerConditionComputer('less_or_equal', lessOrEqualForDateFunc, 'datetime');
registerConditionComputer('is_empty', isEmptyFunc);
registerConditionComputer('is_not_empty', isNotEmptyFunc);
registerConditionComputer('between', betweenFunc);
registerConditionComputer('between', betweenForDateFunc, 'date');
registerConditionComputer('between', betweenForDateFunc, 'time');
registerConditionComputer('between', betweenForDateFunc, 'datetime');
registerConditionComputer('not_between', notBetweenFunc);
registerConditionComputer('not_between', notBetweenForDateFunc, 'date');
registerConditionComputer('not_between', notBetweenForDateFunc, 'time');
registerConditionComputer('not_between', notBetweenForDateFunc, 'datetime');
registerConditionComputer('equal', equalFunc);
registerConditionComputer('not_equal', notEqualFunc);
registerConditionComputer('like', likeFunc);
registerConditionComputer('not_like', notLikeFunc);
registerConditionComputer('select_any_in', selectAnyInFunc);
registerConditionComputer('select_not_any_in', selectNotAnyInFunc);
registerConditionComputer('starts_with', startsWithFunc);
registerConditionComputer('ends_with', endsWithFunc);

View File

@ -0,0 +1,30 @@
import {AsyncEvaluator, parse} from 'amis-formula';
export const resolveVariableAndFilterForAsync = async (
path?: string,
data: object = {},
defaultFilter: string = '| html',
fallbackValue = (value: any) => value
) => {
if (!path || typeof path !== 'string') {
return undefined;
}
try {
const ast = parse(path, {
evalMode: false,
allowFilter: true
});
const ret = await new AsyncEvaluator(data, {
defaultFilter
}).evalute(ast);
return ret == null && !~path.indexOf('default') && !~path.indexOf('now')
? fallbackValue(ret)
: ret;
} catch (e) {
console.warn(e);
return undefined;
}
};

View File

@ -1,7 +1,7 @@
import {createObject} from './helper';
import {register as registerBulitin, getFilters} from './tpl-builtin';
import {register as registerLodash} from './tpl-lodash';
import {parse, evaluate} from 'amis-formula';
import {resolveCondition} from './resolveCondition';
export interface Enginer {
test: (tpl: string) => boolean;
@ -99,6 +99,25 @@ export function evalExpression(expression: string, data?: object): boolean {
}
}
/**
* condition-builder
* @param expression or condition-builder对象
* @param data
* @returns
*/
export async function evalExpressionWithConditionBuilder(
expression: any,
data?: object,
defaultResult?: boolean
): Promise<boolean> {
// 支持ConditionBuilder
if (Object.prototype.toString.call(expression) === '[object Object]') {
return await resolveCondition(expression, data, defaultResult);
}
return evalExpression(expression, data);
}
const AST_CACHE: {[key: string]: any} = {};
function evalFormula(expression: string, data: any) {
const ast =

View File

@ -0,0 +1,589 @@
import {evaluateForAsync, parse} from '../src';
test('evalute:simple', async () => {
expect(
await evaluateForAsync('a is ${a}', {
a: 123
})
).toBe('a is 123');
});
test('evalute:filter', async () => {
expect(
await evaluateForAsync(
'a is ${a | abc}',
{
a: 123
},
{
filters: {
abc(input: any) {
return `${input}456`;
}
}
}
)
).toBe('a is 123456');
expect(
await evaluateForAsync(
'a is ${a | concat:233}',
{
a: 123
},
{
filters: {
concat(input: any, arg: string) {
return `${input}${arg}`;
}
}
}
)
).toBe('a is 123233');
expect(
await evaluateForAsync(
'a is ${concat(a, a)}',
{
a: 123
},
{
filters: {
concat(input: any, arg: string) {
return `${input}${arg}`;
}
}
}
)
).toBe('a is 123123');
});
test('evalute:filter2', async () => {
expect(
await evaluateForAsync(
'a is ${[1, 2, 3] | concat:4 | join}',
{},
{
filters: {
concat(input: any, ...args: Array<any>) {
return input.concat.apply(input, args);
},
join(input: any) {
return input.join(',');
}
}
}
)
).toBe('a is 1,2,3,4');
});
test('evalute:filter3', async () => {
expect(
await evaluateForAsync(
'a is ${[1, 2, 3] | concat:"4" | join}',
{},
{
filters: {
concat(input: any, ...args: Array<any>) {
return input.concat.apply(input, args);
},
join(input: any) {
return input.join(',');
}
}
}
)
).toBe('a is 1,2,3,4');
});
test('evalute:filter4', async () => {
expect(
await evaluateForAsync(
'a is ${[1, 2, 3] | concat:${a + 3} | join}',
{
a: 4
},
{
filters: {
concat(input: any, ...args: Array<any>) {
return input.concat.apply(input, args);
},
join(input: any) {
return input.join(',');
}
}
}
)
).toBe('a is 1,2,3,7');
});
test('evalute:oldVariable', async () => {
expect(
await evaluateForAsync('a is $a', {
a: 4
})
).toBe('a is 4');
expect(
await evaluateForAsync('b is $b', {
a: 4
})
).toBe('b is ');
expect(
await evaluateForAsync('a.b is $a.b', {
a: {
b: 233
}
})
).toBe('a.b is 233');
});
test('evalute:ariable2', async () => {
expect(
await evaluateForAsync('a is $$', {
a: 4
})
).toBe('a is [object Object]');
});
test('evalute:ariable3', async () => {
expect(
await evaluateForAsync(
'$$',
{
a: 4
},
{
defaultFilter: 'raw'
}
)
).toMatchObject({
a: 4
});
});
test('evalute:object-variable', async () => {
const data = {
key: 'x',
obj: {
x: 1,
y: 2
}
};
expect(await evaluateForAsync('a is ${obj.x}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${obj[x]}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${obj[`x`]}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${obj["x"]}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${obj[key]}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${obj[`${key}`]}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${obj[${key}]}', data)).toBe('a is 1');
});
test('evalute:literal-variable', async () => {
const data = {
key: 'x',
index: 0,
obj: {
x: 1,
y: 2
}
};
expect(await evaluateForAsync('a is ${({x: 1})["x"]}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${({x: 1}).x}', data)).toBe('a is 1');
expect(await evaluateForAsync('a is ${(["a", "b"])[index]}', data)).toBe(
'a is a'
);
expect(await evaluateForAsync('a is ${(["a", "b"])[1]}', data)).toBe(
'a is b'
);
expect(await evaluateForAsync('a is ${(["a", "b"]).0}', data)).toBe('a is a');
});
test('evalute:tempalte', async () => {
const data = {
key: 'x'
};
expect(await evaluateForAsync('abc${`11${3}22`}xyz', data)).toBe(
'abc11322xyz'
);
expect(await evaluateForAsync('abc${`${3}22`}xyz', data)).toBe('abc322xyz');
expect(await evaluateForAsync('abc${`11${3}`}xyz', data)).toBe('abc113xyz');
expect(await evaluateForAsync('abc${`${3}`}xyz', data)).toBe('abc3xyz');
expect(await evaluateForAsync('abc${`${key}`}xyz', data)).toBe('abcxxyz');
});
test('evalute:literal', async () => {
const data = {
dynamicKey: 'alpha'
};
expect(
await evaluateForAsync('${{a: 1, 0: 2, "3": 3}}', data, {
defaultFilter: 'raw'
})
).toMatchObject({
a: 1,
0: 2,
3: 3
});
expect(
await evaluateForAsync('${{a: 1, 0: 2, "3": 3, [`4`]: 4}}', data, {
defaultFilter: 'raw'
})
).toMatchObject({
a: 1,
0: 2,
3: 3,
4: 4
});
expect(
await evaluateForAsync(
'${{a: 1, 0: 2, "3": 3, [`${dynamicKey}233`]: 4}}',
data,
{
defaultFilter: 'raw'
}
)
).toMatchObject({
a: 1,
0: 2,
3: 3,
alpha233: 4
});
expect(
await evaluateForAsync(
'${[1, 2, `2${dynamicKey}2`, {a: 1, 0: 2, [`2`]: "3"}]}',
data,
{
defaultFilter: 'raw'
}
)
).toMatchObject([1, 2, `2alpha2`, {a: 1, 0: 2, [`2`]: '3'}]);
});
test('evalute:variableName', async () => {
const data = {
'a-b': 'c',
'222': 10222,
'222_221': 233,
'222_abcde': 'abcde',
'222-221': 333
};
expect(await evaluateForAsync('${a-b}', data)).toBe('c');
expect(await evaluateForAsync('${222}', data)).toBe(222);
expect(await evaluateForAsync('${222_221}', data)).toBe('233');
expect(await evaluateForAsync('${222-221}', data)).toBe(1);
expect(await evaluateForAsync('${222_abcde}', data)).toBe('abcde');
expect(
await evaluateForAsync('${&["222-221"]}', data, {
defaultFilter: 'raw'
})
).toBe(333);
expect(
await evaluateForAsync('222', data, {
variableMode: true
})
).toBe(10222);
});
test('evalute:3-1', async () => {
const data = {};
expect(await evaluateForAsync('${3-1}', data)).toBe(2);
expect(await evaluateForAsync('${-1 + 2.5 + 3}', data)).toBe(4.5);
expect(await evaluateForAsync('${-1 + -1}', data)).toBe(-2);
expect(await evaluateForAsync('${3 * -1}', data)).toBe(-3);
expect(await evaluateForAsync('${3 + +1}', data)).toBe(4);
});
test('evalate:0.1+0.2', async () => {
expect(await evaluateForAsync('${0.1 + 0.2}', {})).toBe(0.3);
});
test('evalute:variable:com.xxx.xx', async () => {
const data = {
'com.xxx.xx': 'abc',
'com xxx%xx': 'cde',
'com[xxx]': 'eee'
};
expect(await evaluateForAsync('${com\\.xxx\\.xx}', data)).toBe('abc');
expect(await evaluateForAsync('${com\\ xxx\\%xx}', data)).toBe('cde');
expect(await evaluateForAsync('${com\\[xxx\\]}', data)).toBe('eee');
});
test('evalute:anonymous:function', async () => {
const data = {
arr: [1, 2, 3],
arr2: [
{
a: 1
},
{
a: 2
},
{
a: 3
}
],
outter: 4
};
expect(await evaluateForAsync('${() => 233}', data)).toMatchObject({
args: [],
return: {type: 'literal', value: 233},
type: 'anonymous_function'
});
expect(
await evaluateForAsync('${ARRAYMAP(arr, () => 1)}', data)
).toMatchObject([1, 1, 1]);
expect(
await evaluateForAsync('${ARRAYMAP(arr, item => item)}', data)
).toMatchObject([1, 2, 3]);
expect(
await evaluateForAsync('${ARRAYMAP(arr, item => item * 2)}', data)
).toMatchObject([2, 4, 6]);
expect(
await evaluateForAsync(
'${ARRAYMAP(arr2, (item, index) => `a${item.a}${index}`)}',
data
)
).toMatchObject(['a10', 'a21', 'a32']);
expect(
await evaluateForAsync(
'${ARRAYMAP(arr2, (item, index) => `a${item.a}${index}${outter}`)}',
data
)
).toMatchObject(['a104', 'a214', 'a324']);
expect(
await evaluateForAsync(
'${ARRAYMAP(arr2, (item, index) => {x: item.a, index: index})}',
data
)
).toMatchObject([
{
x: 1,
index: 0
},
{
x: 2,
index: 1
},
{
x: 3,
index: 2
}
]);
});
test('evalute:anonymous:function2', async () => {
const data = {
arr: [1, 2, 3],
arr2: [
{
x: 1,
y: [
{
z: 1
},
{
z: 1
}
]
},
{
x: 2,
y: [
{
z: 2
},
{
z: 2
}
]
}
]
};
expect(
await evaluateForAsync(
'${ARRAYMAP(ARRAYMAP(arr, item => item * 2), item => item + 2)}',
data
)
).toMatchObject([4, 6, 8]);
expect(
await evaluateForAsync(
'${ARRAYMAP(arr2, item => ARRAYMAP(item.y, i => i.z))}',
data
)
).toMatchObject([
[1, 1],
[2, 2]
]);
});
test('evalute:array:func', async () => {
const data = {
arr1: [0, 1, false, 2, '', 3],
arr2: ['a', 'b', 'c'],
arr3: [1, 2, 3],
arr4: [2, 4, 6],
arr5: [
{
id: 1.1
},
{
id: 2.2
},
{
id: 1.1,
name: 1.3
}
],
obj1: {
p1: 'name',
p2: 'age',
p3: 'obj',
p4: [
{
p41: 'Tom',
p42: 'Jerry'
},
{
p41: 'baidu',
p42: 'amis'
}
]
}
};
expect(await evaluateForAsync('${COMPACT(arr1)}', data)).toMatchObject([
1, 2, 3
]);
expect(
await evaluateForAsync("${COMPACT([0, 1, false, 2, '', 3])}", data)
).toMatchObject([1, 2, 3]);
expect(await evaluateForAsync('${JOIN(arr2, "~")}', data)).toMatch('a~b~c');
expect(await evaluateForAsync('${SUM(arr3)}', data)).toBe(6);
expect(await evaluateForAsync('${AVG(arr4)}', data)).toBe(4);
expect(await evaluateForAsync('${MIN(arr4)}', data)).toBe(2);
expect(await evaluateForAsync('${MAX(arr4)}', data)).toBe(6);
expect(await evaluateForAsync('${CONCAT(arr3, arr4)}', data)).toMatchObject([
1, 2, 3, 2, 4, 6
]);
expect(await evaluateForAsync('${CONCAT(arr, arr4)}', data)).toMatchObject([
2, 4, 6
]);
expect(await evaluateForAsync('${UNIQ(arr5)}', data)).toMatchObject(
data.arr5
);
expect(await evaluateForAsync('${UNIQ(arr5, "id")}', data)).toMatchObject([
{id: 1.1},
{id: 2.2}
]);
expect(
await evaluateForAsync('${ARRAYFILTER(arr1, item => item)}', data)
).toMatchObject([1, 2, 3]);
expect(
await evaluateForAsync(
'${ARRAYFILTER(arr1, item => item && item >=2)}',
data
)
).toMatchObject([2, 3]);
expect(
await evaluateForAsync('${ARRAYFINDINDEX(arr3, item => item === 2)}', data)
).toBe(1);
expect(
await evaluateForAsync(
'${ARRAYFIND(arr5, item => item.name === 1.3)}',
data
)
).toMatchObject({
id: 1.1,
name: 1.3
});
expect(
await evaluateForAsync(
'${ARRAYSOME(arr5, item => item.name === 1.3)}',
data
)
).toBe(true);
expect(
await evaluateForAsync(
'${ARRAYEVERY(arr5, item => item.name === 1.3)}',
data
)
).toBe(false);
expect(await evaluateForAsync('${ARRAYINCLUDES(arr1, false)}', data)).toBe(
true
);
expect(await evaluateForAsync('${GET(arr1, 2)}', data)).toBe(false);
expect(await evaluateForAsync('${GET(arr1, 6, "not-found")}', data)).toBe(
'not-found'
);
expect(await evaluateForAsync('${GET(arr5, "[2].name")}', data)).toBe(1.3);
expect(await evaluateForAsync('${GET(arr5, "2.name")}', data)).toBe(1.3);
expect(await evaluateForAsync('${GET(obj1, "p2")}', data)).toBe('age');
expect(await evaluateForAsync('${GET(obj1, "p4.1.p42")}', data)).toBe('amis');
expect(await evaluateForAsync('${GET(obj1, "p4[1].p42")}', data)).toBe(
'amis'
);
expect(await evaluateForAsync('${ENCODEJSON(obj1)}', data)).toBe(
JSON.stringify(data.obj1)
);
expect(
await evaluateForAsync('${DECODEJSON("{\\"name\\":\\"amis\\"}")}', data)
).toMatchObject(JSON.parse('{"name":"amis"}'));
});
test('evalute:ISTYPE', async () => {
const data = {
a: 1,
b: 'string',
c: null,
d: undefined,
e: [1, 2],
f: {a: 1, b: 2},
g: new Date()
};
expect(await evaluateForAsync('${ISTYPE(a, "number")}', data)).toBe(true);
expect(await evaluateForAsync('${ISTYPE(b, "number")}', data)).toBe(false);
expect(await evaluateForAsync('${ISTYPE(b, "string")}', data)).toBe(true);
expect(await evaluateForAsync('${ISTYPE(c, "nil")}', data)).toBe(true);
expect(await evaluateForAsync('${ISTYPE(d, "nil")}', data)).toBe(true);
expect(await evaluateForAsync('${ISTYPE(e, "array")}', data)).toBe(true);
expect(await evaluateForAsync('${ISTYPE(f, "array")}', data)).toBe(false);
expect(await evaluateForAsync('${ISTYPE(f, "plain-object")}', data)).toBe(
true
);
expect(await evaluateForAsync('${ISTYPE(g, "date")}', data)).toBe(true);
});

View File

@ -0,0 +1,390 @@
import moment from 'moment';
import {evaluateForAsync, registerFunction} from '../src';
const defaultContext = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5
};
async function evalFormual(expression: string, data: any = defaultContext) {
return evaluateForAsync(expression, data, {
evalMode: true
});
}
test('formula:expression', async () => {
expect(await evalFormual('a + 3')).toBe(4);
expect(await evalFormual('b * 3')).toBe(6);
expect(await evalFormual('b * 3 + 4')).toBe(10);
expect(await evalFormual('c * (3 + 4)')).toBe(21);
expect(await evalFormual('d / (a + 1)')).toBe(2);
expect(await evalFormual('5 % 3')).toBe(2);
expect(await evalFormual('3 | 4')).toBe(7);
expect(await evalFormual('4 ^ 4')).toBe(0);
expect(await evalFormual('4 ^ 4')).toBe(0);
expect(await evalFormual('4 & 4')).toBe(4);
expect(await evalFormual('4 & 3')).toBe(0);
expect(await evalFormual('~-1')).toBe(0);
expect(await evalFormual('!!1')).toBe(true);
expect(await evalFormual('!!""')).toBe(false);
expect(await evalFormual('1 || 2')).toBe(1);
expect(await evalFormual('1 && 2')).toBe(2);
expect(await evalFormual('1 && 2 || 3')).toBe(2);
expect(await evalFormual('1 || 2 || 3')).toBe(1);
expect(await evalFormual('1 || 2 && 3')).toBe(1);
expect(await evalFormual('(1 || 2) && 3')).toBe(3);
expect(await evalFormual('1 == "1"')).toBe(true);
expect(await evalFormual('1 === "1"')).toBe(false);
expect(await evalFormual('1 < 1')).toBe(false);
expect(await evalFormual('1 <= 1')).toBe(true);
expect(await evalFormual('1 > 1')).toBe(false);
expect(await evalFormual('1 >= 1')).toBe(true);
expect(await evalFormual('3 >> 1')).toBe(1);
expect(await evalFormual('3 << 1')).toBe(6);
expect(await evalFormual('10 ** 3')).toBe(1000);
expect(await evalFormual('10 ? 3 : 2')).toBe(3);
expect(await evalFormual('0 ? 3 : 2')).toBe(2);
});
test('formula:expression2', async () => {
expect(await evalFormual('a[0]', {a: [1, 2, 3]})).toBe(1);
expect(await evalFormual('a[b]', {a: [1, 2, 3], b: 1})).toBe(2);
expect(await evalFormual('a[b - 1]', {a: [1, 2, 3], b: 1})).toBe(1);
expect(await evalFormual('a[b ? 1 : 2]', {a: [1, 2, 3], b: 1})).toBe(2);
expect(await evalFormual('a[c ? 1 : 2]', {a: [1, 2, 3], b: 1})).toBe(3);
});
test('formula:expression3', async () => {
// expect(await evalFormual('${a} === "b"', {a: 'b'})).toBe(true);
expect(await evalFormual('b === "b"')).toBe(false);
// expect(await evalFormual('${a}', {a: 'b'})).toBe('b');
expect(await evalFormual('obj.x.a', {obj: {x: {a: 1}}})).toBe(1);
expect(await evalFormual('obj.y.a', {obj: {x: {a: 1}}})).toBe(undefined);
});
test('formula:if', async () => {
expect(await evalFormual('IF(true, 2, 3)')).toBe(2);
expect(await evalFormual('IF(false, 2, 3)')).toBe(3);
expect(await evalFormual('IF(false, 2, IF(true, 3, 4))')).toBe(3);
});
test('formula:and', async () => {
expect(!!(await evalFormual('AND(0, 1)'))).toBe(false);
expect(!!(await evalFormual('AND(1, 1)'))).toBe(true);
expect(!!(await evalFormual('AND(1, 1, 1, 0)'))).toBe(false);
});
test('formula:or', async () => {
expect(!!(await evalFormual('OR(0, 1)'))).toBe(true);
expect(!!(await evalFormual('OR(1, 1)'))).toBe(true);
expect(!!(await evalFormual('OR(1, 1, 1, 0)'))).toBe(true);
expect(!!(await evalFormual('OR(0, 0, 0, 0)'))).toBe(false);
});
test('formula:xor', async () => {
expect(await evalFormual('XOR(0, 1)')).toBe(true);
expect(await evalFormual('XOR(1, 0)')).toBe(true);
expect(await evalFormual('XOR(1, 1)')).toBe(false);
expect(await evalFormual('XOR(0, 0)')).toBe(false);
expect(await evalFormual('XOR(0, 0, 1)')).toBe(true);
expect(await evalFormual('XOR(0, 1, 1)')).toBe(false);
});
test('formula:ifs', async () => {
expect(!!(await evalFormual('IFS(0, 1, 2)'))).toBe(true);
expect(!!(await evalFormual('IFS(0, 1, 2, 2, 3)'))).toBe(true);
expect(!!(await evalFormual('IFS(0, 1, 0, 2, 0)'))).toBe(false);
expect(await evalFormual('IFS(0, 1, 2, 2)')).toBe(2);
expect(await evalFormual('IFS(0, 1, 0, 2)')).toBe(undefined);
});
test('formula:math', async () => {
expect(await evalFormual('ABS(1)')).toBe(1);
expect(await evalFormual('ABS(-1)')).toBe(1);
expect(await evalFormual('ABS(0)')).toBe(0);
expect(await evalFormual('MAX(1, -1, 2, 3, 5, -9)')).toBe(5);
expect(await evalFormual('MIN(1, -1, 2, 3, 5, -9)')).toBe(-9);
expect(await evalFormual('MOD(3, 2)')).toBe(1);
expect(await evalFormual('PI()')).toBe(Math.PI);
expect(await evalFormual('ROUND(3.55)')).toBe(3.55);
expect(await evalFormual('ROUND(3.45)')).toBe(3.45);
expect(await evalFormual('ROUND(3.456789, 2)')).toBe(3.46);
expect(await evalFormual('CEIL(3.456789)')).toBe(3.46);
expect(await evalFormual('FLOOR(3.456789)')).toBe(3.45);
expect(await evalFormual('SQRT(4)')).toBe(2);
expect(await evalFormual('AVG(4, 6, 10, 10, 10)')).toBe(8);
// 示例来自 https://support.microsoft.com/zh-cn/office/devsq-%E5%87%BD%E6%95%B0-8b739616-8376-4df5-8bd0-cfe0a6caf444
expect(await evalFormual('DEVSQ(4,5,8,7,11,4,3)')).toBe(48);
// 示例来自 https://support.microsoft.com/zh-cn/office/avedev-%E5%87%BD%E6%95%B0-58fe8d65-2a84-4dc7-8052-f3f87b5c6639
expect(await evalFormual('ROUND(AVEDEV(4,5,6,7,5,4,3), 2)')).toBe(1.02);
// 示例来自 https://support.microsoft.com/zh-cn/office/harmean-%E5%87%BD%E6%95%B0-5efd9184-fab5-42f9-b1d3-57883a1d3bc6
expect(await evalFormual('ROUND(HARMEAN(4,5,8,7,11,4,3), 3)')).toBe(5.028);
expect(await evalFormual('LARGE([1,3,5,4,7,6], 3)')).toBe(5);
expect(await evalFormual('LARGE([1,3,5,4,7,6], 1)')).toBe(7);
expect(await evalFormual('UPPERMONEY(7682.01)')).toBe('柒仟陆佰捌拾贰元壹分');
expect(await evalFormual('UPPERMONEY(7682)')).toBe('柒仟陆佰捌拾贰元整');
// 非数字类型转换是否正常?
expect(await evalFormual('"3" + "3"')).toBe(6);
expect(await evalFormual('"3" - "3"')).toBe(0);
expect(await evalFormual('AVG(4, "6", "10", 10, 10)')).toBe(8);
expect(await evalFormual('MAX(4, "6", "10", 2, 3)')).toBe(10);
expect(await evalFormual('"a" + "b"')).toBe('ab');
});
test('formula:text', async () => {
expect(await evalFormual('LEFT("abcdefg", 2)')).toBe('ab');
expect(await evalFormual('RIGHT("abcdefg", 2)')).toBe('fg');
expect(await evalFormual('LENGTH("abcdefg")')).toBe(7);
expect(await evalFormual('LEN("abcdefg")')).toBe(7);
expect(await evalFormual('ISEMPTY("abcdefg")')).toBe(false);
expect(await evalFormual('ISEMPTY("")')).toBe(true);
expect(await evalFormual('CONCATENATE("a", "b", "c", "d")')).toBe('abcd');
expect(await evalFormual('CHAR(97)')).toBe('a');
expect(await evalFormual('LOWER("AB")')).toBe('ab');
expect(await evalFormual('UPPER("ab")')).toBe('AB');
expect(await evalFormual('SPLIT("a,b,c")')).toMatchObject(['a', 'b', 'c']);
expect(await evalFormual('TRIM(" ab ")')).toBe('ab');
expect(await evalFormual('STARTSWITH("xab", "ab")')).toBe(false);
expect(await evalFormual('STARTSWITH("xab", "x")')).toBe(true);
expect(await evalFormual('ENDSWITH("xab", "x")')).toBe(false);
expect(await evalFormual('ENDSWITH("xab", "b")')).toBe(true);
expect(await evalFormual('UPPERFIRST("xab")')).toBe('Xab');
expect(await evalFormual('PADSTART("5", 3, "0")')).toBe('005');
expect(await evalFormual('PADSTART(5, 3, 0)')).toBe('005');
expect(await evalFormual('CAPITALIZE("star")')).toBe('Star');
expect(await evalFormual('ESCAPE("&")')).toBe('&amp;');
expect(await evalFormual('TRUNCATE("amis.baidu.com", 7)')).toBe('amis...');
expect(await evalFormual('BEFORELAST("amis.baidu.com", ".")')).toBe(
'amis.baidu'
);
expect(await evalFormual('BEFORELAST("amis", ".")')).toBe('amis');
expect(await evalFormual('STRIPTAG("<b>amis</b>")')).toBe('amis');
expect(await evalFormual('LINEBREAK("am\nis")')).toBe('am<br/>is');
expect(await evalFormual('CONTAINS("xab", "x")')).toBe(true);
expect(await evalFormual('CONTAINS("xab", "b")')).toBe(true);
expect(await evalFormual('REPLACE("xabab", "ab", "cd")')).toBe('xcdcd');
expect(await evalFormual('SEARCH("xabab", "ab")')).toBe(1);
expect(await evalFormual('SEARCH("xabab", "cd")')).toBe(-1);
expect(await evalFormual('SEARCH("xabab", "ab", 2)')).toBe(3);
expect(await evalFormual('MID("xabab", 2, 2)')).toBe('ba');
});
test('formula:date', async () => {
expect(await evalFormual('TIMESTAMP(DATE(2021, 11, 21, 0, 0, 0), "x")')).toBe(
new Date(2021, 11, 21, 0, 0, 0).getTime()
);
expect(
await evalFormual('DATETOSTR(DATE(2021, 11, 21, 0, 0, 0), "YYYY-MM-DD")')
).toBe('2021-12-21');
expect(await evalFormual('DATETOSTR(DATE("2021-12-21"), "YYYY-MM-DD")')).toBe(
'2021-12-21'
);
expect(await evalFormual('DATETOSTR(TODAY(), "YYYY-MM-DD")')).toBe(
moment().format('YYYY-MM-DD')
);
expect(await evalFormual('DATETOSTR(NOW(), "YYYY-MM-DD")')).toBe(
moment().format('YYYY-MM-DD')
);
expect(await evalFormual('DATETOSTR(1676563200, "YYYY-MM-DD")')).toBe(
moment(1676563200, 'X').format('YYYY-MM-DD')
);
expect(await evalFormual('DATETOSTR(1676563200000, "YYYY-MM-DD")')).toBe(
moment(1676563200000, 'x').format('YYYY-MM-DD')
);
expect(await evalFormual('DATETOSTR("12/25/2022", "YYYY-MM-DD")')).toBe(
moment('12/25/2022').format('YYYY-MM-DD')
);
expect(await evalFormual('DATETOSTR("12-25-2022", "YYYY/MM/DD")')).toBe(
moment('12-25-2022').format('YYYY/MM/DD')
);
expect(await evalFormual('DATETOSTR("2022年12月25日", "YYYY/MM/DD")')).toBe(
moment('2022年12月25日', 'YYYY-MM-DD').format('YYYY/MM/DD')
);
expect(
await evalFormual(
'DATETOSTR("2022年12月25日 14时23分56秒", "YYYY/MM/DD HH:mm:ss")'
)
).toBe(
moment('2022年12月25日 14时23分56秒', 'YYYY-MM-DD HH:mm:ss').format(
'YYYY/MM/DD HH:mm:ss'
)
);
expect(await evalFormual('DATETOSTR("20230105", "YYYY/MM/DD")')).toBe(
moment('20230105', 'YYYY-MM-DD').format('YYYY/MM/DD')
);
expect(await evalFormual('DATETOSTR("2023.01.05", "YYYY/MM/DD")')).toBe(
moment('2023.01.05', 'YYYY-MM-DD').format('YYYY/MM/DD')
);
expect(
await evalFormual(
'DATETOSTR("2010-10-20 4:30 +0000", "YYYY-MM-DD HH:mm Z")'
)
).toBe(moment('2010-10-20 4:30 +0000').format('YYYY-MM-DD HH:mm Z'));
expect(
await evalFormual(
'DATETOSTR("2013-02-04T10:35:24-08:00", "YYYY-MM-DD HH:mm:ss")'
)
).toBe(moment('2013-02-04T10:35:24-08:00').format('YYYY-MM-DD HH:mm:ss'));
expect(await evalFormual('YEAR(STRTODATE("2021-10-24 10:10:10"))')).toBe(
2021
);
expect(
await evalFormual(
'DATERANGESPLIT("1676563200,1676735999", undefined, "YYYY.MM.DD hh:mm:ss")'
)
).toEqual(['2023.02.17 12:00:00', '2023.02.18 11:59:59']);
expect(await evalFormual('DATERANGESPLIT("1676563200,1676735999", 0)')).toBe(
'1676563200'
);
expect(
await evalFormual(
'DATERANGESPLIT("1676563200,1676735999", 0 , "YYYY.MM.DD hh:mm:ss")'
)
).toBe('2023.02.17 12:00:00');
expect(
await evalFormual(
'DATERANGESPLIT("1676563200,1676735999", "start" , "YYYY.MM.DD hh:mm:ss")'
)
).toBe('2023.02.17 12:00:00');
expect(
await evalFormual(
'DATERANGESPLIT("1676563200,1676735999", 1 , "YYYY.MM.DD hh:mm:ss")'
)
).toBe('2023.02.18 11:59:59');
expect(
await evalFormual(
'DATERANGESPLIT("1676563200,1676735999", "end" , "YYYY.MM.DD hh:mm:ss")'
)
).toBe('2023.02.18 11:59:59');
expect(await evalFormual('WEEKDAY("2023-02-27")')).toBe(
moment('2023-02-27').weekday()
);
expect(await evalFormual('WEEKDAY("2023-02-27", 2)')).toBe(
moment('2023-02-27').isoWeekday()
);
expect(await evalFormual('WEEK("2023-03-05")')).toBe(
moment('2023-03-05').week()
);
expect(
await evalFormual(
'BETWEENRANGE("2023-03-08", ["2023-03-01", "2024-04-07"], "year")'
)
).toBe(
moment('2023-03-08').isBetween('2023-03-01', '2024-04-07', 'year', '[]')
);
expect(
await evalFormual(
'BETWEENRANGE("2022-03-08", ["2023-03-01", "2024-04-07"], "year")'
)
).toBe(
moment('2022-03-08').isBetween('2023-03-01', '2024-04-07', 'year', '[]')
);
expect(
await evalFormual(
'BETWEENRANGE("2023-03-08", ["2023-03-01", "2023-04-07"], "month")'
)
).toBe(
moment('2023-03-08').isBetween('2023-03-01', '2023-04-07', 'month', '[]')
);
expect(
await evalFormual(
'BETWEENRANGE("2023-05-08", ["2023-03-01", "2023-04-07", "month"])'
)
).toBe(
moment('2023-05-08').isBetween('2023-03-01', '2023-04-07', 'month', '[]')
);
expect(
await evalFormual(
'BETWEENRANGE("2023-03-06", ["2023-03-01", "2023-05-07"])'
)
).toBe(
moment('2023-03-06').isBetween('2023-03-01', '2023-05-07', 'day', '[]')
);
expect(
await evalFormual(
'BETWEENRANGE("2023-05-08", ["2023-03-01", "2023-05-07"])'
)
).toBe(
moment('2023-05-08').isBetween('2023-03-01', '2023-05-07', 'day', '[]')
);
expect(
await evalFormual(
'BETWEENRANGE("2023-05-07", ["2023-03-01", "2023-05-07"], "day", "()")'
)
).toBe(
moment('2023-05-07').isBetween('2023-03-01', '2023-05-07', 'day', '()')
);
expect(
await evalFormual(
'CONCATENATE(STARTOF("2023-02-28", "day"), "," ,ENDOF("2023-02-28", "day"))'
)
).toBe(
`${moment('2023-02-28').startOf('day').toDate()},${moment('2023-02-28')
.endOf('day')
.toDate()}`
);
expect(
await evalFormual(
'CONCATENATE(STARTOF("2023-02-28", "day", "YYYY-MM-DD HH:mm:ss"), ",", ENDOF("2023-02-28", "day", "YYYY-MM-DD HH:mm:ss"))'
)
).toBe(
`${moment('2023-02-28')
.startOf('day')
.format('YYYY-MM-DD HH:mm:ss')},${moment('2023-02-28')
.endOf('day')
.format('YYYY-MM-DD HH:mm:ss')}`
);
expect(
await evalFormual(
'CONCATENATE(STARTOF("2023-02-28", "day", "X"), "," ,ENDOF("2023-02-28", "day", "X"))'
)
).toBe(
`${moment('2023-02-28').startOf('day').format('X')},${moment('2023-02-28')
.endOf('day')
.format('X')}`
);
});
test('formula:last', async () => {
expect(await evalFormual('LAST([1, 2, 3])')).toBe(3);
});
test('formula:basename', async () => {
expect(await evalFormual('BASENAME("/home/amis/a.json")')).toBe('a.json');
});
test('formula:customFunction', async () => {
registerFunction('ASYNCFUNCTION', async input => {
const response = await Promise.resolve({
status: 0,
data: {
name: 'amis'
}
});
return response;
});
expect(await evalFormual('ASYNCFUNCTION()')).toMatchObject({
status: 0,
data: {
name: 'amis'
}
});
});

View File

@ -2265,7 +2265,7 @@ export class Evaluator {
}
}
function getCookie(name: string) {
export function getCookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
@ -2274,7 +2274,7 @@ function getCookie(name: string) {
return undefined;
}
function parseJson(str: string, defaultValue?: any) {
export function parseJson(str: string, defaultValue?: any) {
try {
return JSON.parse(str);
} catch (e) {
@ -2282,7 +2282,7 @@ function parseJson(str: string, defaultValue?: any) {
}
}
function stripNumber(number: number) {
export function stripNumber(number: number) {
if (typeof number === 'number' && !Number.isInteger(number)) {
return parseFloat(number.toPrecision(16));
} else {
@ -2292,7 +2292,7 @@ function stripNumber(number: number) {
// 如果只有一个成员,同时第一个成员为 args
// 则把它展开,当成是多个参数,毕竟公式里面还不支持 ...args 语法,
function normalizeArgs(args: Array<any>) {
export function normalizeArgs(args: Array<any>) {
if (args.length === 1 && Array.isArray(args[0])) {
args = args[0];
}

View File

@ -0,0 +1,659 @@
/**
* @file
*/
import {FilterContext} from './types';
import {createObject, Evaluator, stripNumber} from './evalutor';
export async function runSequence<T, U>(
arr: Array<T>,
fn: (item: T, index: number) => Promise<U>
) {
const result: Array<U> = [];
await arr.reduce(async (promise, item: T, index: number) => {
await promise;
result.push(await fn(item, index));
}, Promise.resolve());
return result;
}
export class AsyncEvaluator extends (Evaluator as any) {
constructor(data: any, options?: any) {
super(data, options);
}
async document(ast: {type: 'document'; body: Array<any>}) {
if (!ast.body.length) {
return undefined;
}
const isString = ast.body.length > 1;
const content = await runSequence(ast.body, async item => {
let result = this.evalute(item);
if (isString && result == null) {
// 不要出现 undefined, null 之类的文案
return '';
}
return result;
});
return content.length === 1 ? content[0] : content.join('');
}
async filter(ast: {
type: 'filter';
input: any;
filters: Array<{name: string; args: Array<any>}>;
}) {
let input = await this.evalute(ast.input);
const filters = ast.filters.concat();
const context: FilterContext = {
filter: undefined,
data: this.context,
restFilters: filters
};
const result = await filters.reduce(async (ps, filter, index) => {
const input = await ps;
const fn = this.filters[filter.name];
if (!fn) {
throw new Error(`filter \`${filter.name}\` not exists.`);
}
context.filter = filter;
const argsRes = await filter.args.reduce(async (promise, item) => {
await promise;
if (item?.type === 'mixed') {
return runSequence(item.body, item =>
typeof item === 'string' ? item : this.evalute(item)
);
} else if (item.type) {
return this.evalute(item);
}
return item;
}, Promise.resolve([]));
return fn.apply(context, [input].concat(argsRes));
}, Promise.resolve(input));
return result;
}
async template(ast: {type: 'template'; body: Array<any>}) {
const args = await runSequence(ast.body, arg => this.evalute(arg));
return args.join('');
}
// 下标获取
async getter(ast: {host: any; key: any}) {
const host = await this.evalute(ast.host);
let key = await this.evalute(ast.key);
if (typeof key === 'undefined' && ast.key?.type === 'variable') {
key = ast.key.name;
}
return host?.[key];
}
// 位操作如 +2 ~3 !
async unary(ast: {op: '+' | '-' | '~' | '!'; value: any}) {
let value = await this.evalute(ast.value);
switch (ast.op) {
case '+':
return +value;
case '-':
return -value;
case '~':
return ~value;
case '!':
return !value;
}
}
async power(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
return Math.pow(this.formatNumber(left), this.formatNumber(right));
}
async multiply(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
return stripNumber(this.formatNumber(left) * this.formatNumber(right));
}
async divide(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
return stripNumber(this.formatNumber(left) / this.formatNumber(right));
}
async remainder(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
return this.formatNumber(left) % this.formatNumber(right);
}
async add(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// 如果有一个不是数字就变成字符串拼接
if (isNaN(left) || isNaN(right)) {
return left + right;
}
return stripNumber(this.formatNumber(left) + this.formatNumber(right));
}
async minus(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
return stripNumber(this.formatNumber(left) - this.formatNumber(right));
}
async shift(ast: {op: '<<' | '>>' | '>>>'; left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.formatNumber(this.evalute(ast.right), true);
if (ast.op === '<<') {
return left << right;
} else if (ast.op == '>>') {
return left >> right;
} else {
return left >>> right;
}
}
async lt(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left < right;
}
async gt(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left > right;
}
async le(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left <= right;
}
async ge(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left >= right;
}
async eq(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left == right;
}
async ne(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left != right;
}
async streq(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left === right;
}
async strneq(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
// todo 如果是日期的对比,这个地方可以优化一下。
return left !== right;
}
async binary(ast: {op: '&' | '^' | '|'; left: any; right: any}) {
const left = await this.evalute(ast.left);
const right = await this.evalute(ast.right);
if (ast.op === '&') {
return left & right;
} else if (ast.op === '^') {
return left ^ right;
} else {
return left | right;
}
}
async and(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
return left && this.evalute(ast.right);
}
async or(ast: {left: any; right: any}) {
const left = await this.evalute(ast.left);
return left || this.evalute(ast.right);
}
array(ast: {type: 'array'; members: Array<any>}) {
return runSequence(ast.members, member => this.evalute(member));
}
async object(ast: {members: Array<{key: string; value: any}>}) {
let object: any = {};
await ast.members.reduce(
async (promise: any, {key, value}: any, index: number) => {
await promise;
const objKey = await this.evalute(key);
const objVal = await this.evalute(value);
object[objKey] = objVal;
},
Promise.resolve()
);
return object;
}
async conditional(ast: {
type: 'conditional';
test: any;
consequent: any;
alternate: any;
}) {
return (await this.evalute(ast.test))
? await this.evalute(ast.consequent)
: await this.evalute(ast.alternate);
}
async funcCall(this: any, ast: {identifier: string; args: Array<any>}) {
const fnName = `fn${ast.identifier}`;
const fn =
this.functions[fnName] ||
this[fnName] ||
(this.filters.hasOwnProperty(ast.identifier) &&
this.filters[ast.identifier]);
if (!fn) {
throw new Error(`${ast.identifier}函数没有定义`);
}
let args: Array<any> = ast.args;
// 逻辑函数特殊处理,因为有时候有些运算是可以跳过的。
if (~['IF', 'AND', 'OR', 'XOR', 'IFS'].indexOf(ast.identifier)) {
args = args.map(a => () => this.evalute(a));
} else {
args = await runSequence(args, a => this.evalute(a));
}
return fn.apply(this, args);
}
async callAnonymousFunction(
ast: {
args: any[];
return: any;
},
args: Array<any>
) {
const ctx: any = createObject(
this.contextStack[this.contextStack.length - 1]('&') || {},
{}
);
ast.args.forEach((arg: any) => {
if (arg.type !== 'variable') {
throw new Error('expected a variable as argument');
}
ctx[arg.name] = args.shift();
});
this.contextStack.push((varName: string) =>
varName === '&' ? ctx : ctx[varName]
);
const result = await this.evalute(ast.return);
this.contextStack.pop();
return result;
}
/**
* IF(A, B, C)
*
* ABCIF函数
*
* A ? B : C
*
* @example IF(condition, consequent, alternate)
* @param {expression} condition - .
* @param {any} consequent
* @param {any} alternate
* @namespace
*
* @returns {any}
*/
async fnIF(
condition: () => any,
trueValue: () => any,
falseValue: () => any
) {
return (await condition()) ? await trueValue() : await falseValue();
}
/**
* true false
*
* AND(>80, >80)
*
* 80 true false
*
* >80 && >80
*
* @example AND(expression1, expression2, ...expressionN)
* @param {...expression} conditions - .
* @namespace
*
* @returns {boolean}
*/
async fnAND(...condtions: Array<() => any>) {
if (!condtions.length) {
return false;
}
return condtions.reduce(async (promise, c) => {
const result = await promise;
if (result) {
return c();
}
return result;
}, Promise.resolve(true));
}
/**
* true false
*
* OR(>80, >80)
*
* 80 true false
*
* >80 || >80
*
* @example OR(expression1, expression2, ...expressionN)
* @param {...expression} conditions - .
* @namespace
*
* @returns {boolean}
*/
async fnOR(...condtions: Array<() => any>) {
if (!condtions.length) {
return false;
}
return condtions.reduce(async (promise, c) => {
const result = await promise;
if (result) {
return true;
}
return c();
}, Promise.resolve(false));
}
/**
*
*
* @example XOR(condition1, condition2)
* @param {expression} condition1 - 1
* @param {expression} condition2 - 2
* @namespace
*
* @returns {boolean}
*/
async fnXOR(...condtions: Array<() => any>) {
if (!condtions.length) {
return false;
}
return !!(
(await runSequence(condtions, c => c())).filter(item => item).length % 2
);
}
/**
* else if
*
* IFS( > 80, "优秀", > 60, "良", "继续努力")
*
* 80 60
*
* @example IFS(condition1, result1, condition2, result2,...conditionN, resultN)
* @param {...any} args -
* @namespace
* @returns {any} false
*/
async fnIFS(...args: Array<() => any>) {
if (args.length % 2) {
args.splice(args.length - 1, 0, () => true);
}
while (args.length) {
const c = args.shift()!;
const v = args.shift()!;
if (await c()) {
return await v();
}
}
return;
}
/**
* 使
*
* @param {Array<any>} arr
* @param {Function<any>} iterator
* @namespace
* @example ARRAYMAP(arr, item => item)
* @returns {boolean}
*/
fnARRAYMAP(value: any, iterator: any) {
if (!iterator || iterator.type !== 'anonymous_function') {
throw new Error('expected an anonymous function get ' + iterator);
}
return (Array.isArray(value) ? value : []).reduce(
async (promise, item, index) => {
const arr = await promise;
arr.push(await this.callAnonymousFunction(iterator, [item, index]));
return arr;
},
Promise.resolve([])
);
}
/**
* 使
* false
*
* @param {Array<any>} arr
* @param {Function<any>} iterator
* @namespace
* @example ARRAYFILTER(arr, item => item)
* @returns {boolean}
*/
async fnARRAYFILTER(value: any, iterator: any) {
if (!iterator || iterator.type !== 'anonymous_function') {
throw new Error('expected an anonymous function get ' + iterator);
}
return await (Array.isArray(value) ? value : []).reduce(
async (promise, item, index) => {
let arr = await promise;
const hit = await this.callAnonymousFunction(iterator, [item, index]);
if (hit) {
arr.push(item);
}
return arr;
},
Promise.resolve([])
);
}
/**
* 使
* true
*
*
*
* ARRAYFINDINDEX([0, 2, false], item => item === 2) 1
*
* @param {Array<any>} arr
* @param {Function<any>} iterator
* @namespace
* @example ARRAYFINDINDEX(arr, item => item === 2)
* @returns {number}
*/
async fnARRAYFINDINDEX(arr: any[], iterator: any) {
if (!iterator || iterator.type !== 'anonymous_function') {
throw new Error('expected an anonymous function get ' + iterator);
}
let hitIndex = -1;
await (Array.isArray(arr) ? arr : []).reduce(
async (promise: any, item: any, index: number) => {
await promise;
const hit = await this.callAnonymousFunction(iterator, [item, index]);
if (hit) {
hitIndex = index;
}
},
Promise.resolve()
);
return hitIndex;
}
/**
* 使
* true
*
*
*
* ARRAYFIND([0, 2, false], item => item === 2) 2
*
* @param {Array<any>} arr
* @param {Function<any>} iterator
* @namespace
* @example ARRAYFIND(arr, item => item === 2)
* @returns {any}
*/
async fnARRAYFIND(arr: any[], iterator: any) {
if (!iterator || iterator.type !== 'anonymous_function') {
throw new Error('expected an anonymous function get ' + iterator);
}
let hitItem = undefined;
await (Array.isArray(arr) ? arr : []).reduce(
async (promise: any, item: any, index: number) => {
await promise;
const hit = await this.callAnonymousFunction(iterator, [item, index]);
if (hit) {
hitItem = item;
}
},
Promise.resolve()
);
return hitItem;
}
/**
* 使
* true
*
*
*
* ARRAYSOME([0, 2, false], item => item === 2) true
*
* @param {Array<any>} arr
* @param {Function<any>} iterator
* @namespace
* @example ARRAYSOME(arr, item => item === 2)
* @returns {boolean}
*/
async fnARRAYSOME(arr: any[], iterator: any) {
if (!iterator || iterator.type !== 'anonymous_function') {
throw new Error('expected an anonymous function get ' + iterator);
}
let result = await (Array.isArray(arr) ? arr : []).reduce(
async (promise: any, item: any, index: number) => {
const prev = await promise;
const hit = await this.callAnonymousFunction(iterator, [item, index]);
return prev || hit;
},
Promise.resolve(false)
);
return result;
}
/**
* 使
* true
*
*
*
* ARRAYEVERY([0, 2, false], item => item === 2) false
*
* @param {Array<any>} arr
* @param {Function<any>} iterator
* @namespace
* @example ARRAYEVERY(arr, item => item === 2)
* @returns {boolean}
*/
async fnARRAYEVERY(arr: any[], iterator: any) {
if (!iterator || iterator.type !== 'anonymous_function') {
throw new Error('expected an anonymous function get ' + iterator);
}
let result = await (Array.isArray(arr) ? arr : []).reduce(
async (promise: any, item: any, index: number) => {
const prev = await promise;
const hit = await this.callAnonymousFunction(iterator, [item, index]);
return prev && hit;
},
Promise.resolve(true)
);
return result;
}
}

View File

@ -1,4 +1,5 @@
import {Evaluator} from './evalutor';
import {AsyncEvaluator} from './evalutorForAsync';
import {parse} from './parser';
import {lexer} from './lexer';
import {registerFilter, filters, getFilters, extendsFilters} from './filter';
@ -13,6 +14,7 @@ export {
parse,
lexer,
Evaluator,
AsyncEvaluator,
FilterContext,
filters,
getFilters,
@ -34,4 +36,18 @@ export function evaluate(
return new Evaluator(data, options).evalute(ast);
}
export async function evaluateForAsync(
astOrString: string | ASTNode,
data: any,
options?: ParserOptions & EvaluatorOptions
) {
let ast: ASTNode = astOrString as ASTNode;
if (typeof astOrString === 'string') {
ast = parse(astOrString, options);
}
return new AsyncEvaluator(data, options).evalute(ast);
}
Evaluator.setDefaultFilters(getFilters());
AsyncEvaluator.setDefaultFilters(getFilters());

View File

@ -1,13 +1,8 @@
import {
ExpressionComplex,
ConditionBuilderField,
ConditionBuilderFuncs,
ConditionFieldFunc,
ExpressionFunc,
ConditionBuilderType,
FieldSimple,
FieldGroup,
OperatorType
FieldSimple
} from './types';
import React from 'react';
import ConditionField from './Field';
@ -27,6 +22,7 @@ import ConditionFunc from './Func';
import {ConditionBuilderConfig} from './config';
import Formula from './Formula';
import {FormulaPickerProps} from '../formula/Picker';
import type {ExpressionComplex, OperatorType, ExpressionFunc} from 'amis-core';
/**
* 4

View File

@ -1,7 +1,6 @@
import React from 'react';
import {
ConditionFieldFunc,
ExpressionFunc,
ConditionBuilderField,
ConditionBuilderFuncs
} from './types';
@ -21,6 +20,7 @@ import ResultBox from '../ResultBox';
import {Icon} from '../icons';
import Expression from './Expression';
import {ConditionBuilderConfig} from './config';
import type {ExpressionFunc} from 'amis-core';
export interface ConditionFuncProps extends ThemeProps, LocaleProps {
value: ExpressionFunc;

View File

@ -1,9 +1,5 @@
import React from 'react';
import {
ConditionBuilderFields,
ConditionGroupValue,
ConditionBuilderFuncs
} from './types';
import {ConditionBuilderFields, ConditionBuilderFuncs} from './types';
import {
ThemeProps,
themeable,
@ -11,7 +7,8 @@ import {
utils,
localeable,
LocaleProps,
guid
guid,
ConditionGroupValue
} from 'amis-core';
import Button from '../Button';
import GroupOrItem from './GroupOrItem';

View File

@ -1,10 +1,5 @@
import {ConditionBuilderConfig} from './config';
import {
ConditionBuilderFields,
ConditionGroupValue,
ConditionBuilderFuncs,
ConditionValue
} from './types';
import {ConditionBuilderFields, ConditionBuilderFuncs} from './types';
import {ThemeProps, themeable, autobind} from 'amis-core';
import React from 'react';
import {Icon} from '../icons';
@ -12,6 +7,7 @@ import ConditionGroup from './Group';
import ConditionItem from './Item';
import {FormulaPickerProps} from '../formula/Picker';
import Button from '../Button';
import type {ConditionGroupValue, ConditionValue} from 'amis-core';
export interface CBGroupOrItemProps extends ThemeProps {
builderMode?: 'simple' | 'full';

View File

@ -2,15 +2,10 @@ import React from 'react';
import {findDOMNode} from 'react-dom';
import {
ConditionBuilderFields,
ConditionRule,
ConditionBuilderFuncs,
ExpressionFunc,
ConditionFieldFunc,
ConditionBuilderField,
FieldSimple,
ExpressionField,
OperatorType,
ExpressionComplex
FieldSimple
} from './types';
import {
ThemeProps,
@ -30,7 +25,14 @@ import GroupedSelection from '../GroupedSelection';
import ResultBox from '../ResultBox';
import {FormulaPickerProps} from '../formula/Picker';
import type {PlainObject} from 'amis-core';
import type {
PlainObject,
ConditionRule,
OperatorType,
ExpressionFunc,
ExpressionField,
ExpressionComplex
} from 'amis-core';
const option2value = (item: any) => item.value;

View File

@ -1,5 +1,5 @@
import React from 'react';
import {FieldSimple, OperatorType} from './types';
import {FieldSimple} from './types';
import {ThemeProps, themeable, localeable, LocaleProps} from 'amis-core';
import InputBox from '../InputBox';
import NumberInput from '../NumberInput';
@ -7,6 +7,7 @@ import DatePicker from '../DatePicker';
import {SelectWithRemoteOptions as Select} from '../Select';
import Switch from '../Switch';
import {FormulaPicker, FormulaPickerProps} from '../formula/Picker';
import type {OperatorType} from 'amis-core';
export interface ValueProps extends ThemeProps, LocaleProps {
value: any;

View File

@ -1,10 +1,10 @@
import {
FieldTypes,
OperatorType,
ConditionBuilderFuncs,
ConditionBuilderFields,
ConditionBuilderType
} from './types';
import type {OperatorType} from 'amis-core';
export interface BaseFieldConfig {
operations: Array<OperatorType>;

View File

@ -14,16 +14,13 @@ import {
noop
} from 'amis-core';
import {uncontrollable} from 'amis-core';
import {
ConditionBuilderFields,
ConditionGroupValue,
ConditionBuilderFuncs
} from './types';
import {ConditionBuilderFields, ConditionBuilderFuncs} from './types';
import ConditionGroup from './Group';
import defaultConfig, {ConditionBuilderConfig} from './config';
import {FormulaPickerProps} from '../formula/Picker';
import PickerContainer from '../PickerContainer';
import ResultBox from '../ResultBox';
import type {ConditionGroupValue} from 'amis-core';
export interface ConditionBuilderProps extends ThemeProps, LocaleProps {
builderMode?: 'simple' | 'full'; // 简单模式|完整模式

View File

@ -1,4 +1,4 @@
import {Api, BaseApiObject} from 'amis-core';
import type {BaseApiObject, OperatorType} from 'amis-core';
export type FieldTypes =
| 'text'
@ -10,78 +10,11 @@ export type FieldTypes =
| 'select'
| 'custom';
export type OperatorType =
| 'equal'
| 'not_equal'
| 'is_empty'
| 'is_not_empty'
| 'like'
| 'not_like'
| 'starts_with'
| 'ends_with'
| 'less'
| 'less_or_equal'
| 'greater'
| 'greater_or_equal'
| 'between'
| 'not_between'
| 'select_equals'
| 'select_not_equals'
| 'select_any_in'
| 'select_not_any_in'
| {
label: string;
value: string;
};
export type FieldItem = {
type: 'text';
operators: Array<OperatorType>;
};
export type ExpressionSimple = string | number | object | undefined;
export type ExpressionValue =
| ExpressionSimple
| {
type: 'value';
value: ExpressionSimple;
};
export type ExpressionFunc = {
type: 'func';
func: string;
args: Array<ExpressionComplex>;
};
export type ExpressionField = {
type: 'field';
field: string;
};
export type ExpressionFormula = {
type: 'formula';
value: string;
};
export type ExpressionComplex =
| ExpressionValue
| ExpressionFunc
| ExpressionField
| ExpressionFormula;
export interface ConditionRule {
id: any;
left?: ExpressionComplex;
op?: OperatorType;
right?: ExpressionComplex | Array<ExpressionComplex>;
}
export interface ConditionGroupValue {
id: string;
conjunction: 'and' | 'or';
not?: boolean;
children?: Array<ConditionRule | ConditionGroupValue>;
}
export interface ConditionValue extends ConditionGroupValue {}
interface customOperator {
lable: string;
value: string;