/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ParsedValue, DimensionType } from '../../util/types'; import { parseDate, numericToNumber } from '../../util/number'; import { createHashMap, trim, hasOwn, isString, isNumber } from 'zrender/src/core/util'; import { throwError } from '../../util/log'; /** * Convert raw the value in to inner value in List. * * [Performance sensitive] * * [Caution]: this is the key logic of user value parser. * For backward compatibility, do not modify it until you have to! */ export function parseDataValue( value: any, // For high performance, do not omit the second param. opt: { // Default type: 'number'. There is no 'unknown' type. That is, a string // will be parsed to NaN if do not set `type` as 'ordinal'. It has been // the logic in `List.ts` for long time. Follow the same way if you need // to get same result as List did from a raw value. type?: DimensionType } ): ParsedValue { // Performance sensitive. const dimType = opt && opt.type; if (dimType === 'ordinal') { // If given value is a category string return value; } if (dimType === 'time' // spead up when using timestamp && !isNumber(value) && value != null && value !== '-' ) { value = +parseDate(value); } // dimType defaults 'number'. // If dimType is not ordinal and value is null or undefined or NaN or '-', // parse to NaN. // number-like string (like ' 123 ') can be converted to a number. // where null/undefined or other string will be converted to NaN. return (value == null || value === '') ? NaN // If string (like '-'), using '+' parse to NaN // If object, also parse to NaN : +value; }; export type RawValueParserType = 'number' | 'time' | 'trim'; type RawValueParser = (val: unknown) => unknown; const valueParserMap = createHashMap({ 'number': function (val): number { // Do not use `numericToNumber` here. We have `numericToNumber` by default. // Here the number parser can have loose rule: // enable to cut suffix: "120px" => 120, "14%" => 14. return parseFloat(val as string); }, 'time': function (val): number { // return timestamp. return +parseDate(val); }, 'trim': function (val) { return isString(val) ? trim(val) : val; } }); export function getRawValueParser(type: RawValueParserType): RawValueParser { return valueParserMap.get(type); } export interface FilterComparator { evaluate(val: unknown): boolean; } const ORDER_COMPARISON_OP_MAP: { [key in OrderRelationOperator]: ((lval: unknown, rval: unknown) => boolean) } = { lt: (lval, rval) => lval < rval, lte: (lval, rval) => lval <= rval, gt: (lval, rval) => lval > rval, gte: (lval, rval) => lval >= rval }; class FilterOrderComparator implements FilterComparator { private _rvalFloat: number; private _opFn: (lval: unknown, rval: unknown) => boolean; constructor(op: OrderRelationOperator, rval: unknown) { if (!isNumber(rval)) { let errMsg = ''; if (__DEV__) { errMsg = 'rvalue of "<", ">", "<=", ">=" can only be number in filter.'; } throwError(errMsg); } this._opFn = ORDER_COMPARISON_OP_MAP[op]; this._rvalFloat = numericToNumber(rval); } // Performance sensitive. evaluate(lval: unknown): boolean { // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat. return isNumber(lval) ? this._opFn(lval, this._rvalFloat) : this._opFn(numericToNumber(lval), this._rvalFloat); } } export class SortOrderComparator { private _incomparable: number; private _resultLT: -1 | 1; /** * @param order by default: 'asc' * @param incomparable by default: Always on the tail. * That is, if 'asc' => 'max', if 'desc' => 'min' * See the definition of "incomparable" in [SORT_COMPARISON_RULE]. */ constructor(order: 'asc' | 'desc', incomparable: 'min' | 'max') { const isDesc = order === 'desc'; this._resultLT = isDesc ? 1 : -1; if (incomparable == null) { incomparable = isDesc ? 'min' : 'max'; } this._incomparable = incomparable === 'min' ? -Infinity : Infinity; } // See [SORT_COMPARISON_RULE]. // Performance sensitive. evaluate(lval: unknown, rval: unknown): -1 | 0 | 1 { // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat. let lvalFloat = isNumber(lval) ? lval : numericToNumber(lval); let rvalFloat = isNumber(rval) ? rval : numericToNumber(rval); const lvalNotNumeric = isNaN(lvalFloat as number); const rvalNotNumeric = isNaN(rvalFloat as number); if (lvalNotNumeric) { lvalFloat = this._incomparable; } if (rvalNotNumeric) { rvalFloat = this._incomparable; } if (lvalNotNumeric && rvalNotNumeric) { const lvalIsStr = isString(lval); const rvalIsStr = isString(rval); if (lvalIsStr) { lvalFloat = rvalIsStr ? lval as unknown as number : 0; } if (rvalIsStr) { rvalFloat = lvalIsStr ? rval as unknown as number : 0; } } return lvalFloat < rvalFloat ? this._resultLT : lvalFloat > rvalFloat ? (-this._resultLT as -1 | 1) : 0; } } class FilterEqualityComparator implements FilterComparator { private _isEQ: boolean; private _rval: unknown; private _rvalTypeof: string; private _rvalFloat: number; constructor(isEq: boolean, rval: unknown) { this._rval = rval; this._isEQ = isEq; this._rvalTypeof = typeof rval; this._rvalFloat = numericToNumber(rval); } // Performance sensitive. evaluate(lval: unknown): boolean { let eqResult = lval === this._rval; if (!eqResult) { const lvalTypeof = typeof lval; if (lvalTypeof !== this._rvalTypeof && (lvalTypeof === 'number' || this._rvalTypeof === 'number')) { eqResult = numericToNumber(lval) === this._rvalFloat; } } return this._isEQ ? eqResult : !eqResult; } } type OrderRelationOperator = 'lt' | 'lte' | 'gt' | 'gte'; export type RelationalOperator = OrderRelationOperator | 'eq' | 'ne'; /** * [FILTER_COMPARISON_RULE] * `lt`|`lte`|`gt`|`gte`: * + rval must be a number. And lval will be converted to number (`numericToNumber`) to compare. * `eq`: * + If same type, compare with `===`. * + If there is one number, convert to number (`numericToNumber`) to compare. * + Else return `false`. * `ne`: * + Not `eq`. * * * [SORT_COMPARISON_RULE] * All the values are grouped into three categories: * + "numeric" (number and numeric string) * + "non-numeric-string" (string that excluding numeric string) * + "others" * "numeric" vs "numeric": values are ordered by number order. * "non-numeric-string" vs "non-numeric-string": values are ordered by ES spec (#sec-abstract-relational-comparison). * "others" vs "others": do not change order (always return 0). * "numeric" vs "non-numeric-string": "non-numeric-string" is treated as "incomparable". * "number" vs "others": "others" is treated as "incomparable". * "non-numeric-string" vs "others": "others" is treated as "incomparable". * "incomparable" will be seen as -Infinity or Infinity (depends on the settings). * MEMO: * Non-numeric string sort makes sense when we need to put the items with the same tag together. * But if we support string sort, we still need to avoid the misleading like `'2' > '12'`, * So we treat "numeric-string" sorted by number order rather than string comparison. * * * [CHECK_LIST_OF_THE_RULE_DESIGN] * + Do not support string comparison until required. And also need to * avoid the misleading of "2" > "12". * + Should avoid the misleading case: * `" 22 " gte "22"` is `true` but `" 22 " eq "22"` is `false`. * + JS bad case should be avoided: null <= 0, [] <= 0, ' ' <= 0, ... * + Only "numeric" can be converted to comparable number, otherwise converted to NaN. * See `util/number.ts#numericToNumber`. * * @return If `op` is not `RelationalOperator`, return null; */ export function createFilterComparator( op: string, rval?: unknown ): FilterComparator { return (op === 'eq' || op === 'ne') ? new FilterEqualityComparator(op === 'eq', rval) : hasOwn(ORDER_COMPARISON_OP_MAP, op) ? new FilterOrderComparator(op as OrderRelationOperator, rval) : null; }