A lightweight DataFrame and tabular-data library for MoonBit
Dependencies
// API shape (illustrative — the "Quick start" below runs this same
// pipeline as a doc test)
read_csv("sales.csv")
.filter(col("product").eq(lit_str("widget")))
.group_by([col("region")])
.agg([col("revenue").sum()])
.to_markdown()moon add ihb2032/MoonFrame///|
import {
"ihb2032/MoonFrame" @moonframe,
}MoonBit v0.10.4 deprecates the legacy JSON package manifest. New and migrated projects should use moon.mod / moon.pkg, as this repository does.
region,product,revenue,quantity
west,widget,100,10
east,gadget,50,5
west,gadget,70,7
east,widget,30,3
north,widget,40,4
north,gadget,60,6
west,gizmo,90,9
east,gizmo,20,2///|
test "readme: keep the widget rows, sort by quantity" {
let path = "_build/readme_sales.csv"
write_csv(
path,
DataFrame::DataFrame([
Series::from_strings("region", [
"west", "east", "west", "east", "north", "north", "west", "east",
]),
Series::from_strings("product", [
"widget", "gadget", "gadget", "widget", "widget", "gadget", "gizmo", "gizmo",
]),
Series::from_ints("revenue", [100, 50, 70, 30, 40, 60, 90, 20]),
Series::from_ints("quantity", [10, 5, 7, 3, 4, 6, 9, 2]),
]),
)
fn widgets(p : String) -> String raise DataError {
read_csv(p)
.filter(col("product").eq(lit_str("widget")))
.select(cols(["region", "revenue", "quantity"]))
// each key is (key, descending, nulls_last)
.sort([(col("quantity"), true, true)])
.to_markdown()
}
inspect(
widgets(path),
content=(
#|| region | revenue | quantity |
#|| ------ | ------- | -------- |
#|| west | 100 | 10 |
#|| north | 40 | 4 |
#|| east | 30 | 3 |
#|
),
)
}///|
test "readme: summarise by region" {
let sales = DataFrame::DataFrame([
Series::from_strings("region", [
"west", "east", "west", "east", "north", "north", "west", "east",
]),
Series::from_ints("revenue", [100, 50, 70, 30, 40, 60, 90, 20]),
Series::from_ints("quantity", [10, 5, 7, 3, 4, 6, 9, 2]),
])
let summary = sales
.group_by([col("region")])
.agg([
col("revenue").sum().with_alias("revenue"),
col("quantity").sum().with_alias("quantity"),
])
inspect(
summary.to_markdown(),
content=(
#|| region | revenue | quantity |
#|| ------ | ------- | -------- |
#|| west | 260 | 26 |
#|| east | 100 | 10 |
#|| north | 100 | 10 |
#|
),
)
}///|
test "readme: bridging a raise back to a Result" {
let df = DataFrame::DataFrame([Series::from_ints("a", [1, 2])])
let result : Result[DataFrame, DataError] = Ok(df.select([col("missing")])) catch {
e => Err(e)
}
assert_true(result is Err(ColumnNotFound(_)))
}moon run examples/sales_analysis # filter → select → sort → describe → markdown
moon run examples/data_cleaning # drop_nulls → fill_null → CSV round-trip
moon run examples/reporting # group_by → to_html + Vega-Lite spec
moon run examples/expressions # with_columns → filter → agg → lazy + explaintypes/ value types, errors (DataError), schemas
internal/buffer/ the raw buffer primitives — the Arrow validity bitmap and the UTF-8 (bytes + offsets) string buffer every columnar layout flattens from
internal/scan/ the scan-driver seam — what one readable source (CSV, NDJSON, a future Parquet) implements so the lazy engine executes, narrows, and renders it format-agnostically
internal/column/ Arrow-style storage — validity bitmap + Builtin/Numeric backends; wrapped by Series and read by internal/kernel (which packages may name it is enforced by check_layering.sh)
internal/kernel/ the vectorized expression kernels — Series broadcasting, arithmetic / logic / comparison / string ops, ternary, map, and the dtype inference behind a computed column; called by frame's evaluator
internal/text/ shared text primitives — lexicographic compare, debug escaping, decimal literal parsing
internal/numeric/ shared numeric primitives — exact Int64/Double comparison and the extremum fold, used from types up through the kernels
internal/order/ shared position primitives — the stable index sort behind every sort, and the row-count clamp behind every head / tail / limit
internal/ir/ module-internal expression AST — ExprNode + the operator tags, walked by the engine
series/ Series + column-level stats + the shared reduction / rebuild / key-cell kernels
expr/ opaque Expr handle — constructors, operators, when/then/otherwise builders, to_string rendering
frame/ DataFrame + the operators (usually one per file) + group_by + join + the expression evaluator (with_columns / select / filter / agg) + to_markdown / to_html
io/ CSV (NyaCSV-backed), JSON, NDJSON read / write + their options types
chart/ Vega-Lite export — ChartSpec / ChartKind / VegaType builders, format_vega_lite / write_vega_lite (shares io's JSON cell conventions)
lazy/ deferred query plan — LazyFrame builders, collect / explain, predicate + projection pushdown
moonframe.mbt the root package — facade over the public API (fluent-chain intermediates stay in their sub-packages)internal/column how a column is laid out: data buffers + validity bitmap
series what a column is: dtype, validity, backend convergence
internal/kernel how a column is computed: one vectorized pass per operator
frame and above what a verb means: row sets, scheduling, schema, errorsmoon check # type-check the workspace
moon test # run all tests (add --target all for every backend)
moon fmt # format sources
moon info # regenerate .mbti interface snapshotstest "col: renders as a reference" {
inspect(col("qty").to_string(), content="col(qty)")
}test "cols: shorthand for the reference list" {
debug_inspect(
cols(["a", "b"]).map(e => e.to_string()),
content="[\"col(a)\", \"col(b)\"]",
)
}test "lit: embeds any Scalar" {
inspect(lit(@types.Scalar::Int(3L)).to_string(), content="3")
}test "lit_bool: renders bare" {
inspect(lit_bool(true).to_string(), content="true")
}test "lit_float: whole values keep their point" {
inspect(lit_float(2.0).to_string(), content="2.0")
}test "lit_int: renders bare" {
inspect(lit_int(5L).to_string(), content="5")
}test "lit_series: names its column after the series" {
let s = @series.Series::from_ints("score", [1, 2])
inspect(lit_series(s).output_name(), content="score")
}test "lit_str: renders quoted" {
inspect(lit_str("ab").to_string(), content="\"ab\"")
}test "map_many: drops to a host closure" {
let total = map_many(label="total", [col("a"), col("b")], cells => {
@types.Scalar::Int(cells[0].as_int() + cells[1].as_int())
})
inspect(total.to_string(), content="map(\"total\", [col(a), col(b)])")
}test "when: the chain is the only construction route" {
let tagged = when(col("qty").gt(lit_int(5)))
.then(lit_str("big"))
.otherwise(lit_str("small"))
inspect(
tagged.to_string(),
content="when((col(qty) > 5)).then(\"big\").otherwise(\"small\")",
)
}Install
Download zipA lightweight DataFrame and tabular-data library for MoonBit
Dependencies