Sign in

    moonods

    Typed OpenDocument spreadsheet (.ods) reader and writer with bounded, deterministic packages

    ods
    opendocument
    spreadsheet
    csv
    Download zip
    Version
    0.3.0
    License
    MIT
    Last updated
    1 hour ago
    Downloads
    46

    #MoonODS

    MoonBit 的数据型 ODS 读写库:类型化 Workbook → Sheet → Cell、有界 ZIP/XML 读取、显式损失诊断、CSV 与表头记录。适合需要把数据以 .ods 交给办公软件的报表和离线处理。

    本文描述 0.3.0 源码提供的接口。0.2.0 发行包只有写出功能,不能取得这里的读取、CSV 或采购修正流程。版本是否已发布以 Mooncakes 实际记录为准;GitHub 合并或本地打包不代表注册表发布。

    0.3.0 正式发布后,新项目可固定安装 moon add sundaysebasidian-byte/moonods@0.3.0,并在 moon.pkg 导入 "sundaysebasidian-byte/moonods" @ods。注册表提供库与示例源码,不安装全局 CLI;下列完整业务工作流需要源码检出及 Node.js,测试脚本和读取夹具不随运行包分发。

    #从源码运行

    需要已有的 MoonBit SDK 和 Node.js。CI 的固定版本见 TOOLCHAIN.lock。

    git clone https://github.com/sundaysebasidian-byte/moonods.git cd moonods moon update moon test --target js moon run examples/generate --target js

    生成的文件位于 examples/generated/。moon update 需要注册表连接;这不是离线安装证明。已有 SDK 的本地依赖恢复可用 python3 scripts/restore_deps.py,随后 moon work use .mooncakes/moonbit-community/flate,使用仓库锁定的原版依赖。

    #公共 API

    fn make_report() -> Bytes raise @ods.OdsError {
    let workbook = @ods.Workbook::new()
    let sheet = workbook.add_sheet("销售")
    sheet.set(0, 0, @ods.Cell::new(Text("十月销售")).styled(Header))
    sheet.merge(0, 0, 1, 2)
    sheet.set(1, 0, @ods.Cell::new(Date(2026, 10, 1)).styled(DateISO))
    sheet.set(1, 1, @ods.Cell::new(Number(399.88)).styled(Decimal2))
    sheet.set(2, 1, @ods.Cell::formula("of:=[.B2]*2", Number(799.76)))
    sheet.set_column_width(1, 45)
    workbook.to_ods()
    }

    在消费模块的 moon.pkg 导入 "sundaysebasidian-byte/moonods" @ods。使用 main 源码时,通过 MoonBit workspace 引用它,不能用注册表旧版代替。公开接口见 pkg.generated.mbti,独立模块测试见 tests/consumer。

    读取 @ods.read_ods(bytes, allow_formatting_loss=true) 得到 Workbook 和带表名、行列、错误码的损失诊断。默认拒绝有损导入;显式接受后保留值、公式文本/缓存及合并,丢弃样式。CSV 按文本导入,导出时丢失公式、类型、样式及 Empty/空字符串区别。公式缓存可能过期,编辑不会自动重算。

    #读取、修改和导出

    以下输入是本项目生成的合成测试样本:

    mkdir -p /tmp/moonods-example-output ODS_INPUT=tests/reader-fixtures/external/libreoffice-dev-types.ods \ ODS_ACCEPT_LOSS=1 ODS_SET=0,1,0,8.5 \ ODS_OUTPUT=/tmp/moonods-example-output/edited.ods \ ODS_CSV=/tmp/moonods-example-output/details.csv \ ODS_CSV_CACHE=1 ODS_CSV_REJECT_FORMULA_TEXT=1 \ moon run examples/ods_tool --target js

    ODS_CSV_REJECT_FORMULA_TEXT=1 拒绝以公式符号开头的文本;默认保留原文本语义,类型化负数不会被当作文本公式拒绝。这个选项不是办公软件沙箱。

    #采购数量修正与预算复核

    先生成完全合成的三表模板,再读取订单数量修正 CSV:

    mkdir -p /tmp/moonods-procurement/input /tmp/moonods-procurement/output REPORT_OUTPUT=/tmp/moonods-procurement/input moon run examples/procurement --target js printf '订单,数量\r\nSYN-202610-001,5\r\nSYN-202610-004,0\r\n' > /tmp/moonods-procurement/changes.csv RECONCILE_INPUT=/tmp/moonods-procurement/input/procurement.ods \ RECONCILE_CHANGES=/tmp/moonods-procurement/changes.csv \ RECONCILE_OUTPUT=/tmp/moonods-procurement/output RECONCILE_ACCEPT_LOSS=1 \ moon run examples/reconcile --target js

    输出 reconciled.ods、details.csv、audit.csv。这个样例的采购额从 80300 分变为 100650 分,研发超预算 18750 分;审计记录 CSV 请求及未请求但缓存实际变化的订单。输入可以替换为符合相同三表契约的 ODS,订单和汇总不是程序中写死的数据。

    这是固定模板业务适配器:数量 0..10000,金额必须能表示为整数分,总额不超过 2,000,000,000 分。仅计算已知模板算术的缓存,未知公式拒绝;不是通用采购系统或公式引擎。使用合成数据不证明实际用户采用或财务系统适用性。

    #验证与边界

    • 项目测试覆盖类型、空值、Unicode、CSV、ZIP/XML、错误输入和资源上限;tests/reader-fixtures 只含项目合成样本及相应故障变体。
    • python3 scripts/verify_reconcile_source.py --output /absolute/new-directory 运行当前源码的 JS 业务与失败保护检查,需要已有 SDK/Node。
    • python3 scripts/package_source.py --output /absolute/source.zip 导出当前干净源码,不含 Git 历史。verify_delivery_consumer.py 可独立消费此 ZIP;verify_reconcile.py 另接收已有 SDK 与 LibreOffice 路径验证业务往返,参数见各脚本 --help。源码 ZIP 不是正式注册表包。
    • .github/workflows/fresh-clone.yml 检查固定 SDK 与最新版;ci.yml 保留固定工具链的完整验收。以准确发行提交的新 CI 为准,版本准备尚未推送时不能沿用主分支结果替代。
    • 完整独立验证依赖见 requirements-verify.txt,入口为 python3 scripts/acceptance.py;它严格要求锁定版本并需要访问官方 schema/依赖来源,不自动安装工具。
    • 运行时守卫继续检查版本漂移;仅适用于历史 0.1.0 发行的注册表候选守卫随旧发行夹具移出本源码树,不计入当前验证范围。
    • 历史审计日志、旧打包材料、用户机器路径和 Wasm 复现产物未随此源码树公开。旧审计中记录的 Wasm/Wasm GC 工具链运行失败、native 组件缺失和正式打包工具缺件不因删除日志而改判通过。
    • Excel、GUI/打印/外观、通用公式重算、并发及崩溃恢复未验证。每个输出文件独立 rename,多个文件不是整体事务。

    库核心为 MoonBit;文件读写示例使用 JS 宿主。支持文本、有限 Double、布尔、公历日期、空值、多表、基础样式、列宽和合并;不提供宏、图表、加密、ZIP64 或大文件流式处理。

    #许可与来源

    原创代码 MIT,压缩/CRC32 依赖 flate 为 Apache-2.0,SDK 不打包。见 LICENSE、THIRD_PARTY_NOTICES.md、SOURCES.md 与 AI_USAGE.md。

    OdsError

    pub suberror OdsError {
    Invalid(String)
    } derive(
    Debug
    )

    A recoverable validation error; no user input is inserted as raw XML.

    OdsError::to_repr

    Cell

    type Cell

    Cell::excel_1900_date

    fn Cell::excel_1900_date(year : Int, month : Int, day : Int) -> Cell raise OdsError

    Optional date policy for Excel's 1900 date system, with an ISO date style. Rejects dates before 1900-01-01 and all invalid Gregorian dates, including Excel's fictional 1900-02-29. Does not configure a reader's date system or guarantee rendering. For historical display, callers may explicitly use Text("0001-01-01"), which has no date arithmetic semantics.

    Cell::expression

    fn Cell::expression(self : Cell) -> String?

    Cell::formula

    fn Cell::formula(expression : String, cache : Value) -> Cell raise OdsError

    OpenFormula text must begin of:=. This validates storage, not formula syntax. The cache is supplied by the caller and may be stale. Neither editing other cells nor serialization recalculates it; application open/save may retain it. Formula text is stored verbatim, including possible external references. Only pass trusted expressions and manage cache freshness explicitly. Date caches default to DateISO so readers keep the date presentation when evaluating DATE formulas. Callers may still explicitly override the style.

    Cell::new

    fn Cell::new(value : Value) -> Cell raise OdsError

    Validate ODF values; application-specific date ranges are not inferred.

    Cell::styled

    fn Cell::styled(self : Cell, style : Style) -> Cell

    Cell::value

    fn Cell::value(self : Cell) -> Value

    Returns the stored literal value or caller/imported formula cache. This does not evaluate the formula or assert that its cache matches current operands.

    ImportDiagnostic

    pub(all) struct ImportDiagnostic {
    sheet : String
    row : Int
    col : Int
    code : String
    message : String
    } derive(Eq,
    Debug
    )

    Coordinates are zero-based. -1 means workbook/sheet scope.

    ImportDiagnostic::equal

    ImportDiagnostic::not_equal

    fn ImportDiagnostic::not_equal(x : ImportDiagnostic, y : ImportDiagnostic) -> Bool

    ImportResult

    type ImportResult

    ImportResult::diagnostics

    fn ImportResult::diagnostics(self : ImportResult) -> Array[ImportDiagnostic]

    A copy: callers cannot mutate the reader's diagnostic record.

    ImportResult::workbook

    fn ImportResult::workbook(self : ImportResult) -> Workbook

    Sheet

    type Sheet

    Sheet::dimensions

    fn Sheet::dimensions(self : Sheet) -> (Int, Int)

    Sheet::get

    fn Sheet::get(self : Sheet, row : Int, col : Int) -> Cell?

    Sheet::is_covered

    fn Sheet::is_covered(self : Sheet, row : Int, col : Int) -> Bool

    Sheet::merge

    fn Sheet::merge(self : Sheet, row : Int, col : Int, rows : Int, cols : Int) -> Unit raise OdsError

    Merge a zero-based rectangle. Covered values are never silently discarded.

    Sheet::merge_span

    fn Sheet::merge_span(self : Sheet, row : Int, col : Int) -> (Int, Int)?

    Sheet::name

    fn Sheet::name(self : Sheet) -> String

    Sheet::records

    fn Sheet::records(self : Sheet, header_row? : Int) -> Array[TableRecord] raise OdsError

    Exact, case-sensitive header names. Every header must be a nonblank Text literal and unique. Rejects merged sheets rather than guessing fill-down. Includes blank data rows; absent cells map to Empty, Text("") stays distinct.

    Sheet::set

    fn Sheet::set(self : Sheet, row : Int, col : Int, cell : Cell) -> Unit raise OdsError

    Zero-based coordinate. Validation precedes mutation.

    Sheet::set_column_width

    fn Sheet::set_column_width(self : Sheet, col : Int, mm : Int) -> Unit raise OdsError

    Emit the requested ODF fixed column length in integer millimeters (1..500). Viewer rounding, font metrics, zoom and physical print layout are not guaranteed. No app-specific pixel/point compensation is applied.

    Sheet::set_row

    fn Sheet::set_row(self : Sheet, row : Int, start_col : Int, cells : Array[Cell]) -> Unit raise OdsError

    Write a contiguous row atomically. All cells are validated before mutation, including merge coverage and the net text budget after replacements. An empty array is a no-op at a valid coordinate and does not grow the grid.

    Sheet::to_csv

    fn Sheet::to_csv(self : Sheet, use_formula_cache? : Bool, reject_formula_like_text? : Bool) -> String raise OdsError

    Deterministic comma CSV, quoted fields and CRLF records. Dates use ISO dates, booleans true/false, numbers binary64 text. Empty and Text("") both emit "". Merges are rejected. Formula caches require an explicit opt-in; formulas, types, styles and sheet metadata cannot survive a CSV roundtrip. External spreadsheet applications may infer types/formulas from CSV text. use_formula_cache exports stored, possibly stale caches without recalculation. reject_formula_like_text skips only leading ASCII space, TAB, CR and LF, then rejects Text values starting =, +, - or @. It never changes data or rejects typed negative numbers, and is a bounded policy rather than an application sandbox. The default preserves the existing lossless text semantics.

    Style

    pub(all) enum Style {
    Plain
    Header
    Highlight
    Decimal2
    DateISO
    }

    Small interoperable style vocabulary; arbitrary properties are outside MVP.

    TableRecord

    pub(all) struct TableRecord {
    row : Int
    values : Map[String, Value]
    }

    A snapshot of literal values or stored formula caches, with zero-based row. Mapping does not evaluate formulas or fill merged cells.

    Value

    pub(all) enum Value {
    Empty
    Text(String)
    Number(Double)
    Boolean(Bool)
    Date(Int, Int, Int)
    } derive(Eq,
    Debug
    )

    Formula cache values have the same types as literal values. Date is a Gregorian ODF date in 1..9999, independent of an application's serial date epoch. It does not promise Excel supports historical dates.

    Value::equal

    fn Value::equal(Value, Value) -> Bool

    Value::not_equal

    fn Value::not_equal(x : Value, y : Value) -> Bool

    Value::to_repr

    Workbook

    type Workbook

    Workbook::add_csv_sheet

    fn Workbook::add_csv_sheet(self : Workbook, name : String, text : String) -> Sheet raise OdsError

    RFC4180 quoting with CRLF or LF record separators; bare CR is rejected. Every field becomes Text (including empty fields and leading '='). No type guessing or formula evaluation. Empty input imports one empty text cell. Parsing/validation finishes before the workbook is mutated.

    Workbook::add_sheet

    fn Workbook::add_sheet(self : Workbook, name : String) -> Sheet raise OdsError

    Workbook::content_xml

    fn Workbook::content_xml(self : Workbook) -> String raise OdsError

    Inspectable XML for diagnostics. ODS output is provided by to_ods().

    Workbook::new

    fn Workbook::new() -> Workbook

    Workbook::sheet

    fn Workbook::sheet(self : Workbook, index : Int) -> Sheet?

    Workbook::sheet_count

    fn Workbook::sheet_count(self : Workbook) -> Int

    Workbook::to_ods

    fn Workbook::to_ods(self : Workbook) -> Bytes raise OdsError

    Return a complete deterministic ODS package. I/O belongs to the caller.

    read_ods

    fn read_ods(data : Bytes, allow_formatting_loss? : Bool) -> ImportResult raise OdsError

    Import a bounded, data-only ODS subset. The explicit opt-in acknowledges formatting, application metadata and view settings are not retained. Unsupported semantic features fail, with sheet/cell context where known. No partial workbook is returned on failure; formulas are never evaluated.