import assert from 'assert'; import { RefAttr } from "./Demand"; import { Geo } from "./Geo"; import DayJs from 'dayjs'; import weekOfYear from 'dayjs/plugin/weekOfYear'; import dayOfYear from 'dayjs/plugin/dayOfYear'; import { getDistanceBetweenPoints } from '../utils/geo'; DayJs.extend(weekOfYear); DayJs.extend(dayOfYear); export type RefOrExpression = RefAttr | Expression; // Math type MathType = RefOrExpression | number; type StringType = RefOrExpression | string interface Add { $add: (MathType)[]; }; interface Subtract { $subtract: [MathType, MathType]; }; interface Multiply { $multiply: (MathType)[]; }; interface Divide { $divide: [MathType, MathType]; }; interface Abs { $abs: MathType; }; interface Round { $round: [MathType, MathType]; }; interface Floor { $floor: MathType; }; interface Ceil { $ceil: MathType; }; interface Pow { $pow: [MathType, MathType]; }; type MathExpression = Add | Subtract | Multiply | Divide | Abs | Round | Floor | Ceil | Pow; // Compare type CmpType = RefOrExpression | string | number; interface Gt { $gt: [CmpType, CmpType]; }; interface Lt { $lt: [CmpType, CmpType]; }; interface Eq { $eq: [CmpType, CmpType]; }; interface Gte { $gte: [CmpType, CmpType]; }; interface Lte { $lte: [CmpType, CmpType]; }; interface Ne { $ne: [CmpType, CmpType]; }; interface StartsWith { $startsWith: [RefOrExpression | string, RefOrExpression | string]; }; interface EndsWith { $endsWith: [RefOrExpression | string, RefOrExpression | string]; }; interface Includes { $includes: [RefOrExpression | string, RefOrExpression | string]; }; type CompareExpression = Lt | Gt | Lte | Gte | Eq | Ne | StartsWith | EndsWith | Includes; // Bool interface BoolTrue { $true: Expression; }; interface BoolFalse { $false: Expression; }; type BoolExpression = BoolTrue | BoolFalse; // Logic interface LogicAnd { $and: Expression[]; }; interface LogicOr { $or: Expression[]; }; interface LogicNot { $not: Expression; }; type LogicExpression = LogicAnd | LogicOr | LogicNot; // Date interface DateYear { $year: RefOrExpression | Date | number; }; interface DateMonth { $month: RefOrExpression | Date | number; }; interface DateWeekday { $weekday: RefOrExpression | Date | number; }; interface DateWeekOfYear { $weekOfYear: RefOrExpression | Date | number; }; interface DateDay { $day: RefOrExpression | Date | number; }; interface DateDayOfMonth { $dayOfMonth: RefOrExpression | Date | number; }; interface DateDayOfWeek { $dayOfWeek: RefOrExpression | Date | number; }; interface DateDayOfYear { $dayOfYear: RefOrExpression | Date | number; } interface DateDiff { $dateDiff: [RefOrExpression | Date | number, RefOrExpression | Date | number, 'y' | 'M' | 'd' | 'h' | 'm' | 's']; }; interface DateCeiling { $dateCeil: [RefOrExpression | Date | number, 'y' | 'M' | 'd' | 'h' | 'm' | 's']; }; interface DateFloor { $dateFloor: [RefOrExpression | Date | number, 'y' | 'M' | 'd' | 'h' | 'm' | 's']; } type DateExpression = DateYear | DateMonth | DateWeekday | DateWeekOfYear | DateDay | DateDayOfYear | DateDayOfMonth | DateDayOfWeek | DateDiff | DateCeiling | DateFloor; // String interface StringConcat { $concat: StringType[]; } type StringExpression = StringConcat; //// Geo interface GeoContains { $contains: [RefOrExpression | Geo, RefOrExpression | Geo]; }; interface GeoDistance { $distance: [RefOrExpression | Geo, RefOrExpression | Geo]; } type GeoExpression = GeoContains | GeoDistance; //// Aggr interface AggrCountExpression { $$count: RefOrExpression; }; interface AggrSumExpression { $$sum: RefOrExpression; } interface AggrMaxExpression { $$max: RefOrExpression; } interface AggrMinExpression { $$min: RefOrExpression; } interface AggrAvgExpression { $$avg: RefOrExpression; } export type AggrExpression = AggrAvgExpression | AggrCountExpression | AggrSumExpression | AggrMaxExpression | AggrMinExpression; export type Expression = GeoExpression | DateExpression | LogicExpression | BoolExpression | CompareExpression | MathExpression | StringExpression | AggrExpression; export type ExpressionConstant = Geo | number | Date | string | boolean; export function isGeoExpression(expression: any): expression is GeoExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$contains', '$distance'].includes(op)) { return true; } } return false; } export function isDateExpression(expression: any): expression is DateExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$year', '$month', '$weekday', '$weekOfYear', '$day', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$dateDiff', '$dateCeil', '$dateFloor'].includes(op)) { return true; } } return false; } export function isLogicExpression(expression: any): expression is LogicExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$and', '$or', '$not'].includes(op)) { return true; } } return false; } export function isBoolExpression(expression: any): expression is BoolExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$true', '$false'].includes(op)) { return true; } } return false; } export function isCompareExpression(expression: any): expression is CompareExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$gt', '$lt', '$gte', '$lte', '$eq', '$ne', '$startsWith', '$endsWith', '$includes'].includes(op)) { return true; } } return false; } export function isMathExpression(expression: any): expression is MathExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$add', '$subtract', '$multiply', '$divide', '$abs', '$pow', '$round', '$floor', '$ceil'].includes(op)) { return true; } } return false; } export function isStringExpression(expression: any): expression is StringExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$concat'].includes(op)) { return true; } } return false; } export function isAggrExpression(expression: any): expression is AggrExpression { if (Object.keys(expression).length == 1) { const op = Object.keys(expression)[0]; if (['$$max', '$$min', '$$sum', '$$avg', '$$count'].includes(op)) { return true; } } return false; } export function isExpression(expression: any): expression is Expression { return typeof expression === 'object' && Object.keys(expression).length === 1 && Object.keys(expression)[0].startsWith('$'); } export function opMultipleParams(op: string) { return !['$year', '$month', '$weekday', '$weekOfYear', '$day', '$dayOfMonth', '$dayOfWeek', '$dayOfYear', '$not', '$true', '$false', '$abs', '$round', '$floor', '$ceil', '$$max', '$$min', '$$sum', '$$avg', '$$count'].includes(op); } export function execOp(op: string, params: any, obscure?: boolean): ExpressionConstant { if (obscure && (params === undefined || params.includes(undefined))) { return true; } switch (op) { case '$gt': { return params[0] > params[1]; } case '$lt': { return params[0] < params[1]; } case '$gte': { return params[0] >= params[1]; } case '$lte': { return params[0] <= params[1]; } case '$eq': { return params[0] === params[1]; } case '$ne': { return params[0] !== params[1]; } case '$startsWith': { return params[0].startsWith(params[1]); } case '$endsWith': { return params[0].endsWith(params[1]); } case '$includes': { return params[0].includes(params[1]); } case '$add': { if (typeof params[0] === 'number') { let result = 0; params.forEach( (ele: number) => result += ele ); return result; } else { let result = ''; params.forEach( (ele: string) => result += ele ); return result; } } case '$subtract': { return params[0] - params[1]; } case '$multiply': { let result = 1; params.forEach( (ele: number) => result = result * ele ); return result; } case '$divide': { return params[0] / params[1]; } case '$abs': { return Math.abs(params); } case '$round': { return Math.round(params); } case '$ceil': { return Math.ceil(params); } case '$floor': { return Math.floor(params); } case '$round': { return Math.round(params); } case '$pow': { return Math.pow(params[0], params[1]); } case '$true': { return !!params; } case '$false': case '$not': { return !params; } case '$and': { for (const p of params) { if (!p) { return false; } } return true; } case '$or': { for (const p of params) { if (!!p) { return true; } } return false; } case '$year': { const value = DayJs(params); return value.year(); } case '$month': { const value = DayJs(params); return value.month(); } case '$weekday': { const value = DayJs(params); return value.day(); // 0~6 } case '$weekOfYear': { const value = DayJs(params); return value.week(); } case '$day': case '$dayOfMonth': { const value = DayJs(params); return value.date(); } case '$dayOfWeek': { const value = DayJs(params); return value.day(); // 0~6 } case '$dayOfYear': { const value = DayJs(params); return value.dayOfYear(); // 0~6 } case '$dateDiff': { const value1 = DayJs(params[0]); const value2 = DayJs(params[1]); switch (params[2]) { case 'y': case 'M': case 'd': case 'h': case 'm': case 's': { return value1.diff(value2, params[2]); } default: { assert(false); } } } case '$dateCeil': { const value = DayJs(params[0]); switch (params[1]) { case 'y': { return value.startOf('year').millisecond(); } case 'M': { return value.startOf('month').millisecond(); } case 'd': { return value.startOf('day').millisecond(); } case 'h': { return value.startOf('hour').millisecond(); } case 'm': { return value.startOf('minute').millisecond(); } case 's': { return value.startOf('second').millisecond(); } default: { assert(false); } } } case '$dateFloor': { const value = DayJs(params[0]); switch (params[1]) { case 'y': { return value.endOf('year').millisecond(); } case 'M': { return value.endOf('month').millisecond(); } case 'd': { return value.endOf('day').millisecond(); } case 'h': { return value.endOf('hour').millisecond(); } case 'm': { return value.endOf('minute').millisecond(); } case 's': { return value.endOf('second').millisecond(); } default: { assert(false); } } } case '$distance': { const [geo1, geo2] = params; const { type: type1, coordinate: coordinate1 } = geo1; const { type: type2, coordinate: coordinate2 } = geo2; if (type1 !== 'point' || type2 !== 'point') { throw new Error('目前只支持point类型的距离运算'); } return getDistanceBetweenPoints(coordinate1[1], coordinate1[0], coordinate2[1], coordinate2[0]); } case '$contains': { throw new Error('$contains类型未实现'); } case '$concat': { return params.join(''); } default: { assert(false, `不能识别的expression运算符:${op}`); } } } /** * 检查一个表达式,并分析其涉及到的属性 * @param expression * @returns { * '#current': [当前结点涉及的属性] * 'node-1': [node-1结点上涉及的属性] * } */ export function getAttrRefInExpression(expression: Expression) { const result: Record = { ['#current']: [], }; const check = (node: RefOrExpression) => { if ((node as any)['#attr']) { result['#current'].push((node as any)['#attr']); } else if ((node as any)['#refAttr']) { if (result[(node as any)['#refId']]) { result[(node as any)['#refId']].push((node as any)['#refAttr']); } else { Object.assign(result, { [(node as any)['#refId']]: [(node as any)['#refAttr']], }); } } else if (node instanceof Array) { for (const subNode of node) { check(subNode); } } else if (typeof node === 'object') { for (const attr in node) { check((node as any)[attr]); } } }; check(expression); return result; }