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