diff --git a/cell/model/FormulaObjects/lookupandreferenceFunctions.js b/cell/model/FormulaObjects/lookupandreferenceFunctions.js index c7922836d8..61962a0acb 100644 --- a/cell/model/FormulaObjects/lookupandreferenceFunctions.js +++ b/cell/model/FormulaObjects/lookupandreferenceFunctions.js @@ -3695,7 +3695,6 @@ function (window, undefined) { valueForSearching = new cString(valueForSearching.getValue().toLowerCase()); } - //TODO opt_arg5 - пока не обрабатываю результат == 2( A wildcard match where *, ?, and ~ have) if (xlookup) { if (Math.abs(opt_arg5) === 1) { if (opt_array) { diff --git a/cell/model/FormulaObjects/statisticalFunctions.js b/cell/model/FormulaObjects/statisticalFunctions.js index f2358f63f6..b717809212 100644 --- a/cell/model/FormulaObjects/statisticalFunctions.js +++ b/cell/model/FormulaObjects/statisticalFunctions.js @@ -12036,8 +12036,49 @@ function (window, undefined) { this.cacheId = {}; this.cacheRanges = {}; } - CountIfCache.prototype.constructor = CountIfCache; + CountIfCache.prototype._getUniversalArrayFromRange = function(range) { + const res = {}; + const bbox = range.getBBox0(); + let emptyCount = (Math.abs(bbox.c1 - bbox.c2) + 1) * (Math.abs(bbox.r1 - bbox.r2) + 1); + range.foreach2(function(cell) { + const type = cell.type; + if (type !== cElementType.empty) { + if (!res[type]) { + res[type] = []; + } + let value = cell; + if (type === cElementType.error) { + value = new AscCommonExcel.cNumber(cell.errorType); + } + res[type].push(value); + } + }); + for (let i in res) { + emptyCount -= res[i].length; + } + res[cElementType.empty] = emptyCount + return res; + }; + CountIfCache.prototype._getUniversalArrayFromArray = function(array) { + const res = {}; + array.foreach(function(cell) { + const type = cell.type; + if (!res[type]) { + if (type === cElementType.empty) { + res[type] = 0; + } else { + res[type] = []; + } + } + let value = cell; + if (type === cElementType.error) { + value = new AscCommonExcel.cNumber(cell.errorType); + } + type === cElementType.empty ? res[type] += 1 : res[type].push(value); + }); + return res; + }; CountIfCache.prototype.calculate = function (arg, _arg1) { let arg0 = arg[0], arg1 = arg[1]; @@ -12058,13 +12099,19 @@ function (window, undefined) { } if (cElementType.array === arg0.type) { - let arr = []; - arg0.foreach(function (_val) { - arr.push(_val); - }); - return this._calculate(arr, arg1); + const uArray = this._getUniversalArrayFromArray(arg0) + return this._calculate(uArray, arg1); } else if (cElementType.cell === arg0.type || cElementType.cell3D === arg0.type) { - return this._calculate([arg0.getValue()], arg1); + const arr = {}; + const value = arg0.getValue(); + if (value.type === cElementType.empty) { + arr[value.type] = 1; + } else if (value.type === cElementType.error) { + arr[value.type] = [new cNumber(value.errorType)]; + } else { + arr[value.type] = [value]; + } + return this._calculate(arr, arg1); } else if (cElementType.cellsRange === arg0.type || cElementType.cellsRange3D === arg0.type) { return this._get(arg0, arg1); } else { @@ -12077,16 +12124,7 @@ function (window, undefined) { valueForSearching = arg1.getValue(); if (!cacheElem) { - cacheElem = {elements: [], results: {}}; - - if (cElementType.cellsRange3D === range.type) { - cacheElem.elements = range.getValue(); - } else { - range.foreach2(function (cell) { - cacheElem.elements.push(cell); - }); - } - + cacheElem = {elements: this._getUniversalArrayFromRange(range), results: {}}; this.cacheId[sRangeName] = cacheElem; let cacheRange = this.cacheRanges[wsId]; if (!cacheRange) { @@ -12104,35 +12142,35 @@ function (window, undefined) { return res; }; CountIfCache.prototype._calculate = function (arr, arg1) { - - let checkEmptyValue = function (res, tempVal, tempMatchingInfo) { - //TODO нужно протестировать на различных вариантах - //когда в ячейке пустое значение - сравниваем его только с пустым значением - //при matchingInfo отличным от пустого значения в данном случае возвращаем false - - //ms excel при несовпадении типов возвращает всегда отрицательное значение - //в нашем случае сравниваемая величина(в tempMatchingInfo) не всегда приводится к нужному типу(например, error, empty) - //TODO рассмотреть добавление подобной правки, проверить все варианты + расскоментировать тесты - /*if ((tempVal.type === cElementType.string || tempVal.type === cElementType.number) && tempMatchingInfo.val && tempMatchingInfo.val.type !== tempVal.type) { - return false; - }*/ - - tempVal = undefined !== tempVal.value ? tempVal.value : tempVal; - let matchingValue = tempMatchingInfo.val && tempMatchingInfo.val.value.toString ? tempMatchingInfo.val.value.toString() : null; - if (tempVal === "" && matchingValue && "" !== matchingValue.replace(/\*|\?/g, '')) { - return false; - } - return res; - }; - let _count = 0; - let val; let matchingInfo = AscCommonExcel.matchingValue(arg1); - - for (let i = 0; i < arr.length; i++) { - _count += checkEmptyValue(matching(arr[i], matchingInfo, true, true), arr[i], matchingInfo); + let type = matchingInfo.val.type; + if (matchingInfo.val.type === cElementType.string) { + const checkErr = new cError(matchingInfo.val.value.toUpperCase()); + if (checkErr.errorType !== -1) { + type = cElementType.error; + matchingInfo.val = new cNumber(checkErr.errorType); + } + } + if (matchingInfo.op === "<>") { + for (let i in arr) { + if (i !== String(type)) { + _count += arr[i].length ? arr[i].length : arr[i]; + } + } + } + const typedArr = arr[type]; + if (matchingInfo.val.value === "" && matchingInfo.op !== "<>") { + if (arr[cElementType.empty]) { + return new cNumber(arr[cElementType.empty]); + } + return new cNumber(0); + } + if (typedArr) { + for (let i = 0; i < typedArr.length; i += 1) { + _count += matching(typedArr[i], matchingInfo, true, true); + } } - return new cNumber(_count); }; CountIfCache.prototype.remove = function (cell) { diff --git a/tests/cell/spreadsheet-calculation/FormulaTests.js b/tests/cell/spreadsheet-calculation/FormulaTests.js index 18b3acd044..444d8d6bcf 100644 --- a/tests/cell/spreadsheet-calculation/FormulaTests.js +++ b/tests/cell/spreadsheet-calculation/FormulaTests.js @@ -19040,32 +19040,37 @@ $(function () { ws.getRange2("C8").setValue("grapes"); ws.getRange2("D8").setValue("melons"); - + // Positive Cases: + // Case #1: Area, String. Find equal number in Area oParser = new parserFormula("COUNTIF(A7:D7,\"=10\")", "A1", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #2: Area, String. Find numbers greater than value in Area oParser = new parserFormula("COUNTIF(A7:D7,\">5\")", "B1", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 3); + // Case #3: Area, String. Find numbers not equal to value in Area oParser = new parserFormula("COUNTIF(A7:D7,\"<>10\")", "C1", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #4: Area, String. Find text ending with pattern using wildcard oParser = new parserFormula("COUNTIF(A8:D8,\"*es\")", "A2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 3); + // Case #5: Area, String. Find text matching pattern with question marks and wildcard oParser = new parserFormula("COUNTIF(A8:D8,\"??a*\")", "B2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #6: Area, String. Find text containing letter using wildcards oParser = new parserFormula("COUNTIF(A8:D8,\"*l*\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); - wb.dependencyFormulas.unlockRecal(); ws.getRange2("CC1").setValue("1"); @@ -19081,67 +19086,76 @@ $(function () { assert.ok( oParser.parse() ); assert.strictEqual( oParser.calculate().getValue(), 1 );*/ + // Case #7: Area, Formula. Count TRUE values using TRUE() function oParser = new parserFormula("COUNTIF(CC1:CC7, TRUE())", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 3); + // Case #8: Area, Boolean. Count TRUE values using boolean literal oParser = new parserFormula("COUNTIF(CC1:CC7, TRUE)", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 3); + // Case #9: Area, Number. Count cells equal to 1 oParser = new parserFormula("COUNTIF(CC1:CC7, 1)", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #10: Area, Number. Count cells equal to 0 oParser = new parserFormula("COUNTIF(CC1:CC7, 0)", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 1); + // Case #11: Cell, String. Count text criteria in single cell (no match) ws.getRange2("CC8").setValue(">3"); oParser = new parserFormula("COUNTIF(CC8,\">3\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 0); + // Case #12: Cell, String. Count text criteria in single cell with equals prefix ws.getRange2("CC8").setValue(">3"); oParser = new parserFormula("COUNTIF(CC8,\"=>3\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 1); + // Case #13: Area, String. Count error values using string representation ws.getRange2("CC9").setValue("=NA()"); ws.getRange2("CC10").setValue("#N/A"); - oParser = new parserFormula("COUNTIF(CC9:CC10,\"#N/A\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #14: Area, Formula. Count error values using NA() function oParser = new parserFormula("COUNTIF(CC9:CC10, NA())", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #15: Area, String. Count formula text (no match for function call) oParser = new parserFormula("COUNTIF(CC9:CC10,\"=NA()\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 0); - oParser = new parserFormula("COUNTIF(#REF!, 1)", "C2", ws); - assert.ok(oParser.parse()); - assert.strictEqual(oParser.calculate().getValue(), "#REF!"); - + // Case #16: Area, String. Count numbers greater than or equal to 1 oParser = new parserFormula("COUNTIF(CC1:CC8,\">=1\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #17: Area, String. Count numbers equal to 1 oParser = new parserFormula("COUNTIF(CC1:CC8,\"=1\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #18: Area, String. Count numbers less than 1 oParser = new parserFormula("COUNTIF(CC1:CC8,\"<1\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 1); + // Case #19: Area, String. Count numbers greater than 1 oParser = new parserFormula("COUNTIF(CC1:CC8,\">1\")", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 0); + // Case #20: Area, String. Count using dynamic criteria with cell reference oParser = new parserFormula("COUNTIF(CC1:CC8,\"=\"&CC8)", "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 1); @@ -19155,54 +19169,63 @@ $(function () { ws.getRange2("A26").setValue(""); ws.getRange2("A27").setValue("apples"); + // Case #21: Area, String. Count text ending with pattern using wildcard oParser = new parserFormula('COUNTIF(A22:A27,"*es")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 4); + // Case #22: Area, String. Count text with exact length ending with pattern oParser = new parserFormula('COUNTIF(A22:A27,"?????es")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #23: Area, String. Count all non-empty text cells using wildcard oParser = new parserFormula('COUNTIF(A22:A27,"*")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 4); + // Case #24: Area, String. Count cells not equal to literal asterisks oParser = new parserFormula('COUNTIF(A22:A27,"<>"&"***")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #25: Area, String. Count cells not equal to single asterisk oParser = new parserFormula('COUNTIF(A22:A27,"<>"&"*")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #26: Area, String. Count cells not equal to single question mark oParser = new parserFormula('COUNTIF(A22:A27,"<>"&"?")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 6); + // Case #27: Area, String. Count exact date string match ws.getRange2("A1").setValue("12/1"); ws.getRange2("A2").setValue("12/1"); - oParser = new parserFormula('COUNTIF(A1:A2,"12/1")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #28: Area, String. Count date string with no match oParser = new parserFormula('COUNTIF(A1:A2,"12/1/1")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 0); + // Case #29: Array, String. Count values greater than 1 in horizontal array oParser = new parserFormula('COUNTIF({1,2,3},">1")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); // ms - doesn't work, gs - 2, lo - 2 + // Case #30: Array, String. Count values greater than 1 in vertical array oParser = new parserFormula('COUNTIF({1;2;3},">1")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); // ms - doesn't work, gs - 2, lo - 2 + // Case #31: Array, String. Count values greater than 1 in 2D array oParser = new parserFormula('COUNTIF({1,2,3;4,5,6},">1")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 5); // ms - doesn't work, gs - 5, lo - 5 - // bug 62491 ws.getRange2("A100").setValue("Math"); ws.getRange2("A101").setValue("87"); ws.getRange2("A102").setValue("99"); @@ -19216,14 +19239,161 @@ $(function () { ws.getRange2("B104").setValue("23"); ws.getRange2("B105").setValue("55"); + // Case #32: Formula, String. Count values greater than 80 in XLOOKUP result (Math). For bug 62491 oParser = new parserFormula('COUNTIF(XLOOKUP(A100,A100:B100,A101:B105),">80")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 2); + // Case #33: Formula, String. Count values greater than 80 in XLOOKUP result (Physics) oParser = new parserFormula('COUNTIF(XLOOKUP(B100,A100:B100,A101:B105),">80")', "C2", ws); assert.ok(oParser.parse()); assert.strictEqual(oParser.calculate().getValue(), 1); + // Case #34: Array, String. Count exact matches in mixed array + oParser = new parserFormula('COUNTIF({10,20,10,30,10},"10")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + + // Case #35: Array, String. Count text matches in string array + oParser = new parserFormula('COUNTIF({"apple","banana","apple","cherry"},"apple")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 2); + + // Case #36: Array, String. Count values less than threshold in numeric array + oParser = new parserFormula('COUNTIF({5,15,25,"0",45},"<20")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 2); + + // Case #37: Array, Boolean. Count TRUE values in boolean array + oParser = new parserFormula('COUNTIF({TRUE,1,TRUE,TRUE,FALSE},TRUE)', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + + // Case #38: Array, String. Count wildcard matches in text array + oParser = new parserFormula('COUNTIF({"test","testing","best","rest"},"*est")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + + // Case #39: Array, String. Count not equal values in mixed array + oParser = new parserFormula('COUNTIF({"as",2,3,2,"4"},"<>2")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + + ws.getRange2("A200").setValue(""); + ws.getRange2("A201").setValue("123"); + ws.getRange2("A202").setValue(""); + ws.getRange2("A203").setValue(""); + ws.getRange2("A204").setValue("#N/A"); + ws.getRange2("A205").setValue(""); + ws.getRange2("B200").setValue(""); + ws.getRange2("B201").setValue(" "); + ws.getRange2("B202").setValue(""); + ws.getRange2("B203").setValue(""); + ws.getRange2("B204").setValue(""); + ws.getRange2("B205").setValue("asd"); + ws.getRange2("B206").setValue('123'); + ws.getRange2("B207").setValue('ASD'); + + // Case #40: Area, String. Empty cells check + oParser = new parserFormula('COUNTIF(A200:B205,"<>"&"*")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 10); + + // Case #41: Area, String. Find empty cells check + oParser = new parserFormula('COUNTIF(A200:B205,"")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 8); + + // Case #42: Area, Cell. second arg as cell + oParser = new parserFormula('COUNTIF(A200:B205,B206)', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 1); + + // Case #43: Area, Cell. second arg as cell, case-sens test + oParser = new parserFormula('COUNTIF(A200:B205,B207)', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 1); + + // Case #44: Area, String. second arg as cell, case-sens test + oParser = new parserFormula('COUNTIF(A200:B205,"#n/A")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 1); + + ws.getRange2("A300").setValue("a*c"); + ws.getRange2("A301").setValue("aac"); + ws.getRange2("A302").setValue("a123c"); + ws.getRange2("A303").setValue("a**c"); + ws.getRange2("A304").setValue(""); + ws.getRange2("A305").setValue(""); + + // Case #45: Area, String. wildcard test with ~ + oParser = new parserFormula('COUNTIF(A300:A305,"a~*c")', "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 1); + + // Negative Cases: + // Case #1: Error, Number. Handle reference error in range + oParser = new parserFormula("COUNTIF(#REF!, 1)", "C2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), "#REF!"); + + // Bounded Cases: + // Case #1: Area, String. Count errors greater than #N/A + ws.getRange2("AB1").setValue("#N/A"); + ws.getRange2("AB2").setValue("#DIV/0!"); + ws.getRange2("AB3").setValue("#VALUE!"); + ws.getRange2("AB4").setValue("5"); + oParser = new parserFormula('COUNTIF(AB1:AB4,">#N/A")', "AC1", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 0); + + // Case #2: Area, String. Count errors less than #DIV/0! + oParser = new parserFormula('COUNTIF(AB1:AB4,"<#DIV/0!")', "AC2", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 0); + + // Case #3: Area, String. Count errors not equal to #N/A + oParser = new parserFormula('COUNTIF(AB1:AB4,"<>#N/A")', "AC3", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + + // Case #4: Area, String. Count errors greater than or equal to #VALUE! + oParser = new parserFormula('COUNTIF(AB1:AB4,">=#VALUE!")', "AC4", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 2); + + // Case #5: Array, String. Count errors in mixed error array + oParser = new parserFormula('COUNTIF({#N/A,#DIV/0!,#VALUE!,#REF!},"<#N/A")', "AC5", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + + // Case #6: Area, String. Count values greater than error boundary + ws.getRange2("AD1").setValue("#N/A"); + ws.getRange2("AD2").setValue("10"); + ws.getRange2("AD3").setValue("20"); + ws.getRange2("AD4").setValue("text"); + oParser = new parserFormula('COUNTIF(AD1:AD4,"<#N/A")', "AC6", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 0); + + // Case #7: Area, String. Count specific error type in mixed range + ws.getRange2("AE1").setValue("#N/A"); + ws.getRange2("AE2").setValue("#DIV/0!"); + ws.getRange2("AE3").setValue("#N/A"); + ws.getRange2("AE4").setValue("42"); + oParser = new parserFormula('COUNTIF(AE1:AE4,"=#N/A")', "AC7", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 2); + + // Case #8: Area, String. Count specific error type in mixed range + ws.getRange2("AF1").setValue("#VALUE!"); + ws.getRange2("AF2").setValue("#DIV/0!"); + ws.getRange2("AF3").setValue("#VALUE!"); + ws.getRange2("AF4").setValue("42"); + oParser = new parserFormula('COUNTIF(AF1:AF4,"<#N/A")', "AC7", ws); + assert.ok(oParser.parse()); + assert.strictEqual(oParser.calculate().getValue(), 3); + // testArrayFormula2(assert, "COUNTIF", 2, 2) }); @@ -19342,7 +19512,7 @@ $(function () { QUnit.test("Test: \"CONCAT\"", function (assert) { - ws.getRange2("AA:AA").cleanAll(); + ws.getRange2("AA:BB").cleanAll(); ws.getRange2("AA1").setValue("a1"); ws.getRange2("AA2").setValue("a2"); ws.getRange2("AA4").setValue("a4");