diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs index 5141aae..8d30470 100644 --- a/ClosedXML/Excel/Cells/XLCell.cs +++ b/ClosedXML/Excel/Cells/XLCell.cs @@ -218,59 +218,129 @@ } var style = GetStyleForRead(); + Boolean parsed; + string parsedValue; + + // For SetValue we set the cell value directly to the parameter + // as opposed to the other SetValue(object value) where we parse the string and try to decude the value if (value is String || value is char) { - _cellValue = value.ToString(); + parsedValue = value.ToInvariantString(); _dataType = XLDataType.Text; - if (_cellValue.Contains(Environment.NewLine) && !style.Alignment.WrapText) + if (parsedValue.Contains(Environment.NewLine) && !style.Alignment.WrapText) Style.Alignment.WrapText = true; - } - else if (value is TimeSpan) - { - _cellValue = value.ToString(); - _dataType = XLDataType.TimeSpan; - if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) - Style.NumberFormat.NumberFormatId = 46; - } - else if (value is DateTime) - { - _dataType = XLDataType.DateTime; - var dtTest = (DateTime)Convert.ChangeType(value, typeof(DateTime)); - if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) - Style.NumberFormat.NumberFormatId = dtTest.Date == dtTest ? 14 : 22; - _cellValue = dtTest.ToOADate().ToInvariantString(); - } - else if (value.GetType().IsNumber()) - { - if ((value is double || value is float) && (Double.IsNaN((Double)Convert.ChangeType(value, typeof(Double))) - || Double.IsInfinity((Double)Convert.ChangeType(value, typeof(Double))))) - { - _cellValue = value.ToString(); - _dataType = XLDataType.Text; - } - else - { - _dataType = XLDataType.Number; - _cellValue = ((Double)Convert.ChangeType(value, typeof(Double))).ToInvariantString(); - } - } - else if (value is Boolean) - { - _dataType = XLDataType.Boolean; - _cellValue = (Boolean)Convert.ChangeType(value, typeof(Boolean)) ? "1" : "0"; + parsed = true; } else { - _cellValue = Convert.ToString(value); - _dataType = XLDataType.Text; + var tuple = SetKnownTypedValue(value, style); + parsedValue = tuple.Item1; + parsed = tuple.Item2; } + // If parsing was unsuccessful, we throw an ArgumentException + // because we are using SetValue (typed). + // Only in SetValue(object value) to we try to fall back to a value of a different type + if (!parsed) + throw new ArgumentException($"Unable to set cell value to {value.ToInvariantString()}"); + + SetInternalCellValueString(parsedValue, validate: true, parseToCachedValue: false); + CachedValue = null; return this; } + // TODO: Replace with (string, bool) ValueTuple later + private Tuple SetKnownTypedValue(T value, XLStyleValue style) + { + string parsedValue; + bool parsed; + if (value is DateTime d && d >= BaseDate) + { + parsedValue = d.ToOADate().ToInvariantString(); + parsed = true; + SetDateTimeFormat(style, d.Date == d); + } + else if (value is TimeSpan ts) + { + parsedValue = ts.TotalDays.ToInvariantString(); + parsed = true; + SetTimeSpanFormat(style); + } + else if (value is Boolean b) + { + parsedValue = b ? "1" : "0"; + _dataType = XLDataType.Boolean; + parsed = true; + } + else if (value.IsNumber()) + { + if ( + (value is double d1 && (double.IsNaN(d1) || double.IsInfinity(d1))) + || (value is float f && (float.IsNaN(f) || float.IsInfinity(f))) + ) + { + parsedValue = value.ToString(); + _dataType = XLDataType.Text; + parsed = parsedValue.Length == 0; + } + else + { + parsedValue = value.ToInvariantString(); + _dataType = XLDataType.Number; + } + parsed = true; + } + else + { + // Here we specifically don't use invariant string, as we want to use the current culture to convert to string + parsedValue = value.ToString(); + _dataType = XLDataType.Text; + parsed = parsedValue.Length == 0; + } + + return new Tuple(parsedValue, parsed); + } + + private string DeduceCellValueByParsing(string value, XLStyleValue style) + { + if (value[0] == '\'') + { + value = value.Substring(1, value.Length - 1); + + _dataType = XLDataType.Text; + if (value.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + } + else if (value.Trim() != "NaN" && Double.TryParse(value, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double _)) + _dataType = XLDataType.Number; + else if (TimeSpan.TryParse(value, out TimeSpan ts)) + { + value = ts.ToInvariantString(); + SetTimeSpanFormat(style); + } + else if (DateTime.TryParse(value, out DateTime dt) && dt >= BaseDate) + { + value = dt.ToOADate().ToInvariantString(); + SetDateTimeFormat(style, dt.Date == dt); + } + else if (Boolean.TryParse(value, out Boolean b)) + { + value = b ? "1" : "0"; + _dataType = XLDataType.Boolean; + } + else + { + _dataType = XLDataType.Text; + if (value.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + } + + return value; + } + public T GetValue() { if (TryGetValue(out T retVal)) @@ -434,12 +504,18 @@ internal void SetInternalCellValueString(String cellValue) { - SetInternalCellValueString(cellValue, this.HasFormula); + SetInternalCellValueString(cellValue, validate: false, parseToCachedValue: this.HasFormula); } - private void SetInternalCellValueString(String cellValue, Boolean parseToCachedValue) + private void SetInternalCellValueString(String cellValue, Boolean validate, Boolean parseToCachedValue) { + if (validate) + { + if (cellValue.Length > 32767) throw new ArgumentOutOfRangeException(nameof(cellValue), "Cells can hold a maximum of 32,767 characters."); + } + this._cellValue = cellValue; + if (parseToCachedValue) CachedValue = ParseCellValueFromString(); } @@ -560,7 +636,7 @@ CachedValue = null; - if (_cellValue.Length > 32767) throw new ArgumentException("Cells can hold only 32,767 characters."); + if (_cellValue.Length > 32767) throw new ArgumentOutOfRangeException(nameof(value), "Cells can hold only 32,767 characters."); } } @@ -1723,10 +1799,10 @@ { foreach (var table in Worksheet.Tables.Where(t => t.ShowHeaderRow)) { - var cells = table.HeadersRow().CellsUsed(c => c.Address.Equals(this.Address)); - if (cells.Any()) + var cell = table.HeadersRow().CellsUsed(c => c.Address.Equals(this.Address)).FirstOrDefault(); + if (cell != null) { - var oldName = cells.First().GetString(); + var oldName = cell.GetString(); var field = table.Field(oldName); field.Name = value.ToString(); return true; @@ -1740,13 +1816,14 @@ { foreach (var table in Worksheet.Tables.Where(t => t.ShowTotalsRow)) { - var cells = table.TotalsRow().Cells(c => c.Address.Equals(this.Address)); - if (cells.Any()) + var cell = table.TotalsRow().Cells(c => c.Address.Equals(this.Address)).FirstOrDefault(); + if (cell != null) { - var cell = cells.First(); var field = table.Fields.First(f => f.Column.ColumnNumber() == cell.WorksheetColumn().ColumnNumber()); field.TotalsRowFunction = XLTotalsRowFunction.None; - _cellValue = value.ToString(); + + SetInternalCellValueString(value.ToInvariantString(), validate: true, parseToCachedValue: false); + field.TotalsRowLabel = _cellValue; this.DataType = XLDataType.Text; return true; @@ -2021,85 +2098,69 @@ private void SetValue(object value) { - FormulaA1 = String.Empty; - string val; if (value == null) - val = string.Empty; - else if (value is DateTime) - val = ((DateTime)value).ToString("o"); - else if (value.IsNumber()) - val = value.ToInvariantString(); - else - val = value.ToString(); + { + this.Clear(XLClearOptions.Contents); + return; + } + + FormulaA1 = String.Empty; _richText = null; - if (val.Length == 0) + + var style = GetStyleForRead(); + Boolean parsed = false; + string parsedValue = string.Empty; + + //// + // Try easy parsing first. If that doesn't work, we'll have to ToString it and parse it slowly + + // When number format starts with @, we treat any value as text - no parsing required + // This doesn't happen in the SetValue() version + if (style.NumberFormat.Format == "@") + { + parsedValue = value.ToInvariantString(); + _dataType = XLDataType.Text; + if (parsedValue.Contains(Environment.NewLine) && !style.Alignment.WrapText) + Style.Alignment.WrapText = true; + + parsed = true; + } else { - var style = GetStyleForRead(); - if (style.NumberFormat.Format == "@") - { - _dataType = XLDataType.Text; - if (val.Contains(Environment.NewLine) && !style.Alignment.WrapText) - Style.Alignment.WrapText = true; - } - else if (val[0] == '\'') - { - val = val.Substring(1, val.Length - 1); - _dataType = XLDataType.Text; - if (val.Contains(Environment.NewLine) && !style.Alignment.WrapText) - Style.Alignment.WrapText = true; - } - // TODO: sort out the double TimeSpan parsing - 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 = 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) - Style.NumberFormat.NumberFormatId = 46; - } - else if (val.Trim() != "NaN" && Double.TryParse(val, XLHelper.NumberStyle, XLHelper.ParseCulture, out Double d2)) - _dataType = XLDataType.Number; - else if (DateTime.TryParse(val, out DateTime dtTest) && dtTest >= BaseDate) - { - _dataType = XLDataType.DateTime; - - if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) - Style.NumberFormat.NumberFormatId = dtTest.Date == dtTest ? 14 : 22; - { - DateTime forMillis; - if (value is DateTime && (forMillis = (DateTime)value).Millisecond > 0) - { - val = forMillis.ToOADate().ToInvariantString(); - } - else - { - val = dtTest.ToOADate().ToInvariantString(); - } - } - } - else if (Boolean.TryParse(val, out Boolean bTest)) - { - _dataType = XLDataType.Boolean; - val = bTest ? "1" : "0"; - } - else - { - _dataType = XLDataType.Text; - if (val.Contains(Environment.NewLine) && !style.Alignment.WrapText) - Style.Alignment.WrapText = true; - } + var tuple = SetKnownTypedValue(value, style); + parsedValue = tuple.Item1; + parsed = tuple.Item2; } - if (val.Length > 32767) throw new ArgumentException("Cells can only hold 32,767 characters."); - if (SetTableHeaderValue(val)) return; - if (SetTableTotalsRowLabel(val)) return; + //// + if (!parsed) + { + // We'll have to parse it slowly :-( + parsedValue = DeduceCellValueByParsing(parsedValue.ToString(), style); + } - _cellValue = val; + if (SetTableHeaderValue(parsedValue)) return; + if (SetTableTotalsRowLabel(parsedValue)) return; + + SetInternalCellValueString(parsedValue, validate: true, parseToCachedValue: false); + CachedValue = null; + } + + private void SetDateTimeFormat(XLStyleValue style, Boolean onlyDatePart) + { + _dataType = XLDataType.DateTime; + + if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = onlyDatePart ? 14 : 22; + } + + private void SetTimeSpanFormat(XLStyleValue style) + { + _dataType = XLDataType.TimeSpan; + + if (style.NumberFormat.Format == String.Empty && style.NumberFormat.NumberFormatId == 0) + Style.NumberFormat.NumberFormatId = 46; } internal string GetFormulaR1C1(string value) diff --git a/ClosedXML_Tests/Excel/Cells/XLCellTests.cs b/ClosedXML_Tests/Excel/Cells/XLCellTests.cs index fd294fb..710373c 100644 --- a/ClosedXML_Tests/Excel/Cells/XLCellTests.cs +++ b/ClosedXML_Tests/Excel/Cells/XLCellTests.cs @@ -59,8 +59,13 @@ IXLCell cell = ws.Cell("A1"); var doubleList = new List { 1.0 / 0.0 }; + cell.Value = 5; cell.Value = doubleList; - Assert.AreNotEqual(XLDataType.Number, cell.DataType); + Assert.AreEqual(XLDataType.Text, cell.DataType); + Assert.AreEqual(CultureInfo.CurrentCulture.NumberFormat.PositiveInfinitySymbol, cell.Value); + + cell.Value = 5; + Assert.Throws(() => cell.SetValue(doubleList)); } [Test] @@ -70,8 +75,13 @@ IXLCell cell = ws.Cell("A1"); var doubleList = new List { 0.0 / 0.0 }; + cell.Value = 5; cell.Value = doubleList; - Assert.AreNotEqual(XLDataType.Number, cell.DataType); + Assert.AreEqual(XLDataType.Text, cell.DataType); + Assert.AreEqual(CultureInfo.CurrentCulture.NumberFormat.NaNSymbol, cell.Value); + + cell.Value = 5; + Assert.Throws(() => cell.SetValue(doubleList)); } [Test] @@ -332,25 +342,37 @@ [Test] public void ValueSetToEmptyString() { + string expected = String.Empty; + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); IXLCell cell = ws.Cell(1, 1); cell.Value = new DateTime(2000, 1, 2); cell.Value = String.Empty; - string actual = cell.GetString(); - string expected = String.Empty; - Assert.AreEqual(expected, actual); + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); + + cell.Value = new DateTime(2000, 1, 2); + cell.SetValue(string.Empty); + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); } [Test] public void ValueSetToNull() { + string expected = String.Empty; + IXLWorksheet ws = new XLWorkbook().Worksheets.Add("Sheet1"); IXLCell cell = ws.Cell(1, 1); cell.Value = new DateTime(2000, 1, 2); cell.Value = null; - string actual = cell.GetString(); - string expected = String.Empty; - Assert.AreEqual(expected, actual); + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); + + cell.Value = new DateTime(2000, 1, 2); + cell.SetValue(null as string); + Assert.AreEqual(expected, cell.GetString()); + Assert.AreEqual(expected, cell.Value); } [Test] @@ -370,6 +392,114 @@ } [Test] + public void SetStringCellValues() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var cell = ws.FirstCell(); + + object expected; + + var date = new DateTime(2018, 4, 18); + expected = date.ToInvariantString(); + cell.Value = expected; + Assert.AreEqual(XLDataType.DateTime, cell.DataType); + Assert.AreEqual(date, cell.Value); + + var b = true; + expected = b.ToInvariantString(); + cell.Value = expected; + Assert.AreEqual(XLDataType.Boolean, cell.DataType); + Assert.AreEqual(b, cell.Value); + + var ts = new TimeSpan(8, 12, 4); + expected = ts.ToInvariantString(); + cell.Value = expected; + Assert.AreEqual(XLDataType.TimeSpan, cell.DataType); + Assert.AreEqual(ts, cell.Value); + } + } + + [Test] + public void SetStringValueTooLong() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = new DateTime(2018, 5, 15); + + ws.FirstCell().SetValue(new String('A', 32767)); + + Assert.Throws(() => ws.FirstCell().Value = new String('A', 32768)); + Assert.Throws(() => ws.FirstCell().SetValue(new String('A', 32768))); + } + } + + [Test] + public void SetDateOutOfRange() + { + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-ZA"); + + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = 5; + + var date = XLCell.BaseDate.AddDays(-1); + ws.FirstCell().Value = date; + + // Should default to string representation using current culture's date format + Assert.AreEqual(XLDataType.Text, ws.FirstCell().DataType); + Assert.AreEqual(date.ToString(), ws.FirstCell().Value); + + Assert.Throws(() => ws.FirstCell().SetValue(XLCell.BaseDate.AddDays(-1))); + } + } + + [Test] + public void SetCellValueWipesFormulas() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().FormulaA1 = "=TODAY()"; + ws.FirstCell().Value = "hello world"; + Assert.IsFalse(ws.FirstCell().HasFormula); + + ws.FirstCell().FormulaA1 = "=TODAY()"; + ws.FirstCell().SetValue("hello world"); + Assert.IsFalse(ws.FirstCell().HasFormula); + } + } + + [Test] + public void CellValueLineWrapping() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + + ws.FirstCell().Value = "hello world"; + Assert.IsFalse(ws.FirstCell().Style.Alignment.WrapText); + + ws.FirstCell().Value = "hello\r\nworld"; + Assert.IsTrue(ws.FirstCell().Style.Alignment.WrapText); + + ws.FirstCell().Style.Alignment.WrapText = false; + + ws.FirstCell().SetValue("hello world"); + Assert.IsFalse(ws.FirstCell().Style.Alignment.WrapText); + + ws.FirstCell().SetValue("hello\r\nworld"); + Assert.IsTrue(ws.FirstCell().Style.Alignment.WrapText); + } + } + + [Test] public void TestInvalidXmlCharacters() { byte[] data;