124 / Input, output and data

A purchase ledger as .xlsx

An ERP hands over the month's purchase ledger in Excel. std/xlsx writes a genuine .xlsx — a zip with XML inside, in pure Nyx, so it runs the same on the server and on wasm32-wasi — that Excel, LibreOffice and openpyxl all open.

124-xlsx-escribir-libro.nxSource →
// Nyx by Example: un libro contable en .xlsx.
//
// El caso real: un ERP entrega el libro de compras del mes en Excel. std/xlsx
// escribe un .xlsx de verdad (un zip con XML adentro, en Nyx puro: corre igual
// en el servidor y en wasm32-wasi) que abren Excel, LibreOffice y openpyxl.
//
// Lo que muestra:
//   - xlsx_sheet_new + row(): una hoja es una lista de filas de celdas;
//   - constructores por tipo: xlsx_text, xlsx_number(valor, decimales),
//     xlsx_int, xlsx_date("AAAA-MM-DD"), xlsx_bool, xlsx_blank, xlsx_bold;
//   - ancho de columna (col_width) y celdas combinadas (merge);
//   - encabezado fijo al bajar (freeze), autofiltro sobre la tabla y
//     montos con separador de miles (xlsx_number_fmt con "#,##0.00");
//   - un total calculado en el programa: las fórmulas NO se escriben, se
//     escribe el valor;
//   - los errores (nombre de hoja repetido, fecha inválida...) salen de
//     xlsx_write como Err, no antes.
//
// Correr: nyx run -- libro.xlsx   (sin argumento solo informa el tamaño)

import "std/xlsx"
import "std/error"

fn libro() -> Result<String, Error> {
    var h: XlsxSheetOut = xlsx_sheet_new("Libro de compras")
    h.row([xlsx_bold(xlsx_text("Libro de compras — septiembre 2026"))])
    h.merge("A1:D1")
    h.row([])
    h.row([xlsx_bold(xlsx_text("Fecha")), xlsx_bold(xlsx_text("Proveedor")),
           xlsx_bold(xlsx_text("Base")), xlsx_bold(xlsx_text("IVA"))])

    let fechas: Array = ["2026-09-01", "2026-09-04", "2026-09-15"]
    let proveedores: Array = ["Ferretería El Tornillo, C.A.", "Cementos del Centro", "Pinturas Aragua"]
    let bases: Array = [1000.0, 2500.5, 320.25]
    var total_base: float = 0.0
    var total_iva: float = 0.0
    var i: int = 0
    while i < fechas.length() {
        let base: float = bases[i]
        let iva: float = base * 0.16
        let fecha: String = fechas[i]
        let prov: String = proveedores[i]
        h.row([xlsx_date(fecha), xlsx_text(prov), xlsx_number_fmt(base, "#,##0.00"), xlsx_number_fmt(iva, "#,##0.00")])
        total_base = total_base + base
        total_iva = total_iva + iva
        i = i + 1
    }
    h.row([xlsx_blank(), xlsx_bold(xlsx_text("Totales")),
           xlsx_bold(xlsx_number_fmt(total_base, "#,##0.00")), xlsx_bold(xlsx_number_fmt(total_iva, "#,##0.00"))])
    h.col_width(1, 12.0)
    h.col_width(2, 30.0)
    h.col_width(3, 14.0)
    h.col_width(4, 14.0)
    // La tabla empieza en la fila 3: se fijan sus tres primeras filas (título,
    // separador y encabezado) y el filtro cubre encabezado + datos, sin totales.
    h.freeze(3, 0)
    h.autofilter("A3:D" + int_to_string(3 + fechas.length()))

    var notas: XlsxSheetOut = xlsx_sheet_new("Notas")
    notas.row([xlsx_text("Generado por Nyx"), xlsx_int(3), xlsx_bool(true)])
    return xlsx_write([h, notas])
}

fn main() -> int {
    match libro() {
        Result.Ok(bytes) => {
            let args: Array = get_args()
            if args.length() > 1 {
                let ruta: String = args[1]
                write_file(ruta, bytes)
                print("libro escrito en " + ruta + " (" + int_to_string(bytes.length()) + " bytes)")
            } else {
                print("libro: " + int_to_string(bytes.length()) + " bytes, empieza con " + bytes.substring(0, 2))
                print("para escribirlo: nyx run -- libro.xlsx")
            }
            return 0
        }
        Result.Err(e) => {
            print("no se pudo armar el libro: " + error_to_string(e))
            return 1
        }
    }
}
Outputstdout
libro: 3375 bytes, empieza con PK
para escribirlo: nyx run -- libro.xlsx

How it works

A sheet is a list of rows of cells, built with xlsx_sheet_new and row(). Cells have one constructor per type: xlsx_text, xlsx_number(value, decimals), xlsx_int, xlsx_date("YYYY-MM-DD"), xlsx_bool, xlsx_blank and xlsx_bold. On top of that come column widths (col_width), merged cells (merge), a header that stays put on scroll (freeze), an autofilter over the table, and amounts with a thousands separator through xlsx_number_fmt and "#,##0.00".

The total is calculated in the program: formulas are NOT written, the value is. And mistakes — a repeated sheet name, an invalid date — surface from xlsx_write as an Err, not earlier. Run it with nyx run -- libro.xlsx to write the file; with no argument it only reports the size, which is what you see above.