UNPKG

adaptive-expressions

Version:
496 lines (448 loc) 16.1 kB
/** * @module adaptive-expressions */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Constant } from './constant'; import dayjs, { ManipulateType } from 'dayjs'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); import { Expression } from './expression'; import { ExpressionType } from './expressionType'; import { Options } from './options'; import { EvaluateExpressionDelegate, ValueWithError } from './expressionEvaluator'; import { MemoryInterface, SimpleObjectMemory, StackedMemory } from './memory'; import { TimexProperty } from '@microsoft/recognizers-text-data-types-timex-expression'; import bigInt from 'big-integer'; import util from 'util'; /** * Utility functions only used internal */ export class InternalFunctionUtils { /** * Constant for converting unix timestamp to ticks. */ static readonly UnixMilliSecondToTicksConstant: bigInt.BigInteger = bigInt('621355968000000000'); /** * Constant to convert between ticks and ms. */ static readonly MillisecondToTickConstant: bigInt.BigInteger = bigInt('10000'); /** * Parse timex funcition. * * @param timexExpr String or TimexProperty input. * @returns TimexProperty and error. */ static parseTimexProperty(timexExpr: any): { timexProperty: TimexProperty; error: string } { let parsed: TimexProperty; if (timexExpr instanceof TimexProperty) { parsed = timexExpr; } else if (typeof timexExpr === 'string') { parsed = new TimexProperty(timexExpr); } else { parsed = new TimexProperty(timexExpr); if (parsed === undefined || Object.keys(parsed).length === 0) { return { timexProperty: parsed, error: `${timexExpr} requires a TimexProperty or a string as a argument`, }; } } return { timexProperty: parsed, error: undefined }; } /** * Sort helper function. * * @param isDescending Descending flag. * @returns The sorted array. */ static sortBy(isDescending: boolean): EvaluateExpressionDelegate { return (expression: Expression, state: any, options: Options): ValueWithError => { let result: any; const { value: oriArr, error: childrenError } = expression.children[0].tryEvaluate(state, options); let error = childrenError; if (!error) { if (Array.isArray(oriArr)) { // Ensures we don't mutate the array in place. const arr: any = oriArr.slice(0); if (expression.children.length === 1) { if (isDescending) { result = arr.sort().reverse(); } else { result = arr.sort(); } } else { let propertyName: string; ({ value: propertyName, error } = expression.children[1].tryEvaluate(state, options)); if (!error) { propertyName = propertyName || ''; } if (isDescending) { result = arr.sort(InternalFunctionUtils.sortByKey(propertyName)).reverse(); } else { result = arr.sort(InternalFunctionUtils.sortByKey(propertyName)); } } } else { error = `${expression.children[0]} is not an array`; } } return { value: result, error }; }; } /** * Lookup a string or number index of an Object. * * @param instance Instance with property. * @param index Property to lookup. * @returns Value and error information if any. */ static accessIndex(instance: any, index: number): ValueWithError { // NOTE: This returns undefined rather than an error if instance is not present if (instance == null) { return { value: undefined, error: undefined }; } let value: any; let error: string; if (Array.isArray(instance)) { if (index >= 0 && index < instance.length) { value = instance[index]; } else { error = `${index} is out of range for ${instance}`; } } else { error = `${instance} is not a collection.`; } return { value, error }; } /** * Verify a timestamp string is valid timestamp format. * * @param value Timestamp string to check. * @returns Error or undefined if invalid. */ static verifyTimestamp(value: any): string | undefined { let error: string; try { const parsedData: Date = new Date(value); if (Number.isNaN(parsedData.getTime())) { error = `${value} is not a valid datetime string.`; } } catch { error = `${value} is not a valid datetime string.`; } return error; } /** * Verify a timestamp string is valid ISO timestamp format. * * @param value Timestamp string to check. * @returns Error or undefined if invalid. */ static verifyISOTimestamp(value: any): string | undefined { let error: string; try { const parsedData: Date = new Date(value); if (Number.isNaN(parsedData.getTime())) { error = `${value} is not a valid datetime string.`; } else if (parsedData.toISOString() !== value) { error = `${value} is not a ISO format datetime string.`; } } catch { error = `${value} is not a valid datetime string.`; } return error; } /** * Convert a string input to ticks number. * * @param timeStamp String timestamp input. * @returns The string converted in ticks. */ static ticks(timeStamp: string): ValueWithError { let result: bigInt.BigInteger; const error = this.verifyISOTimestamp(timeStamp); if (!error) { const unixMilliSec: number = dayjs(timeStamp).utc().valueOf(); result = this.UnixMilliSecondToTicksConstant.add( bigInt(unixMilliSec).times(this.MillisecondToTickConstant), ); } return { value: result, error }; } /** * Lookup a property in Map or Object. * * @param instance Instance with property. * @param property Property to lookup. * @returns Value and error information if any. */ static accessProperty(instance: any, property: string): ValueWithError { // NOTE: This returns undefined rather than an error if property is not present if (!instance) { return { value: undefined, error: undefined }; } let value: any; let error: string; if (instance instanceof Map && (instance as Map<string, any>) !== undefined) { const instanceMap: Map<string, any> = instance as Map<string, any>; value = instanceMap.get(property); if (value === undefined) { const prop: string = Array.from(instanceMap.keys()).find( (k: string): boolean => k.toLowerCase() === property.toLowerCase(), ); if (prop !== undefined) { value = instanceMap.get(prop); } } } else { const prop: string = Object.keys(instance).find( (k: string): boolean => k.toLowerCase() === property.toLowerCase(), ); if (prop !== undefined) { value = instance[prop]; } } return { value, error }; } /** * Get the value of a path from a memory. * * @param state Memory. * @param path Path string. * @param options Options. * @returns The value of a path from a memory. */ static wrapGetValue(state: MemoryInterface, path: string, options: Options): any { const result = state.getValue(path); if (result !== undefined) { return result; } if (options.nullSubstitution !== undefined) { return options.nullSubstitution(path); } return undefined; } /** * Wrap string or undefined into string. Default to empty string. * * @param input Input string * @returns The wrapped string. */ static parseStringOrUndefined(input: string | undefined): string { if (typeof input === 'string') { return input; } else { return ''; } } /** * Test result to see if True in logical comparison functions. * * @param instance Computed value. * @returns True if boolean true or non-null. */ static isLogicTrue(instance: any): boolean { let result = true; if (typeof instance === 'boolean') { result = instance; } else if (instance == null) { result = false; } return result; } /** * Evaluator for foreach and select functions. * * @param expression Expression. * @param state Memory scope. * @param options Options. * @returns The evaluated list. */ static foreach(expression: Expression, state: MemoryInterface, options: Options): ValueWithError { let result: any[]; const { value: instance, error: childrenError } = expression.children[0].tryEvaluate(state, options); let error = childrenError; if (!instance) { error = `'${expression.children[0]}' evaluated to null.`; } if (!error) { const list = InternalFunctionUtils.convertToList(instance); if (!list) { error = `${expression.children[0]} is not a collection or structure object to run Foreach`; } else { result = []; InternalFunctionUtils.lambdaEvaluator(expression, state, options, list, (currentItem, r, e) => { if (e) { error = e; return true; } else { result.push(r); return false; } }); } } return { value: result, error }; } /** * Lambda evaluator. * * @param expression expression. * @param state memory state. * @param options options. * @param list item list. * @param callback call back. return the should break flag. */ static lambdaEvaluator<T = unknown, U = unknown>( expression: Expression, state: MemoryInterface, options: Options, list: T[], callback: (currentItem: T, result: U, error: string) => boolean, ): void { const firstChild = expression.children[1].children[0]; if (!(firstChild instanceof Constant) || typeof firstChild.value !== 'string') { return; } const iteratorName = firstChild.value; const stackedMemory = StackedMemory.wrap(state); for (const item of list) { const currentItem = item; const local: Map<string, any> = new Map<string, any>([[iteratorName, item]]); // the local iterator is pushed as one memory layer in the memory stack stackedMemory.push(SimpleObjectMemory.wrap(local)); const { value: r, error: e } = expression.children[2].tryEvaluate(stackedMemory, options); stackedMemory.pop(); const shouldBreak = callback(currentItem, r, e); if (shouldBreak) { break; } } } /** * Convert an object into array. * If the instance is array, return itself. * If the instance is object, return {key, value} pair list. * Else return undefined. * * @param instance input instance. * @returns The generated list. */ static convertToList(instance: unknown): unknown[] | undefined { let arr: unknown[] | undefined; if (Array.isArray(instance)) { arr = instance; } else if (typeof instance === 'object') { arr = []; Object.keys(instance).forEach((u): number => arr.push({ key: u, value: instance[u] })); } return arr; } /** * Validator for foreach, select, and where functions. * * @param expression The expression to validate. */ static ValidateLambdaExpression(expression: Expression): void { if (expression.children.length !== 3) { throw new Error(`Lambda expression expect 3 parameters, found ${expression.children.length}`); } const second: any = expression.children[1]; if (!(second.type === ExpressionType.Accessor && second.children.length === 1)) { throw new Error(`Second parameter is not an identifier : ${second}`); } } /** * Parse string into URL object. * * @param uri Input string uri. * @returns The parsed URL object. */ static parseUri(uri: string): ValueWithError { let result: URL; let error: string; try { result = new URL(uri); } catch { error = `Invalid URI: ${uri}`; } return { value: result, error }; } /** * Transform C# period and unit into js period and unit. * * @param duration C# duration. * @param cSharpStr C# unit. * @returns The transformed timeUnit. */ static timeUnitTransformer(duration: number, cSharpStr: string): { duration: number; tsStr: ManipulateType } { switch (cSharpStr) { case 'Day': return { duration, tsStr: 'day' }; case 'Week': return { duration: duration * 7, tsStr: 'day' }; case 'Second': return { duration, tsStr: 'second' }; case 'Minute': return { duration, tsStr: 'minute' }; case 'Hour': return { duration, tsStr: 'hour' }; case 'Month': return { duration, tsStr: 'month' }; case 'Year': return { duration, tsStr: 'year' }; default: return { duration, tsStr: undefined }; } } /** * TextEncoder helper function. * * @returns The text encoder. */ static getTextEncoder(): TextEncoder { if (typeof window !== 'undefined' || typeof self !== 'undefined') { return new TextEncoder(); } return new util.TextEncoder(); } /** * TextDecoder helper function. * * @param code The encoding format. * @returns The text decoder. */ static getTextDecoder(code = 'utf-8'): TextDecoder { if (typeof window !== 'undefined' || typeof self !== 'undefined') { return new TextDecoder(code); } return new util.TextDecoder(code); } /** * Common Stringify an object. * * @param input input object. * @returns the stringified object. */ static commonStringify(input: unknown): string { if (input == null) { return ''; } if (typeof input === 'object') { return JSON.stringify(input) .replace(/(^['"]*)/g, '') .replace(/(['"]*$)/g, ''); } else { return input.toString(); } } /** * @private */ private static sortByKey(key: string) { return (a: unknown, b: unknown) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0); } }