Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WorksheetStartXml iterator #64

Merged
merged 12 commits into from
Jul 12, 2024
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
Loading