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 use std::fmt::Write;
128
129 let mut buf = String::new();
130
131 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 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 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 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 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}