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