From 5be169a675d037447c5a22bf83d23da3f6aadc90 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Fri, 24 Jul 2020 17:23:28 -0700 Subject: [PATCH] Add validation to CellType for number types --- src/DocumentFormat.OpenXml/OpenXmlElement.cs | 4 +- .../OpenXmlElementExtensionMethods.cs | 9 +- .../OpenXmlElementList.cs | 4 +- .../Spreadsheet/CellType.cs | 39 ++++++ .../Spreadsheet/CellValue.cs | 92 ++++++++++++- .../Validation/Schema/SchemaTypeValidator.cs | 5 + .../ValidationResources.Designer.cs | 9 ++ .../Validation/ValidationResources.resx | 3 + .../Spreadsheet/CellTests.cs | 39 ++++++ .../Spreadsheet/SpreadsheetCellTests.cs | 128 ++++++++++++++++++ .../SpreadsheetCellTests.cs | 48 ------- 11 files changed, 313 insertions(+), 67 deletions(-) create mode 100644 src/DocumentFormat.OpenXml/Spreadsheet/CellType.cs create mode 100644 test/DocumentFormat.OpenXml.Tests/Spreadsheet/CellTests.cs create mode 100644 test/DocumentFormat.OpenXml.Tests/Spreadsheet/SpreadsheetCellTests.cs delete mode 100644 test/DocumentFormat.OpenXml.Tests/SpreadsheetCellTests.cs diff --git a/src/DocumentFormat.OpenXml/OpenXmlElement.cs b/src/DocumentFormat.OpenXml/OpenXmlElement.cs index 840763bd56..26ae19d36c 100644 --- a/src/DocumentFormat.OpenXml/OpenXmlElement.cs +++ b/src/DocumentFormat.OpenXml/OpenXmlElement.cs @@ -2901,9 +2901,7 @@ internal OpenXmlPartRootElement GetPartRootElement() root = root.Parent; } - var partRootElement = root as OpenXmlPartRootElement; - - return partRootElement; + return root as OpenXmlPartRootElement; } } } diff --git a/src/DocumentFormat.OpenXml/OpenXmlElementExtensionMethods.cs b/src/DocumentFormat.OpenXml/OpenXmlElementExtensionMethods.cs index e579cee964..86eb9c28a2 100644 --- a/src/DocumentFormat.OpenXml/OpenXmlElementExtensionMethods.cs +++ b/src/DocumentFormat.OpenXml/OpenXmlElementExtensionMethods.cs @@ -92,14 +92,7 @@ internal static OpenXmlPart GetPart(this OpenXmlElement element) throw new ArgumentNullException(nameof(element)); } - OpenXmlPartRootElement partRootElement = element.GetPartRootElement(); - - if (partRootElement != null && partRootElement.OpenXmlPart != null) - { - return partRootElement.OpenXmlPart; - } - - return null; + return element.GetPartRootElement()?.OpenXmlPart; } /// diff --git a/src/DocumentFormat.OpenXml/OpenXmlElementList.cs b/src/DocumentFormat.OpenXml/OpenXmlElementList.cs index 472f5750ac..e50ae0482a 100644 --- a/src/DocumentFormat.OpenXml/OpenXmlElementList.cs +++ b/src/DocumentFormat.OpenXml/OpenXmlElementList.cs @@ -80,9 +80,9 @@ public IEnumerable OfType() { foreach (OpenXmlElement item in this) { - if (item is T) + if (item is T t) { - yield return (T)item; + yield return t; } } } diff --git a/src/DocumentFormat.OpenXml/Spreadsheet/CellType.cs b/src/DocumentFormat.OpenXml/Spreadsheet/CellType.cs new file mode 100644 index 0000000000..c696a58d89 --- /dev/null +++ b/src/DocumentFormat.OpenXml/Spreadsheet/CellType.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DocumentFormat.OpenXml.Framework; +using DocumentFormat.OpenXml.Validation; + +namespace DocumentFormat.OpenXml.Spreadsheet +{ + public partial class CellType : IValidator + { + void IValidator.Validate(ValidationContext context) + { + if (DataType is null || !DataType.HasValue) + { + return; + } + + if (CellValue is CellValue value) + { + var success = DataType.Value switch + { + CellValues.Boolean => value.TryGetBoolean(out _), + CellValues.Date => value.TryGetDateTimeOffset(out _) || value.TryGetDateTime(out _), + CellValues.Number => value.TryGetInt(out _) || value.TryGetDouble(out _) || value.TryGetDecimal(out _), + _ => true, + }; + + if (success) + { + context.CreateError( + id: "Sem_CellValue", + errorType: ValidationErrorType.Semantic, + description: string.Format(ValidationResources.Sem_CellValue, value.InnerText, DataType.Value) + ); + } + } + } + } +} diff --git a/src/DocumentFormat.OpenXml/Spreadsheet/CellValue.cs b/src/DocumentFormat.OpenXml/Spreadsheet/CellValue.cs index 341e576870..a30bf09a78 100644 --- a/src/DocumentFormat.OpenXml/Spreadsheet/CellValue.cs +++ b/src/DocumentFormat.OpenXml/Spreadsheet/CellValue.cs @@ -11,6 +11,9 @@ namespace DocumentFormat.OpenXml.Spreadsheet /// public partial class CellValue { + private const string DateTimeFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; + private const string DateTimeOffsetFormatString = DateTimeFormatString + "zzz"; + /// /// Instantiates an instance of for a . Dates must /// be in ISO 8601 format, which this constructor ensures @@ -31,17 +34,94 @@ public CellValue(DateTimeOffset dateTimeOffset) { } - private const string DateTimeFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; - private const string DateTimeOffsetFormatString = DateTimeFormatString + "zzz"; + /// + /// Instantiates an instance of for a . + /// + /// Boolean value + public CellValue(bool value) + : this(value.ToString()) + { + } - private static string ToCellFormat(DateTime dateTime) + /// + /// Instantiates an instance of for a . + /// + /// Number. + public CellValue(double value) + : this(value.ToString()) { - return dateTime.ToString(DateTimeFormatString, CultureInfo.InvariantCulture); } - private static string ToCellFormat(DateTimeOffset dateTime) + /// + /// Instantiates an instance of for a . + /// + /// Number. + public CellValue(int value) + : this(value.ToString()) + { + } + + /// + /// Instantiates an instance of for a . + /// + /// Number. + public CellValue(decimal value) + : this(value.ToString()) { - return dateTime.ToString(DateTimeOffsetFormatString, CultureInfo.InvariantCulture); } + + /// + /// Attempts to parse cell value to retrieve a . + /// + /// The result if successful. + /// Success or failure + public bool TryGetDateTime(out DateTime dt) + => DateTime.TryParseExact(InnerText, DateTimeFormatString, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt); + + /// + /// Attempts to parse cell value to retrieve a . + /// + /// The result if successful. + /// Success or failure + public bool TryGetDateTimeOffset(out DateTimeOffset dt) + => DateTimeOffset.TryParseExact(InnerText, DateTimeOffsetFormatString, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt); + + /// + /// Attempts to parse cell value to retrieve a . + /// + /// The result if successful. + /// Success or failure + public bool TryGetDouble(out double dbl) + => double.TryParse(InnerText, NumberStyles.Number, CultureInfo.InvariantCulture, out dbl); + + /// + /// Attempts to parse cell value to retrieve a . + /// + /// The result if successful. + /// Success or failure + public bool TryGetInt(out int value) + => int.TryParse(InnerText, NumberStyles.Number, CultureInfo.InvariantCulture, out value); + + /// + /// Attempts to parse cell value to retrieve a . + /// + /// The result if successful. + /// Success or failure + public bool TryGetDecimal(out decimal value) + => decimal.TryParse(InnerText, NumberStyles.Number, CultureInfo.InvariantCulture, out value); + + /// + /// Attempts to parse cell value to retrieve a . + /// + /// The result if successful. + /// Success or failure + public bool TryGetBoolean(out bool value) + => bool.TryParse(InnerText, out value); + + private static string ToCellFormat(DateTime dateTime) + => dateTime.ToString(DateTimeFormatString, CultureInfo.InvariantCulture); + + private static string ToCellFormat(DateTimeOffset dateTime) + => dateTime.ToString(DateTimeOffsetFormatString, CultureInfo.InvariantCulture); } } diff --git a/src/DocumentFormat.OpenXml/Validation/Schema/SchemaTypeValidator.cs b/src/DocumentFormat.OpenXml/Validation/Schema/SchemaTypeValidator.cs index 90416f05b9..f4ec2cfc4c 100644 --- a/src/DocumentFormat.OpenXml/Validation/Schema/SchemaTypeValidator.cs +++ b/src/DocumentFormat.OpenXml/Validation/Schema/SchemaTypeValidator.cs @@ -45,6 +45,11 @@ public static void Validate(ValidationContext validationContext) // validate Ignorable, ProcessContent, etc. compatibility-rule attributes CompatibilityRuleAttributesValidator.ValidateMcAttributes(validationContext); + if (theElement is IValidator validator) + { + validator.Validate(validationContext); + } + ValidateAttributes(validationContext); // validate particles diff --git a/src/DocumentFormat.OpenXml/Validation/ValidationResources.Designer.cs b/src/DocumentFormat.OpenXml/Validation/ValidationResources.Designer.cs index dcebd424c7..22939c9a2b 100644 --- a/src/DocumentFormat.OpenXml/Validation/ValidationResources.Designer.cs +++ b/src/DocumentFormat.OpenXml/Validation/ValidationResources.Designer.cs @@ -574,6 +574,15 @@ internal static string Sem_AttributeValueUniqueInDocument { } } + /// + /// Looks up a localized string similar to Cell contents have invalid value '{0}' for type '{1}'.. + /// + internal static string Sem_CellValue { + get { + return ResourceManager.GetString("Sem_CellValue", resourceCulture); + } + } + /// /// Looks up a localized string similar to Relationship '{0}' referenced by attribute '{1}' has incorrect type. Its type should be '{2}'.. /// diff --git a/src/DocumentFormat.OpenXml/Validation/ValidationResources.resx b/src/DocumentFormat.OpenXml/Validation/ValidationResources.resx index 050744b59f..ed8489361d 100644 --- a/src/DocumentFormat.OpenXml/Validation/ValidationResources.resx +++ b/src/DocumentFormat.OpenXml/Validation/ValidationResources.resx @@ -339,4 +339,7 @@ Invalid document error: more than one part retrieved for one URI. + + Cell contents have invalid value '{0}' for type '{1}'. + \ No newline at end of file diff --git a/test/DocumentFormat.OpenXml.Tests/Spreadsheet/CellTests.cs b/test/DocumentFormat.OpenXml.Tests/Spreadsheet/CellTests.cs new file mode 100644 index 0000000000..4ee206f32e --- /dev/null +++ b/test/DocumentFormat.OpenXml.Tests/Spreadsheet/CellTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DocumentFormat.OpenXml.Spreadsheet; +using DocumentFormat.OpenXml.Validation; +using Xunit; + +namespace DocumentFormat.OpenXml.Tests +{ + public class CellTests + { + [InlineData("StringValue", CellValues.Number, false)] + [InlineData("1", CellValues.Number, true)] + [InlineData("1.0", CellValues.Number, true)] + [InlineData("-1.0", CellValues.Number, true)] + [InlineData("StringValue", CellValues.String, true)] + [Theory] + public void CellValidationTest(string value, CellValues type, bool success) + { + var cell = new Cell + { + CellValue = new CellValue(value), + DataType = type, + }; + + var validator = new OpenXmlValidator(); + var results = validator.Validate(cell); + + if (success) + { + Assert.Empty(results); + } + else + { + Assert.Single(results); + } + } + } +} diff --git a/test/DocumentFormat.OpenXml.Tests/Spreadsheet/SpreadsheetCellTests.cs b/test/DocumentFormat.OpenXml.Tests/Spreadsheet/SpreadsheetCellTests.cs new file mode 100644 index 0000000000..c154282a17 --- /dev/null +++ b/test/DocumentFormat.OpenXml.Tests/Spreadsheet/SpreadsheetCellTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DocumentFormat.OpenXml.Spreadsheet; +using System; +using System.Collections.Generic; +using Xunit; + +namespace DocumentFormat.OpenXml.Tests +{ + public class SpreadsheetCellTests + { + [Fact] + public void CellDateTimeTest() + { + var dt = new DateTime(2017, 11, 28, 12, 25, 2); + var value = new CellValue(dt); + + Assert.Equal("2017-11-28T12:25:02.000", value.Text); + + Assert.True(value.TryGetDateTime(out var result)); + Assert.Equal(dt, result); + } + + [Fact] + public void CellDateTimeOffsetTest() + { + var dt = new DateTimeOffset(2017, 11, 28, 12, 25, 2, TimeSpan.Zero); + var value = new CellValue(dt); + + Assert.Equal("2017-11-28T12:25:02.000+00:00", value.Text); + + Assert.True(value.TryGetDateTimeOffset(out var result)); + Assert.Equal(dt, result); + } + + [Fact] + public void CellDateTimeWithMillisecondsTest() + { + var dt = new DateTime(2017, 11, 28, 12, 25, 2).AddMilliseconds(123); + var value = new CellValue(dt); + + Assert.Equal("2017-11-28T12:25:02.123", value.Text); + + Assert.True(value.TryGetDateTime(out var result)); + Assert.Equal(dt, result); + } + + [Fact] + public void CellDateTimeOffsetWithMillisecondsTest() + { + var dt = new DateTimeOffset(2017, 11, 28, 12, 25, 2, TimeSpan.Zero).AddMilliseconds(123); + var value = new CellValue(dt); + + Assert.Equal("2017-11-28T12:25:02.123+00:00", value.Text); + Assert.True(value.TryGetDateTimeOffset(out var result)); + Assert.Equal(dt, result); + } + + [InlineData(-1.5)] + [InlineData(-1.0)] + [InlineData(0.0)] + [InlineData(1.0)] + [InlineData(1.5)] + [Theory] + public void CellDoubleTest(double num) + { + var value = new CellValue(num); + + Assert.Equal(num.ToString(), value.Text); + Assert.Equal(num.ToString(), value.InnerText); + Assert.Equal(@$"{num}", value.OuterXml); + Assert.True(value.TryGetDouble(out var result)); + Assert.Equal(num, result); + } + + [InlineData(int.MinValue)] + [InlineData(int.MinValue + 1)] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + [InlineData(int.MaxValue - 1)] + [InlineData(int.MaxValue)] + [Theory] + public void CellIntTest(int num) + { + var value = new CellValue(num); + + Assert.Equal(num.ToString(), value.Text); + Assert.Equal(num.ToString(), value.InnerText); + Assert.Equal(@$"{num}", value.OuterXml); + Assert.True(value.TryGetInt(out var result)); + Assert.Equal(num, result); + } + + [MemberData(nameof(DecimalTests))] + [Theory] + public void CellDecimalTest(decimal num) + { + var value = new CellValue(num); + + Assert.Equal(num.ToString(), value.Text); + Assert.Equal(num.ToString(), value.InnerText); + Assert.Equal(@$"{num}", value.OuterXml); + Assert.True(value.TryGetDecimal(out var result)); + Assert.Equal(num, result); + } + + private static readonly decimal[] _decimalValues = new decimal[] + { + decimal.MinValue, + decimal.MinValue + 1, + -1M, + 0M, + 1M, + decimal.MaxValue - 1, + decimal.MaxValue, + }; + + public static IEnumerable DecimalTests() + { + foreach (var v in _decimalValues) + { + yield return new object[] { v }; + } + } + } +} diff --git a/test/DocumentFormat.OpenXml.Tests/SpreadsheetCellTests.cs b/test/DocumentFormat.OpenXml.Tests/SpreadsheetCellTests.cs deleted file mode 100644 index c5937b2d59..0000000000 --- a/test/DocumentFormat.OpenXml.Tests/SpreadsheetCellTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using DocumentFormat.OpenXml.Spreadsheet; -using System; -using Xunit; - -namespace DocumentFormat.OpenXml.Tests -{ - public class SpreadsheetCellTests - { - [Fact] - public void CellDateTimeTest() - { - var dt = new DateTime(2017, 11, 28, 12, 25, 2); - var value = new CellValue(dt); - - Assert.Equal("2017-11-28T12:25:02.000", value.Text); - } - - [Fact] - public void CellDateTimeOffsetTest() - { - var dt = new DateTimeOffset(2017, 11, 28, 12, 25, 2, TimeSpan.Zero); - var value = new CellValue(dt); - - Assert.Equal("2017-11-28T12:25:02.000+00:00", value.Text); - } - - [Fact] - public void CellDateTimeWithMillisecondsTest() - { - var dt = new DateTime(2017, 11, 28, 12, 25, 2).AddMilliseconds(123); - var value = new CellValue(dt); - - Assert.Equal("2017-11-28T12:25:02.123", value.Text); - } - - [Fact] - public void CellDateTimeOffsetWithMillisecondsTest() - { - var dt = new DateTimeOffset(2017, 11, 28, 12, 25, 2, TimeSpan.Zero).AddMilliseconds(123); - var value = new CellValue(dt); - - Assert.Equal("2017-11-28T12:25:02.123+00:00", value.Text); - } - } -}