Skip to main content

pond_rs/
json.rs

1//! pond-ts's JSON wire format: [`TimeSeries::from_json`] and
2//! [`TimeSeries::to_json`], as pond-ts `TimeSeries.fromJSON` / `toJSON`.
3
4use serde_json::Value;
5
6use crate::json_text::{Json, NULL};
7
8use crate::column::{ArrayColumn, BooleanColumn, ColumnData, Float64Column, Scalar, StringColumn};
9use crate::error::{PondError, Result};
10use crate::series::{
11    Column, ColumnDef, ColumnKind, IntervalKeys, Key, Labels, TimeRangeKeys, TimeSeries,
12};
13
14/// What [`TimeSeries::from_json`] does when a row's key sorts before the
15/// row above it, as pond-ts `fromJSON({ ordering })`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum KeyOrder {
18    /// Refuse it (pond-ts `'strict'`, the default).
19    #[default]
20    Strict,
21    /// Sort the rows by key (start, then end), keeping the order of rows
22    /// with the same key (pond-ts `'reorder'`).
23    Reorder,
24    /// Keep the rows where they are and move a backwards time up to the
25    /// time above it (pond-ts `'clamp'`). Point-in-time keys only: a time
26    /// range or interval is refused, even when it is in order.
27    Clamp,
28}
29
30/// Options for [`TimeSeries::from_json`].
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct JsonOptions {
33    time_zone: String,
34    order: KeyOrder,
35}
36
37impl Default for JsonOptions {
38    fn default() -> Self {
39        Self {
40            time_zone: "UTC".into(),
41            order: KeyOrder::Strict,
42        }
43    }
44}
45
46impl JsonOptions {
47    /// Times as text in UTC, keys in order: the defaults, as in pond-ts.
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// The zone that times written as text without an offset are in
53    /// (pond-ts `parse: { timeZone }`): an IANA name (`"Europe/Madrid"`) or
54    /// a fixed offset (`"+02:00"`).
55    pub fn time_zone(mut self, zone: impl Into<String>) -> Self {
56        self.time_zone = zone.into();
57        self
58    }
59
60    /// What to do with keys out of order; see [`KeyOrder`].
61    pub fn order(mut self, order: KeyOrder) -> Self {
62        self.order = order;
63        self
64    }
65}
66
67/// How [`TimeSeries::to_json`] writes each row, as pond-ts `toJSON({
68/// rowFormat })`.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub enum RowFormat {
71    /// An array in schema order (pond-ts `'array'`, the default).
72    #[default]
73    Array,
74    /// An object keyed by column name (pond-ts `'object'`).
75    Object,
76}
77
78const FROM: &str = "from_json";
79const TO: &str = "to_json";
80
81pub(crate) fn invalid(call: &'static str, message: String) -> PondError {
82    PondError::InvalidData { call, message }
83}
84
85impl TimeSeries {
86    /// Read a series written in pond-ts's JSON format, as pond-ts
87    /// `TimeSeries.fromJSON`.
88    ///
89    /// The shape is `{ "name", "schema", "rows" }`. `schema` lists the key
90    /// column first (`{"name": "time", "kind": "time"}`), then each value
91    /// column (`"number"`, `"string"` or `"boolean"`; `"required": false`
92    /// allows gaps). Each row is an array in schema order, or an object
93    /// keyed by column name ([`RowFormat`]). A gap is `null` (or, in an
94    /// object row, a missing name).
95    ///
96    /// Keys are written as pond-ts writes them:
97    ///
98    /// | Key kind    | Array row               | Object row                    |
99    /// | ----------- | ----------------------- | ----------------------------- |
100    /// | `time`      | `1000`                  | `1000`                        |
101    /// | `timeRange` | `[begin, end]`          | `{"start", "end"}`            |
102    /// | `interval`  | `[label, begin, end]`   | `{"value", "start", "end"}`   |
103    ///
104    /// On the way in, every time may also be text, read as
105    /// [`parse_time_in`](crate::parse_time_in) reads it in
106    /// [`JsonOptions::time_zone`] (UTC unless set), and either key form is
107    /// accepted in either row form.
108    ///
109    /// ```
110    /// # use pond_rs::{JsonOptions, RowFormat, TimeSeries};
111    /// let text = r#"{
112    ///   "name": "cpu",
113    ///   "schema": [
114    ///     {"name": "time", "kind": "time"},
115    ///     {"name": "load", "kind": "number", "required": false}
116    ///   ],
117    ///   "rows": [["2026-03-29T01:30", 0.5], ["2026-03-29T03:30", null]]
118    /// }"#;
119    /// let s = TimeSeries::from_json(text, &JsonOptions::new().time_zone("Europe/Madrid"))?;
120    /// assert_eq!(s.key().begin(), &[1_774_744_200_000, 1_774_747_800_000]);
121    /// assert_eq!(
122    ///     s.to_json(RowFormat::Array)?,
123    ///     r#"{"name":"cpu","schema":[{"name":"time","kind":"time"},{"name":"load","kind":"number","required":false}],"rows":[[1774744200000,0.5],[1774747800000,null]]}"#
124    /// );
125    /// # Ok::<(), pond_rs::PondError>(())
126    /// ```
127    ///
128    /// Errors, as in pond-ts: text that is not JSON; a schema whose first column is not
129    /// `time`, `timeRange` or `interval`, or whose value kinds are not
130    /// `number`, `string` or `boolean`; a row of the wrong length; a cell of
131    /// the wrong kind; a gap in a required column; a key that cannot be
132    /// read, or that ends before it starts; interval labels that are not all
133    /// text or all numbers; keys out of order (see [`KeyOrder`]).
134    ///
135    /// Also errors, where pond-ts accepts: text holding a lone UTF-16
136    /// surrogate (`"\ud800"`), which is not valid Unicode; a time that is
137    /// not a whole millisecond (`1.5`), or outside
138    /// ±[`MAX_TIME_MS`](crate::MAX_TIME_MS); a missing `name`; an `array`
139    /// column (not in pond-rs yet).
140    pub fn from_json(text: &str, options: &JsonOptions) -> Result<TimeSeries> {
141        read_json(&parse(FROM, text)?, options)
142    }
143
144    /// [`TimeSeries::from_json`] from JSON already parsed with
145    /// [`serde_json`].
146    pub fn from_json_value(value: &Value, options: &JsonOptions) -> Result<TimeSeries> {
147        read_json(&Json::from_value(value), options)
148    }
149
150    /// Write the series in pond-ts's JSON format (see
151    /// [`TimeSeries::from_json`]), as pond-ts `toJSON({ rowFormat })`: the text
152    /// [`TimeSeries::from_json`] and pond-ts `fromJSON` read back. Times are
153    /// epoch milliseconds; a gap is `null`; numbers are written as
154    /// JavaScript writes them (`0.000001`, `1e-7`, `1e+21`, `-0` as `0`).
155    ///
156    /// The schema holds each column's `name` and `kind`, and
157    /// `"required": false` on a value column that allows gaps. pond-ts
158    /// writes back the schema it was given, so a schema read with extra
159    /// entries (`"required": true`, other properties) comes out without
160    /// them here. Object rows list names in schema order; JavaScript puts
161    /// names that look like whole numbers (`"1"`) first, so for such a
162    /// column pond-ts's text has another order (the JSON means the same).
163    ///
164    /// An error for a decimal column, which pond-ts has no kind for: convert
165    /// it with [`TimeSeries::map_decimals`] first, or write Arrow
166    /// ([`TimeSeries::write_ipc`]), which keeps it exact.
167    pub fn to_json(&self, format: RowFormat) -> Result<String> {
168        refuse_decimals(TO, self.columns())?;
169        // Written straight to text: building a `Value` tree first cost
170        // several times as much (a map per object row).
171        let mut out = Vec::with_capacity(64 + self.len() * 16 * (1 + self.columns().len()));
172        out.extend_from_slice(b"{\"name\":");
173        push_text(&mut out, self.name());
174        out.extend_from_slice(b",\"schema\":");
175        push_schema(&mut out, &self.schema());
176        out.extend_from_slice(b",\"rows\":[");
177        // Each name once, escaped, with its colon: `"cpu":`.
178        let field = |name: &str| {
179            let mut f = Vec::new();
180            push_text(&mut f, name);
181            f.push(b':');
182            f
183        };
184        let key_field = field(self.key_name());
185        let fields: Vec<Vec<u8>> = self.columns().iter().map(|c| field(c.name())).collect();
186        let object = format == RowFormat::Object;
187        for i in 0..self.len() {
188            if i > 0 {
189                out.push(b',');
190            }
191            out.push(if object { b'{' } else { b'[' });
192            if object {
193                out.extend_from_slice(&key_field);
194            }
195            self.push_key(&mut out, i, format);
196            for (c, column) in self.columns().iter().enumerate() {
197                out.push(b',');
198                if object {
199                    out.extend_from_slice(&fields[c]);
200                }
201                push_cell(&mut out, column.data(), i);
202            }
203            out.push(if object { b'}' } else { b']' });
204        }
205        out.extend_from_slice(b"]}");
206        Ok(String::from_utf8(out).expect("JSON text is UTF-8"))
207    }
208
209    /// [`TimeSeries::to_json`] as a [`serde_json::Value`]. Its objects keep
210    /// their keys in the order serde_json keeps them: sorted, unless your
211    /// build turns on serde_json's `preserve_order` feature (the text from
212    /// `to_json` always has schema order).
213    pub fn to_json_value(&self, format: RowFormat) -> Result<Value> {
214        Ok(serde_json::from_str(&self.to_json(format)?).expect("to_json writes JSON"))
215    }
216
217    fn push_key(&self, out: &mut Vec<u8>, i: usize, format: RowFormat) {
218        let begin = self.key().begin()[i];
219        let Some(end) = self.key().end() else {
220            push_int(out, begin);
221            return;
222        };
223        let object = format == RowFormat::Object;
224        out.push(if object { b'{' } else { b'[' });
225        if let Key::Interval(k) = self.key() {
226            if object {
227                out.extend_from_slice(b"\"value\":");
228            }
229            match &k.label {
230                Labels::Number(v) => push_number(out, v[i]),
231                Labels::String(s) => push_text(out, s.get(i).expect("labels have no gaps")),
232            }
233            out.push(b',');
234        }
235        if object {
236            out.extend_from_slice(b"\"start\":");
237        }
238        push_int(out, begin);
239        out.extend_from_slice(if object { b",\"end\":" } else { b"," });
240        push_int(out, end[i]);
241        out.push(if object { b'}' } else { b']' });
242    }
243}
244
245/// [`TimeSeries::from_json`] on parsed JSON.
246fn read_json(value: &Json, options: &JsonOptions) -> Result<TimeSeries> {
247    let name = read_name(FROM, value)?;
248    let schema = read_schema(FROM, value.get("schema"))?;
249    let rows = value
250        .get("rows")
251        .and_then(|v| v.as_array())
252        .ok_or_else(|| invalid(FROM, "expected \"rows\" as an array".into()))?;
253    read_rows(
254        FROM,
255        name,
256        &schema,
257        rows,
258        RowShape::Rows,
259        Some(&options.time_zone),
260        options.order,
261    )
262}
263
264/// How each row of [`read_rows`] is laid out.
265#[derive(Clone, Copy, PartialEq, Eq)]
266pub(crate) enum RowShape {
267    /// pond-ts `fromJSON` rows: an array in schema order or an object keyed
268    /// by column name.
269    Rows,
270    /// pond-ts `fromPoints`: an object with the time under `ts`.
271    Points,
272}
273
274/// Build a series from row-shaped JSON. `zone` is `None` where pond-ts takes
275/// times only as numbers.
276pub(crate) fn read_rows(
277    call: &'static str,
278    name: &str,
279    schema: &[(String, ColumnKind, bool)],
280    rows: &[Json],
281    shape: RowShape,
282    zone: Option<&str>,
283    order: KeyOrder,
284) -> Result<TimeSeries> {
285    let (key_name, key_kind) = (&schema[0].0, schema[0].1);
286    if order == KeyOrder::Clamp && key_kind != ColumnKind::Time {
287        return Err(invalid(
288            call,
289            format!(
290                "KeyOrder::Clamp is for point-in-time keys only; this key is '{}' (use KeyOrder::Reorder, or sort the rows first)",
291                key_kind.as_str()
292            ),
293        ));
294    }
295    if key_kind == ColumnKind::Value {
296        return Err(invalid(
297            call,
298            format!("the key '{key_name}' is a 'value' axis: read it with ValueSeries::from_json"),
299        ));
300    }
301    let read_time = |v: &Json, row: usize| read_time(call, v, row, zone);
302
303    let cells = row_cells(call, schema, rows, shape)?;
304    let n = rows.len();
305    let mut begin = Vec::with_capacity(n);
306    let mut end = Vec::with_capacity(n);
307    let mut labels: Vec<LabelCell> = Vec::new();
308    for (i, &key) in cells[0].iter().enumerate() {
309        match key_kind {
310            ColumnKind::Time => {
311                let t = read_time(key, i)?;
312                begin.push(t);
313            }
314            ColumnKind::TimeRange => {
315                let (b, e) = read_span(call, key, i, false)?;
316                begin.push(read_time(b, i)?);
317                end.push(read_time(e, i)?);
318            }
319            _ => {
320                let (b, e) = read_span(call, key, i, true)?;
321                let label = match key {
322                    Json::Array(a) => &a[0],
323                    _ => key.get("value").unwrap_or(NULL),
324                };
325                labels.push(read_label(call, label, i, labels.first())?);
326                begin.push(read_time(b, i)?);
327                end.push(read_time(e, i)?);
328            }
329        }
330        if let Some(&b) = end.last()
331            && b < *begin.last().expect("pushed with end")
332        {
333            return Err(invalid(
334                call,
335                format!(
336                    "the key at row {i} ends ({b}) before it starts ({})",
337                    begin[i]
338                ),
339            ));
340        }
341    }
342
343    // Rows in the order the series will hold them.
344    let order: Option<Vec<usize>> = match order {
345        KeyOrder::Reorder => {
346            let mut order: Vec<usize> = (0..n).collect();
347            order.sort_by_key(|&i| (begin[i], end.get(i).copied().unwrap_or(0)));
348            Some(order)
349        }
350        KeyOrder::Clamp => {
351            for i in 1..n {
352                begin[i] = begin[i].max(begin[i - 1]);
353            }
354            None
355        }
356        KeyOrder::Strict => None,
357    };
358    let pick = |v: &mut Vec<i64>| {
359        if let Some(order) = order.as_ref().filter(|_| !v.is_empty()) {
360            *v = order.iter().map(|&i| v[i]).collect();
361        }
362    };
363    pick(&mut begin);
364    pick(&mut end);
365    let row_at = |r: usize| order.as_ref().map_or(r, |o| o[r]);
366
367    let columns = row_columns(call, &schema[1..], &cells[1..], n, &row_at)?;
368
369    let key = match key_kind {
370        ColumnKind::Time => Key::Time(begin),
371        ColumnKind::TimeRange => Key::TimeRange(TimeRangeKeys { begin, end }),
372        _ => {
373            let label = if matches!(labels.first(), Some(LabelCell::Number(_))) {
374                Labels::Number(
375                    (0..n)
376                        .map(|r| match labels[row_at(r)] {
377                            LabelCell::Number(v) => v,
378                            LabelCell::String(_) => unreachable!("one kind"),
379                        })
380                        .collect(),
381                )
382            } else {
383                Labels::String(StringColumn::from_options((0..n).map(|r| {
384                    match &labels[row_at(r)] {
385                        LabelCell::String(s) => Some(*s),
386                        LabelCell::Number(_) => unreachable!("one kind"),
387                    }
388                })))
389            };
390            Key::Interval(IntervalKeys { label, begin, end })
391        }
392    };
393    TimeSeries::with_key(call, name.to_string(), key_name.clone(), key, columns)
394}
395
396/// Each row's cells, column by column (`cells[c][i]`, key first). A row is
397/// an array in schema order or an object keyed by column name; a point is
398/// an object with the key under `ts`. A name an object row leaves out is a
399/// `null`.
400pub(crate) fn row_cells<'b, 'a>(
401    call: &'static str,
402    schema: &[(String, ColumnKind, bool)],
403    rows: &'b [Json<'a>],
404    shape: RowShape,
405) -> Result<Vec<Vec<&'b Json<'a>>>> {
406    let n = rows.len();
407    let mut cells: Vec<Vec<&Json>> = vec![Vec::with_capacity(n); schema.len()];
408    // Where each name in an object row goes. Points hold the time under
409    // "ts".
410    let names: Vec<&str> = std::iter::once(match shape {
411        RowShape::Points => "ts",
412        RowShape::Rows => schema[0].0.as_str(),
413    })
414    .chain(schema[1..].iter().map(|c| c.0.as_str()))
415    .collect();
416    let mut column_of: std::collections::HashMap<&str, usize> = Default::default();
417    for (c, name) in names.iter().enumerate().rev() {
418        column_of.insert(name, c);
419    }
420    let mut slots: Vec<&Json> = vec![NULL; schema.len()];
421    for (i, row) in rows.iter().enumerate() {
422        match row {
423            Json::Object(o) => {
424                // A later entry with the same name wins, as in JSON.parse.
425                slots.fill(NULL);
426                for (p, (k, v)) in o.entries().iter().enumerate() {
427                    // Rows usually list names in schema order: check that
428                    // before hashing.
429                    let c = match names.get(p) {
430                        Some(name) if *name == k.as_ref() => Some(p),
431                        _ => column_of.get(k.as_ref()).copied(),
432                    };
433                    if let Some(c) = c {
434                        slots[c] = v;
435                    }
436                }
437            }
438            _ if shape == RowShape::Points => {
439                return Err(invalid(call, format!("point {i} is not an object")));
440            }
441            Json::Array(a) if a.len() != schema.len() => {
442                return Err(invalid(
443                    call,
444                    format!(
445                        "row {i} has {} values; the schema has {} columns",
446                        a.len(),
447                        schema.len()
448                    ),
449                ));
450            }
451            Json::Array(a) => {
452                for (slot, v) in slots.iter_mut().zip(a) {
453                    *slot = v;
454                }
455            }
456            _ => {
457                return Err(invalid(
458                    call,
459                    format!("row {i} is not an array or an object"),
460                ));
461            }
462        }
463        for (cells, v) in cells.iter_mut().zip(&slots) {
464            cells.push(v);
465        }
466    }
467    Ok(cells)
468}
469
470/// The value columns of row-shaped JSON, as [`row_cells`] splits it:
471/// `schema` and `cells` without the key. Row `r` of the result is row
472/// `row_at(r)` of the input. A gap in a required column is refused when
473/// the series is built.
474pub(crate) fn row_columns(
475    call: &'static str,
476    schema: &[(String, ColumnKind, bool)],
477    cells: &[Vec<&Json>],
478    n: usize,
479    row_at: &dyn Fn(usize) -> usize,
480) -> Result<Vec<Column>> {
481    let mut columns = Vec::with_capacity(schema.len());
482    for (c, (col_name, kind, required)) in schema.iter().enumerate() {
483        let cell = |r: usize| cells[c][row_at(r)];
484        let wrong = |r: usize, want: &str| {
485            invalid(
486                call,
487                format!(
488                    "row {} column '{col_name}': expected {want}, got {}",
489                    row_at(r),
490                    cells[c][row_at(r)]
491                ),
492            )
493        };
494        let data: ColumnData = match kind {
495            ColumnKind::Number => Float64Column::from_options(
496                (0..n)
497                    .map(|r| match cell(r) {
498                        Json::Null => Ok(None),
499                        Json::Number(v) => Ok(v.as_f64()),
500                        _ => Err(wrong(r, "a number")),
501                    })
502                    .collect::<Result<Vec<_>>>()?,
503            )
504            .into(),
505            ColumnKind::String => StringColumn::from_options(
506                (0..n)
507                    .map(|r| match cell(r) {
508                        Json::Null => Ok(None),
509                        Json::String(v) => Ok(Some(v.as_ref())),
510                        _ => Err(wrong(r, "a string")),
511                    })
512                    .collect::<Result<Vec<_>>>()?,
513            )
514            .into(),
515            ColumnKind::Array => ArrayColumn::from_options(
516                (0..n)
517                    .map(|r| match cell(r) {
518                        Json::Null => Ok(None),
519                        Json::Array(a) => read_array(call, a, row_at(r), col_name).map(Some),
520                        _ => Err(wrong(r, "an array")),
521                    })
522                    .collect::<Result<Vec<_>>>()?,
523            )
524            .into(),
525            _ => BooleanColumn::from_options(
526                (0..n)
527                    .map(|r| match cell(r) {
528                        Json::Null => Ok(None),
529                        Json::Bool(v) => Ok(Some(*v)),
530                        _ => Err(wrong(r, "true or false")),
531                    })
532                    .collect::<Result<Vec<_>>>()?,
533            )
534            .into(),
535        };
536        columns.push(Column::from_data(col_name.clone(), data, *required));
537    }
538    Ok(columns)
539}
540
541/// The schema as pond-ts writes it.
542pub(crate) fn push_schema(out: &mut Vec<u8>, schema: &[ColumnDef]) {
543    out.push(b'[');
544    for (i, def) in schema.iter().enumerate() {
545        if i > 0 {
546            out.push(b',');
547        }
548        out.extend_from_slice(b"{\"name\":");
549        push_text(out, &def.name);
550        out.extend_from_slice(b",\"kind\":");
551        push_text(out, def.kind.as_str());
552        if i > 0 && !def.required {
553            out.extend_from_slice(b",\"required\":false");
554        }
555        out.push(b'}');
556    }
557    out.push(b']');
558}
559
560/// One value cell; `null` for a gap. Not for decimal columns.
561pub(crate) fn push_cell(out: &mut Vec<u8>, data: &ColumnData, i: usize) {
562    match data {
563        ColumnData::Number(v) => match v.get(i) {
564            Some(x) => push_number(out, x),
565            None => out.extend_from_slice(b"null"),
566        },
567        ColumnData::String(v) => match v.get(i) {
568            Some(x) => push_text(out, x),
569            None => out.extend_from_slice(b"null"),
570        },
571        ColumnData::Boolean(v) => out.extend_from_slice(match v.get(i) {
572            Some(true) => b"true",
573            Some(false) => b"false",
574            None => b"null",
575        }),
576        ColumnData::Array(v) => match v.get(i) {
577            Some(x) => push_array(out, x),
578            None => out.extend_from_slice(b"null"),
579        },
580        ColumnData::Decimal(_) => unreachable!("decimals are refused first"),
581    }
582}
583
584/// One array cell, as `JSON.stringify` writes it (`-0` as `0`).
585pub(crate) fn push_array(out: &mut Vec<u8>, cell: &[Scalar]) {
586    out.push(b'[');
587    for (i, e) in cell.iter().enumerate() {
588        if i > 0 {
589            out.push(b',');
590        }
591        match e {
592            Scalar::Number(x) => push_number(out, *x),
593            Scalar::String(x) => push_text(out, x),
594            Scalar::Boolean(x) => out.extend_from_slice(if *x { b"true" } else { b"false" }),
595        }
596    }
597    out.push(b']');
598}
599
600/// pond-ts has no kind for exact decimals.
601pub(crate) fn refuse_decimals(call: &'static str, columns: &[Column]) -> Result<()> {
602    match columns.iter().find(|c| c.kind() == ColumnKind::Decimal) {
603        Some(c) => Err(PondError::Unsupported {
604            call,
605            message: format!(
606                "column '{}' holds exact decimals, which pond-ts's JSON has no kind for; map it to numbers first, or write Arrow, which keeps it exact",
607                c.name()
608            ),
609        }),
610        None => Ok(()),
611    }
612}
613
614pub(crate) fn push_int(out: &mut Vec<u8>, v: i64) {
615    use std::io::Write;
616    write!(out, "{v}").expect("writing to a Vec cannot fail");
617}
618
619/// Text as a JSON string, escaped as `serde_json` escapes it.
620pub(crate) fn push_text(out: &mut Vec<u8>, text: &str) {
621    serde_json::to_writer(out, text).expect("writing to a Vec cannot fail");
622}
623
624/// A number as JavaScript's `JSON.stringify` writes it (ECMAScript
625/// `Number::toString`): the shortest digits that read back to the same
626/// number, written out in full from 1e-6 up to 1e21 and with an exponent
627/// (`1e-7`, `1.5e+21`) outside that; `-0` as `0`; `null` when not finite.
628pub(crate) fn push_number(out: &mut Vec<u8>, v: f64) {
629    if !v.is_finite() {
630        out.extend_from_slice(b"null");
631        return;
632    }
633    if v == 0.0 {
634        out.push(b'0');
635        return;
636    }
637    // Below 2^53 a whole number's exact digits are its shortest ones.
638    if v.fract() == 0.0 && v.abs() < 9_007_199_254_740_992.0 {
639        push_int(out, v as i64); // the common case
640        return;
641    }
642    // The shortest digits, from zmij (which serde_json writes numbers
643    // with): `1.5`, `0.001`, `1e-7`, `1.2345e+20`. Read them as
644    // 0.d1..dk × 10^n.
645    let mut buffer = zmij::Buffer::new();
646    let text = buffer.format_finite(v.abs()).as_bytes();
647    let (mantissa, exp10) = match text.iter().position(|&b| b == b'e') {
648        Some(i) => (
649            &text[..i],
650            std::str::from_utf8(&text[i + 1..])
651                .ok()
652                .and_then(|e| e.parse::<i32>().ok())
653                .expect("zmij writes a whole exponent"),
654        ),
655        None => (text, 0),
656    };
657    let point = mantissa
658        .iter()
659        .position(|&b| b == b'.')
660        .unwrap_or(mantissa.len());
661    let mut all = [0u8; 40];
662    let mut len = 0;
663    for &b in mantissa.iter().filter(|b| **b != b'.') {
664        all[len] = b;
665        len += 1;
666    }
667    let lead = all[..len].iter().take_while(|&&b| b == b'0').count();
668    let mut end = len;
669    while end > lead + 1 && all[end - 1] == b'0' {
670        end -= 1;
671    }
672    let digits = &mut all[lead..end];
673    let n = point as i32 - lead as i32 + exp10;
674    js_tie_to_even(v.abs(), digits, n);
675    let digits: &[u8] = digits;
676    let k = digits.len() as i32;
677    if v < 0.0 {
678        out.push(b'-');
679    }
680    if k <= n && n <= 21 {
681        out.extend_from_slice(digits);
682        out.extend(std::iter::repeat_n(b'0', (n - k) as usize));
683    } else if 0 < n && n <= 21 {
684        out.extend_from_slice(&digits[..n as usize]);
685        out.push(b'.');
686        out.extend_from_slice(&digits[n as usize..]);
687    } else if -6 < n && n <= 0 {
688        out.extend_from_slice(b"0.");
689        out.extend(std::iter::repeat_n(b'0', (-n) as usize));
690        out.extend_from_slice(digits);
691    } else {
692        out.push(digits[0]);
693        if k > 1 {
694            out.push(b'.');
695            out.extend_from_slice(&digits[1..]);
696        }
697        out.push(b'e');
698        out.push(if n - 1 < 0 { b'-' } else { b'+' });
699        push_int(out, i64::from((n - 1).abs()));
700    }
701}
702
703/// When the number lies exactly halfway between two shortest candidates
704/// (`…095.25` between `…095.2` and `…095.3`), JavaScript takes the one
705/// ending in an even digit; Rust's formatter rounds up. That needs the
706/// number's exact decimal value to have one more significant digit than
707/// the shortest form, ending in 5, which [`exact_digits`] finds cheaply.
708/// `digits` is `0.d1..dk × 10^n`.
709fn js_tie_to_even(v: f64, digits: &mut [u8], n: i32) {
710    let k = digits.len();
711    let Some(exact) = exact_digits(v, k + 1) else {
712        return;
713    };
714    if exact.len() != k + 1 || exact[k] != b'5' {
715        return;
716    }
717    // The two candidates: the exact digits cut to k, and one up from that.
718    let lower = &exact[..k];
719    let mut upper = lower.to_vec();
720    let mut i = k;
721    loop {
722        if i == 0 {
723            return; // 99…9 + 1 changes length; not a tie between k digits
724        }
725        i -= 1;
726        if upper[i] == b'9' {
727            upper[i] = b'0';
728        } else {
729            upper[i] += 1;
730            break;
731        }
732    }
733    let even: &[u8] = if lower[k - 1] % 2 == 0 { lower } else { &upper };
734    if *digits == *even {
735        return;
736    }
737    let mut text = String::from("0.");
738    text.push_str(std::str::from_utf8(even).expect("digits"));
739    if format!("{text}e{n}").parse::<f64>() == Ok(v) {
740        digits.copy_from_slice(even);
741    }
742}
743
744/// The significant digits of `v`'s exact decimal value (`v` > 0), or
745/// `None` when there are more than `most`. A double is `m × 2^e`; with `m`
746/// odd, the exact value has the digits of `m × 5^-e` (for `e < 0`) or of
747/// `m × 2^e` (for `e ≥ 0`), so this is integer arithmetic.
748fn exact_digits(v: f64, most: usize) -> Option<Vec<u8>> {
749    let bits = v.to_bits();
750    let raw_exp = ((bits >> 52) & 0x7ff) as i32;
751    let frac = bits & ((1u64 << 52) - 1);
752    let (mut m, mut e) = if raw_exp == 0 {
753        (frac, -1074)
754    } else {
755        (frac | (1u64 << 52), raw_exp - 1075)
756    };
757    let zeros = m.trailing_zeros();
758    m >>= zeros;
759    e += zeros as i32;
760    let value: u128 = if e >= 0 {
761        // Past u128 the number has well over `most` digits... unless it is
762        // a short digit run followed by zeros, which cannot be odd-halfway
763        // for the doubles this path sees (they are whole and ≥ 2^53, where
764        // every candidate is a whole number). Leave those alone.
765        if e as u32 + (64 - m.leading_zeros()) > 127 {
766            return None;
767        }
768        u128::from(m) << e
769    } else {
770        // m × 5^n has more than 18 digits once n > 26.
771        if -e > 26 {
772            return None;
773        }
774        u128::from(m) * 5u128.pow((-e) as u32)
775    };
776    let text = value.to_string();
777    let significant = text.trim_end_matches('0');
778    (significant.len() <= most).then(|| significant.as_bytes().to_vec())
779}
780
781/// Parse JSON text.
782pub(crate) fn parse<'a>(call: &'static str, text: &'a str) -> Result<Json<'a>> {
783    serde_json::from_str(text).map_err(|e| invalid(call, format!("the text is not JSON: {e}")))
784}
785
786/// The `name` of a JSON series.
787pub(crate) fn read_name<'a>(call: &'static str, value: &'a Json<'_>) -> Result<&'a str> {
788    value
789        .get("name")
790        .and_then(|v| v.as_str())
791        .ok_or_else(|| invalid(call, "expected a \"name\" that is a string".into()))
792}
793
794/// One array cell's elements: numbers, text and true/false values only.
795pub(crate) fn read_array(
796    call: &'static str,
797    cell: &[Json],
798    row: usize,
799    column: &str,
800) -> Result<Vec<Scalar>> {
801    cell.iter()
802        .enumerate()
803        .map(|(e, v)| match v {
804            Json::Number(x) => Ok(Scalar::Number(x.as_f64().expect("always a number"))),
805            Json::String(x) => Ok(Scalar::String(x.to_string())),
806            Json::Bool(x) => Ok(Scalar::Boolean(*x)),
807            _ => Err(invalid(
808                call,
809                format!(
810                    "row {row} column '{column}': array element {e} must be a finite number, string, or boolean, got {v}"
811                ),
812            )),
813        })
814        .collect()
815}
816
817/// The schema as (name, kind, required), key first.
818pub(crate) fn read_schema(
819    call: &'static str,
820    schema: Option<&Json>,
821) -> Result<Vec<(String, ColumnKind, bool)>> {
822    let entries = schema
823        .and_then(|v| v.as_array())
824        .ok_or_else(|| invalid(call, "expected \"schema\" as an array".into()))?;
825    if entries.is_empty() {
826        return Err(invalid(
827            call,
828            "the schema needs at least a key column".into(),
829        ));
830    }
831    entries
832        .iter()
833        .enumerate()
834        .map(|(c, entry)| {
835            let name = entry
836                .get("name")
837                .and_then(|v| v.as_str())
838                .ok_or_else(|| invalid(call, format!("schema column {c} has no \"name\"")))?;
839            let kind_text = entry.get("kind").and_then(|v| v.as_str()).unwrap_or("");
840            let kind = match (c, kind_text) {
841                (0, "time") => ColumnKind::Time,
842                (0, "timeRange") => ColumnKind::TimeRange,
843                (0, "interval") => ColumnKind::Interval,
844                (0, "value") => ColumnKind::Value,
845                (0, other) => {
846                    return Err(invalid(call, format!(
847                        "the first schema column is the key, and must be 'time', 'timeRange', 'interval' or (for a ValueSeries) 'value'; got '{other}'"
848                    )));
849                }
850                (_, "number") => ColumnKind::Number,
851                (_, "string") => ColumnKind::String,
852                (_, "boolean") => ColumnKind::Boolean,
853                (_, "array") => ColumnKind::Array,
854                (_, other) => {
855                    return Err(invalid(call, format!(
856                        "column '{name}' has kind '{other}'; value columns are 'number', 'string', 'boolean' or 'array'"
857                    )));
858                }
859            };
860            // As in pond-ts, only `required: false` allows gaps.
861            let required = entry.get("required") != Some(&Json::Bool(false));
862            Ok((name.to_string(), kind, required))
863        })
864        .collect()
865}
866
867/// A time: epoch milliseconds, or text read in `zone` (refused when `zone`
868/// is `None`).
869pub(crate) fn read_time(
870    call: &'static str,
871    v: &Json,
872    row: usize,
873    zone: Option<&str>,
874) -> Result<i64> {
875    match v {
876        Json::Number(n) => {
877            if let Some(t) = n.as_i64() {
878                return Ok(t);
879            }
880            let f = n.as_f64().unwrap_or(f64::NAN);
881            // Whole numbers written as floats (`1000.0`, `2e3`) are fine.
882            if f.fract() == 0.0 && f.abs() < 9.3e18 {
883                return Ok(f as i64);
884            }
885            Err(invalid(
886                call,
887                format!(
888                    "row {row}: the time {n} is not a whole number of milliseconds (pond-rs times are whole milliseconds)"
889                ),
890            ))
891        }
892        Json::String(text) if zone.is_some() => {
893            let zone = zone.unwrap_or_default();
894            crate::parse_time_in(text, zone).map_err(|e| invalid(call, format!("row {row}: {e}")))
895        }
896        other if zone.is_some() => Err(invalid(
897            call,
898            format!(
899                "row {row}: expected a time as a number of milliseconds or as text, got {other}"
900            ),
901        )),
902        other => Err(invalid(
903            call,
904            format!("row {row}: expected a time as a number of milliseconds, got {other}"),
905        )),
906    }
907}
908
909/// The start and end of a time range (`[begin, end]` / `{start, end}`) or
910/// an interval (`[label, begin, end]` / `{value, start, end}`).
911pub(crate) fn read_span<'a>(
912    call: &'static str,
913    v: &'a Json<'a>,
914    row: usize,
915    interval: bool,
916) -> Result<(&'a Json<'a>, &'a Json<'a>)> {
917    match v {
918        Json::Array(a) if !interval && a.len() == 2 => Ok((&a[0], &a[1])),
919        Json::Array(a) if interval && a.len() == 3 => Ok((&a[1], &a[2])),
920        Json::Object(o)
921            if o.contains_key("start")
922                && o.contains_key("end")
923                && o.contains_key("value") == interval =>
924        {
925            Ok((o.get("start").unwrap_or(NULL), o.get("end").unwrap_or(NULL)))
926        }
927        _ if interval => Err(invalid(
928            call,
929            format!(
930                "row {row}: expected an interval as [label, start, end] or {{\"value\", \"start\", \"end\"}}, got {v}"
931            ),
932        )),
933        _ => Err(invalid(
934            call,
935            format!(
936                "row {row}: expected a time range as [start, end] or {{\"start\", \"end\"}}, got {v}"
937            ),
938        )),
939    }
940}
941
942pub(crate) enum LabelCell<'a> {
943    Number(f64),
944    String(&'a str),
945}
946
947/// An interval label: text or a number, the same kind as the first row's.
948pub(crate) fn read_label<'a>(
949    call: &'static str,
950    v: &'a Json<'_>,
951    row: usize,
952    first: Option<&LabelCell>,
953) -> Result<LabelCell<'a>> {
954    let label = match v {
955        Json::String(s) => LabelCell::String(s.as_ref()),
956        Json::Number(n) => LabelCell::Number(n.as_f64().expect("JSON numbers are finite")),
957        other => {
958            return Err(invalid(
959                call,
960                format!("row {row}: an interval label must be text or a number, got {other}"),
961            ));
962        }
963    };
964    match (first, &label) {
965        (Some(LabelCell::Number(_)), LabelCell::String(_))
966        | (Some(LabelCell::String(_)), LabelCell::Number(_)) => Err(invalid(
967            call,
968            format!(
969                "row {row}: interval labels must all be text or all numbers; this one is {v}, unlike row 0's"
970            ),
971        )),
972        _ => Ok(label),
973    }
974}
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979
980    fn time_series(rows: &str) -> String {
981        format!(
982            r#"{{"name":"t","schema":[{{"name":"time","kind":"time"}},{{"name":"v","kind":"number","required":false}}],"rows":{rows}}}"#
983        )
984    }
985
986    #[test]
987    fn numbers_are_written_as_javascript_writes_them() {
988        let text = |v: f64| {
989            let mut out = Vec::new();
990            push_number(&mut out, v);
991            String::from_utf8(out).unwrap()
992        };
993        assert_eq!(text(-0.0), "0");
994        assert_eq!(text(2.0), "2");
995        assert_eq!(text(0.1), "0.1");
996        assert_eq!(text(f64::INFINITY), "null");
997        assert_eq!(text(f64::NAN), "null");
998        assert_eq!(text(1e300), "1e+300"); // as JavaScript
999        assert_eq!(text(2f64.powi(53)), "9007199254740992");
1000        assert_eq!(text(1e-7), "1e-7");
1001        assert_eq!(text(1e-6), "0.000001");
1002        assert_eq!(text(-1.5e-6), "-0.0000015");
1003        assert_eq!(text(1e19), "10000000000000000000");
1004        assert_eq!(text(1.2345678901234568e20), "123456789012345680000");
1005        assert_eq!(text(1e21), "1e+21");
1006        assert_eq!(text(-1.5e21), "-1.5e+21");
1007        assert_eq!(text(5e-324), "5e-324");
1008    }
1009
1010    #[test]
1011    fn clamp_and_reorder() {
1012        let text = time_series("[[3000,1],[1000,2],[1000,3]]");
1013        assert!(TimeSeries::from_json(&text, &JsonOptions::new()).is_err());
1014        let s = TimeSeries::from_json(&text, &JsonOptions::new().order(KeyOrder::Clamp)).unwrap();
1015        assert_eq!(s.key().begin(), &[3000, 3000, 3000]);
1016        let s = TimeSeries::from_json(&text, &JsonOptions::new().order(KeyOrder::Reorder)).unwrap();
1017        assert_eq!(s.key().begin(), &[1000, 1000, 3000]);
1018        let v = s.column("v").unwrap().numbers().unwrap();
1019        assert_eq!(
1020            (v.get(0), v.get(1), v.get(2)),
1021            (Some(2.0), Some(3.0), Some(1.0))
1022        );
1023    }
1024
1025    #[test]
1026    fn round_trips_and_refuses_fractions_and_decimals() {
1027        let text = time_series("[[0,1.5],[1000,null]]");
1028        let s = TimeSeries::from_json(&text, &JsonOptions::new()).unwrap();
1029        assert_eq!(s.to_json(RowFormat::Array).unwrap(), text);
1030        assert!(TimeSeries::from_json(&time_series("[[1.5,1]]"), &JsonOptions::new()).is_err());
1031        assert!(TimeSeries::from_json("{", &JsonOptions::new()).is_err());
1032        let d = s
1033            .with_column(Column::required(
1034                "d",
1035                crate::DecimalColumn::from_values(vec![1, 2], 1).unwrap(),
1036            ))
1037            .unwrap();
1038        assert!(d.to_json(RowFormat::Object).is_err());
1039    }
1040}