///|
/// A single cell value in a DataFrame.
///
/// `Null` represents a missing value. The `as_*` conversions `raise
/// `TypeMismatch(Expected(expected, got, ""))` on a wrong dtype (`got = Null` for a null
/// cell). Comparisons between cells are column-level — an expression's
/// `eq` / `lt` / `gt` verbs — and their dtype matrix lives on the comparison
/// kernel, not here.
///
/// The `Float` variant carries a 64-bit `Double`; the `Int` variant carries
/// a 64-bit `Int64`. Use `as_float` for explicit access; `Int` is also
/// accepted via numeric promotion to `Double`. The `Date` variant carries
/// whole days since the Unix epoch (`1970-01-01` = day `0`), rendering as an
/// ISO `YYYY-MM-DD` string.
pub(all) enum Scalar {
Int(Int64)
Float(Double)
Bool(Bool)
String(String)
Date(Int64)
Timestamp(Int64)
Decimal(Int32, Int64)
Null
} derive(Eq, Debug)
///|
pub extend Scalar with Eq::{equal, not_equal}
///|
pub extend Scalar with Show::{output}
///|
pub extend Scalar with Debug::{to_repr}
///|
/// The `DataType` corresponding to this value's variant.
pub fn Scalar::dtype(self : Scalar) -> DataType {
match self {
Int(_) => DataType::Int
Float(_) => DataType::Float
Bool(_) => DataType::Bool
String(_) => DataType::String
Date(_) => DataType::Date
Timestamp(_) => DataType::Timestamp
Decimal(scale, _) => DataType::Decimal(scale)
Null => DataType::Null
}
}
///|
/// `true` only for the `Null` (missing-value) variant.
pub fn Scalar::is_null(self : Scalar) -> Bool {
self is Null
}
///|
/// Value-style rendering: `Int(42)` → `"42"`, `String("ab")` → `"ab"`,
/// `Date(0)` → `"1970-01-01"`, `Null` → `""`. Use the `Show` impl for the
/// variant-form rendering.
pub fn Scalar::to_string(self : Scalar) -> String {
match self {
Int(v) => v.to_string()
Float(v) => v.to_string()
Bool(v) => v.to_string()
String(v) => v
Date(v) => date_days_to_iso(v)
Timestamp(v) => timestamp_micros_to_iso(v)
Decimal(scale, v) => decimal_unscaled_to_string(scale, v)
Null => ""
}
}
///|
/// The `TypeMismatch(Expected(want, got, ""))` a typed accessor raises when a
/// cell is not `want`: the dtype is read off `got`, and `message()` rebuilds
/// the "expected T, got X" wording (`X = Null` for a null cell, whose dtype is
/// `Null`). Generic in the accessor's return type since it only ever raises.
fn[T] expected_type(got : Scalar, want : DataType) -> T raise DataError {
raise TypeMismatch(TypeMismatchDetail::Expected(want, got.dtype(), ""))
}
///|
/// `Int` cells return their `Int64`; other concrete variants and `Null`
/// `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_int(self : Scalar) -> Int64 raise DataError {
match self {
Int(v) => v
other => expected_type(other, DataType::Int)
}
}
///|
/// `Float` cells return their `Double`; `Int` cells are promoted to
/// `Double`; other concrete variants and `Null` `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_float(self : Scalar) -> Double raise DataError {
match self {
Float(v) => v
Int(v) => v.to_double()
other => expected_type(other, DataType::Float)
}
}
///|
/// `Bool` cells return their value; other concrete variants and `Null`
/// `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_bool(self : Scalar) -> Bool raise DataError {
match self {
Bool(v) => v
other => expected_type(other, DataType::Bool)
}
}
///|
/// `String` cells return their value; other concrete variants and `Null`
/// `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_string(self : Scalar) -> String raise DataError {
match self {
String(v) => v
other => expected_type(other, DataType::String)
}
}
///|
/// `Show` renders the variant form (`Int(42)`, `String("ab")`, `Null`).
/// For value-style rendering use `to_string()`.
pub impl Show for Scalar with fn output(self, logger) {
match self {
Int(v) => logger <+ "Int(\{v})"
Float(v) => logger <+ "Float(\{v})"
Bool(v) => logger <+ "Bool(\{v})"
String(v) => logger <+ "String(\"\{@text.escape_debug(v)}\")"
Date(v) => logger <+ "Date(\{date_days_to_iso(v)})"
Timestamp(v) => logger <+ "Timestamp(\{timestamp_micros_to_iso(v)})"
Decimal(scale, v) =>
logger <+ "Decimal(\{scale}, \{decimal_unscaled_to_string(scale, v)})"
Null => logger.write_string("Null")
}
}
///|
/// Render whole days since the Unix epoch as an ISO `YYYY-MM-DD` date —
/// the one rendering a `Scalar::Date` has (value form, `Show` form, CSV /
/// JSON cell form all share it, so the spellings cannot drift). Proleptic
/// Gregorian, via the days-to-civil algorithm: negative day counts (dates
/// before 1970) included. The year is zero-padded to four digits — ISO
/// 8601's `YYYY` basic range is `0000`–`9999`, so year 800 renders
/// `0800-…` and year 0 `0000-…`; a year outside the basic range carries
/// its sign (the expanded-representation form).
fn date_days_to_iso(days : Int64) -> String {
let (year, m, d) = date_days_to_civil(days)
fn pad2(n : Int64) -> String {
if n < 10 {
"0\{n}"
} else {
n.to_string()
}
}
// The year pads its magnitude to four digits and carries its own sign —
// `Int64::MIN` cannot occur here (the algorithm's `y` is bounded by
// `|days| / 365`), so negating is safe.
fn pad4(n : Int64) -> String {
if n < 10 {
"000\{n}"
} else if n < 100 {
"00\{n}"
} else if n < 1000 {
"0\{n}"
} else {
n.to_string()
}
}
let year_text = if year < 0 { "-" + pad4(-year) } else { pad4(year) }
"\{year_text}-\{pad2(m)}-\{pad2(d)}"
}
///|
/// The ISO 8601 rendering of a microsecond count since the epoch — the one
/// rendering a `Scalar::Timestamp` has (value form, `Show` form, CSV / JSON
/// cell form all share it), composed from the date rendering above plus the
/// time-of-day the remainder names. The microsecond fraction is always six
/// digits, so the rendering round-trips (a parser can read back exactly the
/// count it came from); instants before the epoch carry the date's negative
/// year sign, the time-of-day stays in `[00:00:00, 24:00:00)`.
fn timestamp_micros_to_iso(micros : Int64) -> String {
// Floor division by remainder first: the C-remainder `r` carries the
// dividend's sign and never wraps, `micros - r` is a multiple of the
// day bound no larger than the dividend, and the corrections keep both
// the day quotient and the time-of-day small — so the trick stays
// equivalent to floor division at every `Int64`, where subtracting the
// bound before dividing (the direct spelling) would underflow near MIN.
let r = micros % 86_400_000_000L
let day = (micros - r) / 86_400_000_000L - (if r < 0L { 1L } else { 0L })
let rem = r + (if r < 0L { 86_400_000_000L } else { 0L })
fn pad2(n : Int64) -> String {
if n < 10 {
"0\{n}"
} else {
n.to_string()
}
}
fn pad6(n : Int64) -> String {
if n < 10 {
"00000\{n}"
} else if n < 100 {
"0000\{n}"
} else if n < 1000 {
"000\{n}"
} else if n < 10000 {
"00\{n}"
} else if n < 100000 {
"0\{n}"
} else {
n.to_string()
}
}
let hour = rem / 3_600_000_000L
let minute = rem % 3_600_000_000L / 60_000_000L
let second = rem % 60_000_000L / 1_000_000L
let frac = rem % 1_000_000L
"\{date_days_to_iso(day)}T\{pad2(hour)}:\{pad2(minute)}:\{pad2(second)}.\{pad6(frac)}"
}
///|
/// The plain decimal rendering of an unscaled magnitude under a scale — the
/// one rendering a `Scalar::Decimal` has (value form, `Show` form, CSV /
/// JSON cell form all share it): the integer part, a point, and the fraction
/// zero-padded to exactly `scale` digits, so the rendering round-trips. A
/// zero scale renders with no point (`Decimal(0)` is an integer spelling);
/// a negative magnitude keeps one sign, its fraction digits from the
/// magnitude (`-5` under scale 2 renders `-0.05`). The magnitude's digits
/// come straight from `Int64::to_string` — `MIN` included, its digits after
/// the sign are the true magnitude — and the point is placed by counting
/// digits, never by arithmetic on the magnitude: no negation to wrap, no
/// power of ten to overflow, no division to trap. A scale outside the
/// enforced envelope `[0, 18]` can only arrive in a hand-built `pub(all)`
/// scalar; it renders as a named non-value rather than a number.
fn decimal_unscaled_to_string(scale : Int32, unscaled : Int64) -> String {
if scale < 0 || scale > 18 {
return ""
}
if scale == 0 {
return unscaled.to_string()
}
let raw = unscaled.to_string()
let (sign, digits) = if unscaled < 0 {
("-", raw[1:].to_owned())
} else {
("", raw)
}
if digits.length() <= scale {
let mut frac = ""
for _ in 0..<(scale - digits.length()) {
frac = frac + "0"
}
"\{sign}0.\{frac}\{digits}"
} else {
let split = digits.length() - scale
"\{sign}\{digits[:split].to_owned()}.\{digits[split:].to_owned()}"
}
}
///|
/// The Gregorian calendar `(year, month, day)` a day count since the epoch
/// names, via the days-to-civil algorithm — negative counts (dates before
/// 1970) included. The one home of the calendar math: the ISO rendering
/// above composes it, and the engine's `dt_*` expression verbs read their
/// components from it, so the string and the verbs cannot disagree.
/// Total and correct over every `Int64`: the era arithmetic never forms
/// `days + 719468` (which wraps within 719468 of `Int64::MAX`), but splits
/// the count into an era quotient and a non-negative remainder first —
/// the same remainder-first floor division the timestamp rendering uses.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn date_days_to_civil(days : Int64) -> (Int64, Int64, Int64) {
// `days = q * 146097 + s` with `s` in `[0, 146097)`: the C-remainder
// never wraps, the quotient's correction shifts it to the floor, and
// `s + 719468` is the original algorithm's `z` reduced into the current
// era — always a small non-negative number.
let s0 = days % 146097L
let q0 = (days - s0) / 146097L
let q = if s0 < 0L { q0 - 1L } else { q0 }
let s = if s0 < 0L { s0 + 146097L } else { s0 }
let z = s + 719468L
// Floor division of `z` by the era length, both now non-negative and
// small — the composite era is the split quotient plus z's own.
let era = q + z / 146097L
let doe = z % 146097L
let yoe = (doe - doe / 1460L + doe / 36524L - doe / 146096L) / 365L
let y = yoe + era * 400L
let doy = doe - (365L * yoe + yoe / 4L - yoe / 100L)
let mp = (5L * doy + 2L) / 153L
let d = doy - (153L * mp + 2L) / 5L + 1L
let m = if mp < 10L { mp + 3L } else { mp - 9L }
let year = if m <= 2L { y + 1L } else { y }
(year, m, d)
}