diff --git a/main/SS/Formula/Functions/DGet.cs b/main/SS/Formula/Functions/DGet.cs index 1f8ddea9a..dea3e25d2 100644 --- a/main/SS/Formula/Functions/DGet.cs +++ b/main/SS/Formula/Functions/DGet.cs @@ -33,14 +33,25 @@ namespace NPOI.SS.Formula.Functions public bool ProcessMatch(ValueEval eval) { - if (result == null) // First match, just Set the value. + if(result == null) // First match, just Set the value. { result = eval; } else // There was a previous match, since there is only exactly one allowed, bail out1. { - result = ErrorEval.NUM_ERROR; - return false; + if(result is BlankEval) + { + result = eval; + } + else + { + // We have a previous filled result. + if(!(eval is BlankEval)) + { + result = ErrorEval.NUM_ERROR; + return false; + } + } } return true; @@ -50,17 +61,18 @@ namespace NPOI.SS.Formula.Functions { get { - if (result == null) + if(result == null) { return ErrorEval.VALUE_INVALID; } - if (result is BlankEval) { + if(result is BlankEval) + { return ErrorEval.VALUE_INVALID; } else try { - if (OperandResolver.CoerceValueToString(OperandResolver.GetSingleValue(result, 0, 0)).Equals("")) + if(OperandResolver.CoerceValueToString(OperandResolver.GetSingleValue(result, 0, 0)).Equals("")) { return ErrorEval.VALUE_INVALID; } @@ -69,7 +81,7 @@ namespace NPOI.SS.Formula.Functions return result; } } - catch (EvaluationException e) + catch(EvaluationException e) { return e.GetErrorEval(); } diff --git a/main/SS/Formula/Functions/DStarRunner.cs b/main/SS/Formula/Functions/DStarRunner.cs index 8d39ce165..274cb31c6 100644 --- a/main/SS/Formula/Functions/DStarRunner.cs +++ b/main/SS/Formula/Functions/DStarRunner.cs @@ -18,7 +18,7 @@ namespace NPOI.SS.Formula.Functions { using System; - + using System.Text.RegularExpressions; using NPOI.SS.Formula; using NPOI.SS.Formula.Eval; using NPOI.SS.Util; @@ -69,6 +69,20 @@ namespace NPOI.SS.Formula.Functions AreaEval db = (AreaEval)database; AreaEval cdb = (AreaEval)conditionDatabase; + // Create an algorithm runner. + IDStarAlgorithm algorithm = null; + switch(algoType) + { + case DStarAlgorithmEnum.DGET: + algorithm = new DGet(); + break; + case DStarAlgorithmEnum.DMIN: + algorithm = new DMin(); + break; + default: + throw new InvalidOperationException("Unexpected algorithm type " + algoType + " encountered."); + } + try { filterColumn = OperandResolver.GetSingleValue(filterColumn, srcRowIndex, srcColumnIndex); @@ -81,26 +95,24 @@ namespace NPOI.SS.Formula.Functions int fc; try { - fc = GetColumnForName(filterColumn, db); + if(filterColumn is NumericValueEval) + { + //fc is zero based while Excel uses 1 based column numbering + fc = (int) Math.Round(((NumericValueEval) filterColumn).NumberValue) - 1; + } + else + fc = GetColumnForName(filterColumn, db); } catch (EvaluationException) { return ErrorEval.VALUE_INVALID; } - if (fc == -1) + if (fc == -1 && !algorithm.AllowEmptyMatchField) { // column not found return ErrorEval.VALUE_INVALID; } - // Create an algorithm runner. - IDStarAlgorithm algorithm = null; - switch (algoType) - { - case DStarAlgorithmEnum.DGET: algorithm = new DGet(); break; - case DStarAlgorithmEnum.DMIN: algorithm = new DMin(); break; - default: - throw new InvalidOperationException("Unexpected algorithm type " + algoType + " encountered."); - } + // Iterate over all db entries. int height = db.Height; @@ -119,6 +131,10 @@ namespace NPOI.SS.Formula.Functions if (matches) { ValueEval currentValueEval = ResolveReference(db, row, fc); + if(fc < 0 && algorithm.AllowEmptyMatchField && !(currentValueEval is NumericValueEval)) + { + currentValueEval = NumberEval.ZERO; + } // Pass the match to the algorithm and conditionally abort the search. bool shouldContinue = algorithm.ProcessMatch(currentValueEval); if (!shouldContinue) @@ -138,7 +154,8 @@ namespace NPOI.SS.Formula.Functions largerEqualThan, smallerThan, smallerEqualThan, - equal + equal, + notEqual, } /** @@ -179,7 +196,7 @@ namespace NPOI.SS.Formula.Functions continue; } String columnName = OperandResolver.CoerceValueToString(columnNameValueEval); - if (name.Equals(columnName)) + if (name.Equals(columnName, StringComparison.OrdinalIgnoreCase)) { resultColumn = column; break; @@ -278,11 +295,24 @@ namespace NPOI.SS.Formula.Functions if (conditionString.StartsWith("<")) { // It's a ")) + { + number = number.Substring(1); + bool itsANumber = IsNumber(number); + if(itsANumber) + { + return testNumericCondition(value, Operator.notEqual, number); + } + else + { + return testStringCondition(value, Operator.notEqual, number); + } + } else { return testNumericCondition(value, Operator.smallerThan, number); @@ -310,32 +340,14 @@ namespace NPOI.SS.Formula.Functions return value is BlankEval; } // Distinguish between string and number. - bool itsANumber = false; - try - { - int.Parse(stringOrNumber); - itsANumber = true; - } - catch (FormatException) - { // It's not an int. - try - { - Double.Parse(stringOrNumber); - itsANumber = true; - } - catch (FormatException) - { // It's a string. - itsANumber = false; - } - } + bool itsANumber = IsNumber(stringOrNumber); if (itsANumber) { return testNumericCondition(value, Operator.equal, stringOrNumber); } else { // It's a string. - String valueString = value is BlankEval ? "" : OperandResolver.CoerceValueToString(value); - return stringOrNumber.Equals(valueString); + return testStringCondition(value, Operator.equal, stringOrNumber); } } else @@ -347,7 +359,17 @@ namespace NPOI.SS.Formula.Functions else { String valueString = value is BlankEval ? "" : OperandResolver.CoerceValueToString(value); - return valueString.StartsWith(conditionString); + String lowerValue = valueString.ToLower(LocaleUtil.GetUserLocale()); + String lowerCondition = conditionString.ToLower(LocaleUtil.GetUserLocale()); + Regex pattern = Countif.StringMatcher.GetWildCardPattern(lowerCondition); + if(pattern == null) + { + return lowerValue.StartsWith(lowerCondition); + } + else + { + return pattern.IsMatch(lowerValue); + } } } } @@ -423,6 +445,30 @@ namespace NPOI.SS.Formula.Functions return result <= 0; case Operator.equal: return result == 0; + case Operator.notEqual: + return result != 0; + } + return false; // Can not be reached. + } + + /** + * Test whether a value matches a text condition. + * @param valueEval Value to check. + * @param op Comparator to use. + * @param condition Value to check against. + * @return whether the condition holds. + */ + private static bool testStringCondition( + ValueEval valueEval, Operator op, String condition) + { + + String valueString = valueEval is BlankEval ? "" : OperandResolver.CoerceValueToString(valueEval); + switch(op) + { + case Operator.equal: + return valueString.Equals(condition, StringComparison.OrdinalIgnoreCase); + case Operator.notEqual: + return !valueString.Equals(condition, StringComparison.OrdinalIgnoreCase); } return false; // Can not be reached. } @@ -469,6 +515,36 @@ namespace NPOI.SS.Formula.Functions return e.GetErrorEval(); } } + + /** + * Determines whether a given string represents a valid number. + * + * @param value The string to be checked if it represents a number. + * @return {@code true} if the string can be parsed as either an integer or + * a double; {@code false} otherwise. + */ + private static bool IsNumber(String value) + { + bool itsANumber; + try + { + int.Parse(value); + itsANumber = true; + } + catch(FormatException) + { // It's not an int. + try + { + double.Parse(value); + itsANumber = true; + } + catch(FormatException) + { // It's a string. + itsANumber = false; + } + } + return itsANumber; + } } } \ No newline at end of file diff --git a/testcases/main/SS/Formula/Functions/TestDGet.cs b/testcases/main/SS/Formula/Functions/TestDGet.cs new file mode 100644 index 000000000..1a7f9e78c --- /dev/null +++ b/testcases/main/SS/Formula/Functions/TestDGet.cs @@ -0,0 +1,170 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +namespace TestCases.SS.Formula.Functions +{ + using NPOI.HSSF.UserModel; + using NPOI.SS.UserModel; + using NUnit.Framework; + using TestCases.SS.Util; + + + /// + /// Testcase for function DGET() + /// + [TestFixture] + public class TestDGet + { + + //https://support.microsoft.com/en-us/office/dget-function-455568bf-4eef-45f7-90f0-ec250d00892e + [Test] + public void TestMicrosoftExample1() + { + + using(HSSFWorkbook wb = initWorkbook1(false, "=Apple")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 10); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 10); + } + } + + [Test] + public void TestMicrosoftExample1CaseInsensitive() + { + + using(HSSFWorkbook wb = initWorkbook1(false, "=apple")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 10); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 10); + } + } + + [Test] + public void TestMicrosoftExample1StartsWith() + { + + using(HSSFWorkbook wb = initWorkbook1(false, "App")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 10); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 10); + } + } + + [Test] + public void TestMicrosoftExample1StartsWithLowercase() + { + + using(HSSFWorkbook wb = initWorkbook1(false, "app")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 10); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 10); + } + } + + [Test] + public void TestMicrosoftExample1Wildcard() + { + + using(HSSFWorkbook wb = initWorkbook1(false, "A*le")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 10); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 10); + } + } + + [Test] + public void TestMicrosoftExample1WildcardLowercase() + { + using(HSSFWorkbook wb = initWorkbook1(false, "a*le")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 10); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 10); + } + } + + [Test] + public void TestMicrosoftExample1AppleWildcardNoMatch() + { + + using(HSSFWorkbook wb = initWorkbook1(false, "A*x")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:A3)", FormulaError.NUM); + Utils.AssertError(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", FormulaError.VALUE); + } + } + + [Test] + public void TestMicrosoftExample1Variant() + { + + using(HSSFWorkbook wb = initWorkbook1(true, "=Apple")) + { + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = wb.GetSheetAt(0).GetRow(0).CreateCell(100) as HSSFCell; + Utils.AssertDouble(fe, cell, "DGET(A5:E11, \"Yield\", A1:F3)", 6); + Utils.AssertDouble(fe, cell, "DGET(A5:E11, 4, A1:F3)", 6); + } + } + + private HSSFWorkbook initWorkbook1(bool adjustAppleCondition, string appleCondition) + { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.CreateSheet() as HSSFSheet; + Utils.AddRow(sheet, 0, "Tree", "Height", "Age", "Yield", "Profit", "Height"); + if(adjustAppleCondition) + { + Utils.AddRow(sheet, 1, appleCondition, ">=8", null, null, null, "<12"); + } + else + { + Utils.AddRow(sheet, 1, appleCondition, ">10", null, null, null, "<16"); + } + Utils.AddRow(sheet, 2, "Pear", ">12"); + Utils.AddRow(sheet, 3); + Utils.AddRow(sheet, 4, "Tree", "Height", "Age", "Yield", "Profit"); + Utils.AddRow(sheet, 5, "Apple", 18, 20, 14, 105); + Utils.AddRow(sheet, 6, "Pear", 12, 12, 10, 96); + Utils.AddRow(sheet, 7, "Cherry", 13, 14, 9, 105); + Utils.AddRow(sheet, 8, "Apple", 14, null, 10, 75); + Utils.AddRow(sheet, 9, "Pear", 9, 8, 8, 77); + Utils.AddRow(sheet, 10, "Apple", 8, 9, 6, 45); + return wb; + } + } +} +