diff --git a/SpreadCheetah.Test/Tests/AlignmentTests.cs b/SpreadCheetah.Test/Tests/AlignmentTests.cs new file mode 100644 index 00000000..de34f296 --- /dev/null +++ b/SpreadCheetah.Test/Tests/AlignmentTests.cs @@ -0,0 +1,16 @@ +using SpreadCheetah.Styling; + +namespace SpreadCheetah.Test.Tests; + +public class AlignmentTests +{ + [Fact] + public void Alignment_Indent_InvalidValue() + { + // Arrange + var alignment = new Alignment(); + + // Act & Assert + Assert.Throws(() => alignment.Indent = -1); + } +} diff --git a/SpreadCheetah.Test/Tests/NamedStyleTests.cs b/SpreadCheetah.Test/Tests/NamedStyleTests.cs new file mode 100644 index 00000000..ffe79f92 --- /dev/null +++ b/SpreadCheetah.Test/Tests/NamedStyleTests.cs @@ -0,0 +1,269 @@ +using OfficeOpenXml; +using SpreadCheetah.Styling; +using SpreadCheetah.Test.Helpers; +using System.Drawing; + +namespace SpreadCheetah.Test.Tests; + +public class NamedStyleTests +{ + private static readonly SpreadCheetahOptions SpreadCheetahOptions = new() { BufferSize = SpreadCheetahOptions.MinimumBufferSize }; + + [Theory] + [InlineData(StyleNameVisibility.Visible)] + [InlineData(StyleNameVisibility.Hidden)] + public async Task Spreadsheet_AddStyle_NamedStyle(StyleNameVisibility visibility) + { + // Arrange + using var stream = new MemoryStream(); + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, SpreadCheetahOptions); + await spreadsheet.StartWorksheetAsync("My sheet"); + var style = new Style { Font = { Bold = true } }; + const string name = "My bold style"; + + // Act + var addedStyleId = spreadsheet.AddStyle(style, name, visibility); + var returnedStyleId = spreadsheet.GetStyleId(name); + await spreadsheet.FinishAsync(); + + // Assert + Assert.Equal(addedStyleId, returnedStyleId); + SpreadsheetAssert.Valid(stream); + using var package = new ExcelPackage(stream); + var namedStyles = package.Workbook.Styles.NamedStyles; + var namedStyle = Assert.Single(namedStyles, x => !x.Name.Equals("Normal", StringComparison.Ordinal)); + Assert.Equal(name, namedStyle.Name); + Assert.True(namedStyle.Style.Font.Bold); + } + + [Fact] + public async Task Spreadsheet_AddStyle_NamedStyleWithNoVisiblity() + { + // Arrange + using var stream = new MemoryStream(); + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, SpreadCheetahOptions); + await spreadsheet.StartWorksheetAsync("My sheet"); + var style = new Style { Font = { Bold = true } }; + const string name = "My bold style"; + + // Act + var addedStyleId = spreadsheet.AddStyle(style, name); + var returnedStyleId = spreadsheet.GetStyleId(name); + await spreadsheet.FinishAsync(); + + // Assert + Assert.Equal(addedStyleId, returnedStyleId); + SpreadsheetAssert.Valid(stream); + using var package = new ExcelPackage(stream); + var namedStyle = Assert.Single(package.Workbook.Styles.NamedStyles); + Assert.Equal("Normal", namedStyle.Name); + Assert.False(namedStyle.Style.Font.Bold); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Spreadsheet_AddStyle_NamedStyleUsedByCell(bool withDefaultDateTimeFormat) + { + // Arrange + using var stream = new MemoryStream(); + var options = new SpreadCheetahOptions { BufferSize = SpreadCheetahOptions.MinimumBufferSize }; + if (!withDefaultDateTimeFormat) + options.DefaultDateTimeFormat = null; + + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, options); + await spreadsheet.StartWorksheetAsync("My sheet"); + var style = new Style { Font = { Bold = true } }; + const string name = "My bold style"; + + // Act + var styleId = spreadsheet.AddStyle(style, name, StyleNameVisibility.Visible); + await spreadsheet.AddRowAsync([new StyledCell("My cell", styleId)]); + await spreadsheet.FinishAsync(); + + // Assert + SpreadsheetAssert.Valid(stream); + using var package = new ExcelPackage(stream); + var worksheet = Assert.Single(package.Workbook.Worksheets); + var actualCell = Assert.Single(worksheet.Cells); + Assert.Equal(name, actualCell.StyleName); + Assert.True(actualCell.Style.Font.Bold); + } + + [Fact] + public async Task Spreadsheet_AddStyle_NamedStyleUsedByMultipleCells() + { + // Arrange + using var stream = new MemoryStream(); + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, SpreadCheetahOptions); + var style = new Style { Font = { Bold = true } }; + const string name = "My bold style"; + + // Act + var styleId = spreadsheet.AddStyle(style, name, StyleNameVisibility.Visible); + await spreadsheet.StartWorksheetAsync("Sheet 1"); + await spreadsheet.AddRowAsync([new StyledCell("A1", styleId), new StyledCell(2, null), new StyledCell(3, styleId)]); + await spreadsheet.AddRowAsync([new StyledCell("A2", styleId)]); + await spreadsheet.StartWorksheetAsync("Sheet 2"); + await spreadsheet.AddRowAsync([new StyledCell("A1", styleId)]); + await spreadsheet.FinishAsync(); + + // Assert + SpreadsheetAssert.Valid(stream); + using var package = new ExcelPackage(stream); + var worksheet1 = package.Workbook.Worksheets["Sheet 1"]; + Assert.Equal(name, worksheet1.Cells["A1"].StyleName); + Assert.NotEqual(name, worksheet1.Cells["B2"].StyleName); + Assert.Equal(name, worksheet1.Cells["C1"].StyleName); + Assert.Equal(name, worksheet1.Cells["A2"].StyleName); + + var worksheet2 = package.Workbook.Worksheets["Sheet 2"]; + Assert.Equal(name, worksheet2.Cells["A1"].StyleName); + } + + [Fact] + public async Task Spreadsheet_AddStyle_MultipleNamedStylesUsedByMultipleCells() + { + // Arrange + using var stream = new MemoryStream(); + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, SpreadCheetahOptions); + (string Name, Style Style)[] styles = + [ + ("Bold", new Style { Font = { Bold = true } }), + ("Italic", new Style { Font = { Italic = true } }), + ("Red", new Style { Fill = { Color = Color.Red } }), + ("Blue", new Style { Fill = { Color = Color.Blue } }) + ]; + + // Act + var styleIds = styles.Select(x => spreadsheet.AddStyle(x.Style, x.Name, StyleNameVisibility.Visible)).ToList(); + + await spreadsheet.StartWorksheetAsync("Sheet 1"); + await spreadsheet.AddRowAsync([new StyledCell("A1", styleIds[0]), new StyledCell(2, null), new StyledCell(3, styleIds[1])]); + await spreadsheet.AddRowAsync([new StyledCell("A2", styleIds[2])]); + await spreadsheet.StartWorksheetAsync("Sheet 2"); + await spreadsheet.AddRowAsync([new StyledCell("A1", styleIds[3])]); + await spreadsheet.FinishAsync(); + + // Assert + SpreadsheetAssert.Valid(stream); + using var package = new ExcelPackage(stream); + var worksheet1 = package.Workbook.Worksheets["Sheet 1"]; + Assert.Equal(styles[0].Name, worksheet1.Cells["A1"].StyleName); + Assert.Equal(styles[1].Name, worksheet1.Cells["C1"].StyleName); + Assert.Equal(styles[2].Name, worksheet1.Cells["A2"].StyleName); + + var worksheet2 = package.Workbook.Worksheets["Sheet 2"]; + Assert.Equal(styles[3].Name, worksheet2.Cells["A1"].StyleName); + } + + [Fact] + public async Task Spreadsheet_AddStyle_NamedStyleWithInvalidVisibility() + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + var style = new Style { Font = { Bold = true } }; + + // Act & Assert + Assert.Throws(() => spreadsheet.AddStyle(style, "Name", (StyleNameVisibility)3)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Style")] + [InlineData("Style ")] + [InlineData("Normal")] + public async Task Spreadsheet_AddStyle_NamedStyleWithInvalidName(string? name) + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + var style = new Style { Font = { Bold = true } }; + + // Act & Assert + Assert.ThrowsAny(() => spreadsheet.AddStyle(style, name!)); + } + + [Fact] + public async Task Spreadsheet_AddStyle_NamedStyleWithTooLongName() + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + var style = new Style { Font = { Bold = true } }; + var name = new string('c', 256); + + // Act & Assert + Assert.Throws(() => spreadsheet.AddStyle(style, name)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Spreadsheet_AddStyle_NamedStyleWithDuplicateName(bool differentCasing) + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + + var style = new Style { Font = { Bold = true } }; + const string name = "My bold style"; + spreadsheet.AddStyle(style, name); + + var otherStyle = new Style { Font = { Italic = true } }; + var duplicateName = differentCasing ? name.ToUpperInvariant() : name; + + // Act & Assert + Assert.Throws(() => spreadsheet.AddStyle(otherStyle, duplicateName)); + } + + [Fact] + public async Task Spreadsheet_AddStyle_DuplicateNamedStylesReturnsDifferentStyleIds() + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + var style1 = new Style { Font = { Bold = true } }; + var style2 = style1 with { }; + + // Act + var styleId1 = spreadsheet.AddStyle(style1, "Style 1"); + var styleId2 = spreadsheet.AddStyle(style2, "Style 2"); + + // Assert + Assert.NotEqual(styleId1.Id, styleId2.Id); + } + + [Fact] + public async Task Spreadsheet_AddStyle_NamedStyleDuplicateOfUnnamedStyleReturnsDifferentStyleIds() + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + var style1 = new Style { Font = { Bold = true } }; + var style2 = style1 with { }; + + // Act + var styleId1 = spreadsheet.AddStyle(style1); + var styleId2 = spreadsheet.AddStyle(style2, "Style 2"); + + // Assert + Assert.NotEqual(styleId1.Id, styleId2.Id); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Spreadsheet_GetStyleId_IncorrectName(bool existingNamedStyle) + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, SpreadCheetahOptions); + + if (existingNamedStyle) + { + var style = new Style { Font = { Bold = true } }; + const string name = "My bold style"; + spreadsheet.AddStyle(style, name); + } + + // Act & Assert + Assert.Throws(() => spreadsheet.GetStyleId("Other style")); + } +} diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt index 3d962e2c..bcd3f900 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt @@ -116,10 +116,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -400,6 +402,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt index 3ec073e4..96336d6b 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt @@ -116,10 +116,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -400,6 +402,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt index b7b215bb..07882bbe 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt @@ -116,10 +116,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -400,6 +402,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt index 5f6676c6..d91077f9 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt @@ -115,10 +115,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -399,6 +401,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/SpreadsheetImageTests.cs b/SpreadCheetah.Test/Tests/SpreadsheetImageTests.cs index f936a173..cb7d4e2b 100644 --- a/SpreadCheetah.Test/Tests/SpreadsheetImageTests.cs +++ b/SpreadCheetah.Test/Tests/SpreadsheetImageTests.cs @@ -92,6 +92,22 @@ public async Task Spreadsheet_EmbedImage_FinishedSpreadsheet() Assert.Contains(nameof(Spreadsheet.FinishAsync), concreteException.Message, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task Spreadsheet_EmbedImage_InvalidFile() + { + // Arrange + await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null); + var bytes = "Invalid file"u8.ToArray(); + var imageStream = new MemoryStream(bytes); + + // Act + var exception = await Record.ExceptionAsync(() => spreadsheet.EmbedImageAsync(imageStream).AsTask()); + + // Assert + var concreteException = Assert.IsType(exception); + Assert.Equal("stream", concreteException.ParamName); + } + [Theory] [InlineData("red-1x1.png", 1, 1)] [InlineData("green-266x183.png", 266, 183)] diff --git a/SpreadCheetah.Test/Tests/SpreadsheetStyledRowTests.cs b/SpreadCheetah.Test/Tests/SpreadsheetStyledRowTests.cs index 82ca5da6..a31d4a29 100644 --- a/SpreadCheetah.Test/Tests/SpreadsheetStyledRowTests.cs +++ b/SpreadCheetah.Test/Tests/SpreadsheetStyledRowTests.cs @@ -5,7 +5,6 @@ using SpreadCheetah.Test.Helpers; using SpreadCheetah.Test.Helpers.Backporting; using System.Globalization; -using Xunit; using Alignment = SpreadCheetah.Styling.Alignment; using Border = SpreadCheetah.Styling.Border; using CellType = SpreadCheetah.Test.Helpers.CellType; @@ -484,6 +483,39 @@ public async Task Spreadsheet_AddRow_MultipleStylesWithTheSameCustomNumberFormat Assert.Equal(styles.Length, actualCells.Count()); } + [Theory] + [MemberData(nameof(TrueAndFalse))] + public async Task Spreadsheet_AddRow_DateTimeCell(bool withDefaultDateTimeFormat, CellType type, RowCollectionType rowType) + { + // Arrange + var options = new SpreadCheetahOptions { BufferSize = SpreadCheetahOptions.MinimumBufferSize }; + if (!withDefaultDateTimeFormat) + options.DefaultDateTimeFormat = null; + + var value = new DateTime(2021, 2, 3, 4, 5, 6, DateTimeKind.Unspecified); + using var stream = new MemoryStream(); + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, options); + await spreadsheet.StartWorksheetAsync("Sheet"); + + var style = new Style { Font = { Italic = true } }; + var styleId = spreadsheet.AddStyle(style); + var styledCell = CellFactory.Create(type, value, styleId); + var expectedNumberFormat = withDefaultDateTimeFormat ? @"yyyy\-mm\-dd\ hh:mm:ss" : ""; + + // Act + await spreadsheet.AddRowAsync(styledCell, rowType); + await spreadsheet.FinishAsync(); + + // Assert + SpreadsheetAssert.Valid(stream); + using var workbook = new XLWorkbook(stream); + var worksheet = workbook.Worksheets.Single(); + var actualCell = worksheet.Cell(1, 1); + Assert.Equal(value.ToOADate(), actualCell.Value.GetUnifiedNumber(), 0.0005); + Assert.Equal(expectedNumberFormat, actualCell.Style.NumberFormat.Format); + Assert.True(actualCell.Style.Font.Italic); + } + [Theory] [MemberData(nameof(TrueAndFalse))] public async Task Spreadsheet_AddRow_DateTimeNumberFormat(bool withExplicitNumberFormat, CellType type, RowCollectionType rowType) diff --git a/SpreadCheetah.Test/Tests/SpreadsheetTests.cs b/SpreadCheetah.Test/Tests/SpreadsheetTests.cs index 69d4d9b1..633dbdd5 100644 --- a/SpreadCheetah.Test/Tests/SpreadsheetTests.cs +++ b/SpreadCheetah.Test/Tests/SpreadsheetTests.cs @@ -573,7 +573,7 @@ public async Task Spreadsheet_AddStyle_DuplicateStylesReturnTheSameStyleId() await spreadsheet.StartWorksheetAsync("Book 1"); var style1 = new Style { Fill = new Fill { Color = Color.Bisque } }; - var style2 = new Style { Fill = new Fill { Color = Color.Bisque } }; + var style2 = style1 with { }; // Act var style1Id = spreadsheet.AddStyle(style1); diff --git a/SpreadCheetah/CompatibilitySuppressions.xml b/SpreadCheetah/CompatibilitySuppressions.xml index 7769d601..4ed57789 100644 --- a/SpreadCheetah/CompatibilitySuppressions.xml +++ b/SpreadCheetah/CompatibilitySuppressions.xml @@ -1,34 +1,6 @@  - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/net6.0/SpreadCheetah.dll - lib/net6.0/SpreadCheetah.dll - true - - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/net7.0/SpreadCheetah.dll - lib/net7.0/SpreadCheetah.dll - true - - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/net8.0/SpreadCheetah.dll - lib/net8.0/SpreadCheetah.dll - true - - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/netstandard2.0/SpreadCheetah.dll - lib/netstandard2.0/SpreadCheetah.dll - true - CP0008 T:SpreadCheetah.SourceGeneration.InheritedColumnsOrder @@ -65,6 +37,12 @@ lib/net7.0/SpreadCheetah.dll lib/net8.0/SpreadCheetah.dll + + CP0008 + T:SpreadCheetah.Styling.StyleNameVisibility + lib/net7.0/SpreadCheetah.dll + lib/net8.0/SpreadCheetah.dll + CP0008 T:SpreadCheetah.Styling.VerticalAlignment diff --git a/SpreadCheetah/Helpers/StringExtensions.cs b/SpreadCheetah/Helpers/StringExtensions.cs index 8f9fac38..c86fdb80 100644 --- a/SpreadCheetah/Helpers/StringExtensions.cs +++ b/SpreadCheetah/Helpers/StringExtensions.cs @@ -19,7 +19,7 @@ value is not null && value.Length > maxLength public static void EnsureValidWorksheetName(this string name, [CallerArgumentExpression(nameof(name))] string? paramName = null) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelper.WorksheetNameEmptyOrWhiteSpace(paramName); + ThrowHelper.NameEmptyOrWhiteSpace(paramName); if (name.Length > 31) ThrowHelper.WorksheetNameTooLong(paramName); diff --git a/SpreadCheetah/Helpers/ThrowHelper.cs b/SpreadCheetah/Helpers/ThrowHelper.cs index 2779a48c..a838226f 100644 --- a/SpreadCheetah/Helpers/ThrowHelper.cs +++ b/SpreadCheetah/Helpers/ThrowHelper.cs @@ -49,6 +49,9 @@ internal static class ThrowHelper [DoesNotReturn] public static void ImageScaleTooSmall(string? paramName, float actualValue) => throw new ArgumentOutOfRangeException(paramName, actualValue, "The image scale must result in image width and height being at least 1 pixel."); + [DoesNotReturn] + public static void NameEmptyOrWhiteSpace(string? paramName) => throw new ArgumentException("The name can not be empty or consist only of whitespace.", paramName); + [DoesNotReturn] public static void NoteTextTooLong(string? paramName) => throw new ArgumentException($"Note text can not exceed {SpreadsheetConstants.MaxNoteTextLength} characters.", paramName); @@ -74,13 +77,25 @@ internal static class ThrowHelper public static void StreamContentNotSupportedImageType(string? paramName) => throw new ArgumentException("The stream content is not a supported image type. Currently only PNG images are supported.", nameof(paramName)); [DoesNotReturn] - public static void ValueIsNegative(string? paramName, T value) => throw new ArgumentOutOfRangeException(paramName, value, "The value can not be negative."); + public static void StyleNameAlreadyExists(string? paramName) => throw new ArgumentException("A style with the given name already exists.", paramName); [DoesNotReturn] - public static void WorksheetNameAlreadyExists(string? paramName) => throw new ArgumentException("A worksheet with the given name already exists.", paramName); + public static void StyleNameCanNotEqualNormal(string? paramName) => throw new ArgumentException("The name can not be equal to 'Normal', since that is the name of the default built-in style.", paramName); + + [DoesNotReturn] + public static void StyleNameNotFound(string name) => throw new SpreadCheetahException($"Style with name '{name}' was not found. Make sure the style is first added to the spreadsheet by calling {nameof(Spreadsheet.AddStyle)} with a name argument."); + + [DoesNotReturn] + public static void StyleNameStartsOrEndsWithWhiteSpace(string? paramName) => throw new ArgumentException("The name can not start or end with white-space.", paramName); + + [DoesNotReturn] + public static void StyleNameTooLong(string? paramName) => throw new ArgumentException("The name can not be more than 255 characters.", paramName); [DoesNotReturn] - public static void WorksheetNameEmptyOrWhiteSpace(string? paramName) => throw new ArgumentException("The name can not be empty or consist only of whitespace.", paramName); + public static void ValueIsNegative(string? paramName, T value) => throw new ArgumentOutOfRangeException(paramName, value, "The value can not be negative."); + + [DoesNotReturn] + public static void WorksheetNameAlreadyExists(string? paramName) => throw new ArgumentException("A worksheet with the given name already exists.", paramName); [DoesNotReturn] public static void WorksheetNameInvalidCharacters(string? paramName, string invalidChars) => throw new ArgumentException("The name can not contain any of the following characters: " + invalidChars, paramName); diff --git a/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs b/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs deleted file mode 100644 index 6a257cdf..00000000 --- a/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs +++ /dev/null @@ -1,85 +0,0 @@ -using SpreadCheetah.Helpers; - -namespace SpreadCheetah.MetadataXml; - -internal struct StyleNumberFormatsXml -{ - private readonly List>? _customNumberFormats; - private Element _next; - private int _nextIndex; - - public StyleNumberFormatsXml(List>? customNumberFormats) - { - _customNumberFormats = customNumberFormats; - } - - public bool TryWrite(Span bytes, ref int bytesWritten) - { - if (_next == Element.Header && !Advance(TryWriteHeader(bytes, ref bytesWritten))) return false; - if (_next == Element.NumberFormats && !Advance(TryWriteNumberFormats(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(TryWriteFooter(bytes, ref bytesWritten))) return false; - - return true; - } - - private bool Advance(bool success) - { - if (success) - ++_next; - - return success; - } - - private readonly bool TryWriteHeader(Span bytes, ref int bytesWritten) - { - if (_customNumberFormats is not { } formats) - return """"""u8.TryCopyTo(bytes, ref bytesWritten); - - var span = bytes.Slice(bytesWritten); - var written = 0; - - if (!""u8.TryCopyTo(span, ref written)) return false; - - bytesWritten += written; - return true; - } - - private bool TryWriteNumberFormats(Span bytes, ref int bytesWritten) - { - if (_customNumberFormats is not { } formats) - return true; - - for (; _nextIndex < formats.Count; ++_nextIndex) - { - var format = formats[_nextIndex]; - var span = bytes.Slice(bytesWritten); - var written = 0; - - if (!""u8.TryCopyTo(span, ref written)) return false; - - bytesWritten += written; - } - - return true; - } - - private readonly bool TryWriteFooter(Span bytes, ref int bytesWritten) - => _customNumberFormats is null || ""u8.TryCopyTo(bytes, ref bytesWritten); - - private enum Element - { - Header, - NumberFormats, - Footer, - Done - } -} diff --git a/SpreadCheetah/MetadataXml/StyleBordersXml.cs b/SpreadCheetah/MetadataXml/Styles/BordersXmlPart.cs similarity index 80% rename from SpreadCheetah/MetadataXml/StyleBordersXml.cs rename to SpreadCheetah/MetadataXml/Styles/BordersXmlPart.cs index 331fd76a..ce751b92 100644 --- a/SpreadCheetah/MetadataXml/StyleBordersXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/BordersXmlPart.cs @@ -3,43 +3,48 @@ using SpreadCheetah.Styling.Internal; using System.Drawing; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; -internal struct StyleBordersXml +internal struct BordersXmlPart(List borders, SpreadsheetBuffer buffer) { - private readonly List _borders; private Element _next; private int _nextIndex; - public StyleBordersXml(List borders) + public bool TryWrite() { - _borders = borders; - } - - public bool TryWrite(Span bytes, ref int bytesWritten) - { - if (_next == Element.Header && !Advance(TryWriteHeader(bytes, ref bytesWritten))) return false; - if (_next == Element.Borders && !Advance(TryWriteBorders(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(""u8.TryCopyTo(bytes, ref bytesWritten))) return false; + while (MoveNext()) + { + if (!Current) + return false; + } return true; } - private bool Advance(bool success) + public bool Current { get; private set; } + + public bool MoveNext() { - if (success) + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.Borders => TryWriteBorders(), + _ => buffer.TryWrite(""u8) + }; + + if (Current) ++_next; - return success; + return _next < Element.Done; } - private readonly bool TryWriteHeader(Span bytes, ref int bytesWritten) + private readonly bool TryWriteHeader() { - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; const int defaultCount = 1; - var totalCount = _borders.Count + defaultCount - 1; + var totalCount = borders.Count + defaultCount - 1; if (!" bytes, ref int bytesWritten) ""u8; if (!defaultBorder.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); return true; } - private bool TryWriteBorders(Span bytes, ref int bytesWritten) + private bool TryWriteBorders() { - var defaultBorder = new ImmutableBorder(); - var borders = _borders; + var bordersLocal = borders; - for (; _nextIndex < borders.Count; ++_nextIndex) + for (; _nextIndex < bordersLocal.Count; ++_nextIndex) { - var border = borders[_nextIndex]; - if (border.Equals(defaultBorder)) continue; + var border = bordersLocal[_nextIndex]; + if (border.Equals(default)) continue; - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; var diag = border.Diagonal; @@ -80,7 +84,7 @@ private bool TryWriteBorders(Span bytes, ref int bytesWritten) if (!""u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); } return true; diff --git a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs new file mode 100644 index 00000000..d2818890 --- /dev/null +++ b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs @@ -0,0 +1,101 @@ +using SpreadCheetah.Helpers; +using SpreadCheetah.Styling; +using SpreadCheetah.Styling.Internal; + +namespace SpreadCheetah.MetadataXml.Styles; + +internal struct CellStylesXmlPart( + List<(string, ImmutableStyle, StyleNameVisibility)>? namedStyles, + SpreadsheetBuffer buffer) +{ + private string? _currentXmlEncodedName; + private int _currentXmlEncodedNameIndex; + private Element _next; + private int _nextIndex; + +#pragma warning disable EPS12 // A struct member can be made readonly + public bool TryWrite() +#pragma warning restore EPS12 // A struct member can be made readonly + { + while (MoveNext()) + { + if (!Current) + return false; + } + + return true; + } + + public bool Current { get; private set; } + + public bool MoveNext() + { + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.CellStyles => TryWriteCellStyles(), + _ => buffer.TryWrite(""u8) + }; + + if (Current) + ++_next; + + return _next < Element.Done; + } + + private readonly bool TryWriteHeader() + { + var count = (namedStyles?.Count ?? 0) + 1; + return buffer.TryWrite( + $"{""u8}"); + } + + private bool TryWriteCellStyles() + { + if (namedStyles is not { } namedStylesLocal) + return true; + + for (; _nextIndex < namedStylesLocal.Count; ++_nextIndex) + { + var (name, _, visibility) = namedStylesLocal[_nextIndex]; + var span = buffer.GetSpan(); + var written = 0; + + if (_currentXmlEncodedName is null) + { + if (!"