Spaces:
Running
Running
/* | |
* 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; | |
} | |