use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
List(std::sync::Arc<Vec<Value>>),
Record(Vec<(String, Value)>),
Table(std::sync::Arc<Table>),
Instant {
secs: i64,
nanos: u32,
offset_min: Option<i16>,
},
Duration { secs: i64, nanos: u32 },
Period {
start: i64,
grain: crate::period::Grain,
},
Quantity {
value: f64,
base: String,
written: Option<(f64, String)>,
},
Foreign(ForeignValue),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Table {
pub header: Option<Vec<String>>,
pub rows: Vec<Vec<Value>>,
}
impl Table {
pub fn width(&self) -> usize {
match &self.header {
Some(h) => h.len(),
None => self.rows.first().map_or(0, Vec::len),
}
}
pub fn shape(&self) -> Value {
Value::list(vec![
Value::Int(self.rows.len() as i64),
Value::Int(self.width() as i64),
])
}
pub fn kind(&self) -> String {
format!(
"{} {}×{}",
if self.header.is_some() { "a frame" } else { "an array" },
self.rows.len(),
self.width()
)
}
pub fn is_sample(&self) -> bool {
self.header.is_none() && self.width() == 1
}
pub fn rows_value(&self) -> Value {
Value::list(self.rows.iter().map(|r| self.row_value(r)).collect())
}
fn row_value(&self, row: &[Value]) -> Value {
match &self.header {
Some(h) => Value::Record(h.iter().cloned().zip(row.iter().cloned()).collect()),
None if self.width() == 1 => row.first().cloned().unwrap_or(Value::Null),
None => Value::list(row.to_vec()),
}
}
pub fn row(&self, i: usize) -> Option<Value> {
(i >= 1).then(|| self.rows.get(i - 1)).flatten().map(|r| self.row_value(r))
}
pub fn columns_value(&self) -> Value {
let cols: Vec<Value> = (0..self.width()).map(|j| self.column_at(j)).collect();
let keys: Vec<String> = match &self.header {
Some(h) => h.clone(),
None => (1..=self.width()).map(|j| j.to_string()).collect(),
};
Value::Record(keys.into_iter().zip(cols).collect())
}
pub fn header_value(&self) -> Value {
match &self.header {
Some(h) => Value::list(h.iter().cloned().map(Value::Str).collect()),
None => Value::Null,
}
}
pub fn slice(&self, a: Option<i64>, b: Option<i64>) -> Table {
let n = self.rows.len() as i64;
let from = a.unwrap_or(1).clamp(1, n + 1) - 1;
let to = b.unwrap_or(n).clamp(0, n);
Table {
header: self.header.clone(),
rows: if from < to {
self.rows[from as usize..to as usize].to_vec()
} else {
Vec::new()
},
}
}
pub fn to_quarb_lines(&self) -> String {
let mut parts: Vec<String> = Vec::with_capacity(self.rows.len() + 1);
if let Some(h) = &self.header {
let names: Vec<String> = h.iter().map(|k| quarb_key(k)).collect();
parts.push(format!("@({})", names.join("; ")));
}
for r in &self.rows {
let cells: Vec<String> = r.iter().map(Value::to_quarb).collect();
parts.push(format!("%({})", cells.join("; ")));
}
if parts.len() <= 1 {
return format!("#({})", parts.join(""));
}
format!("#({})", parts.join(";\n "))
}
fn column_at(&self, j: usize) -> Value {
Value::list(
self.rows
.iter()
.map(|r| r.get(j).cloned().unwrap_or(Value::Null))
.collect(),
)
}
pub fn column(&self, name: &str) -> Option<Value> {
match &self.header {
Some(h) => h.iter().position(|c| c == name).map(|j| self.column_at(j)),
None => name
.parse::<usize>()
.ok()
.filter(|j| *j >= 1 && *j <= self.width())
.map(|j| self.column_at(j - 1)),
}
}
pub fn transposed(&self) -> Table {
let w = self.width();
Table {
header: None,
rows: (0..w)
.map(|j| {
self.rows
.iter()
.map(|r| r.get(j).cloned().unwrap_or(Value::Null))
.collect()
})
.collect(),
}
}
pub fn to_quarb(&self) -> String {
let mut parts: Vec<String> = Vec::with_capacity(self.rows.len() + 1);
if let Some(h) = &self.header {
let names: Vec<String> = h.iter().map(|k| quarb_key(k)).collect();
parts.push(format!("@({})", names.join("; ")));
}
for r in &self.rows {
let cells: Vec<String> = r.iter().map(Value::to_quarb).collect();
parts.push(format!("%({})", cells.join("; ")));
}
format!("#({})", parts.join("; "))
}
}
pub trait Foreign: Send + Sync {
fn kind(&self) -> String;
fn display(&self) -> String;
fn rows(&self) -> Result<Value, String>;
fn columns(&self) -> Result<Value, String>;
fn as_any(&self) -> &dyn std::any::Any;
}
#[derive(Clone)]
pub struct ForeignValue(pub std::sync::Arc<dyn Foreign>);
impl ForeignValue {
pub fn new(f: impl Foreign + 'static) -> ForeignValue {
ForeignValue(std::sync::Arc::new(f))
}
pub fn refusal(&self, command: &str) -> String {
format!(
"{command}: {} is a library value; '| @' exports its rows, '| %' its columns",
self.0.kind()
)
}
}
impl std::ops::Deref for ForeignValue {
type Target = dyn Foreign;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl PartialEq for ForeignValue {
fn eq(&self, other: &ForeignValue) -> bool {
std::sync::Arc::ptr_eq(&self.0, &other.0)
}
}
impl std::fmt::Debug for ForeignValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Foreign({})", self.0.kind())
}
}
impl Value {
pub fn list(items: Vec<Value>) -> Value {
Value::List(std::sync::Arc::new(items))
}
pub fn table(t: Table) -> Value {
Value::Table(std::sync::Arc::new(t))
}
pub fn into_items(self) -> Option<Vec<Value>> {
match self {
Value::List(l) => Some(std::sync::Arc::try_unwrap(l).unwrap_or_else(|a| (*a).clone())),
_ => None,
}
}
pub fn bytes(n: i64) -> Value {
Value::Quantity {
value: n as f64,
base: "B".to_string(),
written: None,
}
}
pub fn is_truthy(&self) -> bool {
match self {
Value::Null => false,
Value::Bool(b) => *b,
Value::Int(n) => *n != 0,
Value::Float(f) => *f != 0.0,
Value::Str(s) => !s.is_empty(),
Value::List(l) => !l.is_empty(),
Value::Record(o) => !o.is_empty(),
Value::Table(t) => !t.rows.is_empty(),
Value::Instant { .. } => true,
Value::Duration { secs, nanos } => *secs != 0 || *nanos != 0,
Value::Period { .. } => true,
Value::Quantity { value, .. } => *value != 0.0,
Value::Foreign(f) => {
crate::exec::record_refusal(f.refusal("a predicate"));
false
}
}
}
pub fn temporal_reading(&self) -> Option<(i64, u32)> {
match self {
Value::Instant { secs, nanos, .. } => Some((*secs, *nanos)),
Value::Period { start, .. } => Some((*start, 0)),
Value::Int(n) => Some((*n, 0)),
Value::Float(f) if f.is_finite() => {
let secs = f.floor() as i64;
let nanos = ((f - f.floor()) * 1e9) as u32;
Some((secs, nanos))
}
Value::Str(s) => crate::temporal::parse_iso(s).map(|(s, n, _)| (s, n)),
_ => None,
}
}
pub fn calendar_reading(&self) -> Option<crate::period::Period> {
use crate::period::Period;
match self {
Value::Period { start, grain } => Some(Period {
start: *start,
grain: *grain,
}),
Value::Int(n) => Period::of(*n, 1, 1, 0, 0, 0, crate::period::Grain::Year),
Value::Instant {
secs,
nanos,
offset_min,
} => Some(Period::of_instant(*secs, *nanos, *offset_min)),
Value::Str(s) => Period::parse(s).or_else(|| {
crate::temporal::parse_iso(s)
.map(|(secs, nanos, off)| Period::of_instant(secs, nanos, off))
}),
_ => None,
}
}
pub fn period(p: crate::period::Period) -> Value {
Value::Period {
start: p.start,
grain: p.grain,
}
}
pub fn unital_reading(&self) -> Option<(f64, String)> {
match self {
Value::Quantity { value, base, .. } => Some((*value, base.clone())),
Value::Str(s) => {
crate::quantity::parse_unit_text(s).map(|(v, base, ..)| (v, base.to_string()))
}
_ => None,
}
}
pub fn durational_reading(&self) -> Option<(i64, u32)> {
match self {
Value::Duration { secs, nanos } => Some((*secs, *nanos)),
Value::Int(n) => Some((*n, 0)),
Value::Float(f) if f.is_finite() => {
let secs = f.floor() as i64;
let nanos = ((f - f.floor()) * 1e9) as u32;
Some((secs, nanos))
}
Value::Str(s) => crate::temporal::parse_span(s),
_ => None,
}
}
pub fn to_quarb(&self) -> String {
match self {
Value::Null => "null".to_string(),
Value::Str(s) => quarb_string(s),
Value::List(l) => {
let inner: Vec<String> = l.iter().map(Value::to_quarb).collect();
format!("@({})", inner.join("; "))
}
Value::Record(o) => {
let inner: Vec<String> = o
.iter()
.map(|(k, v)| format!("{} = {}", quarb_key(k), v.to_quarb()))
.collect();
format!("%({})", inner.join("; "))
}
Value::Table(t) => t.to_quarb(),
other => other.to_string(),
}
}
pub fn display_form(&self) -> String {
match self {
Value::List(_) | Value::Record(_) => self.to_quarb(),
Value::Table(t) => t.to_quarb_lines(),
other => other.to_string(),
}
}
pub fn to_json(&self) -> String {
match self {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(f) if f.is_finite() => format_float(*f),
Value::Float(_) => "null".to_string(),
Value::Str(s) => json_string(s),
Value::List(l) => {
let inner: Vec<String> = l.iter().map(Value::to_json).collect();
format!("[{}]", inner.join(", "))
}
Value::Record(o) => {
let inner: Vec<String> = o
.iter()
.map(|(k, v)| format!("{}: {}", json_string(k), v.to_json()))
.collect();
format!("{{{}}}", inner.join(", "))
}
Value::Instant { .. }
| Value::Duration { .. }
| Value::Period { .. }
| Value::Quantity { .. } => json_string(&self.to_string()),
Value::Table(t) => t.rows_value().to_json(),
Value::Foreign(f) => match f.rows() {
Ok(rows) => rows.to_json(),
Err(_) => json_string(&f.display()),
},
}
}
pub fn equality_key(&self) -> Option<String> {
match self {
Value::Int(_) | Value::Float(_) | Value::Str(_) => Some(match self.numeric_reading() {
Some(Value::Int(n)) => format!("n:{n}"),
Some(Value::Float(f)) if f.fract() == 0.0 && f.abs() < 9.2e18 => {
format!("n:{}", f as i64)
}
Some(Value::Float(f)) => format!("n:{f}"),
_ => format!("s:{self}"),
}),
Value::Bool(b) => Some(format!("b:{b}")),
Value::Null => Some("0:".to_string()),
_ => None,
}
}
pub fn numeric(&self) -> Option<f64> {
match self {
Value::Int(n) => Some(*n as f64),
Value::Float(f) => Some(*f),
Value::Str(s) => {
crate::number::read(s, crate::number::Decimal::Point).and_then(|v| v.numeric())
}
Value::Quantity { value, .. } => Some(*value),
_ => None,
}
}
pub fn numeric_reading(&self) -> Option<Value> {
match self {
Value::Int(n) => Some(Value::Int(*n)),
Value::Float(f) => Some(Value::Float(*f)),
Value::Str(s) => crate::number::read(s, crate::number::Decimal::Point),
_ => None,
}
}
pub fn compare(&self, other: &Value) -> std::cmp::Ordering {
self.compare_with(other, &crate::quantity::scale_expr)
}
pub fn compare_with(
&self,
other: &Value,
scale: &dyn Fn(&str) -> Option<(f64, String)>,
) -> std::cmp::Ordering {
sort_key(self, scale).cmp_key(&sort_key(other, scale))
}
pub(crate) fn sort_key_with(&self, scale: &dyn Fn(&str) -> Option<(f64, String)>) -> SortKey {
sort_key(self, scale)
}
}
#[derive(Debug, Clone)]
pub(crate) enum SortKey {
Null,
Bool(bool),
Line(f64, i64),
Period(i64, u8),
Text(String),
ListText(String),
RecordText(String),
}
impl PartialEq for SortKey {
fn eq(&self, other: &SortKey) -> bool {
self.cmp_key(other) == std::cmp::Ordering::Equal
}
}
impl Eq for SortKey {}
impl std::hash::Hash for SortKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.rank().hash(state);
match self {
SortKey::Null => {}
SortKey::Bool(b) => b.hash(state),
SortKey::Line(f, i) => {
(if *f == 0.0 { 0.0f64 } else { *f }).to_bits().hash(state);
i.hash(state);
}
SortKey::Period(start, grain) => {
start.hash(state);
grain.hash(state);
}
SortKey::Text(s) | SortKey::ListText(s) | SortKey::RecordText(s) => s.hash(state),
}
}
}
impl SortKey {
fn line(f: f64) -> SortKey {
let whole = f.fract() == 0.0 && f.abs() < 9.2e18;
SortKey::Line(f, if whole { f as i64 } else { 0 })
}
fn rank(&self) -> u8 {
match self {
SortKey::Null => 0,
SortKey::Bool(_) => 1,
SortKey::Line(..) => 2,
SortKey::Period(..) => 3,
SortKey::Text(_) => 4,
SortKey::ListText(_) => 5,
SortKey::RecordText(_) => 6,
}
}
pub(crate) fn cmp_key(&self, other: &SortKey) -> std::cmp::Ordering {
use SortKey::*;
match (self, other) {
(Bool(a), Bool(b)) => a.cmp(b),
(Line(a, i), Line(b, j)) => {
if *a == 0.0 && *b == 0.0 {
i.cmp(j)
} else {
a.total_cmp(b).then(i.cmp(j))
}
}
(Period(a, g), Period(b, h)) => a.cmp(b).then(g.cmp(h)),
(Text(a), Text(b)) | (ListText(a), ListText(b)) | (RecordText(a), RecordText(b)) => {
a.cmp(b)
}
_ => self.rank().cmp(&other.rank()),
}
}
}
fn sort_key(v: &Value, scale: &dyn Fn(&str) -> Option<(f64, String)>) -> SortKey {
match v {
Value::Null => SortKey::Null,
Value::Bool(b) => SortKey::Bool(*b),
Value::Int(n) => SortKey::Line(*n as f64, *n),
Value::Float(f) => SortKey::line(*f),
Value::Instant { secs, nanos, .. } | Value::Duration { secs, nanos } => {
SortKey::line(*secs as f64 + *nanos as f64 / 1e9)
}
Value::Period { start, grain } => SortKey::Period(*start, *grain as u8),
Value::Quantity { value, .. } => SortKey::line(*value),
Value::Foreign(x) => SortKey::RecordText(format!("\u{0}foreign:{:p}", std::sync::Arc::as_ptr(&x.0))),
Value::Table(t) => SortKey::RecordText(format!("\u{0}table:{}", t.to_quarb())),
Value::Str(s) => {
if let Some(f) =
crate::number::read(s, crate::number::Decimal::Point).and_then(|v| v.numeric())
{
SortKey::line(f)
} else if let Some((bv, ..)) = crate::quantity::parse_unit_text_with(s, scale) {
SortKey::line(bv)
} else if let Some((secs, nanos)) = crate::temporal::parse_span(s) {
SortKey::line(secs as f64 + nanos as f64 / 1e9)
} else if let Some((secs, nanos, _)) = crate::temporal::parse_iso(s) {
SortKey::line(secs as f64 + nanos as f64 / 1e9)
} else {
SortKey::Text(s.clone())
}
}
Value::List(_) => SortKey::ListText(v.to_string()),
Value::Record(_) => SortKey::RecordText(v.to_string()),
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => Ok(()),
Value::Bool(b) => write!(f, "{b}"),
Value::Int(n) => write!(f, "{n}"),
Value::Float(x) => write!(f, "{}", format_float(*x)),
Value::Str(s) => write!(f, "{s}"),
Value::List(items) => {
let parts: Vec<String> = items.iter().map(|v| v.to_string()).collect();
write!(f, "{}", parts.join(", "))
}
Value::Record(_) => write!(f, "{}", self.to_quarb()),
Value::Instant {
secs,
nanos,
offset_min,
} => write!(
f,
"{}",
crate::temporal::format_instant(*secs, *nanos, *offset_min)
),
Value::Duration { secs, nanos } => {
write!(f, "{}", crate::temporal::format_duration(*secs, *nanos))
}
Value::Period { start, grain } => write!(
f,
"{}",
crate::period::Period {
start: *start,
grain: *grain
}
.format()
),
Value::Quantity {
value,
base,
written,
} => match written {
Some((v, u)) => write!(f, "{} {}", format_float(*v), u),
None => write!(f, "{} {}", format_float(*value), base),
},
Value::Foreign(x) => write!(f, "{}", x.display()),
Value::Table(t) => write!(f, "{}", t.to_quarb()),
}
}
}
pub(crate) fn quantital_pair_with(
a: &Value,
b: &Value,
scale: &dyn Fn(&str) -> Option<(f64, String)>,
) -> Option<(f64, f64)> {
let read = |v: &Value| -> Option<(f64, String)> {
match v {
Value::Quantity { value, base, .. } => Some((*value, base.clone())),
Value::Str(s) => {
crate::quantity::parse_unit_text_with(s, scale).map(|(bv, b, ..)| (bv, b))
}
_ => None,
}
};
match (read(a), read(b)) {
(Some((va, ba)), Some((vb, bb))) => (ba == bb).then_some((va, vb)),
(Some((va, _)), None) => b.numeric().map(|n| (va, n)),
(None, Some((vb, _))) => a.numeric().map(|n| (n, vb)),
(None, None) => None,
}
}
pub(crate) fn format_float(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f > 0.0 { "inf" } else { "-inf" }.to_string();
}
if f == 0.0 {
return f.to_string();
}
let sci = format!("{f:e}");
let (mantissa, exponent) = sci.split_once('e').expect("an exponent");
let exponent: i32 = exponent.parse().expect("a decimal exponent");
if (-4..16).contains(&exponent) {
return f.to_string();
}
format!(
"{mantissa}e{}{:02}",
if exponent < 0 { '-' } else { '+' },
exponent.abs()
)
}
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
impl From<i64> for Value {
fn from(n: i64) -> Self {
Value::Int(n)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Bool(b)
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::Str(s)
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::Str(s.to_string())
}
}
#[cfg(test)]
mod tests {
#[test]
fn floats_print_as_python_prints_them() {
for (f, want) in [
(2.89e-82, "2.89e-82"),
(0.00001, "1e-05"),
(1e16, "1e+16"),
(1.2345678901234568e29, "1.2345678901234568e+29"),
(0.001, "0.001"),
(0.0001, "0.0001"),
(1234567890123456.0, "1234567890123456"),
(3.0, "3"),
(-5e-8, "-5e-08"),
(0.0, "0"),
] {
assert_eq!(format_float(f), want, "{f}");
assert_eq!(Value::Float(f).to_string(), want);
}
assert_eq!(Value::Float(2.89e-82).to_json(), "2.89e-82");
}
use super::*;
#[test]
fn sort_key_hashes_like_it_compares() {
use std::collections::HashSet;
let scale = |_: &str| None;
let keys: HashSet<SortKey> = [
Value::Str("1".into()),
Value::Int(1),
Value::Float(1.0),
Value::Str("x".into()),
Value::Str("X".into()),
Value::Null,
Value::Bool(true),
]
.iter()
.map(|v| v.sort_key_with(&scale))
.collect();
assert_eq!(keys.len(), 5);
assert_eq!(
Value::Int(1).sort_key_with(&scale),
Value::Str(" 1.0 ".into()).sort_key_with(&scale)
);
assert_ne!(
Value::Str("x".into()).sort_key_with(&scale),
Value::Str("y".into()).sort_key_with(&scale)
);
}
#[test]
fn json_rendering() {
let obj = Value::Record(vec![
("name".into(), Value::Str("A\"da\n".into())),
("age".into(), Value::Int(36)),
("score".into(), Value::Float(2.5)),
("ok".into(), Value::Bool(true)),
("gone".into(), Value::Null),
(
"tags".into(),
Value::list(vec![Value::Str("x".into()), Value::Int(1)]),
),
]);
assert_eq!(
obj.to_json(),
r#"{"name": "A\"da\n", "age": 36, "score": 2.5, "ok": true, "gone": null, "tags": ["x", 1]}"#
);
assert_eq!(
obj.to_quarb(),
"%(name = \"A\\\"da\n\"; age = 36; score = 2.5; ok = true; gone = null; tags = @(\"x\"; 1))"
);
assert_eq!(obj.to_string(), obj.to_quarb());
assert!(!Value::Record(Vec::new()).is_truthy());
assert_eq!(Value::list(Vec::new()).to_quarb(), "@()");
assert_eq!(Value::Record(Vec::new()).to_quarb(), "%()");
assert_eq!(
Value::Record(vec![("my-key".into(), Value::Int(1))]).to_quarb(),
"%(\"my-key\" = 1)"
);
assert_eq!(
Value::list(vec![Value::Str("a".into()), Value::Str("b".into())]).to_string(),
"a, b"
);
}
}
pub fn quarb_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'"' | '\\' => {
out.push('\\');
out.push(c);
}
'$' if chars.peek() == Some(&'{') => {
out.push('\\');
out.push('$');
}
'(' if chars.peek() == Some(&',') => {
out.push('\\');
out.push('(');
}
_ => out.push(c),
}
}
out.push('"');
out
}
pub fn quarb_key(k: &str) -> String {
let position = k.len() > 1 && k.starts_with('.') && k[1..].bytes().all(|c| c.is_ascii_digit());
let bare = position
|| !k.is_empty()
&& k.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_')
&& k.chars().all(|c| c.is_alphanumeric() || c == '_')
&& !crate::parser::is_bool_word(k);
if bare { k.to_string() } else { quarb_string(k) }
}