1use crate::codecs::CodecError;
4use crate::codecs::shared;
5use crate::data::{HDict, HGrid};
6use crate::kinds::Kind;
7
8pub 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
41fn 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
50fn encode_time(t: &chrono::NaiveTime) -> String {
52 shared::format_time(t)
53}
54
55use chrono::Timelike;
56
57fn 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
66fn encode_str(s: &str) -> String {
68 format!("\"{}\"", escape_str(s))
69}
70
71pub 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
90fn 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
98fn 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
111pub 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
125pub fn encode_grid(grid: &HGrid) -> Result<String, CodecError> {
127 let mut buf = String::new();
128
129 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 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 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 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 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}