mbtexcel

A MoonBit port of the Go excelize library for reading and writing XLSX (Excel) spreadsheets.

excel
xlsx
spreadsheet
ooxml
office
moon add bobzhang/mbtexcel@0.1.9
Download zip
Author
Version
0.1.9
License
Apache-2.0
Last updated
18 days ago
Downloads
2K

Dependencies

README

#mbtexcel

A pure MoonBit library for reading and writing Microsoft Excel (XLSX) files. This is a port of the popular Go excelize library.

#Features

  • 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
  • Streaming API for large files

#Installation

Add to your moon.mod.json:

{ "deps": { "bobzhang/mbtexcel": "0.1.1" } }

Then add to your package's moon.pkg.json:

{ "import": ["bobzhang/mbtexcel"] }

#Quick Start

#Creating and Reading Workbooks

///|
test "workbook roundtrip" {
let workbook = new_workbook()
let sheet = workbook.add_sheet("Sheet1")
sheet.set_cell("A1", "hello")
sheet.set_cell_formula("B1", "A1", value="hello")
let bytes = write(workbook)
let parsed = read(bytes)
inspect(parsed.get_cell("Sheet1", "A1"), content="Some(\"hello\")")
inspect(parsed.get_cell_formula("Sheet1", "B1"), content="Some(\"A1\")")
}

#Row and Column Helpers

///|
test "row and column helpers" {
let workbook = new_workbook()
ignore(workbook.add_sheet("Sheet1"))
workbook.set_row("Sheet1", 1, ["a", "b", "c"])
workbook.set_col("Sheet1", 2, ["x", "y"])
inspect(
workbook.get_row("Sheet1", 1),
content=(
#|["a", "x", "c"]
),
)
inspect(workbook.get_col("Sheet1", 2), content="[\"x\", \"y\"]")
}

#Cell Reference Utilities

///|
test "cell reference conversion" {
// Split cell name into column and row
inspect(split_cell_name("AB123"), content="(\"AB\", 123)")

// Join column and row into cell name
inspect(join_cell_name("AB", 123), content="AB123")

// Convert between cell name and coordinates (1-indexed)
inspect(cell_name_to_coordinates("B3"), content="(2, 3)")
inspect(coordinates_to_cell_name(2, 3), content="B3")

// Absolute references
inspect(coordinates_to_cell_name(2, 3, abs=true), content="$B$3")

// Column name/number conversion
inspect(column_name_to_number("AB"), content="28")
inspect(column_number_to_name(28), content="AB")
}

#Working with Multiple Sheets

///|
test "multiple sheets" {
let workbook = new_workbook()
ignore(workbook.add_sheet("Sales"))
ignore(workbook.add_sheet("Expenses"))
ignore(workbook.add_sheet("Summary"))

// Get list of all sheets
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")
inspect(sales.get_cell("A1"), content="Some(\"Revenue\")")
}

#Cell Types and Values

///|
test "cell value types" {
let workbook = 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
inspect(sheet.get_cell("A1"), content="Some(\"Hello\")")
inspect(sheet.get_cell("A2"), content="Some(\"42\")")
inspect(sheet.get_cell_value_raw("B2"), content="Some(Numeric(100.5))")
inspect(sheet.get_cell_value_raw("B3"), content="Some(Bool(true))")
}

#Formulas

///|
test "formulas" {
let workbook = 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
inspect(sheet.get_cell_formula("A4"), content="Some(\"SUM(A1:A3)\")")

// Calculate formula value
inspect(workbook.calc_cell_value("Calc", "A4"), content="60")
}

#Merged Cells

///|
test "merged cells" {
let workbook = 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
inspect(sheet.merged_cells(), content="[\"A1:D1\"]")
}

#Demos

This repo includes a runnable demo generator that produces real .xlsx files you can open in Excel/Numbers/LibreOffice.

moon run cmd/demos

This 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 50000

Run the demo roundtrip regression gate (local/CI):

scripts/test_demo_roundtrip.sh

Run the combined parity + demo regression gate:

scripts/test_parity_gates.sh

#Parity Commands

For semantic parity and CI wrapper usage details, see:

  • docs/excelize-parity.md
  • docs/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.sh

See docs/demos.md for what each demo generates and how the code is structured.

#API Reference

#Workbook Creation

FunctionDescription
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

#Workbook Output

FunctionDescription
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

#Cell Reference Utilities

FunctionDescription
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"

#Color Utilities

FunctionDescription
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

#Date Utilities

FunctionDescription
excel_date_to_time(serial)Convert Excel date serial to ZonedDateTime

#Core Types

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.

#Error Handling

All functions that can fail raise @xlsx.XlsxError. Common error variants:

  • SheetNotFound - Referenced sheet does not exist
  • InvalidCellRef - Invalid cell reference format
  • InvalidSheetName - Sheet name is invalid or too long
  • EncryptedPackage - File is encrypted but no password provided
  • InvalidPassword - Decryption failed with given password

Use try? to convert errors to Result:

///|
fn safe_read(bytes : Bytes) -> Result[@xlsx.Workbook, Error] {
try! @mbtexcel.read(bytes)
}

#Package Structure

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 -> bobzhang/mbtexcel/base64 # Base64 encoding

#License

Apache-2.0

#
UtfIndexError

pub suberror UtfIndexError {
OutOfRange(Int, Int)
InvalidBoundary(Int)
InvalidUtf8(Int)
} derive(Show)

Errors for mapping between UTF-8 byte offsets and UTF-16 code unit indices.

#
cell_name_to_coordinates

fn cell_name_to_coordinates(cell : StringView) -> (Int, Int) raise
XlsxError

Converts a cell reference to column and row coordinates.

Parameters

  • cell: Cell reference like "A1", "B3", "5"

Returns

Tuple of (column, row) where both are 1-indexed

Example

let (col, row) = cell_name_to_coordinates("B3")
// col = 2, row = 3

#
column_name_to_number

fn column_name_to_number(name : StringView) -> Int raise
XlsxError

Converts a column name to a column number.

Parameters

  • name: Column name like "A", "Z", "AA", "XFD"

Returns

Column number (1-indexed, where "A" = 1)

Example

let num = column_name_to_number("AA")
// num = 27

#
column_number_to_name

fn column_number_to_name(col : Int) -> String raise
XlsxError

Converts a column number to a column name.

Parameters

  • col: Column number (1-indexed, where 1 = "A")

Returns

Column name like "A", "Z", "AA", "XFD"

Example

let name = column_number_to_name(27)
// name = "AA"

#
coordinates_to_cell_name

fn coordinates_to_cell_name(col : Int, row : Int, abs? : Bool) -> String raise
XlsxError

Converts column and row coordinates to a cell reference.

Parameters

  • col: Column number (1-indexed, where 1 = "A")
  • row: Row number (1-indexed)
  • abs: If true, creates absolute reference with $ signs (default: false)

Returns

Cell reference string like "B3" or "3" if abs=true

Example

let ref = coordinates_to_cell_name(2, 3) // ref = "B3" let abs_ref = coordinates_to_cell_name(2, 3, abs=true) // abs_ref = "$B$3"

#
decrypt

fn decrypt(raw : BytesView, options? :
Options
) -> Bytes raise
XlsxError

Decrypts encrypted XLSX bytes.

Parameters

  • raw: Encrypted XLSX file content
  • options: Options containing the password

Returns

Decrypted XLSX file content

#
encrypt

fn encrypt(raw : BytesView, options? :
Options
) -> Bytes raise
XlsxError

Encrypts raw XLSX bytes with a password.

Parameters

  • raw: Unencrypted XLSX file content
  • options: Options containing the password

Returns

Encrypted file content

#
excel_date_to_time

fn excel_date_to_time(excel_date : Double, use_1904_format? : Bool) ->
ZonedDateTime
raise
XlsxError

Converts an Excel date serial number to a ZonedDateTime.

Excel stores dates as floating-point numbers where:
  • The integer part is days since the epoch
  • The fractional part is the time of day

Parameters

  • excel_date: Excel date serial number
  • use_1904_format: If true, use Mac Excel's 1904 date system (default: false)

Returns

ZonedDateTime representing the date and time

Example

let dt = excel_date_to_time(44197.5) // 2021-01-01 12:00:00

#
hsl_to_rgb

fn hsl_to_rgb(h : Double, s : Double, l : Double) -> (Byte, Byte, Byte)

Converts HSL (Hue, Saturation, Lightness) color values to RGB.

Parameters

  • h: Hue (0.0 to 1.0, representing 0-360 degrees)
  • s: Saturation (0.0 to 1.0)
  • l: Lightness (0.0 to 1.0)

Returns

Tuple of (red, green, blue) where each is 0-255

Example

let (r, g, b) = hsl_to_rgb(0.0, 1.0, 0.5) // Red
// r = 255, g = 0, b = 0

#
join_cell_name

fn join_cell_name(col : StringView, row : Int) -> String raise
XlsxError

Joins column name and row number into a cell reference.

Parameters

  • col: Column name like "A", "AB", "XFD"
  • row: Row number (1-indexed)

Returns

Cell reference string like "A1", "AB123"

Example

let ref = join_cell_name("AB", 123) // ref = "AB123"

#
new_data_validation

fn new_data_validation(allow_blank : Bool) ->
DataValidation

Creates a new data validation object.

Data validations restrict what users can enter in cells.

Parameters

  • allow_blank: Whether empty cells are considered valid

Example

let dv = new_data_validation(true)
dv.set_drop_list(["Option1", "Option2", "Option3"])
dv.set_sqref("A1:A100")
sheet.add_data_validation(dv)

#
new_file

Creates a new workbook with a default sheet named "Sheet1".

This is a convenience function for the common case of creating a workbook with one initial worksheet.

Example

let wb = new_file()
wb.set_cell("Sheet1", "A1", "Hello")

#
new_workbook

Creates a new empty workbook without any sheets.

Use add_sheet on the returned workbook to add worksheets.

Example

let wb = new_workbook()

let sheet = wb.add_sheet("Data")

#
open_file

async fn open_file(path : String, password? : String, options? :
Options
, transcoder? : (String, Bytes) -> String raise
XlsxError
) ->
Workbook
raise
XlsxError

Opens an XLSX file from a file path asynchronously.

Parameters

  • path: Path to the XLSX file
  • password: Password if the file is encrypted (default: empty)
  • options: Optional read options
  • transcoder: Optional charset transcoder

Returns

Parsed Workbook object

Example

let wb = open_file("report.xlsx")

let wb_protected = open_file("secret.xlsx", password="pass123")

#
open_reader

Opens an XLSX file from a Reader asynchronously.

Parameters

  • reader: Any type implementing the Reader trait
  • password: Password if the file is encrypted (default: empty)
  • options: Optional read options
  • transcoder: Optional charset transcoder

Returns

Parsed Workbook object

#
read

Reads an XLSX file from bytes into a Workbook.

Parameters

  • bytes: Raw XLSX file content
  • options: Optional read options
  • transcoder: Optional function for charset transcoding (for non-UTF8 files)

Returns

Parsed Workbook object

Example

let bytes = read_file("report.xlsx")

let wb = read(bytes)

let value = wb.get_cell("Sheet1", "A1")

#
read_with_password

fn read_with_password(bytes : BytesView, password : String, options? :
Options
, transcoder? : (String, Bytes) -> String raise
XlsxError
) ->
Workbook
raise
XlsxError

Reads a password-protected XLSX file from bytes.

Parameters

  • bytes: Raw encrypted XLSX file content
  • password: Password used to encrypt the file
  • options: Optional read options
  • transcoder: Optional charset transcoder

Returns

Parsed Workbook object

Errors

  • InvalidPassword: If the password is incorrect
  • EncryptedPackage: If decryption fails

Example

let bytes = read_file("protected.xlsx")

let wb = read_with_password(bytes, "secret123")

#
read_zip_reader

async fn[R :
Reader
] read_zip_reader(reader : R, password? : String, options? :
Options
, transcoder? : (String, Bytes) -> String raise
XlsxError
) ->
Workbook
raise
XlsxError

Reads an XLSX from a ZIP reader asynchronously.

This is a lower-level function that allows reading from a ZIP stream that's already being read.

Parameters

  • reader: Any type implementing the Reader trait
  • password: Password if the file is encrypted (default: empty)
  • options: Optional read options
  • transcoder: Optional charset transcoder

Returns

Parsed Workbook object

#
rgb_to_hsl

fn rgb_to_hsl(r : Byte, g : Byte, b : Byte) -> (Double, Double, Double)

Converts RGB color values to HSL (Hue, Saturation, Lightness).

Parameters

  • r: Red component (0-255)
  • g: Green component (0-255)
  • b: Blue component (0-255)

Returns

Tuple of (hue, saturation, lightness) where:
  • hue: 0.0 to 1.0 (representing 0-360 degrees)
  • saturation: 0.0 to 1.0
  • lightness: 0.0 to 1.0

Example

let (h, s, l) = rgb_to_hsl(255, 0, 0) // Red
// h ≈ 0, s = 1.0, l = 0.5

#
split_cell_name

fn split_cell_name(cell : StringView) -> (String, Int) raise
XlsxError

Splits a cell reference into column name and row number.

Parameters

  • cell: Cell reference like "A1", "AB123", "5"

Returns

Tuple of (column_name, row_number) where row is 1-indexed

Example

let (col, row) = split_cell_name("AB123")
// col = "AB", row = 123

#
theme_color

fn theme_color(base_color : String, tint : Double) -> String

Applies a tint to a base color.

Theme colors in Excel can have tint values that lighten or darken the base color.

Parameters

  • base_color: Hex color string like "FF0000"
  • tint: Tint value from -1.0 (darken) to 1.0 (lighten)

Returns

Tinted hex color string

Example

let lighter = theme_color("FF0000", 0.5) // Lighter red

let darker = theme_color("FF0000", -0.5) // Darker red

#
utf16_index_to_utf8_offset

fn utf16_index_to_utf8_offset(s : StringView, index : Int) -> Int raise UtfIndexError

Convert a UTF-16 code unit index to a UTF-8 byte offset.

#
utf16_len

fn utf16_len(s : StringView) -> Int

Return UTF-16 code unit length for a string.

#
utf8_len

fn utf8_len(s : StringView) -> Int

Return UTF-8 byte length for a string.

#
utf8_offset_to_utf16_index

fn utf8_offset_to_utf16_index(bytes : BytesView, offset : Int) -> Int raise UtfIndexError

Convert a UTF-8 byte offset to a UTF-16 code unit index.

#
write

Writes a Workbook to XLSX bytes.

Parameters

  • workbook: The workbook to serialize

Returns

XLSX file content as bytes

Example

let wb = new_file()
wb.set_cell("Sheet1", "A1", "Hello")
let bytes = write(wb)
write_file("output.xlsx", bytes)

#
write_with_password

fn write_with_password(workbook :
Workbook
, password : String) -> Bytes raise
XlsxError

Writes a Workbook to password-protected XLSX bytes.

The file will be encrypted using the ECMA-376 encryption standard.

Parameters

  • workbook: The workbook to serialize
  • password: Password to protect the file with

Returns

Encrypted XLSX file content as bytes

Example

let wb = new_file()
wb.set_cell("Sheet1", "A1", "Confidential")
let bytes = write_with_password(wb, "secret123")