1use crate::codecs::CodecError;
4use crate::data::{HCol, HDict, HGrid};
5use crate::kinds::*;
6use chrono::{NaiveDate, NaiveTime};
7
8pub struct ZincParser<'a> {
10 src: &'a str,
11 pos: usize,
12}
13
14impl<'a> ZincParser<'a> {
15 pub fn new(src: &'a str) -> Self {
17 Self { src, pos: 0 }
18 }
19
20 pub fn new_at(src: &'a str, pos: usize) -> Self {
22 Self { src, pos }
23 }
24
25 pub fn pos(&self) -> usize {
27 self.pos
28 }
29
30 pub fn parse_scalar(&mut self) -> Result<Kind, CodecError> {
32 let val = self.read_val()?;
33 self.skip_spaces();
34 if !self.at_end() {
35 return Err(self.err(format!(
36 "unexpected trailing input: {:?}",
37 &self.src[self.pos..]
38 )));
39 }
40 Ok(val)
41 }
42
43 pub fn at_end(&self) -> bool {
46 self.pos >= self.src.len()
47 }
48
49 fn peek(&self) -> Option<char> {
50 self.src[self.pos..].chars().next()
51 }
52
53 fn peek_ahead(&self, n: usize) -> Option<char> {
54 self.src[self.pos..].chars().nth(n)
55 }
56
57 fn consume(&mut self) -> Option<char> {
58 let ch = self.peek()?;
59 self.pos += ch.len_utf8();
60 Some(ch)
61 }
62
63 fn consume_if(&mut self, ch: char) -> bool {
64 if self.peek() == Some(ch) {
65 self.pos += ch.len_utf8();
66 true
67 } else {
68 false
69 }
70 }
71
72 pub fn skip_spaces(&mut self) {
73 while let Some(ch) = self.peek() {
74 if ch == ' ' || ch == '\t' {
75 self.pos += 1;
76 } else {
77 break;
78 }
79 }
80 }
81
82 fn remaining(&self) -> &str {
83 &self.src[self.pos..]
84 }
85
86 fn err(&self, msg: impl Into<String>) -> CodecError {
87 CodecError::Parse {
88 pos: self.pos,
89 message: msg.into(),
90 }
91 }
92
93 pub fn read_val(&mut self) -> Result<Kind, CodecError> {
96 self.skip_spaces();
97 if self.at_end() {
98 return Ok(Kind::Null);
99 }
100
101 let ch = self.peek().unwrap();
102
103 if ch == 'N' {
105 let next = self.peek_ahead(1);
106 if next == Some('A') && !self.is_alpha_at(2) {
107 self.pos += 2;
108 return Ok(Kind::NA);
109 }
110 if next.is_none() || !next.unwrap().is_alphanumeric() {
111 self.pos += 1;
112 return Ok(Kind::Null);
113 }
114 }
116
117 if ch == 'T' && !self.is_alpha_at(1) {
119 self.pos += 1;
120 return Ok(Kind::Bool(true));
121 }
122
123 if ch == 'F' && !self.is_alpha_at(1) {
125 self.pos += 1;
126 return Ok(Kind::Bool(false));
127 }
128
129 if ch == 'M' && !self.is_alpha_at(1) {
131 self.pos += 1;
132 return Ok(Kind::Marker);
133 }
134
135 if ch == 'R' && !self.is_alpha_at(1) {
137 self.pos += 1;
138 return Ok(Kind::Remove);
139 }
140
141 if ch == '-' && self.remaining().starts_with("-INF") {
143 self.pos += 4;
144 return Ok(Kind::Number(Number::unitless(f64::NEG_INFINITY)));
145 }
146
147 let is_neg_num = ch == '-' && self.peek_ahead(1).is_some_and(|c| c.is_ascii_digit());
149 if ch.is_ascii_digit() || is_neg_num {
150 return self.read_number();
151 }
152
153 if ch == '"' {
155 let s = self.read_str()?;
156 return Ok(Kind::Str(s));
157 }
158
159 if ch == '@' {
161 return self.read_ref();
162 }
163
164 if ch == '`' {
166 return self.read_uri();
167 }
168
169 if ch == '^' {
171 return self.read_symbol();
172 }
173
174 if ch == 'C' && self.peek_ahead(1) == Some('(') {
176 return self.read_coord();
177 }
178
179 if ch == '[' {
181 return self.read_list();
182 }
183
184 if ch == '{' {
186 return self.read_dict();
187 }
188
189 if ch.is_uppercase() {
191 return self.read_xstr_or_keyword();
192 }
193
194 Err(self.err(format!("unexpected character '{ch}'")))
195 }
196
197 fn is_alpha_at(&self, offset: usize) -> bool {
198 self.src[self.pos..]
199 .chars()
200 .nth(offset)
201 .is_some_and(|c| c.is_alphanumeric())
202 }
203
204 fn read_number(&mut self) -> Result<Kind, CodecError> {
207 if self.looks_like_date() {
209 return self.read_date_or_datetime();
210 }
211
212 if self.looks_like_time() {
214 return self.read_time();
215 }
216
217 let neg = self.consume_if('-');
219
220 let int_part = self.read_digits()?;
222
223 let frac_part = if self.peek() == Some('.') {
225 self.pos += 1;
226 Some(self.read_digits()?)
227 } else {
228 None
229 };
230
231 let exp_part = if self.peek() == Some('e') || self.peek() == Some('E') {
233 let mut exp = String::new();
234 exp.push(self.consume().unwrap());
235 if self.peek() == Some('+') || self.peek() == Some('-') {
236 exp.push(self.consume().unwrap());
237 }
238 exp.push_str(&self.read_digits()?);
239 Some(exp)
240 } else {
241 None
242 };
243
244 let mut num_str = String::new();
246 if neg {
247 num_str.push('-');
248 }
249 num_str.push_str(&int_part);
250 if let Some(ref frac) = frac_part {
251 num_str.push('.');
252 num_str.push_str(frac);
253 }
254 if let Some(ref exp) = exp_part {
255 num_str.push_str(exp);
256 }
257
258 let val: f64 = num_str
259 .parse()
260 .map_err(|_| self.err(format!("invalid number: {num_str}")))?;
261
262 let unit = self.read_unit();
264
265 Ok(Kind::Number(Number::new(
266 val,
267 if unit.is_empty() { None } else { Some(unit) },
268 )))
269 }
270
271 fn read_digits(&mut self) -> Result<String, CodecError> {
272 let start = self.pos;
273 while let Some(ch) = self.peek() {
274 if ch.is_ascii_digit() || ch == '_' {
275 self.pos += ch.len_utf8();
276 } else {
277 break;
278 }
279 }
280 let raw = &self.src[start..self.pos];
281 let result: String = raw.chars().filter(|&c| c != '_').collect();
282 if result.is_empty() {
283 return Err(self.err("expected digits"));
284 }
285 Ok(result)
286 }
287
288 fn read_unit(&mut self) -> String {
289 let start = self.pos;
290 let mut first = true;
291 while let Some(ch) = self.peek() {
292 if ch.is_alphabetic()
293 || ch as u32 > 127
294 || ch == '_'
295 || ch == '/'
296 || ch == '%'
297 || ch == '$'
298 {
299 self.pos += ch.len_utf8();
300 first = false;
301 } else if ch.is_ascii_digit() && !first {
302 self.pos += 1;
304 } else {
305 break;
306 }
307 }
308 self.src[start..self.pos].to_string()
309 }
310
311 fn looks_like_date(&self) -> bool {
312 let rem = self.remaining();
314 if rem.len() < 10 {
315 return false;
316 }
317 let bytes = rem.as_bytes();
318 bytes[0..4].iter().all(|b| b.is_ascii_digit())
319 && bytes[4] == b'-'
320 && bytes[5..7].iter().all(|b| b.is_ascii_digit())
321 && bytes[7] == b'-'
322 && bytes[8..10].iter().all(|b| b.is_ascii_digit())
323 }
324
325 fn looks_like_time(&self) -> bool {
326 let rem = self.remaining();
328 if rem.len() < 5 {
329 return false;
330 }
331 let bytes = rem.as_bytes();
332 bytes[0..2].iter().all(|b| b.is_ascii_digit())
333 && bytes[2] == b':'
334 && bytes[3..5].iter().all(|b| b.is_ascii_digit())
335 }
336
337 fn read_date_or_datetime(&mut self) -> Result<Kind, CodecError> {
338 let date_str = &self.src[self.pos..self.pos + 10];
340 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
341 .map_err(|e| self.err(format!("invalid date: {e}")))?;
342 self.pos += 10;
343
344 if self.peek() == Some('T') {
346 return self.read_datetime_after_date(date);
347 }
348
349 Ok(Kind::Date(date))
350 }
351
352 fn read_datetime_after_date(&mut self, date: NaiveDate) -> Result<Kind, CodecError> {
353 self.pos += 1; let time_str = self.read_time_str()?;
355 let offset_str = self.read_offset()?;
356
357 let iso = format!("{}T{}{}", date, time_str, offset_str);
359 let dt = chrono::DateTime::parse_from_str(&iso, "%Y-%m-%dT%H:%M:%S%.f%:z")
360 .or_else(|_| chrono::DateTime::parse_from_str(&iso, "%Y-%m-%dT%H:%M:%S%:z"))
361 .map_err(|e| self.err(format!("invalid datetime: {e} (from '{iso}')")))?;
362
363 self.skip_spaces();
365 let tz_name = self.read_tz_name();
366
367 let tz = if tz_name.is_empty() {
368 "UTC".to_string()
369 } else {
370 tz_name
371 };
372
373 Ok(Kind::DateTime(HDateTime::new(dt, tz)))
374 }
375
376 fn read_time_str(&mut self) -> Result<String, CodecError> {
377 let start = self.pos;
378 if self.remaining().len() < 5 {
380 return Err(self.err("expected time HH:MM"));
381 }
382 self.pos += 5;
383 if self.peek() == Some(':') {
385 if self.remaining().len() < 3 {
386 return Err(self.err("incomplete seconds in time"));
387 }
388 self.pos += 3; if self.peek() == Some('.') {
391 self.pos += 1;
392 while let Some(ch) = self.peek() {
393 if ch.is_ascii_digit() {
394 self.pos += 1;
395 } else {
396 break;
397 }
398 }
399 }
400 }
401 Ok(self.src[start..self.pos].to_string())
402 }
403
404 fn read_offset(&mut self) -> Result<String, CodecError> {
405 if self.at_end() {
406 return Ok(String::new());
407 }
408 if self.peek() == Some('Z') {
409 self.pos += 1;
410 return Ok("+00:00".to_string());
411 }
412 if self.peek() == Some('+') || self.peek() == Some('-') {
413 let start = self.pos;
414 if self.remaining().len() < 3 {
415 return Err(self.err("incomplete UTC offset"));
416 }
417 self.pos += 1; self.pos += 2; if self.peek() == Some(':') {
420 if self.remaining().len() < 3 {
421 return Err(self.err("incomplete UTC offset minutes"));
422 }
423 self.pos += 3; }
425 return Ok(self.src[start..self.pos].to_string());
426 }
427 Ok(String::new())
428 }
429
430 fn read_tz_name(&mut self) -> String {
431 let start = self.pos;
432 while let Some(ch) = self.peek() {
433 if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '/' {
434 self.pos += ch.len_utf8();
435 } else {
436 break;
437 }
438 }
439 self.src[start..self.pos].to_string()
440 }
441
442 fn read_time(&mut self) -> Result<Kind, CodecError> {
443 let time_str = self.read_time_str()?;
444 let time = NaiveTime::parse_from_str(&time_str, "%H:%M:%S%.f")
445 .or_else(|_| NaiveTime::parse_from_str(&time_str, "%H:%M:%S"))
446 .or_else(|_| NaiveTime::parse_from_str(&time_str, "%H:%M"))
447 .map_err(|e| self.err(format!("invalid time: {e}")))?;
448 Ok(Kind::Time(time))
449 }
450
451 fn read_str(&mut self) -> Result<String, CodecError> {
454 self.pos += 1; let mut result = String::new();
456 while !self.at_end() {
457 let ch = self.peek().unwrap();
458 if ch == '"' {
459 self.pos += 1;
460 return Ok(result);
461 }
462 if ch == '\\' {
463 self.pos += 1;
464 result.push(self.read_escape()?);
465 } else {
466 result.push(ch);
467 self.pos += ch.len_utf8();
468 }
469 }
470 Err(self.err("unterminated string"))
471 }
472
473 fn read_escape(&mut self) -> Result<char, CodecError> {
474 if self.at_end() {
475 return Err(self.err("unexpected end of escape sequence"));
476 }
477 let ch = self.consume().unwrap();
478 match ch {
479 'n' => Ok('\n'),
480 'r' => Ok('\r'),
481 't' => Ok('\t'),
482 '\\' => Ok('\\'),
483 '"' => Ok('"'),
484 '$' => Ok('$'),
485 'b' => Ok('\u{0008}'),
486 'f' => Ok('\u{000C}'),
487 'u' => {
488 if self.remaining().len() < 4 {
489 return Err(self.err("incomplete unicode escape"));
490 }
491 let hex = &self.src[self.pos..self.pos + 4];
492 self.pos += 4;
493 let code = u32::from_str_radix(hex, 16)
494 .map_err(|_| self.err(format!("invalid unicode escape: {hex}")))?;
495 char::from_u32(code)
496 .ok_or_else(|| self.err(format!("invalid unicode codepoint: {code}")))
497 }
498 _ => Err(self.err(format!("unknown escape sequence: \\{ch}"))),
499 }
500 }
501
502 fn read_ref(&mut self) -> Result<Kind, CodecError> {
505 self.pos += 1; let start = self.pos;
507 while let Some(ch) = self.peek() {
508 if is_ref_char(ch) {
509 self.pos += ch.len_utf8();
510 } else {
511 break;
512 }
513 }
514 let val = self.src[start..self.pos].to_string();
515
516 self.skip_spaces();
518 let dis = if self.peek() == Some('"') {
519 Some(self.read_str()?)
520 } else {
521 None
522 };
523
524 Ok(Kind::Ref(HRef::new(val, dis)))
525 }
526
527 fn read_uri(&mut self) -> Result<Kind, CodecError> {
530 self.pos += 1; let mut result = String::new();
532 while !self.at_end() {
533 let ch = self.peek().unwrap();
534 if ch == '`' {
535 self.pos += 1;
536 return Ok(Kind::Uri(Uri::new(result)));
537 }
538 if ch == '\\' {
539 self.pos += 1;
540 if let Some(next) = self.consume() {
541 result.push(next);
542 }
543 } else {
544 result.push(ch);
545 self.pos += ch.len_utf8();
546 }
547 }
548 Err(self.err("unterminated URI"))
549 }
550
551 fn read_symbol(&mut self) -> Result<Kind, CodecError> {
554 self.pos += 1; let start = self.pos;
556 while let Some(ch) = self.peek() {
557 if is_ref_char(ch) {
558 self.pos += ch.len_utf8();
559 } else {
560 break;
561 }
562 }
563 Ok(Kind::Symbol(Symbol::new(&self.src[start..self.pos])))
564 }
565
566 fn read_coord(&mut self) -> Result<Kind, CodecError> {
569 self.pos += 2; let start = self.pos;
571 while self.peek() != Some(',') && !self.at_end() {
572 self.pos += 1;
573 }
574 if self.at_end() {
575 return Err(self.err("unterminated coord literal, expected ','"));
576 }
577 let lat: f64 = self.src[start..self.pos]
578 .trim()
579 .parse()
580 .map_err(|_| self.err("invalid coord latitude"))?;
581 if !(-90.0..=90.0).contains(&lat) {
582 return Err(self.err("coord latitude must be between -90 and 90"));
583 }
584 self.pos += 1; let start = self.pos;
586 while self.peek() != Some(')') && !self.at_end() {
587 self.pos += 1;
588 }
589 if self.at_end() {
590 return Err(self.err("unterminated coord literal, expected ')'"));
591 }
592 let lng: f64 = self.src[start..self.pos]
593 .trim()
594 .parse()
595 .map_err(|_| self.err("invalid coord longitude"))?;
596 if !(-180.0..=180.0).contains(&lng) {
597 return Err(self.err("coord longitude must be between -180 and 180"));
598 }
599 self.pos += 1; Ok(Kind::Coord(Coord::new(lat, lng)))
601 }
602
603 fn read_list(&mut self) -> Result<Kind, CodecError> {
606 self.pos += 1; let mut vals = Vec::new();
608 self.skip_spaces();
609 while !self.at_end() && self.peek() != Some(']') {
610 vals.push(self.read_val()?);
611 self.skip_spaces();
612 self.consume_if(',');
613 self.skip_spaces();
614 }
615 if !self.at_end() {
616 self.pos += 1; }
618 Ok(Kind::List(vals))
619 }
620
621 fn read_dict(&mut self) -> Result<Kind, CodecError> {
624 self.pos += 1; let mut dict = HDict::new();
626 self.skip_spaces();
627 while !self.at_end() && self.peek() != Some('}') {
628 let name = self.read_tag_name()?;
629 self.skip_spaces();
630 if self.peek() == Some(':') {
631 self.pos += 1;
632 self.skip_spaces();
633 let val = self.read_val()?;
634 dict.set(name, val);
635 } else {
636 dict.set(name, Kind::Marker);
637 }
638 self.skip_spaces();
639 self.consume_if(',');
640 self.skip_spaces();
641 }
642 if !self.at_end() {
643 self.pos += 1; }
645 Ok(Kind::Dict(Box::new(dict)))
646 }
647
648 fn read_tag_name(&mut self) -> Result<String, CodecError> {
649 let start = self.pos;
650 while let Some(ch) = self.peek() {
651 if ch.is_alphanumeric() || ch == '_' {
652 self.pos += ch.len_utf8();
653 } else {
654 break;
655 }
656 }
657 let name = self.src[start..self.pos].to_string();
658 if name.is_empty() {
659 return Err(self.err("expected tag name"));
660 }
661 Ok(name)
662 }
663
664 fn read_xstr_or_keyword(&mut self) -> Result<Kind, CodecError> {
667 let start = self.pos;
668 while let Some(ch) = self.peek() {
669 if ch.is_alphanumeric() || ch == '_' {
670 self.pos += ch.len_utf8();
671 } else {
672 break;
673 }
674 }
675 let name = &self.src[start..self.pos];
676
677 match name {
678 "INF" => return Ok(Kind::Number(Number::unitless(f64::INFINITY))),
679 "NaN" => return Ok(Kind::Number(Number::unitless(f64::NAN))),
680 "NA" => return Ok(Kind::NA),
681 _ => {}
682 }
683
684 if self.peek() == Some('(') {
686 self.pos += 1; self.skip_spaces();
688 let val = self.read_str()?;
689 self.skip_spaces();
690 if self.peek() == Some(')') {
691 self.pos += 1;
692 }
693 return Ok(Kind::XStr(XStr::new(name, val)));
694 }
695
696 Err(self.err(format!("unknown keyword '{name}'")))
697 }
698
699 pub fn read_id(&mut self) -> String {
701 let start = self.pos;
702 while let Some(ch) = self.peek() {
703 if ch.is_alphanumeric() || ch == '_' {
704 self.pos += ch.len_utf8();
705 } else {
706 break;
707 }
708 }
709 self.src[start..self.pos].to_string()
710 }
711}
712
713fn is_ref_char(ch: char) -> bool {
714 ch.is_alphanumeric() || ch == '_' || ch == ':' || ch == '-' || ch == '.' || ch == '~'
715}
716
717pub fn decode_scalar(input: &str) -> Result<Kind, CodecError> {
719 let mut parser = ZincParser::new(input.trim());
720 parser.parse_scalar()
721}
722
723pub fn decode_grid(input: &str) -> Result<HGrid, CodecError> {
727 let lines: Vec<&str> = input
728 .lines()
729 .map(|l| l.trim())
730 .filter(|l| !l.is_empty() && !l.starts_with("//"))
731 .collect();
732
733 if lines.is_empty() {
734 return Ok(HGrid::new());
735 }
736
737 let mut line_idx = 0;
738
739 let ver_line = lines[line_idx];
741 line_idx += 1;
742
743 if !ver_line.starts_with("ver:") {
744 return Err(CodecError::Parse {
745 pos: 0,
746 message: format!("expected 'ver:' header, got: {ver_line:?}"),
747 });
748 }
749
750 let meta = parse_ver_line_meta(ver_line)?;
752
753 if line_idx >= lines.len() {
755 return Ok(HGrid::from_parts(meta, vec![], vec![]));
756 }
757
758 let col_line = lines[line_idx];
759 line_idx += 1;
760
761 let cols = if col_line == "empty" {
762 vec![]
763 } else {
764 parse_cols(col_line)?
765 };
766
767 let mut rows = Vec::new();
769 while line_idx < lines.len() {
770 let row_line = lines[line_idx];
771 line_idx += 1;
772 if row_line.is_empty() {
773 continue;
774 }
775 let row = parse_row(row_line, &cols)?;
776 rows.push(row);
777 }
778
779 Ok(HGrid::from_parts(meta, cols, rows))
780}
781
782fn parse_ver_line_meta(ver_line: &str) -> Result<HDict, CodecError> {
783 let mut parser = ZincParser::new(ver_line);
785 while !parser.at_end() && parser.peek() != Some(' ') {
787 parser.consume();
788 }
789 parser.skip_spaces();
790 if parser.at_end() {
791 return Ok(HDict::new());
792 }
793 parse_inline_meta(&mut parser)
794}
795
796fn parse_cols(line: &str) -> Result<Vec<HCol>, CodecError> {
797 let parts = split_csv_aware(line);
798 let mut cols = Vec::new();
799 for part in parts {
800 let part = part.trim();
801 if part.is_empty() {
802 continue;
803 }
804 let mut parser = ZincParser::new(part);
805 let name = read_col_name(&mut parser);
806 parser.skip_spaces();
807 let meta = if !parser.at_end() {
808 parse_inline_meta(&mut parser)?
809 } else {
810 HDict::new()
811 };
812 cols.push(HCol::with_meta(name, meta));
813 }
814 Ok(cols)
815}
816
817fn parse_row(line: &str, cols: &[HCol]) -> Result<HDict, CodecError> {
818 let parts = split_csv_aware(line);
819 let mut dict = HDict::new();
820 for (i, col) in cols.iter().enumerate() {
821 if i < parts.len() {
822 let cell = parts[i].trim();
823 if !cell.is_empty() && cell != "N" {
824 let mut parser = ZincParser::new(cell);
825 let val = parser.read_val()?;
826 dict.set(&col.name, val);
827 }
828 }
829 }
830 Ok(dict)
831}
832
833fn parse_inline_meta(parser: &mut ZincParser<'_>) -> Result<HDict, CodecError> {
834 let mut dict = HDict::new();
835 while !parser.at_end() {
836 parser.skip_spaces();
837 if parser.at_end() {
838 break;
839 }
840 let name = read_col_name(parser);
841 if name.is_empty() {
842 break;
843 }
844 parser.skip_spaces();
845 if parser.peek() == Some(':') {
846 parser.consume();
847 parser.skip_spaces();
848 let val = parser.read_val()?;
849 dict.set(name, val);
850 } else {
851 dict.set(name, Kind::Marker);
852 }
853 parser.skip_spaces();
854 }
855 Ok(dict)
856}
857
858fn read_col_name(parser: &mut ZincParser<'_>) -> String {
859 parser.read_id()
860}
861
862fn split_csv_aware(line: &str) -> Vec<String> {
864 let mut parts = Vec::new();
865 let mut current = String::new();
866 let mut depth = 0i32;
867 let mut in_str = false;
868 let mut escaped = false;
869
870 for ch in line.chars() {
871 if escaped {
872 current.push(ch);
873 escaped = false;
874 continue;
875 }
876 if ch == '\\' {
877 current.push(ch);
878 escaped = true;
879 continue;
880 }
881 if ch == '"' && depth == 0 {
882 in_str = !in_str;
883 current.push(ch);
884 continue;
885 }
886 if in_str {
887 current.push(ch);
888 continue;
889 }
890 match ch {
891 '(' | '[' | '{' => {
892 depth += 1;
893 current.push(ch);
894 }
895 ')' | ']' | '}' => {
896 depth -= 1;
897 current.push(ch);
898 }
899 ',' if depth == 0 => {
900 parts.push(std::mem::take(&mut current));
901 }
902 _ => {
903 current.push(ch);
904 }
905 }
906 }
907 parts.push(current);
908 parts
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use crate::data::{HDict, HGrid};
915 use chrono::{Datelike, FixedOffset, NaiveDate, NaiveTime, TimeZone};
916
917 fn round_trip(kind: &Kind) -> Kind {
920 let encoded = crate::codecs::zinc::encode_scalar(kind).unwrap();
921 decode_scalar(&encoded).unwrap()
922 }
923
924 #[test]
925 fn parse_null() {
926 assert_eq!(decode_scalar("N").unwrap(), Kind::Null);
927 }
928
929 #[test]
930 fn parse_true() {
931 assert_eq!(decode_scalar("T").unwrap(), Kind::Bool(true));
932 }
933
934 #[test]
935 fn parse_false() {
936 assert_eq!(decode_scalar("F").unwrap(), Kind::Bool(false));
937 }
938
939 #[test]
940 fn parse_marker() {
941 assert_eq!(decode_scalar("M").unwrap(), Kind::Marker);
942 }
943
944 #[test]
945 fn parse_na() {
946 assert_eq!(decode_scalar("NA").unwrap(), Kind::NA);
947 }
948
949 #[test]
950 fn parse_remove() {
951 assert_eq!(decode_scalar("R").unwrap(), Kind::Remove);
952 }
953
954 #[test]
955 fn roundtrip_null() {
956 assert_eq!(round_trip(&Kind::Null), Kind::Null);
957 }
958
959 #[test]
960 fn roundtrip_bool_true() {
961 assert_eq!(round_trip(&Kind::Bool(true)), Kind::Bool(true));
962 }
963
964 #[test]
965 fn roundtrip_bool_false() {
966 assert_eq!(round_trip(&Kind::Bool(false)), Kind::Bool(false));
967 }
968
969 #[test]
970 fn roundtrip_marker() {
971 assert_eq!(round_trip(&Kind::Marker), Kind::Marker);
972 }
973
974 #[test]
975 fn roundtrip_na() {
976 assert_eq!(round_trip(&Kind::NA), Kind::NA);
977 }
978
979 #[test]
980 fn roundtrip_remove() {
981 assert_eq!(round_trip(&Kind::Remove), Kind::Remove);
982 }
983
984 #[test]
987 fn parse_number_zero() {
988 assert_eq!(
989 decode_scalar("0").unwrap(),
990 Kind::Number(Number::unitless(0.0))
991 );
992 }
993
994 #[test]
995 fn parse_number_integer() {
996 assert_eq!(
997 decode_scalar("42").unwrap(),
998 Kind::Number(Number::unitless(42.0))
999 );
1000 }
1001
1002 #[test]
1003 fn parse_number_float() {
1004 assert_eq!(
1005 decode_scalar("72.5").unwrap(),
1006 Kind::Number(Number::unitless(72.5))
1007 );
1008 }
1009
1010 #[test]
1011 fn parse_number_negative() {
1012 assert_eq!(
1013 decode_scalar("-23.45").unwrap(),
1014 Kind::Number(Number::unitless(-23.45))
1015 );
1016 }
1017
1018 #[test]
1019 fn parse_number_scientific() {
1020 let k = decode_scalar("5.4e8").unwrap();
1021 if let Kind::Number(n) = &k {
1022 assert!((n.val - 5.4e8).abs() < 1.0);
1023 } else {
1024 panic!("expected Number, got {k:?}");
1025 }
1026 }
1027
1028 #[test]
1029 fn parse_number_inf() {
1030 let k = decode_scalar("INF").unwrap();
1031 if let Kind::Number(n) = &k {
1032 assert!(n.val.is_infinite() && n.val > 0.0);
1033 } else {
1034 panic!("expected Number(INF)");
1035 }
1036 }
1037
1038 #[test]
1039 fn parse_number_neg_inf() {
1040 let k = decode_scalar("-INF").unwrap();
1041 if let Kind::Number(n) = &k {
1042 assert!(n.val.is_infinite() && n.val < 0.0);
1043 } else {
1044 panic!("expected Number(-INF)");
1045 }
1046 }
1047
1048 #[test]
1049 fn parse_number_nan() {
1050 let k = decode_scalar("NaN").unwrap();
1051 if let Kind::Number(n) = &k {
1052 assert!(n.val.is_nan());
1053 } else {
1054 panic!("expected Number(NaN)");
1055 }
1056 }
1057
1058 #[test]
1059 fn parse_number_with_unit() {
1060 let k = decode_scalar("72.5\u{00B0}F").unwrap();
1061 if let Kind::Number(n) = &k {
1062 assert_eq!(n.val, 72.5);
1063 assert_eq!(n.unit.as_deref(), Some("\u{00B0}F"));
1064 } else {
1065 panic!("expected Number with unit");
1066 }
1067 }
1068
1069 #[test]
1070 fn roundtrip_number_zero() {
1071 assert_eq!(
1072 round_trip(&Kind::Number(Number::unitless(0.0))),
1073 Kind::Number(Number::unitless(0.0))
1074 );
1075 }
1076
1077 #[test]
1078 fn roundtrip_number_integer() {
1079 assert_eq!(
1080 round_trip(&Kind::Number(Number::unitless(42.0))),
1081 Kind::Number(Number::unitless(42.0))
1082 );
1083 }
1084
1085 #[test]
1086 fn roundtrip_number_float() {
1087 assert_eq!(
1088 round_trip(&Kind::Number(Number::unitless(72.5))),
1089 Kind::Number(Number::unitless(72.5))
1090 );
1091 }
1092
1093 #[test]
1094 fn roundtrip_number_negative() {
1095 assert_eq!(
1096 round_trip(&Kind::Number(Number::unitless(-23.45))),
1097 Kind::Number(Number::unitless(-23.45))
1098 );
1099 }
1100
1101 #[test]
1102 fn roundtrip_number_with_unit() {
1103 let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
1104 let rt = round_trip(&k);
1105 if let Kind::Number(n) = &rt {
1106 assert_eq!(n.val, 72.5);
1107 assert_eq!(n.unit.as_deref(), Some("\u{00B0}F"));
1108 } else {
1109 panic!("expected Number");
1110 }
1111 }
1112
1113 #[test]
1114 fn roundtrip_inf() {
1115 let k = Kind::Number(Number::unitless(f64::INFINITY));
1116 let rt = round_trip(&k);
1117 if let Kind::Number(n) = &rt {
1118 assert!(n.val.is_infinite() && n.val > 0.0);
1119 } else {
1120 panic!("expected Number(INF)");
1121 }
1122 }
1123
1124 #[test]
1125 fn roundtrip_neg_inf() {
1126 let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
1127 let rt = round_trip(&k);
1128 if let Kind::Number(n) = &rt {
1129 assert!(n.val.is_infinite() && n.val < 0.0);
1130 } else {
1131 panic!("expected Number(-INF)");
1132 }
1133 }
1134
1135 #[test]
1136 fn roundtrip_nan() {
1137 let k = Kind::Number(Number::unitless(f64::NAN));
1138 let rt = round_trip(&k);
1139 if let Kind::Number(n) = &rt {
1140 assert!(n.val.is_nan());
1141 } else {
1142 panic!("expected Number(NaN)");
1143 }
1144 }
1145
1146 #[test]
1149 fn parse_string_empty() {
1150 assert_eq!(decode_scalar("\"\"").unwrap(), Kind::Str(String::new()));
1151 }
1152
1153 #[test]
1154 fn parse_string_simple() {
1155 assert_eq!(
1156 decode_scalar("\"hello\"").unwrap(),
1157 Kind::Str("hello".into())
1158 );
1159 }
1160
1161 #[test]
1162 fn parse_string_escapes() {
1163 assert_eq!(
1164 decode_scalar("\"line1\\nline2\"").unwrap(),
1165 Kind::Str("line1\nline2".into())
1166 );
1167 assert_eq!(
1168 decode_scalar("\"tab\\there\"").unwrap(),
1169 Kind::Str("tab\there".into())
1170 );
1171 assert_eq!(
1172 decode_scalar("\"back\\\\slash\"").unwrap(),
1173 Kind::Str("back\\slash".into())
1174 );
1175 assert_eq!(
1176 decode_scalar("\"q\\\"uote\"").unwrap(),
1177 Kind::Str("q\"uote".into())
1178 );
1179 assert_eq!(
1180 decode_scalar("\"dollar\\$sign\"").unwrap(),
1181 Kind::Str("dollar$sign".into())
1182 );
1183 }
1184
1185 #[test]
1186 fn parse_string_unicode_escape() {
1187 assert_eq!(decode_scalar("\"\\u0041\"").unwrap(), Kind::Str("A".into()));
1188 }
1189
1190 #[test]
1191 fn roundtrip_string_empty() {
1192 assert_eq!(
1193 round_trip(&Kind::Str(String::new())),
1194 Kind::Str(String::new())
1195 );
1196 }
1197
1198 #[test]
1199 fn roundtrip_string_escapes() {
1200 let s = "line1\nline2\ttab\\slash\"quote$dollar";
1201 assert_eq!(round_trip(&Kind::Str(s.into())), Kind::Str(s.into()));
1202 }
1203
1204 #[test]
1207 fn parse_ref_simple() {
1208 let k = decode_scalar("@site-1").unwrap();
1209 if let Kind::Ref(r) = &k {
1210 assert_eq!(r.val, "site-1");
1211 assert_eq!(r.dis, None);
1212 } else {
1213 panic!("expected Ref");
1214 }
1215 }
1216
1217 #[test]
1218 fn parse_ref_with_dis() {
1219 let k = decode_scalar("@site-1 \"Main Site\"").unwrap();
1220 if let Kind::Ref(r) = &k {
1221 assert_eq!(r.val, "site-1");
1222 assert_eq!(r.dis, Some("Main Site".into()));
1223 } else {
1224 panic!("expected Ref");
1225 }
1226 }
1227
1228 #[test]
1229 fn roundtrip_ref_simple() {
1230 let k = Kind::Ref(HRef::from_val("site-1"));
1231 let rt = round_trip(&k);
1232 if let Kind::Ref(r) = &rt {
1233 assert_eq!(r.val, "site-1");
1234 } else {
1235 panic!("expected Ref");
1236 }
1237 }
1238
1239 #[test]
1240 fn roundtrip_ref_with_dis() {
1241 let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
1242 let rt = round_trip(&k);
1243 if let Kind::Ref(r) = &rt {
1244 assert_eq!(r.val, "site-1");
1245 assert_eq!(r.dis, Some("Main Site".into()));
1246 } else {
1247 panic!("expected Ref");
1248 }
1249 }
1250
1251 #[test]
1254 fn parse_uri_simple() {
1255 let k = decode_scalar("`http://example.com`").unwrap();
1256 assert_eq!(k, Kind::Uri(Uri::new("http://example.com")));
1257 }
1258
1259 #[test]
1260 fn parse_uri_with_special() {
1261 let k = decode_scalar("`http://ex.com/path?q=1&b=2`").unwrap();
1262 assert_eq!(k, Kind::Uri(Uri::new("http://ex.com/path?q=1&b=2")));
1263 }
1264
1265 #[test]
1266 fn roundtrip_uri() {
1267 let k = Kind::Uri(Uri::new("http://example.com/path"));
1268 assert_eq!(round_trip(&k), k);
1269 }
1270
1271 #[test]
1274 fn parse_symbol_simple() {
1275 let k = decode_scalar("^site").unwrap();
1276 assert_eq!(k, Kind::Symbol(Symbol::new("site")));
1277 }
1278
1279 #[test]
1280 fn parse_symbol_compound() {
1281 let k = decode_scalar("^hot-water").unwrap();
1282 assert_eq!(k, Kind::Symbol(Symbol::new("hot-water")));
1283 }
1284
1285 #[test]
1286 fn roundtrip_symbol() {
1287 let k = Kind::Symbol(Symbol::new("hot-water"));
1288 assert_eq!(round_trip(&k), k);
1289 }
1290
1291 #[test]
1294 fn parse_date() {
1295 let k = decode_scalar("2024-03-13").unwrap();
1296 assert_eq!(k, Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap()));
1297 }
1298
1299 #[test]
1300 fn roundtrip_date() {
1301 let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
1302 assert_eq!(round_trip(&k), k);
1303 }
1304
1305 #[test]
1308 fn parse_time() {
1309 let k = decode_scalar("08:12:05").unwrap();
1310 assert_eq!(k, Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap()));
1311 }
1312
1313 #[test]
1314 fn parse_time_with_frac() {
1315 let k = decode_scalar("14:30:00.123").unwrap();
1316 assert_eq!(
1317 k,
1318 Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap())
1319 );
1320 }
1321
1322 #[test]
1323 fn roundtrip_time() {
1324 let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
1325 assert_eq!(round_trip(&k), k);
1326 }
1327
1328 #[test]
1329 fn roundtrip_time_frac() {
1330 let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
1331 assert_eq!(round_trip(&k), k);
1332 }
1333
1334 #[test]
1337 fn parse_datetime() {
1338 let k = decode_scalar("2024-01-01T08:12:05-05:00 New_York").unwrap();
1339 if let Kind::DateTime(hdt) = &k {
1340 assert_eq!(hdt.tz_name, "New_York");
1341 assert_eq!(hdt.dt.year(), 2024);
1342 } else {
1343 panic!("expected DateTime");
1344 }
1345 }
1346
1347 #[test]
1348 fn parse_datetime_utc() {
1349 let k = decode_scalar("2024-06-15T12:00:00+00:00 UTC").unwrap();
1350 if let Kind::DateTime(hdt) = &k {
1351 assert_eq!(hdt.tz_name, "UTC");
1352 } else {
1353 panic!("expected DateTime");
1354 }
1355 }
1356
1357 #[test]
1358 fn parse_datetime_z() {
1359 let k = decode_scalar("2024-06-15T12:00:00Z UTC").unwrap();
1360 if let Kind::DateTime(hdt) = &k {
1361 assert_eq!(hdt.tz_name, "UTC");
1362 assert_eq!(hdt.dt.offset(), &FixedOffset::east_opt(0).unwrap());
1363 } else {
1364 panic!("expected DateTime");
1365 }
1366 }
1367
1368 #[test]
1369 fn roundtrip_datetime() {
1370 let offset = FixedOffset::west_opt(5 * 3600).unwrap();
1371 let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
1372 let k = Kind::DateTime(HDateTime::new(dt, "New_York"));
1373 let rt = round_trip(&k);
1374 if let Kind::DateTime(hdt) = &rt {
1375 assert_eq!(hdt.tz_name, "New_York");
1376 assert_eq!(hdt.dt, dt);
1377 } else {
1378 panic!("expected DateTime");
1379 }
1380 }
1381
1382 #[test]
1383 fn roundtrip_datetime_utc() {
1384 let offset = FixedOffset::east_opt(0).unwrap();
1385 let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
1386 let k = Kind::DateTime(HDateTime::new(dt, "UTC"));
1387 let rt = round_trip(&k);
1388 if let Kind::DateTime(hdt) = &rt {
1389 assert_eq!(hdt.tz_name, "UTC");
1390 assert_eq!(hdt.dt, dt);
1391 } else {
1392 panic!("expected DateTime");
1393 }
1394 }
1395
1396 #[test]
1399 fn parse_coord() {
1400 let k = decode_scalar("C(37.5458266,-77.4491888)").unwrap();
1401 assert_eq!(k, Kind::Coord(Coord::new(37.5458266, -77.4491888)));
1402 }
1403
1404 #[test]
1405 fn parse_coord_negative() {
1406 let k = decode_scalar("C(-33.8688,151.2093)").unwrap();
1407 assert_eq!(k, Kind::Coord(Coord::new(-33.8688, 151.2093)));
1408 }
1409
1410 #[test]
1411 fn roundtrip_coord() {
1412 let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
1413 assert_eq!(round_trip(&k), k);
1414 }
1415
1416 #[test]
1419 fn parse_xstr() {
1420 let k = decode_scalar("Color(\"red\")").unwrap();
1421 assert_eq!(k, Kind::XStr(XStr::new("Color", "red")));
1422 }
1423
1424 #[test]
1425 fn roundtrip_xstr() {
1426 let k = Kind::XStr(XStr::new("Color", "red"));
1427 assert_eq!(round_trip(&k), k);
1428 }
1429
1430 #[test]
1433 fn parse_list_empty() {
1434 assert_eq!(decode_scalar("[]").unwrap(), Kind::List(vec![]));
1435 }
1436
1437 #[test]
1438 fn parse_list_mixed() {
1439 let k = decode_scalar("[1, \"two\", M]").unwrap();
1440 assert_eq!(
1441 k,
1442 Kind::List(vec![
1443 Kind::Number(Number::unitless(1.0)),
1444 Kind::Str("two".into()),
1445 Kind::Marker,
1446 ])
1447 );
1448 }
1449
1450 #[test]
1451 fn parse_list_nested() {
1452 let k = decode_scalar("[[1, 2], [3, 4]]").unwrap();
1453 assert_eq!(
1454 k,
1455 Kind::List(vec![
1456 Kind::List(vec![
1457 Kind::Number(Number::unitless(1.0)),
1458 Kind::Number(Number::unitless(2.0)),
1459 ]),
1460 Kind::List(vec![
1461 Kind::Number(Number::unitless(3.0)),
1462 Kind::Number(Number::unitless(4.0)),
1463 ]),
1464 ])
1465 );
1466 }
1467
1468 #[test]
1469 fn roundtrip_list_empty() {
1470 assert_eq!(round_trip(&Kind::List(vec![])), Kind::List(vec![]));
1471 }
1472
1473 #[test]
1474 fn roundtrip_list_mixed() {
1475 let k = Kind::List(vec![
1476 Kind::Number(Number::unitless(1.0)),
1477 Kind::Str("two".into()),
1478 Kind::Marker,
1479 ]);
1480 assert_eq!(round_trip(&k), k);
1481 }
1482
1483 #[test]
1486 fn parse_dict_empty() {
1487 let k = decode_scalar("{}").unwrap();
1488 assert_eq!(k, Kind::Dict(Box::new(HDict::new())));
1489 }
1490
1491 #[test]
1492 fn parse_dict_with_marker() {
1493 let k = decode_scalar("{site}").unwrap();
1494 if let Kind::Dict(d) = &k {
1495 assert_eq!(d.get("site"), Some(&Kind::Marker));
1496 } else {
1497 panic!("expected Dict");
1498 }
1499 }
1500
1501 #[test]
1502 fn parse_dict_with_values() {
1503 let k = decode_scalar("{dis:\"Main\" area:42}").unwrap();
1504 if let Kind::Dict(d) = &k {
1505 assert_eq!(d.get("dis"), Some(&Kind::Str("Main".into())));
1506 assert_eq!(d.get("area"), Some(&Kind::Number(Number::unitless(42.0))));
1507 } else {
1508 panic!("expected Dict");
1509 }
1510 }
1511
1512 #[test]
1513 fn parse_dict_mixed() {
1514 let k = decode_scalar("{site dis:\"Main\" area:42}").unwrap();
1515 if let Kind::Dict(d) = &k {
1516 assert_eq!(d.get("site"), Some(&Kind::Marker));
1517 assert_eq!(d.get("dis"), Some(&Kind::Str("Main".into())));
1518 assert_eq!(d.get("area"), Some(&Kind::Number(Number::unitless(42.0))));
1519 } else {
1520 panic!("expected Dict");
1521 }
1522 }
1523
1524 #[test]
1525 fn roundtrip_dict_empty() {
1526 let k = Kind::Dict(Box::new(HDict::new()));
1527 assert_eq!(round_trip(&k), k);
1528 }
1529
1530 #[test]
1531 fn roundtrip_dict_with_values() {
1532 let mut d = HDict::new();
1533 d.set("dis", Kind::Str("Main".into()));
1534 d.set("site", Kind::Marker);
1535 let k = Kind::Dict(Box::new(d));
1536 let rt = round_trip(&k);
1537 if let Kind::Dict(d) = &rt {
1538 assert_eq!(d.get("dis"), Some(&Kind::Str("Main".into())));
1539 assert_eq!(d.get("site"), Some(&Kind::Marker));
1540 } else {
1541 panic!("expected Dict");
1542 }
1543 }
1544
1545 #[test]
1548 fn decode_grid_empty() {
1549 let zinc = "ver:\"3.0\"\nempty\n";
1550 let g = decode_grid(zinc).unwrap();
1551 assert!(g.cols.is_empty());
1552 assert!(g.rows.is_empty());
1553 }
1554
1555 #[test]
1556 fn decode_grid_simple() {
1557 let zinc = "ver:\"3.0\"\ndis,area\n\"Site One\",4500\n\"Site Two\",3200\n";
1558 let g = decode_grid(zinc).unwrap();
1559 assert_eq!(g.num_cols(), 2);
1560 assert_eq!(g.cols[0].name, "dis");
1561 assert_eq!(g.cols[1].name, "area");
1562 assert_eq!(g.len(), 2);
1563
1564 let r0 = g.row(0).unwrap();
1565 assert_eq!(r0.get("dis"), Some(&Kind::Str("Site One".into())));
1566 assert_eq!(
1567 r0.get("area"),
1568 Some(&Kind::Number(Number::unitless(4500.0)))
1569 );
1570
1571 let r1 = g.row(1).unwrap();
1572 assert_eq!(r1.get("dis"), Some(&Kind::Str("Site Two".into())));
1573 assert_eq!(
1574 r1.get("area"),
1575 Some(&Kind::Number(Number::unitless(3200.0)))
1576 );
1577 }
1578
1579 #[test]
1580 fn decode_grid_with_meta() {
1581 let zinc = "ver:\"3.0\" err dis:\"some error\"\nempty\n";
1582 let g = decode_grid(zinc).unwrap();
1583 assert!(g.is_err());
1584 assert_eq!(g.meta.get("dis"), Some(&Kind::Str("some error".into())));
1585 }
1586
1587 #[test]
1588 fn decode_grid_with_col_meta() {
1589 let zinc = "ver:\"3.0\"\nname,power unit:\"kW\"\n\"AHU-1\",75\n";
1590 let g = decode_grid(zinc).unwrap();
1591 assert_eq!(g.num_cols(), 2);
1592 assert_eq!(g.cols[0].name, "name");
1593 assert_eq!(g.cols[1].name, "power");
1594 assert_eq!(g.cols[1].meta.get("unit"), Some(&Kind::Str("kW".into())));
1595 }
1596
1597 #[test]
1598 fn decode_grid_with_null_cells() {
1599 let zinc = "ver:\"3.0\"\na,b\n1,N\nN,2\n";
1600 let g = decode_grid(zinc).unwrap();
1601 assert_eq!(g.len(), 2);
1602 let r0 = g.row(0).unwrap();
1603 assert_eq!(r0.get("a"), Some(&Kind::Number(Number::unitless(1.0))));
1604 assert!(r0.missing("b"));
1605
1606 let r1 = g.row(1).unwrap();
1607 assert!(r1.missing("a"));
1608 assert_eq!(r1.get("b"), Some(&Kind::Number(Number::unitless(2.0))));
1609 }
1610
1611 #[test]
1612 fn decode_grid_with_comments() {
1613 let zinc = "// comment\nver:\"3.0\"\nempty\n";
1614 let g = decode_grid(zinc).unwrap();
1615 assert!(g.cols.is_empty());
1616 }
1617
1618 #[test]
1621 fn grid_roundtrip_empty() {
1622 let g = HGrid::new();
1623 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1624 let decoded = decode_grid(&encoded).unwrap();
1625 assert!(decoded.cols.is_empty());
1626 assert!(decoded.rows.is_empty());
1627 }
1628
1629 #[test]
1630 fn grid_roundtrip_with_data() {
1631 let cols = vec![HCol::new("dis"), HCol::new("area")];
1632 let mut row1 = HDict::new();
1633 row1.set("dis", Kind::Str("Site One".into()));
1634 row1.set("area", Kind::Number(Number::unitless(4500.0)));
1635 let mut row2 = HDict::new();
1636 row2.set("dis", Kind::Str("Site Two".into()));
1637 row2.set("area", Kind::Number(Number::unitless(3200.0)));
1638 let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
1639
1640 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1641 let decoded = decode_grid(&encoded).unwrap();
1642 assert_eq!(decoded.num_cols(), 2);
1643 assert_eq!(decoded.len(), 2);
1644 assert_eq!(
1645 decoded.row(0).unwrap().get("dis"),
1646 Some(&Kind::Str("Site One".into()))
1647 );
1648 assert_eq!(
1649 decoded.row(0).unwrap().get("area"),
1650 Some(&Kind::Number(Number::unitless(4500.0)))
1651 );
1652 }
1653
1654 #[test]
1655 fn grid_roundtrip_with_meta() {
1656 let mut meta = HDict::new();
1657 meta.set("err", Kind::Marker);
1658 meta.set("dis", Kind::Str("some error".into()));
1659 let g = HGrid::from_parts(meta, vec![], vec![]);
1660
1661 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1662 let decoded = decode_grid(&encoded).unwrap();
1663 assert!(decoded.is_err());
1664 assert_eq!(
1665 decoded.meta.get("dis"),
1666 Some(&Kind::Str("some error".into()))
1667 );
1668 }
1669
1670 #[test]
1671 fn grid_roundtrip_error_grid() {
1672 let mut meta = HDict::new();
1673 meta.set("err", Kind::Marker);
1674 meta.set("dis", Kind::Str("Error occurred".into()));
1675 meta.set("errTrace", Kind::Str("stack trace here".into()));
1676 let g = HGrid::from_parts(meta, vec![], vec![]);
1677
1678 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1679 let decoded = decode_grid(&encoded).unwrap();
1680 assert!(decoded.is_err());
1681 assert_eq!(
1682 decoded.meta.get("errTrace"),
1683 Some(&Kind::Str("stack trace here".into()))
1684 );
1685 }
1686
1687 #[test]
1690 fn split_csv_simple() {
1691 let parts = split_csv_aware("a,b,c");
1692 assert_eq!(parts, vec!["a", "b", "c"]);
1693 }
1694
1695 #[test]
1696 fn split_csv_with_quotes() {
1697 let parts = split_csv_aware("\"a,b\",c");
1698 assert_eq!(parts, vec!["\"a,b\"", "c"]);
1699 }
1700
1701 #[test]
1702 fn split_csv_with_nested() {
1703 let parts = split_csv_aware("[1,2],3");
1704 assert_eq!(parts, vec!["[1,2]", "3"]);
1705 }
1706
1707 #[test]
1710 fn parse_neg_inf_standalone() {
1711 let k = decode_scalar("-INF").unwrap();
1712 if let Kind::Number(n) = &k {
1713 assert!(n.val.is_infinite() && n.val < 0.0);
1714 } else {
1715 panic!("expected Number(-INF)");
1716 }
1717 }
1718
1719 #[test]
1722 fn zinc_codec_trait() {
1723 use crate::codecs::Codec;
1724 let codec = crate::codecs::zinc::ZincCodec;
1725 assert_eq!(codec.mime_type(), "text/zinc");
1726
1727 let encoded = codec.encode_scalar(&Kind::Bool(true)).unwrap();
1728 assert_eq!(encoded, "T");
1729
1730 let decoded = codec.decode_scalar("T").unwrap();
1731 assert_eq!(decoded, Kind::Bool(true));
1732
1733 let g = HGrid::new();
1734 let grid_str = codec.encode_grid(&g).unwrap();
1735 let decoded_grid = codec.decode_grid(&grid_str).unwrap();
1736 assert!(decoded_grid.cols.is_empty());
1737 }
1738
1739 #[test]
1742 fn parse_scalar_rejects_trailing_input() {
1743 assert!(decode_scalar("T extra garbage").is_err());
1744 assert!(decode_scalar("42 xyz").is_err());
1745 assert!(decode_scalar("\"hello\" world").is_err());
1746 assert!(decode_scalar("M extra").is_err());
1747 }
1748
1749 #[test]
1750 fn parse_scalar_allows_trailing_whitespace() {
1751 assert_eq!(decode_scalar("T ").unwrap(), Kind::Bool(true));
1752 assert_eq!(decode_scalar("M ").unwrap(), Kind::Marker);
1753 assert_eq!(
1754 decode_scalar("42 ").unwrap(),
1755 Kind::Number(Number::unitless(42.0))
1756 );
1757 }
1758
1759 #[test]
1762 fn parse_string_rejects_unknown_escapes() {
1763 assert!(decode_scalar("\"bad\\x\"").is_err());
1764 assert!(decode_scalar("\"bad\\a\"").is_err());
1765 assert!(decode_scalar("\"bad\\z\"").is_err());
1766 }
1767
1768 #[test]
1769 fn parse_string_accepts_valid_escapes() {
1770 assert!(decode_scalar("\"\\n\"").is_ok());
1771 assert!(decode_scalar("\"\\r\"").is_ok());
1772 assert!(decode_scalar("\"\\t\"").is_ok());
1773 assert!(decode_scalar("\"\\\\\"").is_ok());
1774 assert!(decode_scalar("\"\\\"\"").is_ok());
1775 assert!(decode_scalar("\"\\$\"").is_ok());
1776 assert!(decode_scalar("\"\\b\"").is_ok());
1777 assert!(decode_scalar("\"\\f\"").is_ok());
1778 assert!(decode_scalar("\"\\u0041\"").is_ok());
1779 }
1780}