diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs index eee7e06..cee10df 100644 --- a/ClosedXML/Excel/Cells/XLCell.cs +++ b/ClosedXML/Excel/Cells/XLCell.cs @@ -65,10 +65,10 @@ #region Fields - internal string _cellValue = String.Empty; + private string _cellValue = String.Empty; private XLComment _comment; - internal XLDataType _dataType; + private XLDataType _dataType; private XLHyperlink _hyperlink; private XLRichText _richText; @@ -276,7 +276,7 @@ if (TryGetValue(out T retVal)) return retVal; - throw new FormatException("Cannot convert cell value to " + typeof(T)); + throw new FormatException($"Cannot convert {this.Address.ToStringRelative(true)}'s value to " + typeof(T)); } public string GetString() @@ -307,7 +307,7 @@ public string GetFormattedString() { String cValue; - if (FormulaA1.Length > 0) + if (HasFormula) { try { @@ -327,7 +327,16 @@ if (_dataType == XLDataType.Boolean) return (cValue != "0").ToExcelFormat(format); - else if (_dataType == XLDataType.TimeSpan || _dataType == XLDataType.DateTime || IsDateFormat()) + else if (_dataType == XLDataType.TimeSpan) + { + if (Double.TryParse(cValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double dTest)) + { + return TimeSpan.FromDays(dTest).ToExcelFormat(format); + } + + return cValue; + } + else if (_dataType == XLDataType.DateTime || IsDateFormat()) { if (Double.TryParse(cValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double dTest) && dTest.IsValidOADateNumber()) @@ -460,25 +469,94 @@ return CachedValue; } - private object StringToCellDataType(string cellValue) + internal void SetInternalCellValueString(String cellValue) { - if (_dataType == XLDataType.Boolean) - return cellValue != "0"; + SetInternalCellValueString(cellValue, this.HasFormula); + } - if (_dataType == XLDataType.DateTime - && Double.TryParse(cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d) - && d.IsValidOADateNumber()) + private void SetInternalCellValueString(String cellValue, Boolean parseToCachedValue) + { + this._cellValue = cellValue; + if (parseToCachedValue) + CachedValue = ParseCellValueFromString(); + } - return DateTime.FromOADate(d); + internal void SetDataTypeFast(XLDataType dataType) + { + this._dataType = dataType; + } - if (_dataType == XLDataType.Number - && double.TryParse(cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double n)) + private Object ParseCellValueFromString() + { + return ParseCellValueFromString(_cellValue, _dataType, out String error); + } - return n; + private Object ParseCellValueFromString(String cellValue, XLDataType dataType, out String error) + { + error = ""; + if ("" == cellValue) + return ""; - if (_dataType == XLDataType.TimeSpan - && TimeSpan.TryParse(cellValue, out TimeSpan t)) - return t; + if (dataType == XLDataType.Boolean) + { + if (bool.TryParse(cellValue, out Boolean b)) + return b; + else if (cellValue == "0") + return false; + else if (cellValue == "1") + return true; + else + return !string.IsNullOrEmpty(cellValue); + } + + if (dataType == XLDataType.DateTime) + { + if (Double.TryParse(cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d)) + { + if (d.IsValidOADateNumber()) + return DateTime.FromOADate(d); + else + return d; + } + else if (DateTime.TryParse(cellValue, out DateTime dt)) + return dt; + else + { + error = string.Format("Cannot set data type to DateTime because '{0}' is not recognized as a date.", cellValue); + return null; + } + } + if (dataType == XLDataType.Number) + { + var v = cellValue; + Double factor = 1.0; + if (v.EndsWith("%")) + { + v = v.Substring(0, v.Length - 1); + factor = 1 / 100.0; + } + + if (Double.TryParse(v, XLHelper.NumberStyle, CultureInfo.InvariantCulture, out Double d)) + return d * factor; + else + { + error = string.Format("Cannot set data type to Number because '{0}' is not recognized as a number.", cellValue); + return null; + } + } + + if (dataType == XLDataType.TimeSpan) + { + if (TimeSpan.TryParse(cellValue, out TimeSpan ts)) + return ts; + else if (Double.TryParse(cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d)) + return TimeSpan.FromDays(d); + else + { + error = string.Format("Cannot set data type to TimeSpan because '{0}' is not recognized as a TimeSpan.", cellValue); + return null; + } + } return cellValue; } @@ -494,7 +572,11 @@ } var cellValue = HasRichText ? _richText.ToString() : _cellValue; - return StringToCellDataType(cellValue); + var parsedValue = ParseCellValueFromString(cellValue, _dataType, out String error); + if ("" == error) + return parsedValue; + else + throw new ArgumentException(error); } set { @@ -973,87 +1055,62 @@ if (!string.IsNullOrEmpty(_cellValue)) { - if (value == XLDataType.Boolean) + // If we're converting the DataType to Text, there are some quirky rules currently + if (value == XLDataType.Text) { - if (Boolean.TryParse(_cellValue, out Boolean bTest)) - _cellValue = bTest ? "1" : "0"; - else - _cellValue = _cellValue == "0" || String.IsNullOrEmpty(_cellValue) ? "0" : "1"; - } - else if (value == XLDataType.DateTime) - { - if (DateTime.TryParse(_cellValue, out DateTime dtTest)) - _cellValue = dtTest.ToOADate().ToInvariantString(); - else if (Double.TryParse(_cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double dblTest)) - _cellValue = dblTest.ToInvariantString(); - else + var v = Value; + switch (v) { - throw new ArgumentException( - string.Format( - "Cannot set data type to DateTime because '{0}' is not recognized as a date.", - _cellValue)); - } - var style = GetStyleForRead(); - if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) - Style.NumberFormat.NumberFormatId = _cellValue.Contains('.') ? 22 : 14; - } - else if (value == XLDataType.TimeSpan) - { - if (TimeSpan.TryParse(_cellValue, out TimeSpan tsTest)) - { - _cellValue = tsTest.ToString(); - var style = GetStyleForRead(); - if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) - Style.NumberFormat.NumberFormatId = 46; - } - else - { - try - { - _cellValue = (DateTime.FromOADate(Double.Parse(_cellValue, XLHelper.NumberStyle, XLHelper.ParseCulture)) - BaseDate).ToString(); - } - catch - { - throw new ArgumentException( - string.Format( - "Cannot set data type to TimeSpan because '{0}' is not recognized as a TimeSpan.", - _cellValue)); - } - } - } - else if (value == XLDataType.Number) - { - var v = _cellValue; - double factor = 1.0; - if (v.EndsWith("%")) - { - v = v.Substring(0, v.Length - 1); - factor = 1 / 100.0; - } + case DateTime d: + _cellValue = d.ToOADate().ToInvariantString(); + break; - if (Double.TryParse(v, XLHelper.NumberStyle, CultureInfo.InvariantCulture, out Double dTest)) - _cellValue = (dTest * factor).ToInvariantString(); - else - { - throw new ArgumentException( - string.Format( - "Cannot set data type to Number because '{0}' is not recognized as a number.", - _cellValue)); + case TimeSpan ts: + _cellValue = ts.TotalDays.ToInvariantString(); + break; + + case Boolean b: + _cellValue = b ? "True" : "False"; + break; + + default: + _cellValue = v.ToInvariantString(); + break; } } else { - if (_dataType == XLDataType.Boolean) - _cellValue = (_cellValue != "0").ToString(); - else if (_dataType == XLDataType.TimeSpan) - _cellValue = BaseDate.Add(GetTimeSpan()).ToOADate().ToInvariantString(); + var v = ParseCellValueFromString(_cellValue, value, out String error); + _cellValue = v?.ToInvariantString() ?? ""; + + var style = GetStyleForRead(); + switch (v) + { + case DateTime d: + _cellValue = d.ToOADate().ToInvariantString(); + + if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = _cellValue.Contains('.') ? 22 : 14; + + break; + + case TimeSpan ts: + if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = 46; + + break; + + case Boolean b: + _cellValue = b ? "1" : "0"; + break; + } } } _dataType = value; if (HasFormula) - CachedValue = StringToCellDataType(_cellValue); + CachedValue = ParseCellValueFromString(); else CachedValue = null; } @@ -2034,7 +2091,9 @@ else if (value is TimeSpan || (!Double.TryParse(val, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d1) && TimeSpan.TryParse(val, out TimeSpan tsTest))) { if (!(value is TimeSpan) && TimeSpan.TryParse(val, out tsTest)) - val = tsTest.ToString(); + val = BaseDate.Add(tsTest).ToOADate().ToInvariantString(); + else + val = BaseDate.Add((TimeSpan)value).ToOADate().ToInvariantString(); _dataType = XLDataType.TimeSpan; if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) diff --git a/ClosedXML/Excel/XLWorkbook_Load.cs b/ClosedXML/Excel/XLWorkbook_Load.cs index c521125..701a799 100644 --- a/ClosedXML/Excel/XLWorkbook_Load.cs +++ b/ClosedXML/Excel/XLWorkbook_Load.cs @@ -1354,7 +1354,7 @@ ApplyStyle(xlCell, styleIndex, s, fills, borders, fonts, numberingFormats); } - if (cell.CellFormula != null && cell.CellFormula.SharedIndex != null && cell.CellFormula.Reference != null) + if (cell.CellFormula?.SharedIndex != null && cell.CellFormula?.Reference != null) { String formula; if (cell.CellFormula.FormulaType != null && cell.CellFormula.FormulaType == CellFormulaValues.Array) @@ -1369,8 +1369,31 @@ xlCell.FormulaA1 = formula; sharedFormulasR1C1.Add(cell.CellFormula.SharedIndex.Value, xlCell.FormulaR1C1); + if (cell.DataType != null) + { + switch (cell.DataType.Value) + { + case CellValues.Boolean: + xlCell.SetDataTypeFast(XLDataType.Boolean); + break; + case CellValues.Number: + xlCell.SetDataTypeFast(XLDataType.Number); + break; + case CellValues.Date: + xlCell.SetDataTypeFast(XLDataType.DateTime); + break; + case CellValues.InlineString: + case CellValues.SharedString: + xlCell.SetDataTypeFast(XLDataType.Text); + break; + } + } + if (cell.CellValue != null) + { xlCell.ValueCached = cell.CellValue.Text; + xlCell.SetInternalCellValueString(cell.CellValue.Text); + } } else if (cell.CellFormula != null) { @@ -1399,72 +1422,97 @@ } } + if (cell.DataType != null) + { + switch (cell.DataType.Value) + { + case CellValues.Boolean: + xlCell.SetDataTypeFast(XLDataType.Boolean); + break; + case CellValues.Number: + xlCell.SetDataTypeFast(XLDataType.Number); + break; + case CellValues.Date: + xlCell.SetDataTypeFast(XLDataType.DateTime); + break; + case CellValues.InlineString: + case CellValues.SharedString: + xlCell.SetDataTypeFast(XLDataType.Text); + break; + } + } + if (cell.CellValue != null) + { xlCell.ValueCached = cell.CellValue.Text; + xlCell.SetInternalCellValueString(cell.CellValue.Text); + } } else if (cell.DataType != null) { if (cell.DataType == CellValues.InlineString) { + xlCell.SetDataTypeFast(XLDataType.Text); + xlCell.ShareString = false; + if (cell.InlineString != null) { if (cell.InlineString.Text != null) - xlCell._cellValue = cell.InlineString.Text.Text.FixNewLines(); + xlCell.SetInternalCellValueString(cell.InlineString.Text.Text.FixNewLines()); else ParseCellValue(cell.InlineString, xlCell); } else - xlCell._cellValue = String.Empty; - - xlCell._dataType = XLDataType.Text; - xlCell.ShareString = false; + xlCell.SetInternalCellValueString(String.Empty); } else if (cell.DataType == CellValues.SharedString) { + xlCell.SetDataTypeFast(XLDataType.Text); + if (cell.CellValue != null && !String.IsNullOrWhiteSpace(cell.CellValue.Text)) { var sharedString = sharedStrings[Int32.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture)]; ParseCellValue(sharedString, xlCell); } else - xlCell._cellValue = String.Empty; - - xlCell._dataType = XLDataType.Text; + xlCell.SetInternalCellValueString(String.Empty); } else if (cell.DataType == CellValues.Date) { + xlCell.SetDataTypeFast(XLDataType.DateTime); + if (cell.CellValue != null && !String.IsNullOrWhiteSpace(cell.CellValue.Text)) - xlCell._cellValue = Double.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture).ToInvariantString(); - xlCell._dataType = XLDataType.DateTime; + xlCell.SetInternalCellValueString(Double.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture).ToInvariantString()); } else if (cell.DataType == CellValues.Boolean) { + xlCell.SetDataTypeFast(XLDataType.Boolean); if (cell.CellValue != null) - xlCell._cellValue = cell.CellValue.Text; - xlCell._dataType = XLDataType.Boolean; + xlCell.SetInternalCellValueString(cell.CellValue.Text); } else if (cell.DataType == CellValues.Number) { - if (cell.CellValue != null && !String.IsNullOrWhiteSpace(cell.CellValue.Text)) - xlCell._cellValue = Double.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture).ToInvariantString(); - if (s == null) - xlCell._dataType = XLDataType.Number; + xlCell.SetDataTypeFast(XLDataType.Number); else xlCell.DataType = GetDataTypeFromCell(xlCell.Style.NumberFormat); + + if (cell.CellValue != null && !String.IsNullOrWhiteSpace(cell.CellValue.Text)) + xlCell.SetInternalCellValueString(Double.Parse(cell.CellValue.Text, XLHelper.NumberStyle, XLHelper.ParseCulture).ToInvariantString()); } } else if (cell.CellValue != null) { if (s == null) { - xlCell._dataType = XLDataType.Number; + xlCell.SetDataTypeFast(XLDataType.Number); } else { + xlCell.DataType = GetDataTypeFromCell(xlCell.Style.NumberFormat); var numberFormatId = ((CellFormat)(s.CellFormats).ElementAt(styleIndex)).NumberFormatId; if (!String.IsNullOrWhiteSpace(cell.CellValue.Text)) - xlCell._cellValue = Double.Parse(cell.CellValue.Text, CultureInfo.InvariantCulture).ToInvariantString(); + xlCell.SetInternalCellValueString(Double.Parse(cell.CellValue.Text, CultureInfo.InvariantCulture).ToInvariantString()); if (s.NumberingFormats != null && s.NumberingFormats.Any(nf => ((NumberingFormat)nf).NumberFormatId.Value == numberFormatId)) @@ -1477,8 +1525,6 @@ } else xlCell.Style.NumberFormat.NumberFormatId = Int32.Parse(numberFormatId); - - xlCell.DataType = GetDataTypeFromCell(xlCell.Style.NumberFormat); } } @@ -1522,7 +1568,7 @@ } if (!hasRuns) - xlCell._cellValue = XmlEncoder.DecodeString(element.Text.InnerText); + xlCell.SetInternalCellValueString(XmlEncoder.DecodeString(element.Text.InnerText)); #region Load PhoneticProperties @@ -2127,11 +2173,11 @@ if (fr.FormatId != null) { - LoadFont(differentialFormats[(Int32) fr.FormatId.Value].Font, conditionalFormat.Style.Font); - LoadFill(differentialFormats[(Int32) fr.FormatId.Value].Fill, conditionalFormat.Style.Fill, + LoadFont(differentialFormats[(Int32)fr.FormatId.Value].Font, conditionalFormat.Style.Font); + LoadFill(differentialFormats[(Int32)fr.FormatId.Value].Fill, conditionalFormat.Style.Fill, differentialFillFormat: true); - LoadBorder(differentialFormats[(Int32) fr.FormatId.Value].Border, conditionalFormat.Style.Border); - LoadNumberFormat(differentialFormats[(Int32) fr.FormatId.Value].NumberingFormat, + LoadBorder(differentialFormats[(Int32)fr.FormatId.Value].Border, conditionalFormat.Style.Border); + LoadNumberFormat(differentialFormats[(Int32)fr.FormatId.Value].NumberingFormat, conditionalFormat.Style.NumberFormat); } @@ -2143,8 +2189,8 @@ if (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.CellIs && fr.Operator != null) conditionalFormat.Operator = fr.Operator.Value.ToClosedXml(); - if (!String.IsNullOrWhiteSpace(fr.Text)) - conditionalFormat.Values.Add(GetFormula(fr.Text.Value)); + if (!String.IsNullOrWhiteSpace(fr.Text)) + conditionalFormat.Values.Add(GetFormula(fr.Text.Value)); if (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.Top10) { @@ -2174,9 +2220,9 @@ if (dataBar.ShowValue != null) conditionalFormat.ShowBarOnly = !dataBar.ShowValue.Value; - var id = fr.Descendants().FirstOrDefault(); - if (id != null && id.Text != null && !String.IsNullOrWhiteSpace(id.Text)) - conditionalFormat.Id = new Guid(id.Text.Substring(1, id.Text.Length - 2)); + var id = fr.Descendants().FirstOrDefault(); + if (id != null && id.Text != null && !String.IsNullOrWhiteSpace(id.Text)) + conditionalFormat.Id = new Guid(id.Text.Substring(1, id.Text.Length - 2)); ExtractConditionalFormatValueObjects(conditionalFormat, dataBar); } diff --git a/ClosedXML/Excel/XLWorkbook_Save.cs b/ClosedXML/Excel/XLWorkbook_Save.cs index 4fcb084..0768579 100644 --- a/ClosedXML/Excel/XLWorkbook_Save.cs +++ b/ClosedXML/Excel/XLWorkbook_Save.cs @@ -5562,8 +5562,28 @@ var cellValue = new CellValue(); try { - cellValue.Text = xlCell.Value.ToInvariantString(); - openXmlCell.DataType = new EnumValue(CellValues.String); + var v = xlCell.Value; + cellValue.Text = v.ToInvariantString(); + switch (v) + { + case String s: + openXmlCell.DataType = new EnumValue(CellValues.String); + break; + case DateTime dt: + openXmlCell.DataType = new EnumValue(CellValues.Date); + break; + + case Boolean b: + openXmlCell.DataType = new EnumValue(CellValues.Boolean); + break; + + default: + if (v.IsNumber()) + openXmlCell.DataType = new EnumValue(CellValues.Number); + else + openXmlCell.DataType = null; + break; + } } catch { diff --git a/ClosedXML_Tests/Excel/Loading/LoadingTests.cs b/ClosedXML_Tests/Excel/Loading/LoadingTests.cs index d484c0f..be5fe4b 100644 --- a/ClosedXML_Tests/Excel/Loading/LoadingTests.cs +++ b/ClosedXML_Tests/Excel/Loading/LoadingTests.cs @@ -113,7 +113,6 @@ Assert.AreEqual(new DateTime(2017, 10, 27, 21, 0, 0), c.GetDateTime()); wb.SaveAs(ms); } - } } @@ -346,14 +345,14 @@ } [Test] - [TestCase("A1*10", "1230")] - [TestCase("A1/10", "12.3")] - [TestCase("A1&\" cells\"", "123 cells")] - [TestCase("A1&\"000\"", "123000")] - [TestCase("ISNUMBER(A1)", "True")] - [TestCase("ISBLANK(A1)", "False")] - [TestCase("DATE(2018,1,28)", "43128")] - public void LoadFormulaCachedValue(string formula, object expectedValue) + [TestCase("A1*10", "1230", 1230)] + [TestCase("A1/10", "12.3", 12.3)] + [TestCase("A1&\" cells\"", "123 cells", "123 cells")] + [TestCase("A1&\"000\"", "123000", "123000")] + [TestCase("ISNUMBER(A1)", "True", true)] + [TestCase("ISBLANK(A1)", "False", false)] + [TestCase("DATE(2018,1,28)", "43128", null)] + public void LoadFormulaCachedValue(string formula, object expectedValueCached, object expectedCachedValue) { using (var ms = new MemoryStream()) { @@ -371,9 +370,11 @@ using (XLWorkbook book2 = new XLWorkbook(ms)) { var ws = book2.Worksheet(1); - var storedValueA2 = ws.Cell("A2").ValueCached; Assert.IsTrue(ws.Cell("A2").NeedsRecalculation); - Assert.AreEqual(expectedValue, storedValueA2); + Assert.AreEqual(expectedValueCached, ws.Cell("A2").ValueCached); + + if (expectedCachedValue != null) + Assert.AreEqual(expectedCachedValue, ws.Cell("A2").CachedValue); } } } diff --git a/ClosedXML_Tests/Excel/Saving/SavingTests.cs b/ClosedXML_Tests/Excel/Saving/SavingTests.cs index 8db6b56..1b0326b 100644 --- a/ClosedXML_Tests/Excel/Saving/SavingTests.cs +++ b/ClosedXML_Tests/Excel/Saving/SavingTests.cs @@ -140,9 +140,9 @@ using (XLWorkbook book2 = new XLWorkbook(ms)) { var ws = book2.Worksheet(1); - var storedValue = ws.Cell("A2").ValueCached; - Assert.IsNull(storedValue); + Assert.IsNull(ws.Cell("A2").ValueCached); + Assert.IsNull(ws.Cell("A2").CachedValue); } } } @@ -157,8 +157,10 @@ var sheet = book1.AddWorksheet("sheet1"); sheet.Cell("A1").Value = 123; sheet.Cell("A2").FormulaA1 = "A1*10"; + sheet.Cell("A3").FormulaA1 = "TEXT(A2, \"# ###\")"; var options = new SaveOptions { EvaluateFormulasBeforeSaving = true }; + book1.SaveAs(@"c:\temp\formulas.xlsx", options); book1.SaveAs(ms, options); } ms.Position = 0; @@ -166,9 +168,12 @@ using (XLWorkbook book2 = new XLWorkbook(ms)) { var ws = book2.Worksheet(1); - var storedValue = ws.Cell("A2").ValueCached; - Assert.AreEqual("1230", storedValue); + Assert.AreEqual("1230", ws.Cell("A2").ValueCached); + Assert.AreEqual(1230, ws.Cell("A2").CachedValue); + + Assert.AreEqual("1 230", ws.Cell("A3").ValueCached); + Assert.AreEqual("1 230", ws.Cell("A3").CachedValue); } } } diff --git a/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx index d7b945b..f61af20 100644 --- a/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx +++ b/ClosedXML_Tests/Resource/Examples/Misc/CellValues.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx b/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx index 131fba0..7cb0a2a 100644 --- a/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx +++ b/ClosedXML_Tests/Resource/Examples/Misc/FormulasWithEvaluation.xlsx Binary files differ