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;
}