1use crate::codecs::CodecError;
4use crate::codecs::shared;
5use crate::data::{HCol, 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_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
111pub 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
125pub fn encode_grid(grid: &HGrid) -> Result<String, CodecError> {
127 let mut buf = encode_grid_header(grid)?;
128
129 for row in &grid.rows {
131 buf.push_str(&encode_grid_row(&grid.cols, row)?);
132 }
133
134 Ok(buf)
135}
136
137pub fn encode_grid_header(grid: &HGrid) -> Result<String, CodecError> {
139 let mut buf = String::new();
140
141 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 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
169pub 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 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 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}