Skip to main content

haystack_core/codecs/zinc/
encoder.rs

1// Zinc scalar and grid encoder.
2
3use crate::codecs::CodecError;
4use crate::codecs::shared;
5use crate::data::{HCol, HDict, HGrid};
6use crate::kinds::Kind;
7
8/// Encode a single Kind value to its Zinc string representation.
9pub fn encode_scalar(val: &Kind) -> Result<String, CodecError> {
10    match val {
11        Kind::Null => Ok("N".to_string()),
12        Kind::Bool(true) => Ok("T".to_string()),
13        Kind::Bool(false) => Ok("F".to_string()),
14        Kind::Marker => Ok("M".to_string()),
15        Kind::NA => Ok("NA".to_string()),
16        Kind::Remove => Ok("R".to_string()),
17        Kind::Number(n) => Ok(encode_number(n)),
18        Kind::Str(s) => Ok(encode_str(s)),
19        Kind::Ref(r) => Ok(encode_ref(r)),
20        Kind::Uri(u) => Ok(format!("`{}`", u.val())),
21        Kind::Symbol(s) => Ok(format!("^{}", s.val())),
22        Kind::Date(d) => Ok(d.format("%Y-%m-%d").to_string()),
23        Kind::Time(t) => Ok(encode_time(t)),
24        Kind::DateTime(hdt) => Ok(encode_datetime(hdt)),
25        Kind::Coord(c) => Ok(format!("C({},{})", c.lat, c.lng)),
26        Kind::XStr(x) => Ok(format!("{}(\"{}\")", x.type_name, escape_str(&x.val))),
27        Kind::List(items) => {
28            let mut parts = Vec::with_capacity(items.len());
29            for item in items {
30                parts.push(encode_scalar(item)?);
31            }
32            Ok(format!("[{}]", parts.join(", ")))
33        }
34        Kind::Dict(d) => Ok(encode_dict_inline(d)?),
35        Kind::Grid(_) => Err(CodecError::Encode(
36            "grids cannot be encoded as scalars".to_string(),
37        )),
38    }
39}
40
41/// Encode a Number to its Zinc string representation.
42fn encode_number(n: &crate::kinds::Number) -> String {
43    let s = shared::format_number_val(n.val);
44    match &n.unit {
45        Some(u) => format!("{s}{u}"),
46        None => s,
47    }
48}
49
50/// Encode a time value, always including seconds.
51fn encode_time(t: &chrono::NaiveTime) -> String {
52    shared::format_time(t)
53}
54
55use chrono::Timelike;
56
57/// Encode a Haystack DateTime to Zinc format.
58fn encode_datetime(hdt: &crate::kinds::HDateTime) -> String {
59    let dt_str = hdt.dt.format("%Y-%m-%dT%H:%M:%S").to_string();
60    let frac = shared::format_frac_seconds(hdt.dt.nanosecond());
61    let offset_str = hdt.dt.format("%:z").to_string();
62    let tz = &hdt.tz_name;
63    format!("{dt_str}{frac}{offset_str} {tz}")
64}
65
66/// Encode a string value (with outer quotes).
67fn encode_str(s: &str) -> String {
68    format!("\"{}\"", escape_str(s))
69}
70
71/// Escape string content for Zinc format (without outer quotes).
72pub fn escape_str(s: &str) -> String {
73    let mut out = String::with_capacity(s.len());
74    for ch in s.chars() {
75        match ch {
76            '\\' => out.push_str("\\\\"),
77            '"' => out.push_str("\\\""),
78            '\n' => out.push_str("\\n"),
79            '\r' => out.push_str("\\r"),
80            '\t' => out.push_str("\\t"),
81            '$' => out.push_str("\\$"),
82            '\u{0008}' => out.push_str("\\b"),
83            '\u{000C}' => out.push_str("\\f"),
84            _ => out.push(ch),
85        }
86    }
87    out
88}
89
90/// Encode an HRef to Zinc format.
91fn encode_ref(r: &crate::kinds::HRef) -> String {
92    match &r.dis {
93        Some(dis) => format!("@{} \"{}\"", r.val, escape_str(dis)),
94        None => format!("@{}", r.val),
95    }
96}
97
98/// Encode an HDict inline (for use inside grid cells or nested dicts).
99fn encode_dict_inline(d: &HDict) -> Result<String, CodecError> {
100    let mut parts = Vec::new();
101    for (k, v) in d.sorted_tags() {
102        if matches!(v, Kind::Marker) {
103            parts.push(k.to_string());
104        } else {
105            parts.push(format!("{}:{}", k, encode_scalar(v)?));
106        }
107    }
108    Ok(format!("{{{}}}", parts.join(" ")))
109}
110
111/// Encode metadata tags in inline format for grid/column metadata.
112/// Format: `"tag1 tag2:val2 tag3:val3"`
113pub fn encode_meta(d: &HDict) -> Result<String, CodecError> {
114    let mut parts = Vec::new();
115    for (k, v) in d.sorted_tags() {
116        if matches!(v, Kind::Marker) {
117            parts.push(k.to_string());
118        } else {
119            parts.push(format!("{}:{}", k, encode_scalar(v)?));
120        }
121    }
122    Ok(parts.join(" "))
123}
124
125/// Encode an HGrid to the Zinc wire format.
126pub fn encode_grid(grid: &HGrid) -> Result<String, CodecError> {
127    let mut buf = encode_grid_header(grid)?;
128
129    // Rows — write cells directly, comma-delimited.
130    for row in &grid.rows {
131        buf.push_str(&encode_grid_row(&grid.cols, row)?);
132    }
133
134    Ok(buf)
135}
136
137/// Encode just the grid header: version line + meta + column definitions.
138pub fn encode_grid_header(grid: &HGrid) -> Result<String, CodecError> {
139    let mut buf = String::new();
140
141    // Line 1: version + grid meta
142    buf.push_str("ver:\"3.0\"");
143    if !grid.meta.is_empty() {
144        buf.push(' ');
145        buf.push_str(&encode_meta(&grid.meta)?);
146    }
147    buf.push('\n');
148
149    // Line 2: columns
150    if grid.cols.is_empty() {
151        buf.push_str("empty\n");
152    } else {
153        for (i, col) in grid.cols.iter().enumerate() {
154            if i > 0 {
155                buf.push(',');
156            }
157            buf.push_str(&col.name);
158            if !col.meta.is_empty() {
159                buf.push(' ');
160                buf.push_str(&encode_meta(&col.meta)?);
161            }
162        }
163        buf.push('\n');
164    }
165
166    Ok(buf)
167}
168
169/// Encode a single grid row as comma-delimited values followed by newline.
170pub fn encode_grid_row(cols: &[HCol], row: &HDict) -> Result<String, CodecError> {
171    use std::fmt::Write;
172    let mut buf = String::new();
173    for (i, col) in cols.iter().enumerate() {
174        if i > 0 {
175            buf.push(',');
176        }
177        match row.get(&col.name) {
178            Some(val) => write!(buf, "{}", encode_scalar(val)?).unwrap(),
179            None => buf.push('N'),
180        }
181    }
182    buf.push('\n');
183    Ok(buf)
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::data::{HCol, HDict, HGrid};
190    use crate::kinds::*;
191    use chrono::{FixedOffset, NaiveDate, NaiveTime, TimeZone};
192
193    #[test]
194    fn encode_null() {
195        assert_eq!(encode_scalar(&Kind::Null).unwrap(), "N");
196    }
197
198    #[test]
199    fn encode_bool_true() {
200        assert_eq!(encode_scalar(&Kind::Bool(true)).unwrap(), "T");
201    }
202
203    #[test]
204    fn encode_bool_false() {
205        assert_eq!(encode_scalar(&Kind::Bool(false)).unwrap(), "F");
206    }
207
208    #[test]
209    fn encode_marker() {
210        assert_eq!(encode_scalar(&Kind::Marker).unwrap(), "M");
211    }
212
213    #[test]
214    fn encode_na() {
215        assert_eq!(encode_scalar(&Kind::NA).unwrap(), "NA");
216    }
217
218    #[test]
219    fn encode_remove() {
220        assert_eq!(encode_scalar(&Kind::Remove).unwrap(), "R");
221    }
222
223    #[test]
224    fn encode_number_zero() {
225        let k = Kind::Number(Number::unitless(0.0));
226        assert_eq!(encode_scalar(&k).unwrap(), "0");
227    }
228
229    #[test]
230    fn encode_number_integer() {
231        let k = Kind::Number(Number::unitless(42.0));
232        assert_eq!(encode_scalar(&k).unwrap(), "42");
233    }
234
235    #[test]
236    fn encode_number_float() {
237        let k = Kind::Number(Number::unitless(72.5));
238        assert_eq!(encode_scalar(&k).unwrap(), "72.5");
239    }
240
241    #[test]
242    fn encode_number_negative() {
243        let k = Kind::Number(Number::unitless(-23.45));
244        assert_eq!(encode_scalar(&k).unwrap(), "-23.45");
245    }
246
247    #[test]
248    fn encode_number_with_unit() {
249        let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
250        assert_eq!(encode_scalar(&k).unwrap(), "72.5\u{00B0}F");
251    }
252
253    #[test]
254    fn encode_number_inf() {
255        let k = Kind::Number(Number::unitless(f64::INFINITY));
256        assert_eq!(encode_scalar(&k).unwrap(), "INF");
257    }
258
259    #[test]
260    fn encode_number_neg_inf() {
261        let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
262        assert_eq!(encode_scalar(&k).unwrap(), "-INF");
263    }
264
265    #[test]
266    fn encode_number_nan() {
267        let k = Kind::Number(Number::unitless(f64::NAN));
268        assert_eq!(encode_scalar(&k).unwrap(), "NaN");
269    }
270
271    #[test]
272    fn encode_string_simple() {
273        let k = Kind::Str("hello".into());
274        assert_eq!(encode_scalar(&k).unwrap(), "\"hello\"");
275    }
276
277    #[test]
278    fn encode_string_empty() {
279        let k = Kind::Str(String::new());
280        assert_eq!(encode_scalar(&k).unwrap(), "\"\"");
281    }
282
283    #[test]
284    fn encode_string_escapes() {
285        let k = Kind::Str("line1\nline2\ttab\\slash\"quote$dollar".into());
286        let encoded = encode_scalar(&k).unwrap();
287        assert_eq!(
288            encoded,
289            "\"line1\\nline2\\ttab\\\\slash\\\"quote\\$dollar\""
290        );
291    }
292
293    #[test]
294    fn encode_ref_simple() {
295        let k = Kind::Ref(HRef::from_val("site-1"));
296        assert_eq!(encode_scalar(&k).unwrap(), "@site-1");
297    }
298
299    #[test]
300    fn encode_ref_with_dis() {
301        let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
302        assert_eq!(encode_scalar(&k).unwrap(), "@site-1 \"Main Site\"");
303    }
304
305    #[test]
306    fn encode_uri() {
307        let k = Kind::Uri(Uri::new("http://example.com"));
308        assert_eq!(encode_scalar(&k).unwrap(), "`http://example.com`");
309    }
310
311    #[test]
312    fn encode_symbol() {
313        let k = Kind::Symbol(Symbol::new("hot-water"));
314        assert_eq!(encode_scalar(&k).unwrap(), "^hot-water");
315    }
316
317    #[test]
318    fn encode_date() {
319        let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
320        assert_eq!(encode_scalar(&k).unwrap(), "2024-03-13");
321    }
322
323    #[test]
324    fn encode_time_no_frac() {
325        let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
326        assert_eq!(encode_scalar(&k).unwrap(), "08:12:05");
327    }
328
329    #[test]
330    fn encode_time_with_frac() {
331        let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
332        assert_eq!(encode_scalar(&k).unwrap(), "14:30:00.123");
333    }
334
335    #[test]
336    fn encode_datetime() {
337        let offset = FixedOffset::west_opt(5 * 3600).unwrap();
338        let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
339        let hdt = HDateTime::new(dt, "New_York");
340        let k = Kind::DateTime(hdt);
341        assert_eq!(
342            encode_scalar(&k).unwrap(),
343            "2024-01-01T08:12:05-05:00 New_York"
344        );
345    }
346
347    #[test]
348    fn encode_datetime_utc() {
349        let offset = FixedOffset::east_opt(0).unwrap();
350        let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
351        let hdt = HDateTime::new(dt, "UTC");
352        let k = Kind::DateTime(hdt);
353        assert_eq!(encode_scalar(&k).unwrap(), "2024-06-15T12:00:00+00:00 UTC");
354    }
355
356    #[test]
357    fn encode_coord() {
358        let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
359        assert_eq!(encode_scalar(&k).unwrap(), "C(37.5458266,-77.4491888)");
360    }
361
362    #[test]
363    fn encode_xstr() {
364        let k = Kind::XStr(XStr::new("Color", "red"));
365        assert_eq!(encode_scalar(&k).unwrap(), "Color(\"red\")");
366    }
367
368    #[test]
369    fn encode_list_empty() {
370        let k = Kind::List(vec![]);
371        assert_eq!(encode_scalar(&k).unwrap(), "[]");
372    }
373
374    #[test]
375    fn encode_list_mixed() {
376        let k = Kind::List(vec![
377            Kind::Number(Number::unitless(1.0)),
378            Kind::Str("two".into()),
379            Kind::Marker,
380        ]);
381        assert_eq!(encode_scalar(&k).unwrap(), "[1, \"two\", M]");
382    }
383
384    #[test]
385    fn encode_dict_empty() {
386        let k = Kind::Dict(Box::new(HDict::new()));
387        assert_eq!(encode_scalar(&k).unwrap(), "{}");
388    }
389
390    #[test]
391    fn encode_dict_with_values() {
392        let mut d = HDict::new();
393        d.set("site", Kind::Marker);
394        d.set("dis", Kind::Str("Main".into()));
395        let k = Kind::Dict(Box::new(d));
396        let encoded = encode_scalar(&k).unwrap();
397        // Sorted keys: dis, site
398        assert_eq!(encoded, "{dis:\"Main\" site}");
399    }
400
401    #[test]
402    fn encode_grid_error() {
403        let k = Kind::Grid(Box::new(HGrid::new()));
404        assert!(encode_scalar(&k).is_err());
405    }
406
407    #[test]
408    fn encode_grid_empty() {
409        let g = HGrid::new();
410        let encoded = encode_grid(&g).unwrap();
411        assert_eq!(encoded, "ver:\"3.0\"\nempty\n");
412    }
413
414    #[test]
415    fn encode_grid_with_data() {
416        let cols = vec![HCol::new("dis"), HCol::new("area")];
417        let mut row1 = HDict::new();
418        row1.set("dis", Kind::Str("Site One".into()));
419        row1.set("area", Kind::Number(Number::unitless(4500.0)));
420        let mut row2 = HDict::new();
421        row2.set("dis", Kind::Str("Site Two".into()));
422        // area missing in row2
423
424        let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
425        let encoded = encode_grid(&g).unwrap();
426        let lines: Vec<&str> = encoded.lines().collect();
427        assert_eq!(lines[0], "ver:\"3.0\"");
428        assert_eq!(lines[1], "dis,area");
429        assert_eq!(lines[2], "\"Site One\",4500");
430        assert_eq!(lines[3], "\"Site Two\",N");
431    }
432
433    #[test]
434    fn encode_grid_with_meta() {
435        let mut meta = HDict::new();
436        meta.set("err", Kind::Marker);
437        meta.set("dis", Kind::Str("some error".into()));
438
439        let g = HGrid::from_parts(meta, vec![], vec![]);
440        let encoded = encode_grid(&g).unwrap();
441        let first_line = encoded.lines().next().unwrap();
442        assert!(first_line.starts_with("ver:\"3.0\" "));
443        assert!(first_line.contains("err"));
444        assert!(first_line.contains("dis:\"some error\""));
445    }
446
447    #[test]
448    fn encode_grid_with_col_meta() {
449        let mut col_meta = HDict::new();
450        col_meta.set("unit", Kind::Str("kW".into()));
451        let cols = vec![HCol::new("name"), HCol::with_meta("power", col_meta)];
452        let g = HGrid::from_parts(HDict::new(), cols, vec![]);
453        let encoded = encode_grid(&g).unwrap();
454        let lines: Vec<&str> = encoded.lines().collect();
455        assert_eq!(lines[1], "name,power unit:\"kW\"");
456    }
457
458    #[test]
459    fn encode_escape_str() {
460        assert_eq!(escape_str("hello"), "hello");
461        assert_eq!(escape_str("a\\b"), "a\\\\b");
462        assert_eq!(escape_str("a\"b"), "a\\\"b");
463        assert_eq!(escape_str("a\nb"), "a\\nb");
464        assert_eq!(escape_str("a\rb"), "a\\rb");
465        assert_eq!(escape_str("a\tb"), "a\\tb");
466        assert_eq!(escape_str("a$b"), "a\\$b");
467        assert_eq!(escape_str("a\u{0008}b"), "a\\bb");
468        assert_eq!(escape_str("a\u{000C}b"), "a\\fb");
469    }
470}