///|
/// A one-dimensional, dtype-aware column with a name. `Series` is the
/// per-column unit behind `DataFrame`, exposing the structural, nullability,
/// transform, cast, and statistics surface that the
/// `frame` and `io` layers build on.
///
/// Its backend is an internal representation — Arrow-style storage with an
/// explicit validity bitmap, or an all-valid unboxed numeric fast path — decided
/// by a column's content rather than by its caller: the row rebuilds and the
/// expression engine's computed columns converge a null-free numeric result onto
/// the fast path (`try_column_to_numeric`), the nullable constructors and
/// `cast` produce a `Builtin` column whatever they started from, and the
/// backend-preserving windows leave a column on whichever
/// backend they found it. The **supported** API neither
/// observes nor selects it: the few methods that do (`storage` /
/// `storage_kind` / `is_canonical`) are engine seams, absent from the generated
/// interface and carrying no compatibility promise, so a caller outside this
/// module has no supported way to tell which one a column is on — and no way at
/// all to choose it, since nothing here moves a column onto a backend its
/// content does not call for.
///
/// The engine machinery the verbs build on is the row-rebuild machine (an
/// index vector, a window, a
/// mask, or a column merge turned into a new `Series`) and the storage-backend
/// machine (converge / preserve / read / assert).
/// Both are engine seams serving `frame` and the kernel layer, not
/// `Series` API.
///
/// The fields are `priv`, so the struct is opaque outside this package: reach a
/// column through the value-level accessors (`name()` / `dtype()` / `len()` /
/// `get()` / `to_scalars()` / …) and build one through the named constructors,
/// so the underlying column always keeps its data-and-validity invariants.
pub struct Series {
priv name : String
priv storage : @column.ColumnStorage
} derive(Eq, Debug)
///|
pub extend Series with Eq::{equal, not_equal}
///|
pub extend Series with Debug::{to_repr}
// ── Constructors ──────────────────────────────────────────────────────
///|
/// Wrap an existing `ColumnStorage` under the given name — the canonical
/// constructor at the backend seam. The backend (and validity / null-count)
/// is whatever the storage already carries. Callers holding a
/// `BuiltinColumn` use `from_builtin` instead.
///
/// Package-private. A `#doc(hidden)` seam is still `pub` to the compiler, so
/// one taking a storage type is an entry point a downstream caller could reach
/// past `Series` with; this one has no caller outside this package, so it need
/// not be `pub` at all. The seams that remain — `storage()` for
/// the kernel layer, `Expr::node` for the evaluator and optimizer — are the
/// ones another package genuinely needs.
fn Series::new(name : String, storage : @column.ColumnStorage) -> Series {
{ name, storage, }
}
///|
/// Wrap an existing `BuiltinColumn` under the given name, behind a
/// `ColumnStorage::Builtin` — the adapter the constructors and transforms in
/// this package use when they have already produced a `BuiltinColumn`.
/// Package-private for the same reason as `new`.
fn Series::from_builtin(
name : String,
storage : @column.BuiltinColumn,
) -> Series {
{ name, storage: @column.ColumnStorage::Builtin(storage), }
}
///|
/// An empty (0-row) `Series` of the given dtype — the physical dispatch behind
/// `DataFrame::empty`, kept here in `series` (which owns the column) so `frame`
/// need not match storage backends to build an empty column. A `Null` dtype has
/// no physical backend, so it `raise`s `Unsupported`.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::empty_of(
name : String,
dtype : @types.DataType,
) -> Series raise @types.DataError {
let storage = match dtype.physical() {
// The I64 riders share one buffer, so the dtype-carrying constructor
// serves both `Int` and its riders (`Date`) — no per-dtype branch.
Some(@types.PhysicalType::I64) =>
@column.BuiltinColumn::from_i64_options(dtype, [])
Some(@types.PhysicalType::F64) => @column.BuiltinColumn::from_floats([])
Some(@types.PhysicalType::Bool) => @column.BuiltinColumn::from_bools([])
Some(@types.PhysicalType::Utf8) => @column.BuiltinColumn::from_strings([])
None =>
raise @types.DataError::Unsupported(
"cannot build an empty column of dtype Null: \{name}",
)
}
Series::from_builtin(name, storage)
}
///|
/// Build an `Int` series from raw values (no nulls). Lands on the `Numeric`
/// fast-path backend — no validity bitmap is allocated.
///
/// `values` is defensively copied, so a `Series` is independent of the
/// caller's array: mutating `values` afterwards does not change the series'
/// cells. (The nullable constructors `from_*_options` likewise copy while
/// boxing into `Option`.)
pub fn Series::from_ints(name : String, values : Array[Int64]) -> Series {
Series::new(
name,
@column.ColumnStorage::Numeric(@column.NumericColumn::from_ints(values)),
)
}
///|
/// Build an `Int` series where `None` entries are nulls. Nullable, so it
/// lands on the general-purpose `Builtin` backend.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::from_int_options(
name : String,
values : Array[Int64?],
) -> Series {
Series::from_builtin(name, @column.BuiltinColumn::from_int_options(values))
}
///|
/// Build a nullable series whose `dtype` fixes the identity of an `I64`
/// rider — `Int`, or a date-like dtype such as `Date` — from optional raw
/// cells. Nullable, so it lands on the general-purpose `Builtin` backend.
/// The dtype-carrying rebuild keeps a `Date` column a `Date` (an `Int`
/// rebuild would silently retype every cell), so a kernel arm that rebuilds
/// same-physical columns without knowing which rider it holds goes through
/// here, not `from_int_options`.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::from_i64_options(
name : String,
dtype : @types.DataType,
values : Array[Int64?],
) -> Series {
Series::from_builtin(
name,
@column.BuiltinColumn::from_i64_options(dtype, values),
)
}
///|
/// Build a `Float` series from raw values (no nulls). Lands on the
/// `Numeric` fast-path backend. Like `from_ints`, `values` is defensively
/// copied, so the series is independent of the caller's array.
pub fn Series::from_floats(name : String, values : Array[Double]) -> Series {
Series::new(
name,
@column.ColumnStorage::Numeric(@column.NumericColumn::from_floats(values)),
)
}
///|
/// Build a `Float` series where `None` entries are nulls. Nullable, so it
/// lands on the `Builtin` backend.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::from_float_options(
name : String,
values : Array[Double?],
) -> Series {
Series::from_builtin(name, @column.BuiltinColumn::from_float_options(values))
}
///|
/// Build a `Bool` series from raw values (no nulls). `Bool` is non-numeric,
/// so it lands on the `Builtin` backend. `values` is defensively copied, so
/// the series is independent of the caller's array.
pub fn Series::from_bools(name : String, values : Array[Bool]) -> Series {
Series::from_builtin(name, @column.BuiltinColumn::from_bools(values))
}
///|
/// Build a `Bool` series where `None` entries are nulls.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::from_bool_options(
name : String,
values : Array[Bool?],
) -> Series {
Series::from_builtin(name, @column.BuiltinColumn::from_bool_options(values))
}
///|
/// Build a `String` series from raw values (no nulls). `String` is
/// non-numeric, so it lands on the `Builtin` backend. `values` is defensively
/// copied, so the series is independent of the caller's array.
pub fn Series::from_strings(name : String, values : Array[String]) -> Series {
Series::from_builtin(name, @column.BuiltinColumn::from_strings(values))
}
///|
/// Build a `String` series where `None` entries are nulls.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::from_string_options(
name : String,
values : Array[String?],
) -> Series {
Series::from_builtin(name, @column.BuiltinColumn::from_string_options(values))
}
///|
/// The one public nullable constructor — Polars' `pl.Series(name, [1, None])`
/// translated to a statically-typed language: the cells carry their own
/// nullness as `Scalar`, and the column's dtype is `dtype~` when given, or
/// the dtype of the first non-null cell when not. The null-free `from_ints` /
/// `from_floats` / `from_bools` / `from_strings` constructors stay the typed
/// fast path; this is the door for cells that may be null — including
/// `Scalar::Date`, which has no other public constructor.
///
/// Every non-null cell must carry the target dtype exactly — an `Int` cell in
/// a `Float` column is a `cast`, not a construction, so it `raise`s
/// `TypeMismatch(Expected(target, got, name))` here (on the first offending
/// cell, in cell order). Two targets cannot be inferred and `raise` instead:
/// an all-null `cells` without `dtype~` has nothing to decide from
/// (`InvalidOperation`), and `dtype~ = Null` names no physical column
/// (`Unsupported`). A nullable series lands on the general-purpose `Builtin`
/// backend, like every `from_*_options` build.
pub fn Series::from_scalars(
name : String,
cells : Array[@types.Scalar],
dtype? : @types.DataType,
) -> Series raise @types.DataError {
let target : @types.DataType = match dtype {
Some(d) => d
None => {
let mut found : @types.DataType? = None
for cell in cells {
if !(cell is @types.Scalar::Null) {
found = Some(cell.dtype())
break
}
}
match found {
Some(d) => d
None =>
raise @types.DataError::InvalidOperation(
"cannot infer a dtype for column \"\{name}\": every cell is null, pass dtype~ to lock one",
)
}
}
}
match target {
@types.DataType::Int =>
Series::from_int_options(
name,
typed_cells(cells, target, name, cell => {
if cell is @types.Scalar::Int(v) {
Some(v)
} else {
None
}
}),
)
@types.DataType::Float =>
Series::from_float_options(
name,
typed_cells(cells, target, name, cell => {
if cell is @types.Scalar::Float(v) {
Some(v)
} else {
None
}
}),
)
@types.DataType::Bool =>
Series::from_bool_options(
name,
typed_cells(cells, target, name, cell => {
if cell is @types.Scalar::Bool(v) {
Some(v)
} else {
None
}
}),
)
@types.DataType::String =>
Series::from_string_options(
name,
typed_cells(cells, target, name, cell => {
if cell is @types.Scalar::String(v) {
Some(v)
} else {
None
}
}),
)
@types.DataType::Date =>
Series::from_date_options(
name,
typed_cells(cells, target, name, cell => {
if cell is @types.Scalar::Date(v) {
Some(v)
} else {
None
}
}),
)
@types.DataType::Null =>
raise @types.DataError::Unsupported(
"cannot build a column of dtype Null: \{name}",
)
}
}
///|
/// Decode `Scalar` cells into the typed optional cells a nullable constructor
/// takes: `Scalar::Null` becomes `None`, a cell `decode` recognises is
/// unwrapped, and anything else `raise`s
/// `TypeMismatch(Expected(target, got, name))`. Cells are visited in order, so
/// the reported cell is the first offending one.
fn[T] typed_cells(
cells : Array[@types.Scalar],
target : @types.DataType,
name : String,
decode : (@types.Scalar) -> T?,
) -> Array[T?] raise @types.DataError {
cells.map(cell => {
match cell {
@types.Scalar::Null => None
c =>
match decode(c) {
Some(v) => Some(v)
None =>
raise @types.DataError::TypeMismatch(
@types.TypeMismatchDetail::Expected(target, c.dtype(), name),
)
}
}
})
}
///|
/// Build a `Date` series from optional raw day-counts (whole days since the
/// Unix epoch, `1970-01-01` = day `0`), `None` entries nulls. The date face
/// of the shared `I64` buffer constructor: the cells are the same `Int64`
/// buffer an int column carries, with date semantics on read — the registry's
/// per-dtype lift turns a cell back into a `Scalar::Date`.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::from_date_options(
name : String,
values : Array[Int64?],
) -> Series {
Series::from_builtin(
name,
@column.BuiltinColumn::from_i64_options(@types.DataType::Date, values),
)
}
// ── Inspection ────────────────────────────────────────────────────────
///|
/// Column name.
pub fn Series::name(self : Series) -> String {
self.name
}
///|
/// Logical dtype. Delegates to the underlying column so the two never
/// drift apart.
pub fn Series::dtype(self : Series) -> @types.DataType {
self.storage.dtype()
}
///|
/// Number of cells (valid plus null).
pub fn Series::len(self : Series) -> Int {
self.storage.len()
}
///|
/// `true` when the series has zero cells.
pub fn Series::is_empty(self : Series) -> Bool {
self.storage.is_empty()
}
///|
/// Number of null cells.
pub fn Series::null_count(self : Series) -> Int {
self.storage.null_count()
}
///|
/// Whether cell `i` is null. Out-of-bounds indices bubble the
/// underlying `IndexOutOfBounds` error.
pub fn Series::is_null(self : Series, i : Int) -> Bool raise @types.DataError {
self.storage.is_null(i)
}
///|
/// Read cell `i` as a `Scalar` (`Null` for null cells) — the bracket
/// spelling `s[0]` (Polars' `s[0]`). Out-of-bounds indices bubble the
/// underlying `IndexOutOfBounds` error; a negative index does **not** wrap
/// from the end (MoonBit indexing convention — the slice spelling `s[-1:]`
/// is where negatives count from the end).
#alias("_[_]")
pub fn Series::get(
self : Series,
i : Int,
) -> @types.Scalar raise @types.DataError {
self.storage.get(i)
}
///|
/// Materialise every cell as a `Scalar` (`Null` for null cells), in order.
/// Total — reads the backing array and validity mask once. Renderers (CSV
/// / JSON / Markdown) use this to walk a column without a per-cell
/// bounds-checked `get`. Each present cell lifts through the dtype registry
/// (`scalar_of`), so an `I64`-riding dtype like `Date` surfaces its own
/// `Scalar` variant, not the buffer's.
pub fn Series::to_scalars(self : Series) -> Array[@types.Scalar] {
let valid = self.validity_bools()
let data = self.storage.data()
let dtype = self.dtype()
Array::makei(valid.length(), i => {
if valid[i] {
@column.scalar_of(dtype, data, i)
} else {
@types.Scalar::Null
}
})
}
///|
/// Expose the underlying `ColumnStorage`. The one caller outside this package
/// is the kernel layer, which walks `data()` — alongside the mask
/// `validity_bools` hands it — to run a
/// column pass without `Scalar` boxing; `frame` and above never call it — they
/// hold the `Series`.
/// The type handed back is `ColumnStorage` rather than a bare
/// `BuiltinColumn`; the `data()` / `validity()` surface is the same either
/// way, so a column-reading call site does not care which backend it got.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::storage(self : Series) -> @column.ColumnStorage {
self.storage
}
///|
/// Return a copy of this series with a different column name. Storage
/// is shared (immutable), so this is `O(1)`.
pub fn Series::rename(self : Series, new_name : String) -> Series {
{ name: new_name, storage: self.storage, }
}