Skip to content

Commit

Permalink
Add validation to CellType for number types
Browse files Browse the repository at this point in the history
  • Loading branch information
twsouthwick committed Jul 25, 2020
1 parent d3175aa commit 5be169a
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 67 deletions.
4 changes: 1 addition & 3 deletions src/DocumentFormat.OpenXml/OpenXmlElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2901,9 +2901,7 @@ internal OpenXmlPartRootElement GetPartRootElement()
root = root.Parent;
}

var partRootElement = root as OpenXmlPartRootElement;

return partRootElement;
return root as OpenXmlPartRootElement;
}
}
}
9 changes: 1 addition & 8 deletions src/DocumentFormat.OpenXml/OpenXmlElementExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/DocumentFormat.OpenXml/OpenXmlElementList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ public IEnumerable<T> OfType<T>()
{
foreach (OpenXmlElement item in this)
{
if (item is T)
if (item is T t)
{
yield return (T)item;
yield return t;
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions src/DocumentFormat.OpenXml/Spreadsheet/CellType.cs
Original file line number Diff line number Diff line change
@@ -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)
);
}
}
}
}
}
92 changes: 86 additions & 6 deletions src/DocumentFormat.OpenXml/Spreadsheet/CellValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ namespace DocumentFormat.OpenXml.Spreadsheet
/// </summary>
public partial class CellValue
{
private const string DateTimeFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff";
private const string DateTimeOffsetFormatString = DateTimeFormatString + "zzz";

/// <summary>
/// Instantiates an instance of <see cref="CellValue"/> for a <see cref="DateTime"/>. Dates must
/// be in ISO 8601 format, which this constructor ensures
Expand All @@ -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";
/// <summary>
/// Instantiates an instance of <see cref="CellValue"/> for a <see cref="bool"/>.
/// </summary>
/// <param name="value">Boolean value</param>
public CellValue(bool value)
: this(value.ToString())
{
}

private static string ToCellFormat(DateTime dateTime)
/// <summary>
/// Instantiates an instance of <see cref="CellValue"/> for a <see cref="double"/>.
/// </summary>
/// <param name="value">Number.</param>
public CellValue(double value)
: this(value.ToString())
{
return dateTime.ToString(DateTimeFormatString, CultureInfo.InvariantCulture);
}

private static string ToCellFormat(DateTimeOffset dateTime)
/// <summary>
/// Instantiates an instance of <see cref="CellValue"/> for a <see cref="int"/>.
/// </summary>
/// <param name="value">Number.</param>
public CellValue(int value)
: this(value.ToString())
{
}

/// <summary>
/// Instantiates an instance of <see cref="CellValue"/> for a <see cref="decimal"/>.
/// </summary>
/// <param name="value">Number.</param>
public CellValue(decimal value)
: this(value.ToString())
{
return dateTime.ToString(DateTimeOffsetFormatString, CultureInfo.InvariantCulture);
}

/// <summary>
/// Attempts to parse cell value to retrieve a <see cref="DateTime"/>.
/// </summary>
/// <param name="dt">The result if successful.</param>
/// <returns>Success or failure</returns>
public bool TryGetDateTime(out DateTime dt)
=> DateTime.TryParseExact(InnerText, DateTimeFormatString, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt);

/// <summary>
/// Attempts to parse cell value to retrieve a <see cref="DateTimeOffset"/>.
/// </summary>
/// <param name="dt">The result if successful.</param>
/// <returns>Success or failure</returns>
public bool TryGetDateTimeOffset(out DateTimeOffset dt)
=> DateTimeOffset.TryParseExact(InnerText, DateTimeOffsetFormatString, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt);

/// <summary>
/// Attempts to parse cell value to retrieve a <see cref="double"/>.
/// </summary>
/// <param name="dbl">The result if successful.</param>
/// <returns>Success or failure</returns>
public bool TryGetDouble(out double dbl)
=> double.TryParse(InnerText, NumberStyles.Number, CultureInfo.InvariantCulture, out dbl);

/// <summary>
/// Attempts to parse cell value to retrieve a <see cref="int"/>.
/// </summary>
/// <param name="value">The result if successful.</param>
/// <returns>Success or failure</returns>
public bool TryGetInt(out int value)
=> int.TryParse(InnerText, NumberStyles.Number, CultureInfo.InvariantCulture, out value);

/// <summary>
/// Attempts to parse cell value to retrieve a <see cref="decimal"/>.
/// </summary>
/// <param name="value">The result if successful.</param>
/// <returns>Success or failure</returns>
public bool TryGetDecimal(out decimal value)
=> decimal.TryParse(InnerText, NumberStyles.Number, CultureInfo.InvariantCulture, out value);

/// <summary>
/// Attempts to parse cell value to retrieve a <see cref="bool"/>.
/// </summary>
/// <param name="value">The result if successful.</param>
/// <returns>Success or failure</returns>
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,7 @@
<data name="MoreThanOnePartForOneUri" xml:space="preserve">
<value>Invalid document error: more than one part retrieved for one URI.</value>
</data>
<data name="Sem_CellValue" xml:space="preserve">
<value>Cell contents have invalid value '{0}' for type '{1}'.</value>
</data>
</root>
39 changes: 39 additions & 0 deletions test/DocumentFormat.OpenXml.Tests/Spreadsheet/CellTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
128 changes: 128 additions & 0 deletions test/DocumentFormat.OpenXml.Tests/Spreadsheet/SpreadsheetCellTests.cs
Original file line number Diff line number Diff line change
@@ -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(@$"<x:v xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">{num}</x:v>", 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(@$"<x:v xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">{num}</x:v>", 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(@$"<x:v xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"">{num}</x:v>", 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<object[]> DecimalTests()
{
foreach (var v in _decimalValues)
{
yield return new object[] { v };
}
}
}
}
Loading

0 comments on commit 5be169a

Please sign in to comment.