You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2020/08/14 04:35:28 UTC
[incubator-echarts] 01/02: fix: [data-transform] Make the
comparison operation accurate in common edge cases. Support parser
'number'.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch dataset-trans2
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit ff906a44006d073926459073ed4bf6288e0639e3
Author: 100pah <su...@gmail.com>
AuthorDate: Wed Aug 12 22:54:37 2020 +0800
fix: [data-transform] Make the comparison operation accurate in common edge cases. Support parser 'number'.
---
src/component/transform/sortTransform.ts | 44 +++--
src/data/List.ts | 2 +-
src/data/helper/dataValueHelper.ts | 232 +++++++++++++++++++++++++++
src/data/helper/parseDataValue.ts | 73 ---------
src/data/helper/transform.ts | 2 +-
src/util/conditionalExpression.ts | 183 ++++++++-------------
src/util/number.ts | 35 +++-
test/ut/spec/data/completeDimensions.test.js | 4 +-
test/ut/spec/data/dataValueHelper.test.js | 209 ++++++++++++++++++++++++
test/ut/spec/util/number.test.js | 54 +++++++
10 files changed, 614 insertions(+), 224 deletions(-)
diff --git a/src/component/transform/sortTransform.ts b/src/component/transform/sortTransform.ts
index e4f75e5..cd47616 100644
--- a/src/component/transform/sortTransform.ts
+++ b/src/component/transform/sortTransform.ts
@@ -24,7 +24,9 @@ import {
import { makePrintable, throwError } from '../../util/log';
import { isArray, each, hasOwn } from 'zrender/src/core/util';
import { normalizeToArray } from '../../util/model';
-import { parseDate } from '../../util/number';
+import {
+ RawValueParserType, getRawValueParser, createRelationalComparator
+} from '../../data/helper/dataValueHelper';
/**
* @usage
@@ -53,7 +55,7 @@ export interface SortTransformOption extends DataTransformOption {
type OrderExpression = {
dimension: DimensionLoose;
order: SortOrder;
- parse?: 'time'
+ parse?: RawValueParserType;
};
type SortOrder = 'asc' | 'desc';
@@ -65,13 +67,9 @@ if (__DEV__) {
'Valid config is like:',
'{ dimension: "age", order: "asc" }',
'or [{ dimension: "age", order: "asc"], { dimension: "date", order: "desc" }]'
- ].join('');
+ ].join(' ');
}
-const timeParser = function (val: OptionDataValue): number {
- return +parseDate(val);
-};
-
export const sortTransform: ExternalDataTransform<SortTransformOption> = {
@@ -98,7 +96,7 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
const orderDefList: {
dimIdx: DimensionIndex;
orderReturn: -1 | 1;
- parser: (val: OptionDataValue) => number;
+ parser: ReturnType<typeof getRawValueParser>;
}[] = [];
each(orderExprList, function (orderExpr) {
const dimLoose = orderExpr.dimension;
@@ -131,18 +129,15 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
throwError(errMsg);
}
- let parser;
- if (parserName) {
- if (parserName !== 'time') {
- if (__DEV__) {
- errMsg = makePrintable(
- 'Invalid parser name' + parserName + '.\n',
- 'Illegal config:', orderExpr, '.\n'
- );
- }
- throwError(errMsg);
+ const parser = parserName ? getRawValueParser(parserName) : null;
+ if (parserName && !parser) {
+ if (__DEV__) {
+ errMsg = makePrintable(
+ 'Invalid parser name ' + parserName + '.\n',
+ 'Illegal config:', orderExpr, '.\n'
+ );
}
- parser = timeParser;
+ throwError(errMsg);
}
orderDefList.push({
@@ -175,6 +170,9 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
resultData.push(source.getRawDataItem(i));
}
+ const lt = createRelationalComparator('lt');
+ const gt = createRelationalComparator('gt');
+
resultData.sort(function (item0, item1) {
if (item0 === headerPlaceholder) {
return -1;
@@ -196,13 +194,13 @@ export const sortTransform: ExternalDataTransform<SortTransformOption> = {
let val0 = source.retrieveItemValue(item0, orderDef.dimIdx);
let val1 = source.retrieveItemValue(item1, orderDef.dimIdx);
if (orderDef.parser) {
- val0 = orderDef.parser(val0);
- val1 = orderDef.parser(val1);
+ val0 = orderDef.parser(val0) as OptionDataValue;
+ val1 = orderDef.parser(val1) as OptionDataValue;
}
- if (val0 < val1) {
+ if (lt.evaluate(val0, val1)) {
return orderDef.orderReturn;
}
- else if (val0 > val1) {
+ else if (gt.evaluate(val0, val1)) {
return -orderDef.orderReturn;
}
}
diff --git a/src/data/List.ts b/src/data/List.ts
index 871f5fd..1eb95fa 100644
--- a/src/data/List.ts
+++ b/src/data/List.ts
@@ -42,7 +42,7 @@ import { PathStyleProps } from 'zrender/src/graphic/Path';
import type Graph from './Graph';
import type Tree from './Tree';
import type { VisualMeta } from '../component/visualMap/VisualMapModel';
-import { parseDataValue } from './helper/parseDataValue';
+import { parseDataValue } from './helper/dataValueHelper';
const isObject = zrUtil.isObject;
diff --git a/src/data/helper/dataValueHelper.ts b/src/data/helper/dataValueHelper.ts
new file mode 100644
index 0000000..9214bb3
--- /dev/null
+++ b/src/data/helper/dataValueHelper.ts
@@ -0,0 +1,232 @@
+/*
+* 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 OrdinalMeta from '../OrdinalMeta';
+import { parseDate, numericToNumber } from '../../util/number';
+import { createHashMap, trim, hasOwn } from 'zrender/src/core/util';
+
+
+/**
+ * Convert raw the value in to inner value in List.
+ *
+ * [Performance sensitive]
+ *
+ * [Caution]: this is the key logic of user value parser.
+ * For backward compatibiliy, do not modify it until 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,
+ ordinalMeta?: OrdinalMeta
+ }
+): ParsedValue {
+ // Performance sensitive.
+ const dimType = opt && opt.type;
+ if (dimType === 'ordinal') {
+ // If given value is a category string
+ const ordinalMeta = opt && opt.ordinalMeta;
+ return ordinalMeta
+ ? ordinalMeta.parseAndCollect(value)
+ : value;
+ }
+
+ if (dimType === 'time'
+ // spead up when using timestamp
+ && typeof value !== 'number'
+ && 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 {
+ return numericToNumber(val);
+ },
+ 'time': function (val): number {
+ // return timestamp.
+ return +parseDate(val);
+ },
+ 'trim': function (val) {
+ return typeof val === 'string' ? trim(val) : val;
+ }
+});
+
+export function getRawValueParser(type: RawValueParserType): RawValueParser {
+ return valueParserMap.get(type);
+}
+
+
+
+
+export interface UnaryExpression {
+ evaluate(val: unknown): unknown;
+}
+export interface BinaryExpression {
+ evaluate(lval: unknown, rval: unknown): unknown;
+}
+
+class OrderComparatorUnary implements UnaryExpression {
+ _rval: unknown;
+ _rvalTypeof: string; // typeof rval
+ _rvalFloat: number;
+ _rvalIsNumeric: boolean;
+ _opFn: (lval: unknown, rval: unknown) => boolean;
+ // Performance sensitive.
+ evaluate(lval: unknown): boolean {
+ // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat.
+ const lvalIsNumber = typeof lval === 'number';
+ return (lvalIsNumber && this._rvalIsNumeric)
+ ? this._opFn(lval, this._rvalFloat)
+ : (lvalIsNumber || this._rvalTypeof === 'number')
+ ? this._opFn(numericToNumber(lval), this._rvalFloat)
+ : false;
+ }
+}
+class OrderComparatorBinary implements BinaryExpression {
+ _opFn: (lval: unknown, rval: unknown) => boolean;
+ // Performance sensitive.
+ evaluate(lval: unknown, rval: unknown): boolean {
+ // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat.
+ const lvalIsNumber = typeof lval === 'number';
+ const rvalIsNumber = typeof rval === 'number';
+ return (lvalIsNumber && rvalIsNumber)
+ ? this._opFn(lval, rval)
+ : (lvalIsNumber || rvalIsNumber)
+ ? this._opFn(numericToNumber(lval), numericToNumber(rval))
+ : false;
+ }
+}
+
+class EqualityComparatorUnary implements UnaryExpression {
+ _rval: unknown;
+ _rvalTypeof: string; // typeof rval
+ _rvalFloat: number;
+ _rvalIsNumeric: boolean;
+ _isEq: boolean;
+ // 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;
+ }
+}
+
+class EqualityComparatorBinary implements BinaryExpression {
+ _isEq: boolean;
+ // Performance sensitive.
+ evaluate(lval: unknown, rval: unknown): boolean {
+ let eqResult = lval === rval;
+ if (!eqResult) {
+ const lvalTypeof = typeof lval;
+ const rvalTypeof = typeof rval;
+ if (lvalTypeof !== rvalTypeof && (lvalTypeof === 'number' || rvalTypeof === 'number')) {
+ eqResult = numericToNumber(lval) === numericToNumber(rval);
+ }
+ }
+ return this._isEq ? eqResult : !eqResult;
+ }
+}
+
+const ORDER_COMPARISON_OP_MAP = {
+ lt: (tarVal: unknown, condVal: unknown) => tarVal < condVal,
+ lte: (tarVal: unknown, condVal: unknown) => tarVal <= condVal,
+ gt: (tarVal: unknown, condVal: unknown) => tarVal > condVal,
+ gte: (tarVal: unknown, condVal: unknown) => tarVal >= condVal
+} as const;
+
+export type RelationalOperator = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'ne';
+
+/**
+ * [COMPARISON_RULE]
+ * `lt`, `lte`, `gt`, `gte`:
+ * + If two "number" or a "number" and a "numeric": convert to number and compare.
+ * + Else return `false`.
+ * `eq`:
+ * + If same type, compare with ===.
+ * + If two "number" or a "number" and a "numeric": convert to number and compare.
+ * + Else return `false`.
+ * `ne`:
+ * + Not `eq`.
+ *
+ * Definition of "numeric": see `util/number.ts#numericToNumber`.
+ *
+ * [MEMO]
+ * + Do not support string comparison until required. And also need to consider the
+ * misleading of "2" > "12".
+ * + JS bad case considered: null <= 0, [] <= 0, ' ' <= 0, ...
+ */
+export function createRelationalComparator(op: RelationalOperator): BinaryExpression;
+export function createRelationalComparator(op: RelationalOperator, isUnary: true, rval: unknown): UnaryExpression;
+export function createRelationalComparator(
+ op: RelationalOperator,
+ isUnary?: true,
+ rval?: unknown
+): UnaryExpression | BinaryExpression {
+ let comparator;
+ if (op === 'eq' || op === 'ne') {
+ comparator = isUnary ? new EqualityComparatorUnary() : new EqualityComparatorBinary();
+ comparator._isEq = op === 'eq';
+ }
+ else {
+ comparator = isUnary ? new OrderComparatorUnary() : new OrderComparatorBinary();
+ comparator._opFn = ORDER_COMPARISON_OP_MAP[op];
+ }
+ if (isUnary) {
+ const unaryComp = comparator as OrderComparatorUnary | EqualityComparatorUnary;
+ unaryComp._rval = rval;
+ unaryComp._rvalTypeof = typeof rval;
+ const rvalFloat = unaryComp._rvalFloat = numericToNumber(rval);
+ unaryComp._rvalIsNumeric = !isNaN(rvalFloat); // eslint-disable-line eqeqeq
+ }
+ return comparator;
+}
+
+export function isRelationalOperator(op: string): op is RelationalOperator {
+ return hasOwn(ORDER_COMPARISON_OP_MAP, op) || op === 'eq' || op === 'ne';
+}
diff --git a/src/data/helper/parseDataValue.ts b/src/data/helper/parseDataValue.ts
deleted file mode 100644
index 6994378..0000000
--- a/src/data/helper/parseDataValue.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
-* 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 OrdinalMeta from '../OrdinalMeta';
-import { parseDate } from '../../util/number';
-
-
-/**
- * Convert raw the value in to inner value in List.
- *
- * [Performance sensitive]
- *
- * [Caution]: this is the key logic of user value parser.
- * For backward compatibiliy, do not modify it until 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,
- ordinalMeta?: OrdinalMeta
- }
-): ParsedValue {
- // Performance sensitive.
- const dimType = opt && opt.type;
- if (dimType === 'ordinal') {
- // If given value is a category string
- const ordinalMeta = opt && opt.ordinalMeta;
- return ordinalMeta
- ? ordinalMeta.parseAndCollect(value)
- : value;
- }
-
- if (dimType === 'time'
- // spead up when using timestamp
- && typeof value !== 'number'
- && 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.
- return (value == null || value === '')
- ? NaN
- // If string (like '-'), using '+' parse to NaN
- // If object, also parse to NaN
- : +value;
-};
-
diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts
index d657d52..db72892 100644
--- a/src/data/helper/transform.ts
+++ b/src/data/helper/transform.ts
@@ -31,7 +31,7 @@ import {
import {
getRawSourceItemGetter, getRawSourceDataCounter, getRawSourceValueGetter
} from './dataProvider';
-import { parseDataValue } from './parseDataValue';
+import { parseDataValue } from './dataValueHelper';
import { createSource, inheritSourceMetaRawOption } from './sourceHelper';
import { consoleLog, makePrintable } from '../../util/log';
diff --git a/src/util/conditionalExpression.ts b/src/util/conditionalExpression.ts
index b48a896..b871b09 100644
--- a/src/util/conditionalExpression.ts
+++ b/src/util/conditionalExpression.ts
@@ -19,10 +19,13 @@
import { OptionDataValue, DimensionLoose, Dictionary } from './types';
import {
- createHashMap, keys, isArray, map, isObject, isString, trim, HashMap, isRegExp, isArrayLike
+ keys, isArray, map, isObject, isString, HashMap, isRegExp, isArrayLike, hasOwn
} from 'zrender/src/core/util';
import { throwError, makePrintable } from './log';
-import { parseDate } from './number';
+import {
+ RawValueParserType, getRawValueParser, isRelationalOperator,
+ createRelationalComparator, RelationalOperator, UnaryExpression
+} from '../data/helper/dataValueHelper';
// PENDING:
@@ -82,6 +85,18 @@ import { parseDate } from './number';
* parse: 'time',
* lt: '2012-12-12'
* }
+ * // Normalize number-like string and make '-' to Null.
+ * {
+ * parse: 'time',
+ * lt: '2012-12-12'
+ * }
+ * // Normalize to number:
+ * // + number-like string (like ' 123 ') can be converted to a number.
+ * // + where null/undefined or other string will be converted to NaN.
+ * {
+ * parse: 'number',
+ * eq: 2011
+ * }
* // RegExp, include the feature in SQL: `like '%xxx%'`.
* {
* reg: /^asdf$/
@@ -118,35 +133,10 @@ import { parseDate } from './number';
/**
* Date string and ordinal string can be accepted.
*/
-interface RelationalExpressionOptionByOp {
- lt?: OptionDataValue; // less than
- lte?: OptionDataValue; // less than or equal
- gt?: OptionDataValue; // greater than
- gte?: OptionDataValue; // greater than or equal
- eq?: OptionDataValue; // equal
- ne?: OptionDataValue; // not equal
+interface RelationalExpressionOptionByOp extends Record<RelationalOperator, OptionDataValue> {
reg?: RegExp | string; // RegExp
};
-interface RelationalExpressionOptionByOpAlias {
- value?: RelationalExpressionOptionByOp['eq'];
-
- '<'?: OptionDataValue; // lt
- '<='?: OptionDataValue; // lte
- '>'?: OptionDataValue; // gt
- '>='?: OptionDataValue; // gte
- '='?: OptionDataValue; // eq
- '!='?: OptionDataValue; // ne
- '<>'?: OptionDataValue; // ne (SQL style)
-
- // '=='?: OptionDataValue; // eq
- // '==='?: OptionDataValue; // eq
- // '!=='?: OptionDataValue; // eq
-
- // ge: RelationalExpressionOptionByOp['gte'];
- // le: RelationalExpressionOptionByOp['lte'];
- // neq: RelationalExpressionOptionByOp['ne'];
-};
-const aliasToOpMap = createHashMap<RelationalExpressionOp, RelationalExpressionOpAlias>({
+const RELATIONAL_EXPRESSION_OP_ALIAS_MAP = {
value: 'eq',
// PENDING: not good for literal semantic?
@@ -168,67 +158,44 @@ const aliasToOpMap = createHashMap<RelationalExpressionOp, RelationalExpressionO
// ge: 'gte',
// le: 'lte',
// neq: 'ne',
-});
-
-type RelationalExpressionOp = keyof RelationalExpressionOptionByOp;
-type RelationalExpressionOpAlias = keyof RelationalExpressionOptionByOpAlias;
+} as const;
+type RelationalExpressionOptionByOpAlias = Record<keyof typeof RELATIONAL_EXPRESSION_OP_ALIAS_MAP, OptionDataValue>;
interface RelationalExpressionOption extends
RelationalExpressionOptionByOp, RelationalExpressionOptionByOpAlias {
dimension?: DimensionLoose;
- parse?: RelationalExpressionValueParserType;
+ parse?: RawValueParserType;
}
type RelationalExpressionOpEvaluate = (tarVal: unknown, condVal: unknown) => boolean;
-const relationalOpEvaluateMap = createHashMap<RelationalExpressionOpEvaluate, RelationalExpressionOp>({
- // PENDING: should keep supporting string compare?
- lt: function (tarVal, condVal) {
- return tarVal < condVal;
- },
- lte: function (tarVal, condVal) {
- return tarVal <= condVal;
- },
- gt: function (tarVal, condVal) {
- return tarVal > condVal;
- },
- gte: function (tarVal, condVal) {
- return tarVal >= condVal;
- },
- eq: function (tarVal, condVal) {
- // eq is probably most used, DO NOT use JS ==,
- // the rule is too complicated.
- return tarVal === condVal;
- },
- ne: function (tarVal, condVal) {
- return tarVal !== condVal;
- },
- reg: function (tarVal, condVal: RegExp) {
- const type = typeof tarVal;
- return type === 'string' ? condVal.test(tarVal as string)
- : type === 'number' ? condVal.test(tarVal + '')
- : false;
+
+class RegExpEvaluator implements UnaryExpression {
+ private _condVal: RegExp;
+
+ constructor(rVal: unknown) {
+ // Support condVal: RegExp | string
+ const condValue = this._condVal = isString(rVal) ? new RegExp(rVal)
+ : isRegExp(rVal) ? rVal as RegExp
+ : null;
+ if (condValue == null) {
+ let errMsg = '';
+ if (__DEV__) {
+ errMsg = makePrintable('Illegal regexp', rVal, 'in');
+ }
+ throwError(errMsg);
+ }
}
-});
-function parseRegCond(condVal: unknown): RegExp {
- // Support condVal: RegExp | string
- return isString(condVal) ? new RegExp(condVal)
- : isRegExp(condVal) ? condVal as RegExp
- : null;
+ evaluate(lVal: unknown): boolean {
+ const type = typeof lVal;
+ return type === 'string' ? this._condVal.test(lVal as string)
+ : type === 'number' ? this._condVal.test(lVal + '')
+ : false;
+ }
}
-type RelationalExpressionValueParserType = 'time' | 'trim';
-type RelationalExpressionValueParser = (val: unknown) => unknown;
-const valueParserMap = createHashMap<RelationalExpressionValueParser, RelationalExpressionValueParserType>({
- time: function (val): number {
- // return timestamp.
- return +parseDate(val);
- },
- trim: function (val) {
- return typeof val === 'string' ? trim(val) : val;
- }
-});
+
// --------------------------------------------------
@@ -309,30 +276,21 @@ class NotConditionInternal implements ParsedConditionInternal {
}
class RelationalConditionInternal implements ParsedConditionInternal {
valueGetterParam: ValueGetterParam;
- valueParser: RelationalExpressionValueParser;
+ valueParser: ReturnType<typeof getRawValueParser>;
// If no parser, be null/undefined.
getValue: ConditionalExpressionValueGetter;
- subCondList: {
- condValue: unknown;
- evaluate: RelationalExpressionOpEvaluate;
- }[];
+ subCondList: UnaryExpression[];
evaluate() {
- const getValue = this.getValue;
const needParse = !!this.valueParser;
// Call getValue with no `this`.
+ const getValue = this.getValue;
const tarValRaw = getValue(this.valueGetterParam);
const tarValParsed = needParse ? this.valueParser(tarValRaw) : null;
// Relational cond follow "and" logic internally.
for (let i = 0; i < this.subCondList.length; i++) {
- const subCond = this.subCondList[i];
- if (
- !subCond.evaluate(
- needParse ? tarValParsed : tarValRaw,
- subCond.condValue
- )
- ) {
+ if (!this.subCondList[i].evaluate(needParse ? tarValParsed : tarValRaw)) {
return false;
}
}
@@ -435,7 +393,7 @@ function parseRelationalOption(
const exprKeys = keys(exprOption);
const parserName = exprOption.parse;
- const valueParser = parserName ? valueParserMap.get(parserName) : null;
+ const valueParser = parserName ? getRawValueParser(parserName) : null;
for (let i = 0; i < exprKeys.length; i++) {
const keyRaw = exprKeys[i];
@@ -443,11 +401,19 @@ function parseRelationalOption(
continue;
}
- const op: RelationalExpressionOp = aliasToOpMap.get(keyRaw as RelationalExpressionOpAlias)
- || (keyRaw as RelationalExpressionOp);
- const evaluateHandler = relationalOpEvaluateMap.get(op);
-
- if (!evaluateHandler) {
+ const op: keyof RelationalExpressionOptionByOp = hasOwn(RELATIONAL_EXPRESSION_OP_ALIAS_MAP, keyRaw)
+ ? RELATIONAL_EXPRESSION_OP_ALIAS_MAP[keyRaw as keyof RelationalExpressionOptionByOpAlias]
+ : (keyRaw as keyof RelationalExpressionOptionByOp);
+ const condValueRaw = exprOption[keyRaw];
+ const condValueParsed = valueParser ? valueParser(condValueRaw) : condValueRaw;
+ const evaluator =
+ isRelationalOperator(op)
+ ? createRelationalComparator(op, true, condValueParsed)
+ : op === 'reg'
+ ? new RegExpEvaluator(condValueParsed)
+ : null;
+
+ if (!evaluator) {
if (__DEV__) {
errMsg = makePrintable(
'Illegal relational operation: "' + keyRaw + '" in condition:', exprOption
@@ -456,28 +422,7 @@ function parseRelationalOption(
throwError(errMsg);
}
- const condValueRaw = exprOption[keyRaw];
- let condValue;
- if (keyRaw === 'reg') {
- condValue = parseRegCond(condValueRaw);
- if (condValue == null) {
- let errMsg = '';
- if (__DEV__) {
- errMsg = makePrintable('Illegal regexp', condValueRaw, 'in', exprOption);
- }
- throwError(errMsg);
- }
- }
- else {
- // At present, all other operators are applicable `RelationalExpressionValueParserType`.
- // But if adding new parser, we should check it again.
- condValue = valueParser ? valueParser(condValueRaw) : condValueRaw;
- }
-
- subCondList.push({
- condValue: condValue,
- evaluate: evaluateHandler
- });
+ subCondList.push(evaluator);
}
if (!subCondList.length) {
diff --git a/src/util/number.ts b/src/util/number.ts
index 68cad8b..b2ae043 100644
--- a/src/util/number.ts
+++ b/src/util/number.ts
@@ -536,10 +536,35 @@ export function reformIntervals(list: IntervalItem[]): IntervalItem[] {
}
/**
- * parseFloat NaNs numeric-cast false positives (null|true|false|"")
- * ...but misinterprets leading-number strings, particularly hex literals ("0x...")
- * subtraction forces infinities to NaN
+ * [Numberic is defined as]:
+ * `parseFloat(val) == val`
+ * For example:
+ * numeric:
+ * typeof number except NaN, '-123', '123', '2e3', '-2e3', '011', 'Infinity', Infinity,
+ * and they rounded by white-spaces or line-terminal like ' -123 \n ' (see es spec)
+ * not-numeric:
+ * null, undefined, [], {}, true, false, 'NaN', NaN, '123ab',
+ * empty string, string with only white-spaces or line-terminal (see es spec),
+ * 0x12, '0x12', '-0x12', 012, '012', '-012',
+ * non-string, ...
+ *
+ * @test See full test cases in `test/ut/spec/util/number.js`.
*/
-export function isNumeric(v: any): v is number {
- return v - parseFloat(v) >= 0;
+export function numericToNumber(val: unknown): number {
+ const valFloat = parseFloat(val as string);
+ return isNumericHavingParseFloat(val, valFloat) ? valFloat : NaN;
+}
+
+/**
+ * Definition of "numeric": see `numericToNumber`.
+ */
+export function isNumeric(val: unknown): val is number {
+ return isNumericHavingParseFloat(val, parseFloat(val as string));
+}
+
+function isNumericHavingParseFloat(val: unknown, valFloat: number): val is number {
+ return (
+ valFloat == val // eslint-disable-line eqeqeq
+ && (valFloat !== 0 || typeof val !== 'string' || val.indexOf('x') <= 0) // For case ' 0x0 '.
+ );
}
diff --git a/test/ut/spec/data/completeDimensions.test.js b/test/ut/spec/data/completeDimensions.test.js
index 4b340bc..eb9c308 100644
--- a/test/ut/spec/data/completeDimensions.test.js
+++ b/test/ut/spec/data/completeDimensions.test.js
@@ -19,7 +19,7 @@
*/
const completeDimensions = require('../../../../lib/data/helper/completeDimensions');
const Source = require('../../../../lib/data/Source');
-const sourceType = require('../../../../lib/data/helper/sourceType');
+const types = require('../../../../lib/util/types');
describe('completeDimensions', function () {
@@ -64,7 +64,7 @@ describe('completeDimensions', function () {
var source = new Source({
data: [],
fromDataset: true,
- sourceFormat: sourceType.SOURCE_FORMAT_ARRAY_ROWS,
+ sourceFormat: types.SOURCE_FORMAT_ARRAY_ROWS,
dimensionsDetectCount: 11
});
diff --git a/test/ut/spec/data/dataValueHelper.test.js b/test/ut/spec/data/dataValueHelper.test.js
new file mode 100644
index 0000000..8eb7309
--- /dev/null
+++ b/test/ut/spec/data/dataValueHelper.test.js
@@ -0,0 +1,209 @@
+/*
+* 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.
+*/
+
+
+const dataValueHelper = require('../../../../lib/data/helper/dataValueHelper');
+
+describe('data/helper/dataValueHelper', function () {
+
+ describe('relational_comparison', function () {
+
+ function expectDual(evalFn, lval, rval, resultLR, resultRL) {
+ expect(evalFn(lval, rval)).toEqual(resultLR);
+ expect(evalFn(rval, lval)).toEqual(resultRL);
+ }
+
+ const testerMap = {
+
+ notEqualAndHasOrder: function (evalFn, op) {
+ let asc;
+ let desc;
+ if (op === 'lt' || op === 'lte') {
+ asc = true;
+ desc = false;
+ }
+ else if (op === 'gt' || op === 'gte') {
+ asc = false;
+ desc = true;
+ }
+ else if (op === 'eq') {
+ asc = desc = false;
+ }
+ else if (op === 'ne') {
+ asc = desc = true;
+ }
+
+ expectDual(evalFn, 123, 555, asc, desc);
+ expectDual(evalFn, -123, -555, desc, asc);
+ expectDual(evalFn, -123, 123, asc, desc);
+
+ expectDual(evalFn, Infinity, 123, desc, asc);
+ expectDual(evalFn, -Infinity, -123, asc, desc);
+ expectDual(evalFn, 'Infinity', 123, desc, asc);
+ expectDual(evalFn, '-Infinity', 123, asc, desc);
+ expectDual(evalFn, 123, '555', asc, desc);
+ expectDual(evalFn, 555, '555.6', asc, desc);
+ expectDual(evalFn, '-555', -555.6, desc, asc);
+ expectDual(evalFn, 123, ' 555 ', asc, desc);
+ expectDual(evalFn, ' -555 ', 123, asc, desc);
+ expectDual(evalFn, 123, ' \r \n 555 \t ' + String.fromCharCode(12288), asc, desc);
+ },
+
+ notEqualAndNoOrder: function (evalFn, op) {
+ const result = op === 'ne';
+ const makeDate = () => new Date(2012, 5, 12);
+ const makeFn = () => function () {};
+
+ expectDual(evalFn, NaN, NaN, result, result);
+ expectDual(evalFn, NaN, -NaN, result, result);
+ expectDual(evalFn, NaN, 0, result, result);
+ expectDual(evalFn, NaN, 2, result, result);
+ expectDual(evalFn, 'NaN', NaN, result, result);
+ expectDual(evalFn, 'NaN', 0, result, result);
+ expectDual(evalFn, 'NaN', 2, result, result);
+ expectDual(evalFn, '-NaN', -NaN, result, result);
+ expectDual(evalFn, '-NaN', 0, result, result);
+ expectDual(evalFn, '-NaN', 2, result, result);
+ expectDual(evalFn, true, 0, result, result);
+ expectDual(evalFn, false, 1, result, result);
+ expectDual(evalFn, 'true', 0, result, result);
+ expectDual(evalFn, 'false', 1, result, result);
+ expectDual(evalFn, undefined, 2, result, result);
+ expectDual(evalFn, undefined, 0, result, result);
+ expectDual(evalFn, null, 2, result, result);
+ expectDual(evalFn, null, 0, result, result);
+ expectDual(evalFn, makeDate(), 0, result, result);
+ expectDual(evalFn, makeDate(), makeDate(), result, result);
+ expectDual(evalFn, makeDate(), +makeDate(), result, result);
+ expectDual(evalFn, [], 1, result, result);
+ expectDual(evalFn, [], 0, result, result);
+ expectDual(evalFn, {}, 1, result, result);
+ expectDual(evalFn, [], '0', result, result);
+ expectDual(evalFn, {}, '1', result, result);
+ expectDual(evalFn, {}, 0, result, result);
+ expectDual(evalFn, {}, '1', result, result);
+ expectDual(evalFn, {}, '0', result, result);
+ expectDual(evalFn, /1/, 0, result, result);
+ expectDual(evalFn, /0/, 0, result, result);
+ expectDual(evalFn, '555a', 123, result, result);
+ expectDual(evalFn, '2', '12', result, result); // '2' > '12' in JS but should not happen here.
+ expectDual(evalFn, ' ', '', result, result);
+ expectDual(evalFn, 0.5, '0. 5', result, result);
+ expectDual(evalFn, '0.5', '0. 5', result, result);
+ expectDual(evalFn, '- 5', -5, result, result);
+ expectDual(evalFn, '-123.5', ' -123.5 ', result, result);
+ expectDual(evalFn, '0x11', 17, result, result); // not 17 in int16.
+ expectDual(evalFn, '0x11', 0, result, result);
+ expectDual(evalFn, '0x0', 0, result, result);
+ expectDual(evalFn, '0. 5', 0.5, result, result);
+ expectDual(evalFn, '0 .5', 0.5, result, result);
+ expectDual(evalFn, '', 2, result, result);
+ expectDual(evalFn, '', 0, result, result);
+ expectDual(evalFn, ' ', 2, result, result);
+ expectDual(evalFn, ' ', 0, result, result);
+ expectDual(evalFn, ' \n', '\n', result, result);
+ expectDual(evalFn, '\n', 0, result, result);
+ expectDual(evalFn, '\n', 2, result, result);
+ expectDual(evalFn, {}, {}, result, result);
+ expectDual(evalFn, {}, [], result, result);
+ expectDual(evalFn, makeFn(), makeFn(), result, result);
+ expectDual(evalFn, makeFn(), 0, result, result);
+ expectDual(evalFn, makeFn(), 1, result, result);
+ expectDual(evalFn, makeFn(), makeFn().toString(), result, result);
+ },
+
+ numericEqual: function (evalFn, op) {
+ const result = op === 'eq' || op === 'lte' || op === 'gte';
+
+ expectDual(evalFn, 123, 123, result, result);
+ expectDual(evalFn, 1e3, 1000, result, result);
+ expectDual(evalFn, -1e3, -1000, result, result);
+ expectDual(evalFn, '1e3', 1000, result, result);
+ expectDual(evalFn, '-1e3', -1000, result, result);
+ expectDual(evalFn, 123, '123', result, result);
+ expectDual(evalFn, 123, ' 123 ', result, result);
+ expectDual(evalFn, 123.5, ' \n \r 123.5 \t ', result, result);
+ expectDual(evalFn, 123.5, 123.5 + String.fromCharCode(12288), result, result);
+ expectDual(evalFn, ' -123.5 ', -123.5, result, result);
+ expectDual(evalFn, '011', 11, result, result); // not 9 in int8.
+ },
+
+ otherTypesEqual: function (evalFn, op) {
+ const result = op === 'eq';
+
+ const emptyObj = {};
+ const emptyArr = [];
+ const date = new Date(2012, 5, 12);
+ const fn = function () {};
+ expectDual(evalFn, emptyObj, emptyObj, result, result);
+ expectDual(evalFn, emptyArr, emptyArr, result, result);
+ expectDual(evalFn, date, date, result, result);
+ expectDual(evalFn, fn, fn, result, result);
+ }
+ };
+
+ function doTest(op) {
+ expect(['lt', 'lte', 'gt', 'gte', 'eq', 'ne'].indexOf(op) >= 0).toEqual(true);
+ it(op, () => {
+ const comparator0 = dataValueHelper.createRelationalComparator(op);
+ Object.keys(testerMap).forEach(name => {
+ const evalFn = (lVal, rVal) => {
+ return comparator0.evaluate(lVal, rVal);
+ };
+ testerMap[name](evalFn, op);
+ });
+
+ Object.keys(testerMap).forEach(name => {
+ const evalFn = (lVal, rVal) => {
+ const comparator1 = dataValueHelper.createRelationalComparator(op, true, rVal);
+ return comparator1.evaluate(lVal);
+ };
+ testerMap[name](evalFn, op);
+ });
+ });
+ }
+
+ doTest('lt');
+ doTest('lte');
+ doTest('gt');
+ doTest('gte');
+ doTest('eq');
+ doTest('ne');
+
+ it('isRelationalOperator', function () {
+ expect(dataValueHelper.isRelationalOperator('lt')).toEqual(true);
+ expect(dataValueHelper.isRelationalOperator('lte')).toEqual(true);
+ expect(dataValueHelper.isRelationalOperator('gt')).toEqual(true);
+ expect(dataValueHelper.isRelationalOperator('gte')).toEqual(true);
+ expect(dataValueHelper.isRelationalOperator('eq')).toEqual(true);
+ expect(dataValueHelper.isRelationalOperator('ne')).toEqual(true);
+ expect(dataValueHelper.isRelationalOperator('')).toEqual(false);
+
+ expect(dataValueHelper.isRelationalOperator(null)).toEqual(false);
+ expect(dataValueHelper.isRelationalOperator(undefined)).toEqual(false);
+ expect(dataValueHelper.isRelationalOperator(NaN)).toEqual(false);
+ expect(dataValueHelper.isRelationalOperator('neq')).toEqual(false);
+ expect(dataValueHelper.isRelationalOperator('ge')).toEqual(false);
+ expect(dataValueHelper.isRelationalOperator('le')).toEqual(false);
+ });
+
+ });
+
+});
+
diff --git a/test/ut/spec/util/number.test.js b/test/ut/spec/util/number.test.js
index 98f1ba9..9f0d922 100755
--- a/test/ut/spec/util/number.test.js
+++ b/test/ut/spec/util/number.test.js
@@ -468,4 +468,58 @@ describe('util/number', function () {
});
});
+ describe('numeric', function () {
+
+ function testNumeric(rawVal, tarVal, beNumeric) {
+ expect(numberUtil.isNumeric(rawVal)).toEqual(beNumeric);
+ expect(numberUtil.numericToNumber(rawVal)).toEqual(tarVal);
+ }
+
+ testNumeric(123, 123, true);
+ testNumeric('123', 123, true);
+ testNumeric(-123, -123, true);
+ testNumeric('555', 555, true);
+ testNumeric('555.6', 555.6, true);
+ testNumeric('0555.6', 555.6, true);
+ testNumeric('-555.6', -555.6, true);
+ testNumeric(' 555 ', 555, true);
+ testNumeric(' -555 ', -555, true);
+ testNumeric(1e3, 1000, true, true);
+ testNumeric(-1e3, -1000, true, true);
+ testNumeric('1e3', 1000, true, true);
+ testNumeric('-1e3', -1000, true, true);
+ testNumeric(' \r \n 555 \t ', 555, true);
+ testNumeric(' \r \n -555.6 \t ', -555.6, true);
+ testNumeric(Infinity, Infinity, true, true);
+ testNumeric(-Infinity, -Infinity, true, true);
+ testNumeric('Infinity', Infinity, true, true);
+ testNumeric('-Infinity', -Infinity, true, true);
+
+ testNumeric(NaN, NaN);
+ testNumeric(-NaN, NaN);
+ testNumeric('NaN', NaN);
+ testNumeric('-NaN', NaN, false);
+ testNumeric(' NaN ', NaN, false);
+ testNumeric(true, NaN, false);
+ testNumeric(false, NaN, false);
+ testNumeric(undefined, NaN, false);
+ testNumeric(null, NaN, false);
+ testNumeric(new Date(2012, 5, 12), NaN, false);
+ testNumeric([], NaN, false);
+ testNumeric({}, NaN, false);
+ testNumeric(/1/, NaN, false);
+ testNumeric(/0/, NaN, false);
+ testNumeric('555a', NaN, false);
+ testNumeric('- 555', NaN, false);
+ testNumeric('0. 5', NaN, false);
+ testNumeric('0 .5', NaN, false);
+ testNumeric('0x11', NaN, false);
+ testNumeric('', NaN, false);
+ testNumeric('\n', NaN, false);
+ testNumeric('\n\r', NaN, false);
+ testNumeric('\t', NaN, false);
+ testNumeric(String.fromCharCode(12288), NaN, false);
+ testNumeric(function () {}, NaN, false);
+ });
+
});
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org