A pure MoonBit library for reading and writing Microsoft Excel (XLSX) files. This is a port of the popular Go excelize library.
- Create, read, and write XLSX spreadsheets
- Cell value manipulation (strings, numbers, booleans, formulas)
- Cell styling (fonts, colors, borders, alignment, number formats)
- Charts and images
- Data validation and conditional formatting
- Pivot tables and slicers
- Sparklines
- Sheet protection and workbook encryption
- Formula evaluation with 300+ built-in functions
- Typed date/time and duration cells with automatic number formats
- Streaming API for large files
- Embedded cell image reading (WPS
DISPIMG, rich-value "Place in cell",IMAGE()) - OOXML package validation and a command-line tool
Add the dependency to your module:
moon add bobzhang/mbtexcelThen import it in the moon.pkg of the package that uses it:
import {
"bobzhang/mbtexcel",
}
Call the library through its default alias @mbtexcel (e.g. @mbtexcel.new_workbook()).
///|
test "workbook roundtrip" {
let workbook = @mbtexcel.new_workbook()
let sheet = workbook.add_sheet("Sheet1")
sheet.set_cell("A1", "hello")
sheet.set_cell_formula("B1", "A1", value="hello")
let bytes = @mbtexcel.write(workbook)
let parsed = @mbtexcel.read(bytes)
debug_inspect(parsed.get_cell("Sheet1", "A1"), content="Some(\"hello\")")
debug_inspect(parsed.get_cell_formula("Sheet1", "B1"), content="Some(\"A1\")")
}///|
test "row and column helpers" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Sheet1"))
workbook.set_row("Sheet1", 1, ["a", "b", "c"])
workbook.set_col("Sheet1", 2, ["x", "y"])
debug_inspect(
workbook.get_row("Sheet1", 1),
content=(
#|["a", "x", "c"]
),
)
debug_inspect(workbook.get_col("Sheet1", 2), content="[\"x\", \"y\"]")
}///|
test "cell reference conversion" {
// Split cell name into column and row
debug_inspect(@mbtexcel.split_cell_name("AB123"), content="(\"AB\", 123)")
// Join column and row into cell name
inspect(@mbtexcel.join_cell_name("AB", 123), content="AB123")
// Convert between cell name and coordinates (1-indexed)
debug_inspect(@mbtexcel.cell_name_to_coordinates("B3"), content="(2, 3)")
inspect(@mbtexcel.coordinates_to_cell_name(2, 3), content="B3")
// Absolute references
inspect(@mbtexcel.coordinates_to_cell_name(2, 3, abs=true), content="$B$3")
// Column name/number conversion
inspect(@mbtexcel.column_name_to_number("AB"), content="28")
inspect(@mbtexcel.column_number_to_name(28), content="AB")
}///|
test "multiple sheets" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Sales"))
ignore(workbook.add_sheet("Expenses"))
ignore(workbook.add_sheet("Summary"))
// Get list of all sheets
debug_inspect(
workbook.get_sheet_list(),
content="[\"Sales\", \"Expenses\", \"Summary\"]",
)
// Access sheet by name
guard workbook.sheet("Sales") is Some(sales) else { return }
sales.set_cell("A1", "Revenue")
debug_inspect(sales.get_cell("A1"), content="Some(\"Revenue\")")
}///|
test "cell value types" {
let workbook = @mbtexcel.new_workbook()
let sheet = workbook.add_sheet("Data")
// String values
sheet.set_cell("A1", "Hello")
// Numeric values (auto-detected from string)
sheet.set_cell("A2", "42")
sheet.set_cell("A3", "3.14159")
// Using typed CellValue enum for explicit types
sheet.set_cell_value("B1", String("Text"))
sheet.set_cell_value("B2", Numeric(100.5))
sheet.set_cell_value("B3", Bool(true))
// Read back values
debug_inspect(sheet.get_cell("A1"), content="Some(\"Hello\")")
debug_inspect(sheet.get_cell("A2"), content="Some(\"42\")")
debug_inspect(sheet.get_cell_value_raw("B2"), content="Some(Numeric(100.5))")
debug_inspect(sheet.get_cell_value_raw("B3"), content="Some(Bool(true))")
}///|
test "formulas" {
let workbook = @mbtexcel.new_workbook()
let sheet = workbook.add_sheet("Calc")
// Set some values
sheet.set_cell("A1", "10")
sheet.set_cell("A2", "20")
sheet.set_cell("A3", "30")
// Set formula with cached value
sheet.set_cell_formula("A4", "SUM(A1:A3)", value="60")
// Read formula back
debug_inspect(sheet.get_cell_formula("A4"), content="Some(\"SUM(A1:A3)\")")
// Calculate formula value
inspect(workbook.calc_cell_value("Calc", "A4"), content="60")
}///|
test "merged cells" {
let workbook = @mbtexcel.new_workbook()
let sheet = workbook.add_sheet("Report")
// Set value before merging
sheet.set_cell("A1", "Title")
// Merge cells A1:D1
sheet.merge_cells("A1:D1")
// Get merged cell ranges
debug_inspect(sheet.merged_cells().to_owned(), content="[\"A1:D1\"]")
}///|
test "cell styling" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Sheet1"))
workbook.set_cell("Sheet1", "A1", "1234.5")
// A style combines a number format with a font, fill, border, etc.
let style = workbook.new_style(
@xlsx.Style::builtin_number_format(2) // "0.00"
.with_font(@xlsx.Font::with_values(bold=true, color="#FF0000")),
)
workbook.set_cell_style("Sheet1", "A1", style)
debug_inspect(
workbook.get_cell_style("Sheet1", "A1"),
content="Some(\{style})",
)
}///|
test "typed dates" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Sheet1"))
// Store a datetime; it is written as an Excel date serial with a default
// date number format (honoring the workbook's 1900/1904 date system).
workbook.set_cell_time("Sheet1", "A1", @time.date_time(2024, 7, 3))
debug_inspect(
workbook.get_cell("Sheet1", "A1"),
content=(
#|Some("45476")
),
)
// The reverse conversion is available directly.
inspect(
@mbtexcel.time_to_excel_date(@time.date_time(2021, 1, 1, hour=12)),
content="44197.5",
)
}For workbooks with many rows, the streaming writer avoids holding every cell in memory at once.
///|
test "streaming writer" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Big"))
let stream = workbook.new_stream_writer("Big")
for r in 1..<=1000 {
stream.set_row_cells("A\{r}", [
@xlsx.StreamCell::new("row \{r}"),
@xlsx.StreamCell::new_value(Numeric(r.to_double())),
])
}
stream.flush()
let parsed = @mbtexcel.read(@mbtexcel.write(workbook))
debug_inspect(
parsed.get_cell("Big", "B1000"),
content=(
#|Some("1000")
),
)
}///|
test "password protection" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Secret"))
workbook.set_cell("Secret", "A1", "classified")
let encrypted = @mbtexcel.write_with_password(workbook, "s3cret")
let reopened = @mbtexcel.read_with_password(encrypted, "s3cret")
debug_inspect(
reopened.get_cell("Secret", "A1"),
content=(
#|Some("classified")
),
)
}validate_ooxml_package runs fast structural checks (content-type
coverage, relationship integrity, required parts, well-formed part
names) — the common causes of Excel's "we found a problem" repair
dialog. An empty result means the package is well-formed.
///|
test "validate output" {
let workbook = @mbtexcel.new_workbook()
ignore(workbook.add_sheet("Sheet1"))
workbook.set_cell("Sheet1", "A1", "hello")
debug_inspect(
@xlsx.validate_ooxml_package(@mbtexcel.write(workbook)),
content="[]",
)
}moonx bobzhang/office is the agent-oriented facade for DOCX and XLSX packages.
Its registry-driven help, format detection, validated raw OOXML fallback, and
structured reads use versioned JSON envelopes and explicit resource limits:
moonx bobzhang/office help docx
moonx bobzhang/office identify report.docx --json
moonx bobzhang/office outline report.docx --json
moonx bobzhang/office get report.docx '/docx/body/p[1]' --json
moonx bobzhang/office text report.docx --under '/docx/comments/comment[id="0"]'
moonx bobzhang/office query report.docx --kind paragraph --text revenue --ignore-case --json
moonx bobzhang/office outline book.xlsx --json
moonx bobzhang/office get book.xlsx '/xlsx/sheet[name="Data"]/range[A1:C12]' --json
moonx bobzhang/office text book.xlsx --under '/xlsx/sheet[name="Data"]' --json
moonx bobzhang/office query book.xlsx 'cell[type=formula]' --under '/xlsx/sheet[name="Data"]' --json
moonx bobzhang/office create xlsx new-book.xlsx --sheet Data --json
moonx bobzhang/office batch new-book.xlsx changes.json --out revised.xlsx --jsonDOCX results use the office.docx.{outline,element,text,query}/1 family and
XLSX reads use office.xlsx.{outline,element,text,query}/1; creation and batch
use office.xlsx.{create,batch}/1. Every result is inside office.output/1.
See
Unified Office DOCX reads,
Unified Office XLSX reads,
Transactional Office XLSX mutations, and
Canonical Office selectors.
The repo ships a small CLI (cmd/xlsx) for common operations without
writing code:
moon run cmd/xlsx -- create book.xlsx --sheet Data
moon run cmd/xlsx -- set book.xlsx Data A1 Hello
moon run cmd/xlsx -- get book.xlsx Data A1 # -> Hello
moon run cmd/xlsx -- sheets book.xlsx # -> Data
moon run cmd/xlsx -- rows book.xlsx # CSV of the sheet
moon run cmd/xlsx -- view book.xlsx # sheet as an ASCII table
moon run cmd/xlsx -- validate book.xlsx # -> validview renders a sheet as an ASCII table (first row treated as a header):
+-------+-------+
| Name | Score |
+-------+-------+
| Alice | 90 |
| Bob | 7 |
+-------+-------+
The library and CLI build for the wasm backend as well as native and js (the CLI needs the nightly toolchain for wasm filesystem support):
moon run --target wasm cmd/xlsx -- view book.xlsxThis repo includes a runnable demo generator that produces real .xlsx files you can open in Excel/Numbers/LibreOffice.
moon run cmd/demosThis writes multiple workbooks into ./demos_out/ (by default). You can also run a single demo:
moon run cmd/demos -- dashboard demos_out
moon run cmd/demos -- stream_big demos_out 50000Run the demo roundtrip regression gate (local/CI):
scripts/test_demo_roundtrip.shRun the combined parity + demo regression gate:
scripts/test_parity_gates.shFor semantic parity and CI wrapper usage details, see:
docs/excelize-parity.mddocs/parity-commands.md
Common commands:
scripts/test_parity_gates.sh
scripts/test_semantic_parity.sh
scripts/test_semantic_parity_fast.sh
scripts/test_semantic_parity_ultrasmoke.shSee docs/demos.md for what each demo generates and how the code is structured.
| Function | Description |
|---|---|
new_workbook() |
Create an empty workbook (no sheets) |
new_file() |
Create a workbook with one sheet named "Sheet1" |
read(bytes) |
Parse XLSX bytes into a workbook |
read_with_password(bytes, password) |
Parse encrypted XLSX |
open_file(path) |
(async) Open XLSX file from path |
| Function | Description |
|---|---|
write(workbook) |
Serialize workbook to XLSX bytes |
write_with_password(workbook, password) |
Serialize with encryption |
encrypt(bytes) |
Encrypt raw XLSX bytes |
decrypt(bytes) |
Decrypt encrypted XLSX bytes |
| Function | Description |
|---|---|
split_cell_name("A1") |
Returns ("A", 1) |
join_cell_name("A", 1) |
Returns "A1" |
cell_name_to_coordinates("B3") |
Returns (2, 3) (col, row) |
coordinates_to_cell_name(2, 3) |
Returns "B3" |
column_name_to_number("AB") |
Returns 28 |
column_number_to_name(28) |
Returns "AB" |
| Function | Description |
|---|---|
rgb_to_hsl(r, g, b) |
Convert RGB to HSL |
hsl_to_rgb(h, s, l) |
Convert HSL to RGB |
theme_color(base, tint) |
Apply tint to theme color |
| Function | Description |
|---|---|
excel_date_to_time(serial) |
Convert Excel date serial to ZonedDateTime |
time_to_excel_date(datetime) |
Convert a ZonedDateTime to an Excel date serial |
| Function | Description |
|---|---|
@xlsx.validate_ooxml_package(bytes) |
Return a list of OOXML package structure problems (empty = valid) |
The main types are available from the @xlsx package:
@xlsx.Workbook- The main workbook container@xlsx.Worksheet- A single worksheet@xlsx.Cell- Cell data with value, type, formula, and style@xlsx.Style- Cell styling (font, fill, border, alignment, number format)@xlsx.Options- Read/write options
For detailed documentation on all types and methods, see the xlsx package documentation.
All functions that can fail raise @xlsx.XlsxError. Common error variants:
SheetNotFound- Referenced sheet does not existInvalidCellRef- Invalid cell reference formatInvalidSheetName- Sheet name is invalid or too longEncryptedPackage- File is encrypted but no password providedInvalidPassword- Decryption failed with given password
Handle errors with try/catch:
///|
fn describe(bytes : Bytes) -> String {
try {
let workbook = @mbtexcel.read(bytes)
"loaded \{workbook.get_sheet_list().length()} sheets"
} catch {
err => "read failed: \{err}"
}
}bobzhang/mbtexcel # Facade package (this package)
-> bobzhang/mbtexcel/xlsx # Core implementation
-> bobzhang/mbtexcel/ooxml # OOXML metadata helpers
-> bobzhang/mbtexcel/zip # ZIP archive handling
-> bobzhang/mbtexcel/crypto # Cryptographic operations
Apache-2.0