|  | /* | 
|  | * 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<RawValueParser, RawValueParserType>({ | 
|  | '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; | 
|  | } |