Skip to content

Commit

Permalink
Merge pull request #64 from sveinungf/dev/xmlwriter-iterator
Browse files Browse the repository at this point in the history
WorksheetStartXml iterator
  • Loading branch information
sveinungf authored Jul 12, 2024
2 parents d4ba3a4 + 000d4ab commit 4512d14
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 45 deletions.
14 changes: 7 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
</PropertyGroup>
<ItemGroup>
<GlobalPackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.4.0-beta.1" />
<GlobalPackageReference Include="Meziantou.Analyzer" Version="2.0.159" />
<GlobalPackageReference Include="Meziantou.Analyzer" Version="2.0.160" />
<GlobalPackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<GlobalPackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48" />
<GlobalPackageReference Include="PrimaryConstructorAnalyzer" Version="1.0.6" />
<GlobalPackageReference Include="Roslynator.Analyzers" Version="4.12.4" />
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="9.28.0.94264" />
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="9.29.0.95321" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
Expand All @@ -26,17 +26,17 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Polyfill" Version="5.5.3" />
<PackageVersion Include="Polyfill" Version="5.6.0" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
<PackageVersion Include="PublicApiGenerator" Version="11.1.0" />
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="TngTech.ArchUnitNET.xUnit" Version="0.10.6" />
<PackageVersion Include="Verify.SourceGenerators" Version="2.2.0" />
<PackageVersion Include="Verify.Xunit" Version="25.0.4" />
<PackageVersion Include="xunit" Version="2.8.1" />
<PackageVersion Include="xunit.assert" Version="2.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageVersion Include="Verify.Xunit" Version="25.3.1" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.assert" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>
39 changes: 39 additions & 0 deletions SpreadCheetah.Benchmark/Benchmarks/MultipleWorksheets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using SpreadCheetah.Worksheets;

namespace SpreadCheetah.Benchmark.Benchmarks;

[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MultipleWorksheets
{
private static readonly SpreadCheetahOptions Options = new() { DefaultDateTimeFormat = null };

[Benchmark]
public async Task SpreadCheetah()
{
await using var spreadsheet = await Spreadsheet.CreateNewAsync(Stream.Null, Options);
var worksheetOptions = new WorksheetOptions();
worksheetOptions.Column(1).Width = 100;
worksheetOptions.Column(2).Hidden = false;

for (var i = 0; i < 1000; ++i)
{
if (i % 2 == 0)
{
await spreadsheet.StartWorksheetAsync(i.ToString());
continue;
}

worksheetOptions.FrozenColumns = i % 4 == 0 ? 1 : null;
worksheetOptions.FrozenRows = i % 8 == 0 ? 1 : null;
worksheetOptions.Visibility = i % 16 == 0 ? WorksheetVisibility.Hidden : WorksheetVisibility.Visible;
worksheetOptions.Column(2).Hidden = i % 32 == 0;

await spreadsheet.StartWorksheetAsync(i.ToString(), worksheetOptions);
}

await spreadsheet.FinishAsync();
}
}
7 changes: 7 additions & 0 deletions SpreadCheetah.Test/Helpers/DoubleEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SpreadCheetah.Test.Helpers;

internal sealed class DoubleEqualityComparer(double tolerance) : IEqualityComparer<double>
{
public bool Equals(double x, double y) => Math.Abs(x - y) <= tolerance;
public int GetHashCode(double obj) => obj.GetHashCode();
}
58 changes: 42 additions & 16 deletions SpreadCheetah.Test/Tests/SpreadsheetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,27 +324,53 @@ public async Task Spreadsheet_StartWorksheet_MultipleWorksheets(int count)
// Arrange
var sheetNames = Enumerable.Range(1, count).Select(x => "Sheet " + x).ToList();
using var stream = new MemoryStream();
await using (var spreadsheet = await Spreadsheet.CreateNewAsync(stream, new SpreadCheetahOptions { BufferSize = SpreadCheetahOptions.MinimumBufferSize }))
{
// Act
foreach (var name in sheetNames)
{
await spreadsheet.StartWorksheetAsync(name);
await spreadsheet.AddRowAsync(new DataCell(name));
}
var spreadsheetOptions = new SpreadCheetahOptions { BufferSize = SpreadCheetahOptions.MinimumBufferSize };
await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, spreadsheetOptions);

await spreadsheet.FinishAsync();
// Act
foreach (var name in sheetNames)
{
await spreadsheet.StartWorksheetAsync(name);
await spreadsheet.AddRowAsync(new DataCell(name));
}

await spreadsheet.FinishAsync();

// Assert
SpreadsheetAssert.Valid(stream);
using var actual = SpreadsheetDocument.Open(stream, true);
var sheets = actual.WorkbookPart!.Workbook.Sheets!.Cast<Sheet>().ToList();
var worksheets = actual.WorkbookPart.WorksheetParts.Select(x => x.Worksheet);
var cells = worksheets.Select(x => x.Descendants<DocumentFormat.OpenXml.Spreadsheet.Cell>().Single());
using var sheets = SpreadsheetAssert.Sheets(stream);
Assert.Equal(count, sheets.Count);
Assert.Equal(sheetNames, sheets.Select(x => x.Name?.Value));
Assert.Equal(sheetNames, cells.Select(x => x.InnerText));
Assert.Equal(sheetNames, sheets.Select(x => x.Name));
Assert.Equal(sheetNames, sheets.Select(x => x["A1"].StringValue));
}

[Theory]
[InlineData(2)]
[InlineData(10)]
[InlineData(100)]
[InlineData(2000)]
[InlineData(16383)]
public async Task Spreadsheet_StartWorksheet_WorksheetWithMultipleColumnOptions(int count)
{
// Arrange
var columnWidths = Enumerable.Range(1, count).Select(x => 20d + (x % 100)).ToList();
using var stream = new MemoryStream();
var spreadsheetOptions = new SpreadCheetahOptions { BufferSize = SpreadCheetahOptions.MinimumBufferSize };
await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream, spreadsheetOptions);
var options = new WorksheetOptions();

// Act
foreach (var (i, columnWidth) in columnWidths.Index())
{
options.Column(i + 1).Width = columnWidth;
}

await spreadsheet.StartWorksheetAsync("Sheet", options);
await spreadsheet.FinishAsync();

// Assert
using var sheet = SpreadsheetAssert.SingleSheet(stream);
Assert.Equal(columnWidths.Count, sheet.Columns.Count);
Assert.Equal(columnWidths, sheet.Columns.Select(x => x.Width), new DoubleEqualityComparer(0.01d));
}

[Theory]
Expand Down
5 changes: 5 additions & 0 deletions SpreadCheetah.TestHelpers/Assertions/ClosedXmlAssertSheet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace SpreadCheetah.TestHelpers.Assertions;
internal sealed class ClosedXmlAssertSheet(XLWorkbook workbook, IXLWorksheet sheet)
: ISpreadsheetAssertSheet
{
public string Name => sheet.Name;

public ISpreadsheetAssertCell this[string reference]
{
get
Expand Down Expand Up @@ -36,6 +38,9 @@ public ISpreadsheetAssertColumn Column(string columnName)
return new ClosedXmlAssertColumn(sheet.Column(columnName));
}

private IReadOnlyList<ISpreadsheetAssertColumn>? _columns;
public IReadOnlyList<ISpreadsheetAssertColumn> Columns => _columns ??= [.. sheet.Columns().Select(x => new ClosedXmlAssertColumn(x))];

public IEnumerable<ISpreadsheetAssertCell> Row(int rowNumber)
{
var row = sheet.Row(rowNumber);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ namespace SpreadCheetah.TestHelpers.Assertions;

public interface ISpreadsheetAssertSheet : IDisposable
{
string Name { get; }

ISpreadsheetAssertCell this[string reference] { get; }
ISpreadsheetAssertCell this[string columnName, int rowNumber] { get; }
int CellCount { get; }
int RowCount { get; }

ISpreadsheetAssertColumn Column(string columnName);
IReadOnlyList<ISpreadsheetAssertColumn> Columns { get; }
IEnumerable<ISpreadsheetAssertCell> Row(int rowNumber);
}
14 changes: 14 additions & 0 deletions SpreadCheetah.TestHelpers/Assertions/SpreadsheetAssert.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using ClosedXML.Excel;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Validation;
using SpreadCheetah.TestHelpers.Collections;
using Xunit;

namespace SpreadCheetah.TestHelpers.Assertions;
Expand All @@ -21,6 +22,19 @@ public static ISpreadsheetAssertSheet SingleSheet(Stream stream)
return new ClosedXmlAssertSheet(workbook, sheet);
}

public static IWorksheetList Sheets(Stream stream)
{
if (stream is null)
throw new ArgumentNullException(nameof(stream));

Valid(stream);

#pragma warning disable CA2000 // Dispose objects before losing scope
var workbook = new XLWorkbook(stream);
#pragma warning restore CA2000 // Dispose objects before losing scope
return new ClosedXmlWorksheetList(workbook);
}

public static void Valid(Stream stream)
{
if (stream is null)
Expand Down
20 changes: 20 additions & 0 deletions SpreadCheetah.TestHelpers/Collections/ClosedXmlWorksheetList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using ClosedXML.Excel;
using SpreadCheetah.TestHelpers.Assertions;
using System.Collections;

namespace SpreadCheetah.TestHelpers.Collections;

internal sealed class ClosedXmlWorksheetList(XLWorkbook workbook) : IWorksheetList
{
private readonly List<ISpreadsheetAssertSheet> _list =
[
.. workbook.Worksheets.Select(x => new ClosedXmlAssertSheet(workbook, x))
];

public ISpreadsheetAssertSheet this[int index] => _list[index];
public int Count => _list.Count;
public IEnumerator<ISpreadsheetAssertSheet> GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();

public void Dispose() => workbook.Dispose();
}
5 changes: 5 additions & 0 deletions SpreadCheetah.TestHelpers/Collections/IWorksheetList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using SpreadCheetah.TestHelpers.Assertions;

namespace SpreadCheetah.TestHelpers.Collections;

public interface IWorksheetList : IReadOnlyList<ISpreadsheetAssertSheet>, IDisposable;
60 changes: 40 additions & 20 deletions SpreadCheetah/MetadataXml/WorksheetStartXml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace SpreadCheetah.MetadataXml;

internal struct WorksheetStartXml : IXmlWriter
internal struct WorksheetStartXml
{
private static ReadOnlySpan<byte> Header =>
"""<?xml version="1.0" encoding="utf-8"?>"""u8 +
Expand All @@ -14,43 +14,59 @@ internal struct WorksheetStartXml : IXmlWriter

private readonly WorksheetOptions? _options;
private readonly List<KeyValuePair<int, ColumnOptions>>? _columns;
private readonly SpreadsheetBuffer _buffer;
private Element _next;
private int _nextIndex;
private bool _anyColumnWritten;

public WorksheetStartXml(WorksheetOptions? options)
private WorksheetStartXml(WorksheetOptions? options, SpreadsheetBuffer buffer)
{
_options = options;
_columns = options?.ColumnOptions is { } columns ? [.. columns] : null;
_buffer = buffer;
}

public bool TryWrite(Span<byte> bytes, out int bytesWritten)
public static async ValueTask WriteAsync(
WorksheetOptions? options,
SpreadsheetBuffer buffer,
Stream stream,
CancellationToken token)
{
bytesWritten = 0;
var writer = new WorksheetStartXml(options, buffer);

if (_next == Element.Header && !Advance(Header.TryCopyTo(bytes, ref bytesWritten))) return false;
if (_next == Element.SheetViews && !Advance(TryWriteSheetViews(bytes, ref bytesWritten))) return false;
if (_next == Element.Columns && !Advance(TryWriteColumns(bytes, ref bytesWritten))) return false;
if (_next == Element.SheetDataBegin && !Advance(SheetDataBegin.TryCopyTo(bytes, ref bytesWritten))) return false;

return true;
foreach (var success in writer)
{
if (!success)
await buffer.FlushToStreamAsync(stream, token).ConfigureAwait(false);
}
}

private bool Advance(bool success)
public readonly WorksheetStartXml GetEnumerator() => this;
public bool Current { get; private set; }

public bool MoveNext()
{
if (success)
Current = _next switch
{
Element.Header => _buffer.TryWrite(Header),
Element.SheetViews => TryWriteSheetViews(_buffer),
Element.Columns => TryWriteColumns(_buffer),
_ => _buffer.TryWrite(SheetDataBegin)
};

if (Current)
++_next;

return success;
return _next < Element.Done;
}

private readonly bool TryWriteSheetViews(Span<byte> bytes, ref int bytesWritten)
private readonly bool TryWriteSheetViews(SpreadsheetBuffer buffer)
{
var options = _options;
if (options?.FrozenColumns is null && options?.FrozenRows is null)
return true;

var span = bytes.Slice(bytesWritten);
var span = buffer.GetSpan();
var written = 0;

if (!"<sheetViews><sheetView workbookViewId=\"0\"><pane "u8.TryCopyTo(span, ref written)) return false;
Expand Down Expand Up @@ -88,7 +104,7 @@ private readonly bool TryWriteSheetViews(Span<byte> bytes, ref int bytesWritten)

if (!"</sheetView></sheetViews>"u8.TryCopyTo(span, ref written)) return false;

bytesWritten += written;
_buffer.Advance(written);
return true;
}

Expand All @@ -112,7 +128,7 @@ private readonly bool TryWriteySplit(Span<byte> bytes, ref int bytesWritten)
return "\" "u8.TryCopyTo(bytes, ref bytesWritten);
}

private bool TryWriteColumns(Span<byte> bytes, ref int bytesWritten)
private bool TryWriteColumns(SpreadsheetBuffer buffer)
{
if (_columns is not { } columns) return true;

Expand All @@ -124,7 +140,7 @@ private bool TryWriteColumns(Span<byte> bytes, ref int bytesWritten)
if (options.Width is null && !options.Hidden)
continue;

var span = bytes.Slice(bytesWritten);
var span = buffer.GetSpan();
var written = 0;

if (!anyColumnWritten)
Expand All @@ -149,10 +165,14 @@ private bool TryWriteColumns(Span<byte> bytes, ref int bytesWritten)
if (!" />"u8.TryCopyTo(span, ref written)) return false;

_anyColumnWritten = anyColumnWritten;
bytesWritten += written;

buffer.Advance(written);
}

return !anyColumnWritten || "</cols>"u8.TryCopyTo(bytes, ref bytesWritten);
if (!anyColumnWritten)
return true;

return buffer.TryWrite("</cols>"u8);
}

private enum Element
Expand Down
3 changes: 1 addition & 2 deletions SpreadCheetah/Worksheet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ public Worksheet(Stream stream, DefaultStyling? defaultStyling, SpreadsheetBuffe

public async ValueTask WriteHeadAsync(WorksheetOptions? options, CancellationToken token)
{
var writer = new WorksheetStartXml(options);
await writer.WriteAsync(_stream, _buffer, token).ConfigureAwait(false);
await WorksheetStartXml.WriteAsync(options, _buffer, _stream, token).ConfigureAwait(false);

if (options?.AutoFilter is not null)
{
Expand Down

0 comments on commit 4512d14

Please sign in to comment.