Unrect.Spreadsheets 0.3.0-alpha.2

This is a prerelease version of Unrect.Spreadsheets.
dotnet add package Unrect.Spreadsheets --version 0.3.0-alpha.2
                    
NuGet\Install-Package Unrect.Spreadsheets -Version 0.3.0-alpha.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Unrect.Spreadsheets" Version="0.3.0-alpha.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Unrect.Spreadsheets" Version="0.3.0-alpha.2" />
                    
Directory.Packages.props
<PackageReference Include="Unrect.Spreadsheets" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Unrect.Spreadsheets --version 0.3.0-alpha.2
                    
#r "nuget: Unrect.Spreadsheets, 0.3.0-alpha.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Unrect.Spreadsheets@0.3.0-alpha.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Unrect.Spreadsheets&version=0.3.0-alpha.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Unrect.Spreadsheets&version=0.3.0-alpha.2&prerelease
                    
Install as a Cake Tool

Unrect

Financial and operational reports live in spreadsheets, and spreadsheets are flat 2D grids carrying hierarchical, heterogeneous data: a header, a summary table, N repeating client blocks each with its own sub-sections. Row-oriented parsers handle this badly — they devolve into stateful cursor logic and index arithmetic that breaks the moment a row shifts. Unrect takes a different approach: you declare the shape of the data — a header, a table bound to a record type, a repeating series bounded by a caption — and the framework decomposes the grid and projects it into typed objects. You never write a loop that walks the grid deciding what comes next.

Install

dotnet add package Unrect
dotnet add package Unrect.Spreadsheets

Unrect is the engine — the projection vocabulary, the layout composites, the strategies that decide boundaries — and works directly over any 2D grid you can adapt to ISpace. Unrect.Spreadsheets adds the adapters that read spreadsheet files — .xls/.xlsx today — straight into that grid; add it when your data lives in a workbook rather than an array you built yourself. The same package also has a streaming door, Workbook, for files too large to read whole — see Large files below.

GridSpace ships in the Unrect package for exactly that case: GridSpace.Create(values, isBlank: ...) turns a plain 2D array into a space, deciding there and then what counts as empty. It needs nothing else installed, which makes it the way to build test fixtures and scripted data without a workbook in sight.

Show me the code

A report with a typed header, a summary table, and the same repeating per-investor block appearing twice under two different captions:

Fund IRR Report
Example Fund I                          2026-06-30
--------------------------------------------------
Investors | Contribution ITD | ... | Irr
--------------------------------------------------
Cash Flows Using Transfer Date
  [investor block]  [investor block]  ...
Cash Flows using inception date
  [investor block]  [investor block]  ...
using Unrect.Spreadsheets;
using static Unrect.Projections.Projection;

var header = VerticalFlow(v => new
{
    Title = v.Next(Text()),
    Fund = v.Next(Text()),
    ReportDate = v.Next(Date()),
});

// Captions bind to record properties by name (case- and whitespace-insensitive).
var summary = Table<SummaryRow>(bind => bind.Column(r => r.Investor, "Investors"));

var investorBlock = Table<CashFlow>();

// Declared once, placed twice — .Until bounds the first series so it stops at the
// second caption instead of trying to parse it as another investor block.
var series = VerticalRepeat(investorBlock, separatedBy: BlankRows());
const string Inception = "Cash Flows using inception date";

var byTransferDate = series
    .Under(Caption("Cash Flows Using Transfer Date"))
    .Until(RowContaining(Inception));

var byInception = series.Under(Caption(Inception));

var report = VerticalFlow(v => new
{
    Header = v.Next(header),
    Summary = v.Next(summary),
    ByTransferDate = v.Next(byTransferDate),
    ByInception = v.Next(byInception),
});

var result = report.Map(SpreadsheetSpace.Create("irr-report.xlsx", "IRR"));

record SummaryRow(string Investor, decimal ContributionItd, decimal DistributionItd,
                   decimal ManagementFeeItd, decimal EndBalance, double Irr);
record CashFlow(string InvestorName, DateTime Date, string Transaction, double Irr);

The ideas

  • Projections are reusable, immutable values. Declare report once, apply it to as many workbooks as you have — workbooks.Select(report.Map).
  • Diagnostics carry a declaration path and an A1 cell location — a failure tells you which projection it came from and exactly where on the sheet it happened.
  • Names are inferred from your own identifiers. The local you assign a projection to (series, byTransferDate) is what shows up in its diagnostics — no separate naming step.
  • Tolerance is declared per projection, never ambient. .Optional() and .Else() mark exactly where a missing or malformed region is acceptable; nothing is silently lenient everywhere.
  • Content anchors survive layout drift. .On, .Below, .RightOf, Caption, and .Until find their place by what a row or column says, not by a hard-coded offset that breaks the next time someone inserts a row.

Large files

SpreadsheetSpace.Create reads a sheet whole, which is the simple default and the right choice for anything that fits comfortably in memory. For a file too big for that, or the same declaration applied to many files in sequence, Workbook reads a window at a time instead of the whole grid:

var report = VerticalFlow(v => ...);              // one declaration, reused

foreach (var path in monthlyCloseOfFunds)
{
  using var book = Workbook.Open(path);
  Publish(report.Map(book.Sheet("Detail")));       // bounded memory per iteration
}

Same projections, same results — the two paths differ only in the shape of their cost. A monotone read through Workbook costs about 35% more wall time for about 2.7× less live memory than SpreadsheetSpace.Create; a declaration that reaches backwards or sweeps a band wider than its window can cost more than that, which book.Statistics("Detail") will tell you. Projections are immutable and workbooks are independent, so Parallel.ForEach(monthlyCloseOfFunds, path => { using var book = ...; }) needs nothing added. The full guide, including the sizing law and the statistics to act on: docs/streaming.md.

Learn more

  • docs/vocabulary.md — the full operator survey, grouped by role.
  • docs/streaming.md — the Workbook guide: when to reach for it, the lifecycle rules, the sizing law, and the statistics vocabulary.
  • docs/design/ — the specs behind the vocabulary (layout, matching, tables, diagnostics, streaming).
  • linqpad/ — worked examples against the workbooks in examples/, including the report above (linqpad/investor-irr.linq).

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0-alpha.2 46 9/9/2026
0.3.0-alpha.1 61 9/7/2026
0.2.0-alpha.1 61 9/6/2026
0.1.0-alpha.4 56 9/5/2026
0.1.0-alpha.3 63 9/5/2026
0.1.0-alpha.2 59 9/4/2026
0.1.0-alpha.1 73 9/2/2026