From 777291154acc6a7f3ad9eaa347f2d0e74dc27272 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 29 Aug 2024 22:42:55 +0400 Subject: [PATCH] Fix filtering issues for string in different locales --- packages/devextreme/js/data/array_query.js | 70 ++++++++++++++----- .../tests/DevExpress.data/queryArray.tests.js | 43 ++++++++++++ 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/packages/devextreme/js/data/array_query.js b/packages/devextreme/js/data/array_query.js index 562a1f7238f6..a93c5ec9acc6 100644 --- a/packages/devextreme/js/data/array_query.js +++ b/packages/devextreme/js/data/array_query.js @@ -244,6 +244,13 @@ const compileCriteria = (function() { const _toComparable = (value, caseSensitivity = false) => toComparable(value, caseSensitivity, langParams); + const toLowerCase = (value) => langParams?.locale ? value.toLocaleLowerCase(langParams.locale) : value.toLowerCase(); + const toUpperCase = (value) => langParams?.locale ? value.toLocaleUpperCase(langParams.locale) : value.toUpperCase(); + + const compareCaseInsensitive = (value1, value2) => { + return toLowerCase(value1) === toLowerCase(value2) || toUpperCase(value1) === toUpperCase(value2); + }; + const compileUniformEqualsCriteria = (crit) => { const getter = compileGetter(crit[0][0]); const filterValues = crit.reduce((acc, item, i) => { @@ -310,6 +317,7 @@ const compileCriteria = (function() { const getter = compileGetter(crit[0]); const op = crit[1]; const origValue = crit[2]; + const value = _toComparable(origValue); const compare = (obj, operatorFn) => { @@ -332,27 +340,49 @@ const compileCriteria = (function() { return (obj) => compare(obj, (a, b) => a <= b); case 'startswith': return function(obj) { - const objValue = _toComparable(toString(getter(obj))); - const result = objValue.indexOf(value) === 0; - /* eslint-disable-next-line no-undef */ - const compareResult = new Intl.Collator(langParams.locale, { sensitivity: 'base', usage: 'search' }).compare(_toComparable(value, true), objValue); + let objValue = toString(getter(obj)); + let result; + + if(objValue.length < origValue.length) { + return false; + } + + if(langParams.collatorOptions?.sensitivity !== 'case') { + objValue = _toComparable(objValue, true); + const searchValue = _toComparable(origValue, true); + + result = toUpperCase(objValue).startsWith(toUpperCase(searchValue)) || + toLowerCase(objValue).startsWith(toLowerCase(searchValue)); + } else { + result = _toComparable(objValue).startsWith(value); + } - return result || (compareResult === 0 || compareResult === -1); + return result; }; case 'endswith': return function(obj) { - const getterValue = _toComparable(toString(getter(obj))); - const searchValue = toString(value); + let objValue = toString(getter(obj)); + let searchValue = toString(origValue); - if(getterValue.length < searchValue.length) { + if(objValue.length < searchValue.length) { return false; } - const index = getterValue.lastIndexOf(value); - return index !== -1 && index === getterValue.length - value.length; + let result = _toComparable(objValue).endsWith(_toComparable(searchValue)); + + if(!result && langParams.collatorOptions?.sensitivity !== 'case') { + objValue = _toComparable(objValue, true); + searchValue = _toComparable(searchValue, true); + + result = toUpperCase(objValue).endsWith(toUpperCase(searchValue)); + } + + return result; }; case 'contains': - return function(obj) { return _toComparable(toString(getter(obj))).indexOf(value) > -1; }; + return function(obj) { + return _toComparable(toString(getter(obj))).indexOf(value) > -1; + }; case 'notcontains': return function(obj) { return _toComparable(toString(getter(obj))).indexOf(value) === -1; }; } @@ -362,24 +392,26 @@ const compileCriteria = (function() { function compileEquals(getter, value, negate) { return function(obj) { - let result; - obj = getter(obj); + // eslint-disable-next-line eqeqeq + let result; - if(typeof obj === 'string' && typeof value === 'string' && langParams?.locale) { + if(typeof obj === 'string' && typeof value === 'string' && langParams.collatorOptions?.sensitivity !== 'case' && !useStrictComparison(value)) { /* eslint-disable-next-line no-undef */ - const compareResult = new Intl.Collator(langParams.locale, { sensitivity: 'base', usage: 'search' }).compare(_toComparable(value, true), _toComparable(obj, true)); - - result = compareResult === 0; + result = compareCaseInsensitive(_toComparable(value, true), _toComparable(obj, true)); } else { - obj = _toComparable(getter(obj)); + obj = _toComparable(obj); value = _toComparable(value); // eslint-disable-next-line eqeqeq result = useStrictComparison(value) ? obj === value : obj == value; } - return negate ? !result : result; + if(negate) { + result = !result; + } + + return result; }; } diff --git a/packages/devextreme/testing/tests/DevExpress.data/queryArray.tests.js b/packages/devextreme/testing/tests/DevExpress.data/queryArray.tests.js index 774a20e9ee70..08729bedcfae 100644 --- a/packages/devextreme/testing/tests/DevExpress.data/queryArray.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.data/queryArray.tests.js @@ -388,6 +388,49 @@ QUnit.test('filter with collatorOptions.sensitivity set to "base"', function(ass assert.false(containsUnwantedValue); }); +QUnit.test('filtering use real case insensitivity equal', function(assert) { + const input = [{ ID: 1, Name: 'AΙΤΗΣ' }, { ID: 2, Name: 'aιτης' }, { ID: 3, Name: 'abcde' }]; + + const array = QUERY(input, { + langParams: { + locale: 'el-GR' + } + }).filter(['Name', '=', 'AΙΤΗΣ']).toArray(); + + assert.equal(array.length, 2); + + const containsUnwantedValue = array.some(item => item.ID === 3); + assert.false(containsUnwantedValue); +}); + +QUnit.test('filtering use real case insensitivity search', function(assert) { + const input = [ + { ID: 1, Name: 'AΙΤΗΣ' }, + { ID: 2, Name: 'aιτης' }, + { ID: 3, Name: 'aιτησa' }, + { ID: 4, Name: 'AΙΤΗΣΗ' }, + { ID: 5, Name: 'ΑBΤΗΣΗ' }, + ]; + + const array = QUERY(input, { + langParams: { + locale: 'el-GR' + } + }).filter(['Name', 'startswith', 'AΙΤΗΣ']).toArray(); + + const array2 = QUERY(input, { + langParams: { + locale: 'el-GR' + } + }).filter(['Name', 'endswith', 'ΙΤΗΣ']).toArray(); + + assert.equal(array.length, 4); + assert.equal(array2.length, 2); + + const containsUnwantedValue = array.some(item => item.ID === 5) || array2.some(item => [5, 4, 3].includes(item.ID)); + assert.false(containsUnwantedValue); +}); + QUnit.test('missing operation means equal', function(assert) { assert.expect(1);