From bc1e07591fc8bcc304ba9a95c8c549bf423a6457 Mon Sep 17 00:00:00 2001 From: suppernovae Date: Wed, 6 Mar 2024 19:18:19 +0100 Subject: [PATCH] [Bug 67475] Better support for edge cases in TEXT function --- main/SS/Formula/Eval/OperandResolver.cs | 133 ++++------- main/SS/Formula/Eval/StringEval.cs | 14 +- main/SS/Formula/Functions/Text/Exact.cs | 4 +- main/SS/Formula/Functions/Text/Text.cs | 187 +++++++-------- .../SS/Formula/Functions/Text/TextFunction.cs | 12 +- main/SS/UserModel/DataFormatter.cs | 25 +- .../main/SS/Formula/Functions/TestText.cs | 226 ++++++++++++++++-- 7 files changed, 358 insertions(+), 243 deletions(-) diff --git a/main/SS/Formula/Eval/OperandResolver.cs b/main/SS/Formula/Eval/OperandResolver.cs index 992f2dbe1..58fab3914 100644 --- a/main/SS/Formula/Eval/OperandResolver.cs +++ b/main/SS/Formula/Eval/OperandResolver.cs @@ -30,9 +30,9 @@ public class OperandResolver { // Based on regular expression defined in JavaDoc at {@link java.lang.Double#valueOf} // modified to remove support for NaN, Infinity, Hexadecimal support and floating type suffixes - private const String Digits = "\\d+"; - private const String Exp = "[eE][+-]?" + Digits; - private const String fpRegex = + private const string Digits = "\\d+"; + private const string Exp = "[eE][+-]?" + Digits; + private const string fpRegex = ("[\\x00-\\x20]*" + "[+-]?(" + "(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|" + @@ -58,21 +58,21 @@ private OperandResolver() public static ValueEval GetSingleValue(ValueEval arg, int srcCellRow, int srcCellCol) { ValueEval result; - if (arg is RefEval) + if (arg is RefEval rev) { - result = ChooseSingleElementFromRef((RefEval)arg); + result = ChooseSingleElementFromRef(rev); } - else if (arg is AreaEval) + else if (arg is AreaEval aev) { - result = ChooseSingleElementFromArea((AreaEval)arg, srcCellRow, srcCellCol); + result = ChooseSingleElementFromArea(aev, srcCellRow, srcCellCol); } else { result = arg; } - if (result is ErrorEval) + if (result is ErrorEval eva) { - throw new EvaluationException((ErrorEval)result); + throw new EvaluationException(eva); } return result; } @@ -126,9 +126,9 @@ public static ValueEval ChooseSingleElementFromArea(AreaEval ae, { ValueEval result = ChooseSingleElementFromAreaInternal(ae, srcCellRow, srcCellCol); - if (result is ErrorEval) + if (result is ErrorEval eva) { - throw new EvaluationException((ErrorEval)result); + throw new EvaluationException(eva); } return result; @@ -236,14 +236,14 @@ public static double CoerceValueToDouble(ValueEval ev) { return 0.0; } - if (ev is NumericValueEval) + if (ev is NumericValueEval nve) { // this also handles bools - return ((NumericValueEval)ev).NumberValue; + return nve.NumberValue; } - if (ev is StringEval) + if (ev is StringEval sev) { - double dd = ParseDouble(((StringEval)ev).StringValue); + double dd = ParseDouble(sev.StringValue); if (double.IsNaN(dd)) { throw EvaluationException.InvalidValue(); @@ -269,64 +269,29 @@ public static double CoerceValueToDouble(ValueEval ev) * @param text * @return null if the specified text cannot be Parsed as a number */ - public static double ParseDouble(String pText) + public static double ParseDouble(string pText) { - //if (Regex.Match(fpRegex, pText).Success) - try - { - double ret = double.Parse(pText, CultureInfo.CurrentCulture); - if (double.IsInfinity(ret)) - return double.NaN; - return ret; - } - catch - { - return Double.NaN; - } - //else + try { - //return Double.NaN; + double ret = double.Parse(pText, CultureInfo.CurrentCulture); + if (double.IsInfinity(ret)) + return double.NaN; + return ret; + } + catch + { + return double.NaN; } - //String text = pText.Trim(); - //if (text.Length < 1) - //{ - // return double.NaN; - //} - //bool isPositive = true; - //if (text[0] == '-') - //{ - // isPositive = false; - // text = text.Substring(1).Trim(); - //} - - //if (text.Length == 0 || !Char.IsDigit(text[0])) - //{ - // // avoid using Exception to tell when string is not a number - // return double.NaN; - //} - //// TODO - support notation like '1E3' (==1000) - - //double val; - //try - //{ - // val = double.Parse(text); - //} - //catch - //{ - // return double.NaN; - //} - //return isPositive ? +val : -val; } /** * @param ve must be a NumberEval, StringEval, BoolEval, or BlankEval * @return the Converted string value. never null */ - public static String CoerceValueToString(ValueEval ve) + public static string CoerceValueToString(ValueEval ve) { - if (ve is StringValueEval) + if (ve is StringValueEval sve) { - StringValueEval sve = (StringValueEval)ve; return sve.StringValue; } @@ -336,11 +301,12 @@ public static String CoerceValueToString(ValueEval ve) } throw new ArgumentException("Unexpected eval class (" + ve.GetType().Name + ")"); } + /** - * @return null to represent blank values - * @throws EvaluationException if ve is an ErrorEval, or if a string value cannot be converted - */ - public static Boolean? CoerceValueToBoolean(ValueEval ve, bool stringsAreBlanks) + * @return null to represent blank values + * @throws EvaluationException if ve is an ErrorEval, or if a string value cannot be converted + */ + public static bool? CoerceValueToBoolean(ValueEval ve, bool stringsAreBlanks) { if (ve == null || ve == BlankEval.instance) @@ -348,18 +314,18 @@ public static String CoerceValueToString(ValueEval ve) // TODO - remove 've == null' condition once AreaEval is fixed return null; } - if (ve is BoolEval) + if (ve is BoolEval be) { - return ((BoolEval)ve).BooleanValue; + return be.BooleanValue; } - if (ve is StringEval) + if (ve is StringEval se) { if (stringsAreBlanks) { return null; } - String str = ((StringEval)ve).StringValue; + string str = se.StringValue; if (str.Equals("true", StringComparison.OrdinalIgnoreCase)) { return true; @@ -372,32 +338,31 @@ public static String CoerceValueToString(ValueEval ve) throw new EvaluationException(ErrorEval.VALUE_INVALID); } - if (ve is NumericValueEval) + if(ve is NumericValueEval ne) { - NumericValueEval ne = (NumericValueEval)ve; double d = ne.NumberValue; - if (Double.IsNaN(d)) + if(double.IsNaN(d)) { throw new EvaluationException(ErrorEval.VALUE_INVALID); } return d != 0; } - if (ve is ErrorEval) + if (ve is ErrorEval ee) { - throw new EvaluationException((ErrorEval)ve); + throw new EvaluationException(ee); } throw new InvalidOperationException("Unexpected eval (" + ve.GetType().Name + ")"); } - /** - * Retrieves a single value from an area evaluation utilizing the 2D indices of the cell - * within its own area reference to index the value in the area evaluation. - * - * @param ae area reference after evaluation - * @param cell the source cell of the formula that contains its 2D indices - * @return a NumberEval, StringEval, BoolEval or BlankEval. or ErrorEval - * Never null. - */ + /** + * Retrieves a single value from an area evaluation utilizing the 2D indices of the cell + * within its own area reference to index the value in the area evaluation. + * + * @param ae area reference after evaluation + * @param cell the source cell of the formula that contains its 2D indices + * @return a NumberEval, StringEval, BoolEval or BlankEval. or ErrorEval + * Never null. + */ public static ValueEval GetElementFromArray(AreaEval ae, IEvaluationCell cell) { CellRangeAddress range = cell.ArrayFormulaRange; diff --git a/main/SS/Formula/Eval/StringEval.cs b/main/SS/Formula/Eval/StringEval.cs index ef776c3ee..85688b585 100644 --- a/main/SS/Formula/Eval/StringEval.cs +++ b/main/SS/Formula/Eval/StringEval.cs @@ -30,27 +30,23 @@ public class StringEval : StringValueEval { public static readonly StringEval EMPTY_INSTANCE = new StringEval(""); - private String value; + private readonly string value; public StringEval(Ptg ptg):this(((StringPtg)ptg).Value) { } - public StringEval(String value) + public StringEval(string value) { - if (value == null) - { - throw new ArgumentException("value must not be null"); - } - this.value = value; + this.value = value ?? throw new ArgumentException("value must not be null"); } - public String StringValue + public string StringValue { get { return value; } } - public override String ToString() + public override string ToString() { StringBuilder sb = new StringBuilder(64); sb.Append(GetType().Name).Append(" ["); diff --git a/main/SS/Formula/Functions/Text/Exact.cs b/main/SS/Formula/Functions/Text/Exact.cs index d91100f6f..40d132ea0 100644 --- a/main/SS/Formula/Functions/Text/Exact.cs +++ b/main/SS/Formula/Functions/Text/Exact.cs @@ -38,8 +38,8 @@ public override ValueEval EvaluateFunc(ValueEval[] args, int srcCellRow, int src return ErrorEval.VALUE_INVALID; } - String s0 = EvaluateStringArg(args[0], srcCellRow, srcCellCol); - String s1 = EvaluateStringArg(args[1], srcCellRow, srcCellCol); + string s0 = EvaluateStringArg(args[0], srcCellRow, srcCellCol); + string s1 = EvaluateStringArg(args[1], srcCellRow, srcCellCol); return BoolEval.ValueOf(s0.Equals(s1)); } } diff --git a/main/SS/Formula/Functions/Text/Text.cs b/main/SS/Formula/Functions/Text/Text.cs index d172c3543..c57efa56f 100644 --- a/main/SS/Formula/Functions/Text/Text.cs +++ b/main/SS/Formula/Functions/Text/Text.cs @@ -14,122 +14,99 @@ namespace NPOI.SS.Formula.Functions */ public class Text : Fixed2ArgFunction { - public static DataFormatter Formatter = new DataFormatter(); + public static DataFormatter Formatter { get; set; } = new(); + + /// + /// An implementation of the TEXT function
+ /// TEXT returns a number value formatted with the given number formatting string.
+ /// This function is not a complete implementation of the Excel function, but
+ /// handles most of the common cases. All work is passed down to
+ /// to be done, as this works much the same as the
+ /// display focused work that does. + ///
+ /// + /// + /// + /// + /// public override ValueEval Evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { - if (arg0 is StringEval) return arg0; - - double s0; - String s1; + ValueEval valueEval; + try { - s0 = TextFunction.EvaluateDoubleArg(arg0, srcRowIndex, srcColumnIndex); - s1 = TextFunction.EvaluateStringArg(arg1, srcRowIndex, srcColumnIndex); + ValueEval valueVe = OperandResolver.GetSingleValue(arg0, srcRowIndex, srcColumnIndex); + ValueEval formatVe = OperandResolver.GetSingleValue(arg1, srcRowIndex, srcColumnIndex); + + try + { + double valueDouble = double.NaN; + string evaluated = null; + + if (valueVe == BlankEval.instance) + { + valueDouble = 0.0; + } + else if (valueVe is BoolEval boolEval) + { + evaluated = boolEval.StringValue; + } + else if (valueVe is NumericValueEval numericEval) + { + valueDouble = numericEval.NumberValue; + + } + else if (valueVe is StringEval stringEval) + { + evaluated = stringEval.StringValue; + valueDouble = OperandResolver.ParseDouble(evaluated); + } + + if(!double.IsNaN(valueDouble)) + { + string format = FormatPatternValueEval2String(formatVe); + evaluated = Formatter.FormatRawCellContents(valueDouble, -1, format); + } + + valueEval = new StringEval(evaluated); + } + catch(Exception) + { + valueEval = ErrorEval.VALUE_INVALID; + } } - catch (EvaluationException e) + catch(EvaluationException e) { - return e.GetErrorEval(); + valueEval = e.GetErrorEval(); } - try + + return valueEval; + } + + /// + /// Using it instead of in order to handle booleans differently. + /// + /// + /// Pattern value eval formatted to string + /// + private string FormatPatternValueEval2String(ValueEval ve) + { + string format; + + if (ve is not BoolEval && ve is StringValueEval sve) + { + format = sve.StringValue; + } + else if (ve == BlankEval.instance) { - // Ask DataFormatter to handle the String for us - String formattedStr = Formatter.FormatRawCellContents(s0, -1, s1); - return new StringEval(formattedStr); + format = ""; } - catch + else { - return ErrorEval.VALUE_INVALID; + throw new ArgumentException("Unexpected eval class (" + ve.GetType().Name + ")"); } - //if (Regex.Match(s1, "[y|m|M|d|s|h]+").Success) - //{ - // //may be datetime string - // ValueEval result = TryParseDateTime(s0, s1); - // if (result != ErrorEval.VALUE_INVALID) - // return result; - //} - ////The regular expression needs ^ and $. - //if (Regex.Match(s1, @"^[\d,\#,\.,\$,\,]+$").Success) - //{ - // //TODO: simulate DecimalFormat class in java. - // FormatBase formatter = new DecimalFormat(s1); - // return new StringEval(formatter.Format(s0, CultureInfo.CurrentCulture)); - //} - //else if (s1.IndexOf("/", StringComparison.Ordinal) == s1.LastIndexOf("/", StringComparison.Ordinal) && s1.IndexOf("/", StringComparison.Ordinal) >= 0 && !s1.Contains("-")) - //{ - // double wholePart = Math.Floor(s0); - // double decPart = s0 - wholePart; - // if (wholePart * decPart == 0) - // { - // return new StringEval("0"); - // } - // String[] parts = s1.Split(' '); - // String[] fractParts; - // if (parts.Length == 2) - // { - // fractParts = parts[1].Split('/'); - // } - // else - // { - // fractParts = s1.Split('/'); - // } - - // if (fractParts.Length == 2) - // { - // double minVal = 1.0; - // double currDenom = Math.Pow(10, fractParts[1].Length) - 1d; - // double currNeum = 0; - // for (int i = (int)(Math.Pow(10, fractParts[1].Length) - 1d); i > 0; i--) - // { - // for (int i2 = (int)(Math.Pow(10, fractParts[1].Length) - 1d); i2 > 0; i2--) - // { - // if (minVal >= Math.Abs((double)i2 / (double)i - decPart)) - // { - // currDenom = i; - // currNeum = i2; - // minVal = Math.Abs((double)i2 / (double)i - decPart); - // } - // } - // } - // FormatBase neumFormatter = new DecimalFormat(fractParts[0]); - // FormatBase denomFormatter = new DecimalFormat(fractParts[1]); - // if (parts.Length == 2) - // { - // FormatBase wholeFormatter = new DecimalFormat(parts[0]); - // String result = wholeFormatter.Format(wholePart, CultureInfo.CurrentCulture) + " " + neumFormatter.Format(currNeum, CultureInfo.CurrentCulture) + "/" + denomFormatter.Format(currDenom, CultureInfo.CurrentCulture); - // return new StringEval(result); - // } - // else - // { - // String result = neumFormatter.Format(currNeum + (currDenom * wholePart), CultureInfo.CurrentCulture) + "/" + denomFormatter.Format(currDenom, CultureInfo.CurrentCulture); - // return new StringEval(result); - // } - // } - // else - // { - // return ErrorEval.VALUE_INVALID; - // } - //} - //else - //{ - // return TryParseDateTime(s0, s1); - //} + + return format; } - //private ValueEval TryParseDateTime(double s0, string s1) - //{ - // try - // { - // FormatBase dateFormatter = new SimpleDateFormat(s1); - // //first month of java Gregorian Calendar month field is 0 - // DateTime dt = new DateTime(1899, 12, 30, 0, 0, 0); - // dt = dt.AddDays((int)Math.Floor(s0)); - // double dayFraction = s0 - Math.Floor(s0); - // dt = dt.AddMilliseconds((int)Math.Round(dayFraction * 24 * 60 * 60 * 1000)); - // return new StringEval(dateFormatter.Format(dt, CultureInfo.CurrentCulture)); - // } - // catch (Exception) - // { - // return ErrorEval.VALUE_INVALID; - // } - //} } } diff --git a/main/SS/Formula/Functions/Text/TextFunction.cs b/main/SS/Formula/Functions/Text/TextFunction.cs index bbbd0f264..6489614de 100644 --- a/main/SS/Formula/Functions/Text/TextFunction.cs +++ b/main/SS/Formula/Functions/Text/TextFunction.cs @@ -36,10 +36,10 @@ public override ValueEval EvaluateFunc(ValueEval[] args, int srcCellRow, int src { return ErrorEval.VALUE_INVALID; } - String arg = EvaluateStringArg(args[0], srcCellRow, srcCellCol); + string arg = EvaluateStringArg(args[0], srcCellRow, srcCellCol); return Evaluate(arg); } - public abstract ValueEval Evaluate(String arg); + public abstract ValueEval Evaluate(string arg); } /** @@ -47,10 +47,9 @@ public override ValueEval EvaluateFunc(ValueEval[] args, int srcCellRow, int src */ public abstract class TextFunction : Function { + protected static string EMPTY_STRING = ""; - protected static String EMPTY_STRING = ""; - - public static String EvaluateStringArg(ValueEval eval, int srcRow, int srcCol) + public static string EvaluateStringArg(ValueEval eval, int srcRow, int srcCol) { ValueEval ve = OperandResolver.GetSingleValue(eval, srcRow, srcCol); return OperandResolver.CoerceValueToString(ve); @@ -78,8 +77,7 @@ public ValueEval Evaluate(ValueEval[] args, int srcCellRow, int srcCellCol) } internal static bool IsPrintable(char c) { - int charCode = (int)c; - return charCode >= 32; + return c >= 32; } public abstract ValueEval EvaluateFunc(ValueEval[] args, int srcCellRow, int srcCellCol); diff --git a/main/SS/UserModel/DataFormatter.cs b/main/SS/UserModel/DataFormatter.cs index 8a2b9102d..f6b1554ec 100644 --- a/main/SS/UserModel/DataFormatter.cs +++ b/main/SS/UserModel/DataFormatter.cs @@ -881,7 +881,7 @@ private String GetFormattedNumberString(ICell cell) * FormatBase index and string, according to excel style rules. * @see #FormatCellValue(Cell) */ - public String FormatRawCellContents(double value, int formatIndex, String formatString) + public string FormatRawCellContents(double value, int formatIndex, string formatString) { return FormatRawCellContents(value, formatIndex, formatString, false); } @@ -889,7 +889,7 @@ public String FormatRawCellContents(double value, int formatIndex, String format * Performs Excel-style date formatting, using the * supplied Date and format */ - private String PerformDateFormatting(DateTime d, FormatBase dateFormat) + private string PerformDateFormatting(DateTime d, FormatBase dateFormat) { if (dateFormat != null) { @@ -902,7 +902,7 @@ private String PerformDateFormatting(DateTime d, FormatBase dateFormat) * format index and string, according to excel style rules. * @see #formatCellValue(Cell) */ - public String FormatRawCellContents(double value, int formatIndex, String formatString, bool use1904Windowing) + public string FormatRawCellContents(double value, int formatIndex, string formatString, bool use1904Windowing) { // Is it a date? if (DateUtil.IsADateFormat(formatIndex, formatString)) @@ -911,10 +911,10 @@ public String FormatRawCellContents(double value, int formatIndex, String format { FormatBase dateFormat = GetFormat(value, formatIndex, formatString); - if (dateFormat is ExcelStyleDateFormatter) + if (dateFormat is ExcelStyleDateFormatter excelStyleDateFormat) { // Hint about the raw excel value - ((ExcelStyleDateFormatter)dateFormat).SetDateToBeFormatted(value); + excelStyleDateFormat.SetDateToBeFormatted(value); } DateTime d = DateUtil.GetJavaDate(value, use1904Windowing); @@ -938,18 +938,23 @@ public String FormatRawCellContents(double value, int formatIndex, String format // previous versions). However, if the value contains E notation, this // would expand the values, which we do not want, so revert to // original method. - String result; - String textValue = NumberToTextConverter.ToText(value); + string result; + string textValue = NumberToTextConverter.ToText(value); if (textValue.IndexOf('E') > -1) { result = numberFormat.Format(value); } else { - result = numberFormat.Format(decimal.Parse(textValue)); + var parsed = decimal.Parse(textValue, currentCulture); + result = numberFormat.Format(parsed); } - // Complete scientific notation by adding the missing +. - if (result.Contains("E") && !result.Contains("E-")) + + // If they requested a non-abbreviated Scientific format, + // and there's an E## (but not E-##), add the missing '+' for E+## + string fslc = formatString.ToLower(currentCulture); + if((fslc.Contains("general") || fslc.Contains("e+0")) && + result.Contains("E") && !result.Contains("E-")) { result = result.Replace("E", "E+"); } diff --git a/testcases/main/SS/Formula/Functions/TestText.cs b/testcases/main/SS/Formula/Functions/TestText.cs index 65cc5b280..d7793d765 100644 --- a/testcases/main/SS/Formula/Functions/TestText.cs +++ b/testcases/main/SS/Formula/Functions/TestText.cs @@ -24,6 +24,8 @@ namespace TestCases.SS.Formula.Functions using NPOI.SS.Util; using NPOI.SS.Formula.Functions; using System.Globalization; + using System.Collections.Generic; + using NPOI.SS.UserModel; /** * Test case for TEXT() @@ -33,28 +35,44 @@ namespace TestCases.SS.Formula.Functions [TestFixture] public class TestText { + private readonly List EXCEL_ERRORS = new(11) { + ErrorEval.NULL_INTERSECTION, + ErrorEval.DIV_ZERO, + ErrorEval.VALUE_INVALID, + ErrorEval.REF_INVALID, + ErrorEval.NAME_INVALID, + ErrorEval.NUM_ERROR, + ErrorEval.NA + }; + + private readonly CultureInfo _currentCulture = CultureInfo.InvariantCulture; + + [OneTimeSetUp] + public void SetUp() + { + Text.Formatter = new DataFormatter(_currentCulture); + } + //private static TextFunction T = null; [Test] public void TestTextWithStringFirstArg() { - ValueEval strArg = new StringEval("abc"); ValueEval formatArg = new StringEval("abc"); ValueEval[] args = { strArg, formatArg }; - ValueEval result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); - Assert.AreEqual(strArg, result); + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); + Assert.AreEqual(strArg.ToString(), result.ToString()); } + [Test] - public void TestTextWithDeciamlFormatSecondArg() + public void TestTextWithDecimalFormatSecondArg() { ValueEval numArg = new NumberEval(321321.321); ValueEval formatArg = new StringEval("#,###.00000"); ValueEval[] args = { numArg, formatArg }; - ValueEval result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); - //char groupSeparator = new DecimalFormatSymbols(Locale.GetDefault()).GetGroupingSeparator(); - //char decimalSeparator = new DecimalFormatSymbols(Locale.GetDefault()).GetDecimalSeparator(); - - NumberFormatInfo fs = CultureInfo.GetCultureInfo("en-US").NumberFormat; + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); + + NumberFormatInfo fs = _currentCulture.NumberFormat; string groupSeparator = fs.NumberGroupSeparator; string decimalSeparator = fs.NumberDecimalSeparator; ; @@ -70,71 +88,227 @@ public void TestTextWithDeciamlFormatSecondArg() formatArg = new StringEval("$#.#"); args[1] = formatArg; - result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + result = TextFunction.TEXT.Evaluate(args, -1, -1); testResult = new StringEval("$321" + decimalSeparator + "3"); Assert.AreEqual(testResult.ToString(), result.ToString()); } + [Test] public void TestTextWithFractionFormatSecondArg() { - ValueEval numArg = new NumberEval(321.321); ValueEval formatArg = new StringEval("# #/#"); ValueEval[] args = { numArg, formatArg }; - ValueEval result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); ValueEval testResult = new StringEval("321 1/3"); - Assert.AreEqual(testResult.ToString(), result.ToString()); //this bug is caused by DecimalFormat + Assert.AreEqual(testResult.ToString(), result.ToString()); formatArg = new StringEval("# #/##"); args[1] = formatArg; - result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + result = TextFunction.TEXT.Evaluate(args, -1, -1); testResult = new StringEval("321 26/81"); Assert.AreEqual(testResult.ToString(), result.ToString()); formatArg = new StringEval("#/##"); args[1] = formatArg; - result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + result = TextFunction.TEXT.Evaluate(args, -1, -1); testResult = new StringEval("26027/81"); Assert.AreEqual(testResult.ToString(), result.ToString()); } + [Test] public void TestTextWithDateFormatSecondArg() { - // Test with Java style M=Month - System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-US"); ValueEval numArg = new NumberEval(321.321); ValueEval formatArg = new StringEval("dd:MM:yyyy hh:mm:ss"); ValueEval[] args = { numArg, formatArg }; - ValueEval result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); ValueEval testResult = new StringEval("16:11:1900 07:42:14"); Assert.AreEqual(testResult.ToString(), result.ToString()); // Excel also supports "m before h is month" formatArg = new StringEval("dd:mm:yyyy hh:mm:ss"); args[1] = formatArg; - result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + result = TextFunction.TEXT.Evaluate(args, -1, -1); testResult = new StringEval("16:11:1900 07:42:14"); - //Assert.AreEqual(testResult.ToString(), result.ToString()); + Assert.AreEqual(testResult.ToString(), result.ToString()); // this line is intended to compute how "November" would look like in the current locale - String november = new SimpleDateFormat("MMMM").Format(new DateTime(2010, 11, 15), CultureInfo.CurrentCulture); + string november = new SimpleDateFormat("MMMM").Format(new DateTime(2010, 11, 15), CultureInfo.CurrentCulture); // Again with Java style formatArg = new StringEval("MMMM dd, yyyy"); args[1] = formatArg; - //fix error in non-en Culture - NPOI.SS.Formula.Functions.Text.Formatter = new NPOI.SS.UserModel.DataFormatter(CultureInfo.CurrentCulture); - result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + + result = TextFunction.TEXT.Evaluate(args, -1, -1); testResult = new StringEval(november + " 16, 1900"); Assert.AreEqual(testResult.ToString(), result.ToString()); // And Excel style formatArg = new StringEval("mmmm dd, yyyy"); args[1] = formatArg; - result = TextFunction.TEXT.Evaluate(args, -1, (short)-1); + result = TextFunction.TEXT.Evaluate(args, -1, -1); testResult = new StringEval(november + " 16, 1900"); Assert.AreEqual(testResult.ToString(), result.ToString()); } - } + [Test] + public void TestTextWithISODateTimeFormatSecondArg() + { + ValueEval numArg = new NumberEval(321.321); + ValueEval formatArg = new StringEval("yyyy-mm-ddThh:MM:ss"); + ValueEval[] args = { numArg, formatArg }; + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); + ValueEval testResult = new StringEval("1900-11-16T07:42:14"); + Assert.AreEqual(testResult.ToString(), result.ToString()); + + // test milliseconds + formatArg = new StringEval("yyyy-mm-ddThh:MM:ss.000"); + args[1] = formatArg; + result = TextFunction.TEXT.Evaluate(args, -1, -1); + testResult = new StringEval("1900-11-16T07:42:14.400"); + Assert.AreEqual(testResult.ToString(), result.ToString()); + } + + // Test cases from the workbook attached to the bug 67475 which were OK + + [Test] + public void TestTextVariousValidNumberFormats() + { + // negative values: 3 decimals + Testtext(new NumberEval(-123456.789012345), new StringEval("#0.000"), "-123456.789"); + // no decimals + Testtext(new NumberEval(-123456.789012345), new StringEval("000000"), "-123457"); + // common format - more digits + Testtext(new NumberEval(-123456.789012345), new StringEval("00.0000"), "-123456.7890"); + // common format - less digits + Testtext(new NumberEval(-12.78), new StringEval("00000.000000"), "-00012.780000"); + // half up + Testtext(new NumberEval(-0.56789012375), new StringEval("#0.0000000000"), "-0.5678901238"); + // half up + Testtext(new NumberEval(-0.56789012385), new StringEval("#0.0000000000"), "-0.5678901239"); + // positive values: 3 decimals + Testtext(new NumberEval(123456.789012345), new StringEval("#0.000"), "123456.789"); + // no decimals + Testtext(new NumberEval(123456.789012345), new StringEval("000000"), "123457"); + // common format - more digits + Testtext(new NumberEval(123456.789012345), new StringEval("00.0000"), "123456.7890"); + // common format - less digits + Testtext(new NumberEval(12.78), new StringEval("00000.000000"), "00012.780000"); + // half up + Testtext(new NumberEval(0.56789012375), new StringEval("#0.0000000000"), "0.5678901238"); + // half up + Testtext(new NumberEval(0.56789012385), new StringEval("#0.0000000000"), "0.5678901239"); + } + + [Test] + public void testTextBlankTreatedAsZero() + { + Testtext(BlankEval.instance, new StringEval("#0.000"), "0.000"); + } + + [Test] + public void testTextStrangeFormat() + { + // number 0 + Testtext(new NumberEval(-123456.789012345), new NumberEval(0), "-123457"); + // negative number with few zeros + Testtext(new NumberEval(-123456.789012345), new NumberEval(-0.0001), "--123456.7891"); + // format starts with "." + Testtext(new NumberEval(0.0123), new StringEval(".000"), ".012"); + // one zero negative + Testtext(new NumberEval(1001.202), new NumberEval(-8808), "-8810018"); + // format contains 0 + Testtext(new NumberEval(43368.0), new NumberEval(909), "9433689"); + } + + [Test] + public void TestTextErrorAsFormat() + { + foreach(ErrorEval errorEval in EXCEL_ERRORS) + { + Testtext(new NumberEval(3.14), errorEval, errorEval); + Testtext(BoolEval.TRUE, errorEval, errorEval); + Testtext(BoolEval.FALSE, errorEval, errorEval); + } + } + + [Test] + public void TestTextErrorAsValue() + { + foreach(ErrorEval errorEval in EXCEL_ERRORS) + { + Testtext(errorEval, new StringEval("#0.000"), errorEval); + Testtext(errorEval, new StringEval("yyyymmmdd"), errorEval); + } + } + + // Test cases from the workbook attached to the bug 67475 which were failing and are fixed by the patch + + [Test] + public void TestTextEmptyStringWithDateFormat() + { + Testtext(new StringEval(""), new StringEval("yyyymmmdd"), ""); + } + + [Test] + public void TestTextAnyTextWithDateFormat() + { + Testtext(new StringEval("anyText"), new StringEval("yyyymmmdd"), "anyText"); + } + + [Test] + public void TestTextBooleanWithDateFormat() + { + Testtext(BoolEval.TRUE, new StringEval("yyyymmmdd"), BoolEval.TRUE.StringValue); + Testtext(BoolEval.FALSE, new StringEval("yyyymmmdd"), BoolEval.FALSE.StringValue); + } + + [Test] + public void TestTextNumberWithBooleanFormat() + { + Testtext(new NumberEval(43368), BoolEval.TRUE, ErrorEval.VALUE_INVALID); + Testtext(new NumberEval(43368), BoolEval.FALSE, ErrorEval.VALUE_INVALID); + + Testtext(new NumberEval(3.14), BoolEval.TRUE, ErrorEval.VALUE_INVALID); + Testtext(new NumberEval(3.14), BoolEval.FALSE, ErrorEval.VALUE_INVALID); + } + + [Test] + public void TestTextEmptyStringWithNumberFormat() + { + Testtext(new StringEval(""), new StringEval("#0.000"), ""); + } + + [Test] + public void TestTextAnyTextWithNumberFormat() + { + Testtext(new StringEval("anyText"), new StringEval("#0.000"), "anyText"); + } + + [Test] + public void TestTextBooleanWithNumberFormat() + { + Testtext(BoolEval.TRUE, new StringEval("#0.000"), BoolEval.TRUE.StringValue); + Testtext(BoolEval.FALSE, new StringEval("#0.000"), BoolEval.FALSE.StringValue); + } + + private static void Testtext(ValueEval valueArg, ValueEval formatArg, string expectedResult) + { + ValueEval[] args = { valueArg, formatArg }; + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); + + Assert.IsTrue(result is StringEval, "Expected StringEval got " + result.GetType().Name); + Assert.AreEqual(expectedResult, ((StringEval) result).StringValue); + } + + private static void Testtext(ValueEval valueArg, ValueEval formatArg, ErrorEval expectedResult) + { + ValueEval[] args = { valueArg, formatArg }; + ValueEval result = TextFunction.TEXT.Evaluate(args, -1, -1); + + Assert.IsTrue(result is ErrorEval, "Expected ErrorEval got " + result.GetType().Name); + Assert.AreEqual(expectedResult, result); + } + } } \ No newline at end of file