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::{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_iter() {
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_iter() {
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    use std::fmt::Write;
128
129    let mut buf = String::new();
130
131    // Line 1: version + grid meta
132    buf.push_str("ver:\"3.0\"");
133    if !grid.meta.is_empty() {
134        buf.push(' ');
135        buf.push_str(&encode_meta(&grid.meta)?);
136    }
137    buf.push('\n');
138
139    // Line 2: columns — write directly, comma-delimited.
140    if grid.cols.is_empty() {
141        buf.push_str("empty\n");
142    } else {
143        for (i, col) in grid.cols.iter().enumerate() {
144            if i > 0 {
145                buf.push(',');
146            }
147            buf.push_str(&col.name);
148            if !col.meta.is_empty() {
149                buf.push(' ');
150                buf.push_str(&encode_meta(&col.meta)?);
151            }
152        }
153        buf.push('\n');
154    }
155
156    // Rows — write cells directly, comma-delimited.
157    for row in &grid.rows {
158        for (i, col) in grid.cols.iter().enumerate() {
159            if i > 0 {
160                buf.push(',');
161            }
162            match row.get(&col.name) {
163                Some(val) => write!(buf, "{}", encode_scalar(val)?).unwrap(),
164                None => buf.push('N'),
165            }
166        }
167        buf.push('\n');
168    }
169
170    Ok(buf)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::data::{HCol, HDict, HGrid};
177    use crate::kinds::*;
178    use chrono::{FixedOffset, NaiveDate, NaiveTime, TimeZone};
179
180    #[test]
181    fn encode_null() {
182        assert_eq!(encode_scalar(&Kind::Null).unwrap(), "N");
183    }
184
185    #[test]
186    fn encode_bool_true() {
187        assert_eq!(encode_scalar(&Kind::Bool(true)).unwrap(), "T");
188    }
189
190    #[test]
191    fn encode_bool_false() {
192        assert_eq!(encode_scalar(&Kind::Bool(false)).unwrap(), "F");
193    }
194
195    #[test]
196    fn encode_marker() {
197        assert_eq!(encode_scalar(&Kind::Marker).unwrap(), "M");
198    }
199
200    #[test]
201    fn encode_na() {
202        assert_eq!(encode_scalar(&Kind::NA).unwrap(), "NA");
203    }
204
205    #[test]
206    fn encode_remove() {
207        assert_eq!(encode_scalar(&Kind::Remove).unwrap(), "R");
208    }
209
210    #[test]
211    fn encode_number_zero() {
212        let k = Kind::Number(Number::unitless(0.0));
213        assert_eq!(encode_scalar(&k).unwrap(), "0");
214    }
215
216    #[test]
217    fn encode_number_integer() {
218        let k = Kind::Number(Number::unitless(42.0));
219        assert_eq!(encode_scalar(&k).unwrap(), "42");
220    }
221
222    #[test]
223    fn encode_number_float() {
224        let k = Kind::Number(Number::unitless(72.5));
225        assert_eq!(encode_scalar(&k).unwrap(), "72.5");
226    }
227
228    #[test]
229    fn encode_number_negative() {
230        let k = Kind::Number(Number::unitless(-23.45));
231        assert_eq!(encode_scalar(&k).unwrap(), "-23.45");
232    }
233
234    #[test]
235    fn encode_number_with_unit() {
236        let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
237        assert_eq!(encode_scalar(&k).unwrap(), "72.5\u{00B0}F");
238    }
239
240    #[test]
241    fn encode_number_inf() {
242        let k = Kind::Number(Number::unitless(f64::INFINITY));
243        assert_eq!(encode_scalar(&k).unwrap(), "INF");
244    }
245
246    #[test]
247    fn encode_number_neg_inf() {
248        let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
249        assert_eq!(encode_scalar(&k).unwrap(), "-INF");
250    }
251
252    #[test]
253    fn encode_number_nan() {
254        let k = Kind::Number(Number::unitless(f64::NAN));
255        assert_eq!(encode_scalar(&k).unwrap(), "NaN");
256    }
257
258    #[test]
259    fn encode_string_simple() {
260        let k = Kind::Str("hello".into());
261        assert_eq!(encode_scalar(&k).unwrap(), "\"hello\"");
262    }
263
264    #[test]
265    fn encode_string_empty() {
266        let k = Kind::Str(String::new());
267        assert_eq!(encode_scalar(&k).unwrap(), "\"\"");
268    }
269
270    #[test]
271    fn encode_string_escapes() {
272        let k = Kind::Str("line1\nline2\ttab\\slash\"quote$dollar".into());
273        let encoded = encode_scalar(&k).unwrap();
274        assert_eq!(
275            encoded,
276            "\"line1\\nline2\\ttab\\\\slash\\\"quote\\$dollar\""
277        );
278    }
279
280    #[test]
281    fn encode_ref_simple() {
282        let k = Kind::Ref(HRef::from_val("site-1"));
283        assert_eq!(encode_scalar(&k).unwrap(), "@site-1");
284    }
285
286    #[test]
287    fn encode_ref_with_dis() {
288        let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
289        assert_eq!(encode_scalar(&k).unwrap(), "@site-1 \"Main Site\"");
290    }
291
292    #[test]
293    fn encode_uri() {
294        let k = Kind::Uri(Uri::new("http://example.com"));
295        assert_eq!(encode_scalar(&k).unwrap(), "`http://example.com`");
296    }
297
298    #[test]
299    fn encode_symbol() {
300        let k = Kind::Symbol(Symbol::new("hot-water"));
301        assert_eq!(encode_scalar(&k).unwrap(), "^hot-water");
302    }
303
304    #[test]
305    fn encode_date() {
306        let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
307        assert_eq!(encode_scalar(&k).unwrap(), "2024-03-13");
308    }
309
310    #[test]
311    fn encode_time_no_frac() {
312        let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
313        assert_eq!(encode_scalar(&k).unwrap(), "08:12:05");
314    }
315
316    #[test]
317    fn encode_time_with_frac() {
318        let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
319        assert_eq!(encode_scalar(&k).unwrap(), "14:30:00.123");
320    }
321
322    #[test]
323    fn encode_datetime() {
324        let offset = FixedOffset::west_opt(5 * 3600).unwrap();
325        let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
326        let hdt = HDateTime::new(dt, "New_York");
327        let k = Kind::DateTime(hdt);
328        assert_eq!(
329            encode_scalar(&k).unwrap(),
330            "2024-01-01T08:12:05-05:00 New_York"
331        );
332    }
333
334    #[test]
335    fn encode_datetime_utc() {
336        let offset = FixedOffset::east_opt(0).unwrap();
337        let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
338        let hdt = HDateTime::new(dt, "UTC");
339        let k = Kind::DateTime(hdt);
340        assert_eq!(encode_scalar(&k).unwrap(), "2024-06-15T12:00:00+00:00 UTC");
341    }
342
343    #[test]
344    fn encode_coord() {
345        let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
346        assert_eq!(encode_scalar(&k).unwrap(), "C(37.5458266,-77.4491888)");
347    }
348
349    #[test]
350    fn encode_xstr() {
351        let k = Kind::XStr(XStr::new("Color", "red"));
352        assert_eq!(encode_scalar(&k).unwrap(), "Color(\"red\")");
353    }
354
355    #[test]
356    fn encode_list_empty() {
357        let k = Kind::List(vec![]);
358        assert_eq!(encode_scalar(&k).unwrap(), "[]");
359    }
360
361    #[test]
362    fn encode_list_mixed() {
363        let k = Kind::List(vec![
364            Kind::Number(Number::unitless(1.0)),
365            Kind::Str("two".into()),
366            Kind::Marker,
367        ]);
368        assert_eq!(encode_scalar(&k).unwrap(), "[1, \"two\", M]");
369    }
370
371    #[test]
372    fn encode_dict_empty() {
373        let k = Kind::Dict(Box::new(HDict::new()));
374        assert_eq!(encode_scalar(&k).unwrap(), "{}");
375    }
376
377    #[test]
378    fn encode_dict_with_values() {
379        let mut d = HDict::new();
380        d.set("site", Kind::Marker);
381        d.set("dis", Kind::Str("Main".into()));
382        let k = Kind::Dict(Box::new(d));
383        let encoded = encode_scalar(&k).unwrap();
384        // Sorted keys: dis, site
385        assert_eq!(encoded, "{dis:\"Main\" site}");
386    }
387
388    #[test]
389    fn encode_grid_error() {
390        let k = Kind::Grid(Box::new(HGrid::new()));
391        assert!(encode_scalar(&k).is_err());
392    }
393
394    #[test]
395    fn encode_grid_empty() {
396        let g = HGrid::new();
397        let encoded = encode_grid(&g).unwrap();
398        assert_eq!(encoded, "ver:\"3.0\"\nempty\n");
399    }
400
401    #[test]
402    fn encode_grid_with_data() {
403        let cols = vec![HCol::new("dis"), HCol::new("area")];
404        let mut row1 = HDict::new();
405        row1.set("dis", Kind::Str("Site One".into()));
406        row1.set("area", Kind::Number(Number::unitless(4500.0)));
407        let mut row2 = HDict::new();
408        row2.set("dis", Kind::Str("Site Two".into()));
409        // area missing in row2
410
411        let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
412        let encoded = encode_grid(&g).unwrap();
413        let lines: Vec<&str> = encoded.lines().collect();
414        assert_eq!(lines[0], "ver:\"3.0\"");
415        assert_eq!(lines[1], "dis,area");
416        assert_eq!(lines[2], "\"Site One\",4500");
417        assert_eq!(lines[3], "\"Site Two\",N");
418    }
419
420    #[test]
421    fn encode_grid_with_meta() {
422        let mut meta = HDict::new();
423        meta.set("err", Kind::Marker);
424        meta.set("dis", Kind::Str("some error".into()));
425
426        let g = HGrid::from_parts(meta, vec![], vec![]);
427        let encoded = encode_grid(&g).unwrap();
428        let first_line = encoded.lines().next().unwrap();
429        assert!(first_line.starts_with("ver:\"3.0\" "));
430        assert!(first_line.contains("err"));
431        assert!(first_line.contains("dis:\"some error\""));
432    }
433
434    #[test]
435    fn encode_grid_with_col_meta() {
436        let mut col_meta = HDict::new();
437        col_meta.set("unit", Kind::Str("kW".into()));
438        let cols = vec![HCol::new("name"), HCol::with_meta("power", col_meta)];
439        let g = HGrid::from_parts(HDict::new(), cols, vec![]);
440        let encoded = encode_grid(&g).unwrap();
441        let lines: Vec<&str> = encoded.lines().collect();
442        assert_eq!(lines[1], "name,power unit:\"kW\"");
443    }
444
445    #[test]
446    fn encode_escape_str() {
447        assert_eq!(escape_str("hello"), "hello");
448        assert_eq!(escape_str("a\\b"), "a\\\\b");
449        assert_eq!(escape_str("a\"b"), "a\\\"b");
450        assert_eq!(escape_str("a\nb"), "a\\nb");
451        assert_eq!(escape_str("a\rb"), "a\\rb");
452        assert_eq!(escape_str("a\tb"), "a\\tb");
453        assert_eq!(escape_str("a$b"), "a\\$b");
454        assert_eq!(escape_str("a\u{0008}b"), "a\\bb");
455        assert_eq!(escape_str("a\u{000C}b"), "a\\fb");
456    }
457}