
In-memory XLSX generation, write-only DSL for sheets, cells, styles, formulas, dates/times, named ranges, overlap detection, no native dependencies, exportable ByteArray for reporting.
CellarKt is a Kotlin Multiplatform library for generating XLSX (Excel) documents — no JVM spreadsheet engine, no native dependencies, just pure Kotlin. Documents can be returned as a ByteArray or streamed directly to any Okio BufferedSink (file, socket, etc.) without buffering the full output in memory.
Write-only. CellarKt creates XLSX files. It does not read or parse existing spreadsheets.
CellarKt was built to cover the common cases for a multiplatform Kotlin application. It is not a full replacement for Apache POI or other comprehensive spreadsheet libraries, but it handles the vast majority of real-world reporting needs.
Targets: Android · iOS (arm64 + simulatorArm64)
// build.gradle.kts
dependencies {
implementation("xyz.kandrac:cellar:0.0.1")
}Requirements:
suspend fun createReport(): ByteArray = cellar {
sheet("Report") {
// Place cells at explicit coordinates (0-based)
cell("Employee Timesheet", x = 0, y = 0, horizontalSpan = 4)
// Sequential rows (left-to-right)
row(y = 1) {
cell("Name"); cell("Role"); cell("Hours"); cell("Rate")
}
// Typed convenience overloads
row(y = 2) {
cell("Alice")
cell("Engineer")
cell(40) // Int → Number
cell(42.50) // Double → Number
}
// Formula (no leading "=")
formula("C3*D3", x = 3, y = 2)
// Date (days since Unix epoch)
date(20241L, x = 0, y = 4, format = DateFormat.Custom("dd/mm/yyyy"))
// Boolean
cell(true, x = 1, y = 4)
// Error
error(ExcelError.DivisionByZero, x = 2, y = 4)
}
}.export().export() is a suspend function that runs on Dispatchers.Default. The returned ByteArray can be written to a file, shared via a content URI, or sent over the network.
For large documents, prefer exportTo(sink) to avoid holding the full output in memory — see Streaming Export.
exportTo(sink: BufferedSink) writes the XLSX package directly to any Okio BufferedSink without ever accumulating the full output in memory. Worksheet XML is emitted row-by-row, and the style and shared-string pre-scan passes run concurrently before output begins.
val file = File(context.cacheDir, "report.xlsx")
file.sink().buffer().use { sink ->
cellar { /* … */ }.exportTo(sink)
}val path = NSHomeDirectory() + "/Documents/report.xlsx"
val sink = FileSystem.SYSTEM.sink(path.toPath()).buffer()
sink.use { cellar { /* … */ }.exportTo(it) }export(): ByteArray |
exportTo(sink) |
|
|---|---|---|
| Output | Full file in memory | Written incrementally to sink |
| Peak extra RAM | ≈ full ZIP size | Near zero |
| Use when | You need a ByteArray (e.g. for upload, sharing) |
Writing to a file or forwarding to a stream |
Both variants run the style-registry and shared-string-table scans concurrently and stream worksheet XML row-by-row internally.
The entire document is built inside a cellar { } lambda:
cellar {
metadata { ... } // Optional: author, timestamps, app info
default { ... } // Optional: document-wide font & format defaults
sheet("Sheet1") { ... } // One or more worksheets
namedRange(...) // Optional: workbook-level named ranges
}.export()
| Function | Description |
|---|---|
cell(value, x, y, horizontalSpan, verticalSpan, style) |
Place at explicit (x, y) coordinates with optional merge |
richText(vararg runs, x, y, style) |
Place a mixed-format rich-text cell (see Rich Text) |
row(y, startX = 0) { cell(...) } |
Sequential cells left-to-right |
column(x, startY = 0) { cell(...) } |
Sequential cells top-to-bottom |
You can also use columnWidth(x, width) and rowHeight(y, height) to control dimensions.
Cell overlap (including merged cells) is detected at build time — an IllegalStateException is thrown when two cells occupy the same range.
| Type | DSL Function | OOXML Representation |
|---|---|---|
| Text | cell("Hello") |
Shared string (t="s", deduplicated) |
| Rich Text | richText(RichTextRun(…), …) |
Inline string t="inlineStr" with <is><r> runs |
| Number (Int/Double/Long/Float) |
cell(42), cell(3.14)
|
Numeric cell |
| Boolean | cell(true) |
t="b" |
| Formula | formula("SUM(A1:A10)") |
<f> element (no leading =) |
| Array Formula | cell(CellValue.ArrayFormula(...)) |
<f t="array"> |
| Date | date(epochDay) |
Serial number with date format |
| Date+Time | dateTime(epochMillis) |
Fractional serial with datetime format |
| Time | time(millisOfDay) |
Fractional serial with time format |
| Error | error(ExcelError.Value) |
t="e" |
| Empty | Omitted | Empty cell |
Use the style parameter on any cell() call to apply fonts, borders, background, and alignment:
cell("Header", x = 0, y = 0, style = CellStyle(
font = Font(size = 14, bold = true, color = ExcelColor.White),
background = Background(ExcelColor("FF1A73E8")),
border = Border.All,
horizontalAlignment = HorizontalAlignment.Center,
verticalAlignment = VerticalAlignment.Center,
wrapText = true,
))Font(
size = 11, // Font size in points
bold = false,
italic = false,
underline = false,
color = null, // ExcelColor ARGB or null for default
name = "Calibri", // Font family name
)Border(
top = BorderLine.Thin,
bottom = BorderLine.Thin,
left = BorderLine.Thin,
right = BorderLine.Thin,
)
// Shorthand:
Border.All // Thin on all sides
Border.AllThick // Thick on all sidesBackground(color = ExcelColor("FFFF0000")) // ARGB hex
Background() // Transparent (default)CellStyle(
horizontalAlignment = HorizontalAlignment.Center, // Start | Center | End
verticalAlignment = VerticalAlignment.Top, // Top | Center | Bottom
wrapText = true, // wrap overflowing text onto multiple lines
textRotation = 45, // 0–90 = CCW degrees; 91–180 = 1°–90° CW; 255 = stacked
indent = 2, // left-padding in character-width units (0–250)
shrinkToFit = true, // shrink font until content fits (mutually exclusive with wrapText)
)
### Number Format Presets
```kotlin
NumberFormat.Integer // "#,##0"
NumberFormat.Decimal2 // "#,##0.00"
NumberFormat.Percent // "0%"
NumberFormat.Dollar // "$#,##0.00"
NumberFormat.Euro // "€#,##0.00"
NumberFormat.Scientific // "0.00E+00"
// … plus many more, or any custom format stringExample:
cell(1234567.89, x = 0, y = 0, style = CellStyle(numberFormat = NumberFormat.Decimal2))richText() places a cell whose content is a sequence of styled runs — each run can have its own font (bold, italic, color, size, etc.) independently of the others and of the overall CellStyle.
richText(
RichTextRun("Status: ", Font(bold = true)),
RichTextRun("PASSED", Font(bold = true, color = ExcelColor("FF00AA00"))),
x = 0, y = 2,
)Runs with font = null inherit the cell's effective CellStyle font at render time. Rich text is stored as t="inlineStr" (not added to the shared-string table), so identical strings in different cells are not deduplicated — keep that in mind if you produce many repeated rich-text cells.
Inside row { } and column { } the same function is available without explicit coordinates:
row(y = 0) {
richText(
RichTextRun("Q1 ", Font(bold = true)),
RichTextRun("Revenue"),
)
cell(1_250_000.0, style = CellStyle(numberFormat = NumberFormat.Accounting))
}These functions are called inside a sheet("Name") { … } block and control the view, appearance, and structure of the sheet.
sheet("Report") {
tabColor(ExcelColor("FF1A73E8")) // blue tab
freeze(rows = 1) // freeze header row
freeze(rows = 1, cols = 2) // freeze header + first two columns
zoom(150) // 150 % zoom
rightToLeft() // RTL column order (Arabic/Hebrew)
visibility(SheetVisibility.Hidden) // hide tab (user can unhide via Excel UI)
visibility(SheetVisibility.VeryHidden) // hidden, cannot be unhidden via Excel UI
autoFilter("A1:E1") // add filter dropdowns to row 1
// … cells …
}| Function | Description |
|---|---|
tabColor(color) |
Colors the sheet tab with the given ExcelColor
|
freeze(rows, cols) |
Freezes the top N rows and/or left N columns; at least one must be > 0 |
zoom(scale) |
Sets the view zoom (10–400 %) |
rightToLeft() |
Displays columns right-to-left |
visibility(SheetVisibility) |
Sets the sheet tab visibility: Visible, Hidden, or VeryHidden
|
autoFilter(ref) |
Adds filter dropdown buttons to the given A1-notation range |
printSetup { } configures how a sheet is rendered when printed or exported to PDF.
sheet("Report") {
printSetup {
orientation = PageOrientation.Landscape
paperSize = PaperSize.A4
fitToPage = true // scale to fit width/height
fitToWidth = 1 // fit to 1 page wide
fitToHeight = 0 // unlimited pages tall
printGridlines = true
horizontalCentered = true
marginTop = 1.0 // inches
marginBottom = 1.0
printArea = "\$A\$1:\$H\$50" // only print this range
}
}| Property | Type | Default | Description |
|---|---|---|---|
orientation |
PageOrientation |
Portrait |
Portrait or Landscape
|
paperSize |
PaperSize |
A4 |
Letter, A4, A3, Legal, B5, … |
scale |
Int |
100 | Print scale % (10–400); ignored when fitToPage
|
fitToPage |
Boolean |
false |
Scale to fit within fitToWidth × fitToHeight
|
fitToWidth |
Int |
1 | Max pages wide (0 = unlimited) |
fitToHeight |
Int |
1 | Max pages tall (0 = unlimited) |
printGridlines |
Boolean |
false |
Print cell gridlines |
printHeadings |
Boolean |
false |
Print row/column headings |
horizontalCentered |
Boolean |
false |
Center content horizontally on page |
verticalCentered |
Boolean |
false |
Center content vertically on page |
marginLeft/Right/… |
Double |
0.7 / 0.75 | Margins in inches (left, right, top, bottom, header, footer) |
printArea |
String? |
null |
A1-notation range (e.g. "$A$1:$H$50"); null = full sheet |
Locks a worksheet so users cannot edit cells without the password.
sheet("Salary Data") {
protect("s3cr3t") // password required to unprotect
protect() // locked with no password (still prevents accidental edits)
// … cells …
}Prevents structural changes (adding, deleting, renaming, moving sheets) at the workbook level.
cellar {
protect("w0rkb00k") // lock structure with password
protect("w0rkb00k", lockStructure = true) // same as above
protect() // lock without password
sheet("Sheet1") { … }
}.export()Security note: Both sheet and workbook protection use the legacy OOXML XOR hash (Excel 97 era). It prevents casual editing but is not strong encryption — specialist tools can bypass it. For sensitive data, encrypt the file at the operating-system or transport layer instead.
Formulas are written without the leading =:
formula("SUM(C2:C10)", x = 0, y = 11)
formula("A1*B1", x = 2, y = 3)For array formulas (CSE, Ctrl+Shift+Enter) use the CellValue.ArrayFormula type directly:
cell(CellValue.ArrayFormula("SUM(B2:B11*C2:C11)", arrayRef = "A12"), x = 0, y = 11)Date/time values are specified in terms of the Unix epoch and automatically converted to Excel serial numbers:
| DSL Function | Input | Excel Representation |
|---|---|---|
date(epochDay, format) |
Days since 1970-01-01 | Integer serial |
dateTime(epochMillis, format) |
ms since 1970-01-01 UTC | Fractional serial |
time(millisOfDay, format) |
ms since midnight (0–86,399,999) | Decimal 0.0–1.0 |
// Using kotlinx-datetime:
date(LocalDate(2025, 6, 1).toEpochDays().toLong(), x = 0, y = 0)
dateTime(Clock.System.now().toEpochMilliseconds(), x = 0, y = 0)
time(LocalTime(14, 30).toMillisecondOfDay().toLong(), x = 0, y = 0)Built-in display presets in DateFormat:
| Preset | Format String | Excel Built-in ID |
|---|---|---|
ShortDate |
mm-dd-yy |
14 |
MediumDate |
d-mmm-yy |
15 |
MonthYear |
mmm-yy |
17 |
ShortDateTime |
m/d/yy h:mm |
22 |
ShortTime |
h:mm |
20 |
LongTime |
h:mm:ss |
21 |
Custom("dd/MM/yyyy") |
Any format code | 164+ |
Optional document properties written to docProps/core.xml and docProps/app.xml:
cellar {
metadata {
author = "Alice"
lastModifiedBy = "Bob"
createdEpochMillis = 1_700_000_000_000L
modifiedEpochMillis = 1_700_000_000_000L
appName = "ReportGenerator"
appVersion = "1.0"
company = "Acme Corp"
}
// …
}All fields are nullable and omitted from the output when unset.
Set document-wide defaults that apply to all cells unless overridden:
cellar {
default {
font = Font(size = 12, name = "Arial")
numberFormat = NumberFormat.Decimal2
dateFormat = DateFormat.Custom("dd/MM/yyyy")
dateTimeFormat = DateFormat.Custom("dd/MM/yyyy HH:mm")
timeFormat = DateFormat.LongTime
}
// …
}Define workbook-level names that can be used in formulas:
cellar {
sheet("Data") { /* … */ }
namedRange("TaxRate", ref = "Data!$C$1")
namedRange("TotalRevenue", ref = "Data!$B$2:$B$10")
}
// Usage in a formula:
formula("TotalRevenue * TaxRate", x = 0, y = 5)Settings are resolved in this order (highest priority first):
| Property | Cell Style | Document Default | Library Fallback |
|---|---|---|---|
| Font | CellStyle.font |
CellarDefaults.font |
Font() (11pt Calibri) |
| Number format | CellStyle.numberFormat |
CellarDefaults.numberFormat |
General |
| Date format |
CellStyle.numberFormat / CellStyle.dateFormat / cell value format |
CellarDefaults.dateFormat/timeFormat/dateTimeFormat |
ShortDate / ShortTime / ShortDateTime
|
| Border | CellStyle.border |
— |
Border() (none) |
| Background | CellStyle.background |
— |
Background() (none) |
CellarKt is intentionally focused on the common spreadsheet generation use cases. Known missing features include:
<headerFooter>)See missing_features.md for the full list.
Large spreadsheets. All input cell data is held in memory while the document is being generated, so documents with millions of rows are not yet supported. The output itself can be streamed to a file via
exportTo(sink)to avoid a second full-size allocation. True single-pass cell streaming (where cells are never all in memory simultaneously) would require bypassing the shared-string-table pre-scan — this is a known future direction.
Contributions are welcome! Whether it's a bug report, a feature request, or a pull request — please open an issue or submit a PR.
This project is licensed under the MIT License.
CellarKt is a Kotlin Multiplatform library for generating XLSX (Excel) documents — no JVM spreadsheet engine, no native dependencies, just pure Kotlin. Documents can be returned as a ByteArray or streamed directly to any Okio BufferedSink (file, socket, etc.) without buffering the full output in memory.
Write-only. CellarKt creates XLSX files. It does not read or parse existing spreadsheets.
CellarKt was built to cover the common cases for a multiplatform Kotlin application. It is not a full replacement for Apache POI or other comprehensive spreadsheet libraries, but it handles the vast majority of real-world reporting needs.
Targets: Android · iOS (arm64 + simulatorArm64)
// build.gradle.kts
dependencies {
implementation("xyz.kandrac:cellar:0.0.1")
}Requirements:
suspend fun createReport(): ByteArray = cellar {
sheet("Report") {
// Place cells at explicit coordinates (0-based)
cell("Employee Timesheet", x = 0, y = 0, horizontalSpan = 4)
// Sequential rows (left-to-right)
row(y = 1) {
cell("Name"); cell("Role"); cell("Hours"); cell("Rate")
}
// Typed convenience overloads
row(y = 2) {
cell("Alice")
cell("Engineer")
cell(40) // Int → Number
cell(42.50) // Double → Number
}
// Formula (no leading "=")
formula("C3*D3", x = 3, y = 2)
// Date (days since Unix epoch)
date(20241L, x = 0, y = 4, format = DateFormat.Custom("dd/mm/yyyy"))
// Boolean
cell(true, x = 1, y = 4)
// Error
error(ExcelError.DivisionByZero, x = 2, y = 4)
}
}.export().export() is a suspend function that runs on Dispatchers.Default. The returned ByteArray can be written to a file, shared via a content URI, or sent over the network.
For large documents, prefer exportTo(sink) to avoid holding the full output in memory — see Streaming Export.
exportTo(sink: BufferedSink) writes the XLSX package directly to any Okio BufferedSink without ever accumulating the full output in memory. Worksheet XML is emitted row-by-row, and the style and shared-string pre-scan passes run concurrently before output begins.
val file = File(context.cacheDir, "report.xlsx")
file.sink().buffer().use { sink ->
cellar { /* … */ }.exportTo(sink)
}val path = NSHomeDirectory() + "/Documents/report.xlsx"
val sink = FileSystem.SYSTEM.sink(path.toPath()).buffer()
sink.use { cellar { /* … */ }.exportTo(it) }export(): ByteArray |
exportTo(sink) |
|
|---|---|---|
| Output | Full file in memory | Written incrementally to sink |
| Peak extra RAM | ≈ full ZIP size | Near zero |
| Use when | You need a ByteArray (e.g. for upload, sharing) |
Writing to a file or forwarding to a stream |
Both variants run the style-registry and shared-string-table scans concurrently and stream worksheet XML row-by-row internally.
The entire document is built inside a cellar { } lambda:
cellar {
metadata { ... } // Optional: author, timestamps, app info
default { ... } // Optional: document-wide font & format defaults
sheet("Sheet1") { ... } // One or more worksheets
namedRange(...) // Optional: workbook-level named ranges
}.export()
| Function | Description |
|---|---|
cell(value, x, y, horizontalSpan, verticalSpan, style) |
Place at explicit (x, y) coordinates with optional merge |
richText(vararg runs, x, y, style) |
Place a mixed-format rich-text cell (see Rich Text) |
row(y, startX = 0) { cell(...) } |
Sequential cells left-to-right |
column(x, startY = 0) { cell(...) } |
Sequential cells top-to-bottom |
You can also use columnWidth(x, width) and rowHeight(y, height) to control dimensions.
Cell overlap (including merged cells) is detected at build time — an IllegalStateException is thrown when two cells occupy the same range.
| Type | DSL Function | OOXML Representation |
|---|---|---|
| Text | cell("Hello") |
Shared string (t="s", deduplicated) |
| Rich Text | richText(RichTextRun(…), …) |
Inline string t="inlineStr" with <is><r> runs |
| Number (Int/Double/Long/Float) |
cell(42), cell(3.14)
|
Numeric cell |
| Boolean | cell(true) |
t="b" |
| Formula | formula("SUM(A1:A10)") |
<f> element (no leading =) |
| Array Formula | cell(CellValue.ArrayFormula(...)) |
<f t="array"> |
| Date | date(epochDay) |
Serial number with date format |
| Date+Time | dateTime(epochMillis) |
Fractional serial with datetime format |
| Time | time(millisOfDay) |
Fractional serial with time format |
| Error | error(ExcelError.Value) |
t="e" |
| Empty | Omitted | Empty cell |
Use the style parameter on any cell() call to apply fonts, borders, background, and alignment:
cell("Header", x = 0, y = 0, style = CellStyle(
font = Font(size = 14, bold = true, color = ExcelColor.White),
background = Background(ExcelColor("FF1A73E8")),
border = Border.All,
horizontalAlignment = HorizontalAlignment.Center,
verticalAlignment = VerticalAlignment.Center,
wrapText = true,
))Font(
size = 11, // Font size in points
bold = false,
italic = false,
underline = false,
color = null, // ExcelColor ARGB or null for default
name = "Calibri", // Font family name
)Border(
top = BorderLine.Thin,
bottom = BorderLine.Thin,
left = BorderLine.Thin,
right = BorderLine.Thin,
)
// Shorthand:
Border.All // Thin on all sides
Border.AllThick // Thick on all sidesBackground(color = ExcelColor("FFFF0000")) // ARGB hex
Background() // Transparent (default)CellStyle(
horizontalAlignment = HorizontalAlignment.Center, // Start | Center | End
verticalAlignment = VerticalAlignment.Top, // Top | Center | Bottom
wrapText = true, // wrap overflowing text onto multiple lines
textRotation = 45, // 0–90 = CCW degrees; 91–180 = 1°–90° CW; 255 = stacked
indent = 2, // left-padding in character-width units (0–250)
shrinkToFit = true, // shrink font until content fits (mutually exclusive with wrapText)
)
### Number Format Presets
```kotlin
NumberFormat.Integer // "#,##0"
NumberFormat.Decimal2 // "#,##0.00"
NumberFormat.Percent // "0%"
NumberFormat.Dollar // "$#,##0.00"
NumberFormat.Euro // "€#,##0.00"
NumberFormat.Scientific // "0.00E+00"
// … plus many more, or any custom format stringExample:
cell(1234567.89, x = 0, y = 0, style = CellStyle(numberFormat = NumberFormat.Decimal2))richText() places a cell whose content is a sequence of styled runs — each run can have its own font (bold, italic, color, size, etc.) independently of the others and of the overall CellStyle.
richText(
RichTextRun("Status: ", Font(bold = true)),
RichTextRun("PASSED", Font(bold = true, color = ExcelColor("FF00AA00"))),
x = 0, y = 2,
)Runs with font = null inherit the cell's effective CellStyle font at render time. Rich text is stored as t="inlineStr" (not added to the shared-string table), so identical strings in different cells are not deduplicated — keep that in mind if you produce many repeated rich-text cells.
Inside row { } and column { } the same function is available without explicit coordinates:
row(y = 0) {
richText(
RichTextRun("Q1 ", Font(bold = true)),
RichTextRun("Revenue"),
)
cell(1_250_000.0, style = CellStyle(numberFormat = NumberFormat.Accounting))
}These functions are called inside a sheet("Name") { … } block and control the view, appearance, and structure of the sheet.
sheet("Report") {
tabColor(ExcelColor("FF1A73E8")) // blue tab
freeze(rows = 1) // freeze header row
freeze(rows = 1, cols = 2) // freeze header + first two columns
zoom(150) // 150 % zoom
rightToLeft() // RTL column order (Arabic/Hebrew)
visibility(SheetVisibility.Hidden) // hide tab (user can unhide via Excel UI)
visibility(SheetVisibility.VeryHidden) // hidden, cannot be unhidden via Excel UI
autoFilter("A1:E1") // add filter dropdowns to row 1
// … cells …
}| Function | Description |
|---|---|
tabColor(color) |
Colors the sheet tab with the given ExcelColor
|
freeze(rows, cols) |
Freezes the top N rows and/or left N columns; at least one must be > 0 |
zoom(scale) |
Sets the view zoom (10–400 %) |
rightToLeft() |
Displays columns right-to-left |
visibility(SheetVisibility) |
Sets the sheet tab visibility: Visible, Hidden, or VeryHidden
|
autoFilter(ref) |
Adds filter dropdown buttons to the given A1-notation range |
printSetup { } configures how a sheet is rendered when printed or exported to PDF.
sheet("Report") {
printSetup {
orientation = PageOrientation.Landscape
paperSize = PaperSize.A4
fitToPage = true // scale to fit width/height
fitToWidth = 1 // fit to 1 page wide
fitToHeight = 0 // unlimited pages tall
printGridlines = true
horizontalCentered = true
marginTop = 1.0 // inches
marginBottom = 1.0
printArea = "\$A\$1:\$H\$50" // only print this range
}
}| Property | Type | Default | Description |
|---|---|---|---|
orientation |
PageOrientation |
Portrait |
Portrait or Landscape
|
paperSize |
PaperSize |
A4 |
Letter, A4, A3, Legal, B5, … |
scale |
Int |
100 | Print scale % (10–400); ignored when fitToPage
|
fitToPage |
Boolean |
false |
Scale to fit within fitToWidth × fitToHeight
|
fitToWidth |
Int |
1 | Max pages wide (0 = unlimited) |
fitToHeight |
Int |
1 | Max pages tall (0 = unlimited) |
printGridlines |
Boolean |
false |
Print cell gridlines |
printHeadings |
Boolean |
false |
Print row/column headings |
horizontalCentered |
Boolean |
false |
Center content horizontally on page |
verticalCentered |
Boolean |
false |
Center content vertically on page |
marginLeft/Right/… |
Double |
0.7 / 0.75 | Margins in inches (left, right, top, bottom, header, footer) |
printArea |
String? |
null |
A1-notation range (e.g. "$A$1:$H$50"); null = full sheet |
Locks a worksheet so users cannot edit cells without the password.
sheet("Salary Data") {
protect("s3cr3t") // password required to unprotect
protect() // locked with no password (still prevents accidental edits)
// … cells …
}Prevents structural changes (adding, deleting, renaming, moving sheets) at the workbook level.
cellar {
protect("w0rkb00k") // lock structure with password
protect("w0rkb00k", lockStructure = true) // same as above
protect() // lock without password
sheet("Sheet1") { … }
}.export()Security note: Both sheet and workbook protection use the legacy OOXML XOR hash (Excel 97 era). It prevents casual editing but is not strong encryption — specialist tools can bypass it. For sensitive data, encrypt the file at the operating-system or transport layer instead.
Formulas are written without the leading =:
formula("SUM(C2:C10)", x = 0, y = 11)
formula("A1*B1", x = 2, y = 3)For array formulas (CSE, Ctrl+Shift+Enter) use the CellValue.ArrayFormula type directly:
cell(CellValue.ArrayFormula("SUM(B2:B11*C2:C11)", arrayRef = "A12"), x = 0, y = 11)Date/time values are specified in terms of the Unix epoch and automatically converted to Excel serial numbers:
| DSL Function | Input | Excel Representation |
|---|---|---|
date(epochDay, format) |
Days since 1970-01-01 | Integer serial |
dateTime(epochMillis, format) |
ms since 1970-01-01 UTC | Fractional serial |
time(millisOfDay, format) |
ms since midnight (0–86,399,999) | Decimal 0.0–1.0 |
// Using kotlinx-datetime:
date(LocalDate(2025, 6, 1).toEpochDays().toLong(), x = 0, y = 0)
dateTime(Clock.System.now().toEpochMilliseconds(), x = 0, y = 0)
time(LocalTime(14, 30).toMillisecondOfDay().toLong(), x = 0, y = 0)Built-in display presets in DateFormat:
| Preset | Format String | Excel Built-in ID |
|---|---|---|
ShortDate |
mm-dd-yy |
14 |
MediumDate |
d-mmm-yy |
15 |
MonthYear |
mmm-yy |
17 |
ShortDateTime |
m/d/yy h:mm |
22 |
ShortTime |
h:mm |
20 |
LongTime |
h:mm:ss |
21 |
Custom("dd/MM/yyyy") |
Any format code | 164+ |
Optional document properties written to docProps/core.xml and docProps/app.xml:
cellar {
metadata {
author = "Alice"
lastModifiedBy = "Bob"
createdEpochMillis = 1_700_000_000_000L
modifiedEpochMillis = 1_700_000_000_000L
appName = "ReportGenerator"
appVersion = "1.0"
company = "Acme Corp"
}
// …
}All fields are nullable and omitted from the output when unset.
Set document-wide defaults that apply to all cells unless overridden:
cellar {
default {
font = Font(size = 12, name = "Arial")
numberFormat = NumberFormat.Decimal2
dateFormat = DateFormat.Custom("dd/MM/yyyy")
dateTimeFormat = DateFormat.Custom("dd/MM/yyyy HH:mm")
timeFormat = DateFormat.LongTime
}
// …
}Define workbook-level names that can be used in formulas:
cellar {
sheet("Data") { /* … */ }
namedRange("TaxRate", ref = "Data!$C$1")
namedRange("TotalRevenue", ref = "Data!$B$2:$B$10")
}
// Usage in a formula:
formula("TotalRevenue * TaxRate", x = 0, y = 5)Settings are resolved in this order (highest priority first):
| Property | Cell Style | Document Default | Library Fallback |
|---|---|---|---|
| Font | CellStyle.font |
CellarDefaults.font |
Font() (11pt Calibri) |
| Number format | CellStyle.numberFormat |
CellarDefaults.numberFormat |
General |
| Date format |
CellStyle.numberFormat / CellStyle.dateFormat / cell value format |
CellarDefaults.dateFormat/timeFormat/dateTimeFormat |
ShortDate / ShortTime / ShortDateTime
|
| Border | CellStyle.border |
— |
Border() (none) |
| Background | CellStyle.background |
— |
Background() (none) |
CellarKt is intentionally focused on the common spreadsheet generation use cases. Known missing features include:
<headerFooter>)See missing_features.md for the full list.
Large spreadsheets. All input cell data is held in memory while the document is being generated, so documents with millions of rows are not yet supported. The output itself can be streamed to a file via
exportTo(sink)to avoid a second full-size allocation. True single-pass cell streaming (where cells are never all in memory simultaneously) would require bypassing the shared-string-table pre-scan — this is a known future direction.
Contributions are welcome! Whether it's a bug report, a feature request, or a pull request — please open an issue or submit a PR.
This project is licensed under the MIT License.