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 self.pos += 3; if self.peek() == Some('.') {
388 self.pos += 1;
389 while let Some(ch) = self.peek() {
390 if ch.is_ascii_digit() {
391 self.pos += 1;
392 } else {
393 break;
394 }
395 }
396 }
397 }
398 Ok(self.src[start..self.pos].to_string())
399 }
400
401 fn read_offset(&mut self) -> Result<String, CodecError> {
402 if self.at_end() {
403 return Ok(String::new());
404 }
405 if self.peek() == Some('Z') {
406 self.pos += 1;
407 return Ok("+00:00".to_string());
408 }
409 if self.peek() == Some('+') || self.peek() == Some('-') {
410 let start = self.pos;
411 self.pos += 1; self.pos += 2; if self.peek() == Some(':') {
414 self.pos += 3; }
416 return Ok(self.src[start..self.pos].to_string());
417 }
418 Ok(String::new())
419 }
420
421 fn read_tz_name(&mut self) -> String {
422 let start = self.pos;
423 while let Some(ch) = self.peek() {
424 if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '/' {
425 self.pos += ch.len_utf8();
426 } else {
427 break;
428 }
429 }
430 self.src[start..self.pos].to_string()
431 }
432
433 fn read_time(&mut self) -> Result<Kind, CodecError> {
434 let time_str = self.read_time_str()?;
435 let time = NaiveTime::parse_from_str(&time_str, "%H:%M:%S%.f")
436 .or_else(|_| NaiveTime::parse_from_str(&time_str, "%H:%M:%S"))
437 .or_else(|_| NaiveTime::parse_from_str(&time_str, "%H:%M"))
438 .map_err(|e| self.err(format!("invalid time: {e}")))?;
439 Ok(Kind::Time(time))
440 }
441
442 fn read_str(&mut self) -> Result<String, CodecError> {
445 self.pos += 1; let mut result = String::new();
447 while !self.at_end() {
448 let ch = self.peek().unwrap();
449 if ch == '"' {
450 self.pos += 1;
451 return Ok(result);
452 }
453 if ch == '\\' {
454 self.pos += 1;
455 result.push(self.read_escape()?);
456 } else {
457 result.push(ch);
458 self.pos += ch.len_utf8();
459 }
460 }
461 Err(self.err("unterminated string"))
462 }
463
464 fn read_escape(&mut self) -> Result<char, CodecError> {
465 if self.at_end() {
466 return Err(self.err("unexpected end of escape sequence"));
467 }
468 let ch = self.consume().unwrap();
469 match ch {
470 'n' => Ok('\n'),
471 'r' => Ok('\r'),
472 't' => Ok('\t'),
473 '\\' => Ok('\\'),
474 '"' => Ok('"'),
475 '$' => Ok('$'),
476 'b' => Ok('\u{0008}'),
477 'f' => Ok('\u{000C}'),
478 'u' => {
479 if self.remaining().len() < 4 {
480 return Err(self.err("incomplete unicode escape"));
481 }
482 let hex = &self.src[self.pos..self.pos + 4];
483 self.pos += 4;
484 let code = u32::from_str_radix(hex, 16)
485 .map_err(|_| self.err(format!("invalid unicode escape: {hex}")))?;
486 char::from_u32(code)
487 .ok_or_else(|| self.err(format!("invalid unicode codepoint: {code}")))
488 }
489 _ => Err(self.err(format!("unknown escape sequence: \\{ch}"))),
490 }
491 }
492
493 fn read_ref(&mut self) -> Result<Kind, CodecError> {
496 self.pos += 1; let start = self.pos;
498 while let Some(ch) = self.peek() {
499 if is_ref_char(ch) {
500 self.pos += ch.len_utf8();
501 } else {
502 break;
503 }
504 }
505 let val = self.src[start..self.pos].to_string();
506
507 self.skip_spaces();
509 let dis = if self.peek() == Some('"') {
510 Some(self.read_str()?)
511 } else {
512 None
513 };
514
515 Ok(Kind::Ref(HRef::new(val, dis)))
516 }
517
518 fn read_uri(&mut self) -> Result<Kind, CodecError> {
521 self.pos += 1; let mut result = String::new();
523 while !self.at_end() {
524 let ch = self.peek().unwrap();
525 if ch == '`' {
526 self.pos += 1;
527 return Ok(Kind::Uri(Uri::new(result)));
528 }
529 if ch == '\\' {
530 self.pos += 1;
531 if let Some(next) = self.consume() {
532 result.push(next);
533 }
534 } else {
535 result.push(ch);
536 self.pos += ch.len_utf8();
537 }
538 }
539 Err(self.err("unterminated URI"))
540 }
541
542 fn read_symbol(&mut self) -> Result<Kind, CodecError> {
545 self.pos += 1; let start = self.pos;
547 while let Some(ch) = self.peek() {
548 if is_ref_char(ch) {
549 self.pos += ch.len_utf8();
550 } else {
551 break;
552 }
553 }
554 Ok(Kind::Symbol(Symbol::new(&self.src[start..self.pos])))
555 }
556
557 fn read_coord(&mut self) -> Result<Kind, CodecError> {
560 self.pos += 2; let start = self.pos;
562 while self.peek() != Some(',') && !self.at_end() {
563 self.pos += 1;
564 }
565 let lat: f64 = self.src[start..self.pos]
566 .trim()
567 .parse()
568 .map_err(|_| self.err("invalid coord latitude"))?;
569 self.pos += 1; let start = self.pos;
571 while self.peek() != Some(')') && !self.at_end() {
572 self.pos += 1;
573 }
574 let lng: f64 = self.src[start..self.pos]
575 .trim()
576 .parse()
577 .map_err(|_| self.err("invalid coord longitude"))?;
578 self.pos += 1; Ok(Kind::Coord(Coord::new(lat, lng)))
580 }
581
582 fn read_list(&mut self) -> Result<Kind, CodecError> {
585 self.pos += 1; let mut vals = Vec::new();
587 self.skip_spaces();
588 while !self.at_end() && self.peek() != Some(']') {
589 vals.push(self.read_val()?);
590 self.skip_spaces();
591 self.consume_if(',');
592 self.skip_spaces();
593 }
594 if !self.at_end() {
595 self.pos += 1; }
597 Ok(Kind::List(vals))
598 }
599
600 fn read_dict(&mut self) -> Result<Kind, CodecError> {
603 self.pos += 1; let mut dict = HDict::new();
605 self.skip_spaces();
606 while !self.at_end() && self.peek() != Some('}') {
607 let name = self.read_tag_name()?;
608 self.skip_spaces();
609 if self.peek() == Some(':') {
610 self.pos += 1;
611 self.skip_spaces();
612 let val = self.read_val()?;
613 dict.set(name, val);
614 } else {
615 dict.set(name, Kind::Marker);
616 }
617 self.skip_spaces();
618 self.consume_if(',');
619 self.skip_spaces();
620 }
621 if !self.at_end() {
622 self.pos += 1; }
624 Ok(Kind::Dict(Box::new(dict)))
625 }
626
627 fn read_tag_name(&mut self) -> Result<String, CodecError> {
628 let start = self.pos;
629 while let Some(ch) = self.peek() {
630 if ch.is_alphanumeric() || ch == '_' {
631 self.pos += ch.len_utf8();
632 } else {
633 break;
634 }
635 }
636 let name = self.src[start..self.pos].to_string();
637 if name.is_empty() {
638 return Err(self.err("expected tag name"));
639 }
640 Ok(name)
641 }
642
643 fn read_xstr_or_keyword(&mut self) -> Result<Kind, CodecError> {
646 let start = self.pos;
647 while let Some(ch) = self.peek() {
648 if ch.is_alphanumeric() || ch == '_' {
649 self.pos += ch.len_utf8();
650 } else {
651 break;
652 }
653 }
654 let name = &self.src[start..self.pos];
655
656 match name {
657 "INF" => return Ok(Kind::Number(Number::unitless(f64::INFINITY))),
658 "NaN" => return Ok(Kind::Number(Number::unitless(f64::NAN))),
659 "NA" => return Ok(Kind::NA),
660 _ => {}
661 }
662
663 if self.peek() == Some('(') {
665 self.pos += 1; self.skip_spaces();
667 let val = self.read_str()?;
668 self.skip_spaces();
669 if self.peek() == Some(')') {
670 self.pos += 1;
671 }
672 return Ok(Kind::XStr(XStr::new(name, val)));
673 }
674
675 Err(self.err(format!("unknown keyword '{name}'")))
676 }
677
678 pub fn read_id(&mut self) -> String {
680 let start = self.pos;
681 while let Some(ch) = self.peek() {
682 if ch.is_alphanumeric() || ch == '_' {
683 self.pos += ch.len_utf8();
684 } else {
685 break;
686 }
687 }
688 self.src[start..self.pos].to_string()
689 }
690}
691
692fn is_ref_char(ch: char) -> bool {
693 ch.is_alphanumeric() || ch == '_' || ch == ':' || ch == '-' || ch == '.' || ch == '~'
694}
695
696pub fn decode_scalar(input: &str) -> Result<Kind, CodecError> {
698 let mut parser = ZincParser::new(input.trim());
699 parser.parse_scalar()
700}
701
702pub fn decode_grid(input: &str) -> Result<HGrid, CodecError> {
706 let lines: Vec<&str> = input
707 .lines()
708 .map(|l| l.trim())
709 .filter(|l| !l.is_empty() && !l.starts_with("//"))
710 .collect();
711
712 if lines.is_empty() {
713 return Ok(HGrid::new());
714 }
715
716 let mut line_idx = 0;
717
718 let ver_line = lines[line_idx];
720 line_idx += 1;
721
722 if !ver_line.starts_with("ver:") {
723 return Err(CodecError::Parse {
724 pos: 0,
725 message: format!("expected 'ver:' header, got: {ver_line:?}"),
726 });
727 }
728
729 let meta = parse_ver_line_meta(ver_line)?;
731
732 if line_idx >= lines.len() {
734 return Ok(HGrid::from_parts(meta, vec![], vec![]));
735 }
736
737 let col_line = lines[line_idx];
738 line_idx += 1;
739
740 let cols = if col_line == "empty" {
741 vec![]
742 } else {
743 parse_cols(col_line)?
744 };
745
746 let mut rows = Vec::new();
748 while line_idx < lines.len() {
749 let row_line = lines[line_idx];
750 line_idx += 1;
751 if row_line.is_empty() {
752 continue;
753 }
754 let row = parse_row(row_line, &cols)?;
755 rows.push(row);
756 }
757
758 Ok(HGrid::from_parts(meta, cols, rows))
759}
760
761fn parse_ver_line_meta(ver_line: &str) -> Result<HDict, CodecError> {
762 let mut parser = ZincParser::new(ver_line);
764 while !parser.at_end() && parser.peek() != Some(' ') {
766 parser.consume();
767 }
768 parser.skip_spaces();
769 if parser.at_end() {
770 return Ok(HDict::new());
771 }
772 parse_inline_meta(&mut parser)
773}
774
775fn parse_cols(line: &str) -> Result<Vec<HCol>, CodecError> {
776 let parts = split_csv_aware(line);
777 let mut cols = Vec::new();
778 for part in parts {
779 let part = part.trim();
780 if part.is_empty() {
781 continue;
782 }
783 let mut parser = ZincParser::new(part);
784 let name = read_col_name(&mut parser);
785 parser.skip_spaces();
786 let meta = if !parser.at_end() {
787 parse_inline_meta(&mut parser)?
788 } else {
789 HDict::new()
790 };
791 cols.push(HCol::with_meta(name, meta));
792 }
793 Ok(cols)
794}
795
796fn parse_row(line: &str, cols: &[HCol]) -> Result<HDict, CodecError> {
797 let parts = split_csv_aware(line);
798 let mut dict = HDict::new();
799 for (i, col) in cols.iter().enumerate() {
800 if i < parts.len() {
801 let cell = parts[i].trim();
802 if !cell.is_empty() && cell != "N" {
803 let mut parser = ZincParser::new(cell);
804 let val = parser.read_val()?;
805 dict.set(&col.name, val);
806 }
807 }
808 }
809 Ok(dict)
810}
811
812fn parse_inline_meta(parser: &mut ZincParser<'_>) -> Result<HDict, CodecError> {
813 let mut dict = HDict::new();
814 while !parser.at_end() {
815 parser.skip_spaces();
816 if parser.at_end() {
817 break;
818 }
819 let name = read_col_name(parser);
820 if name.is_empty() {
821 break;
822 }
823 parser.skip_spaces();
824 if parser.peek() == Some(':') {
825 parser.consume();
826 parser.skip_spaces();
827 let val = parser.read_val()?;
828 dict.set(name, val);
829 } else {
830 dict.set(name, Kind::Marker);
831 }
832 parser.skip_spaces();
833 }
834 Ok(dict)
835}
836
837fn read_col_name(parser: &mut ZincParser<'_>) -> String {
838 parser.read_id()
839}
840
841fn split_csv_aware(line: &str) -> Vec<String> {
843 let mut parts = Vec::new();
844 let mut current = String::new();
845 let mut depth = 0i32;
846 let mut in_str = false;
847 let mut escaped = false;
848
849 for ch in line.chars() {
850 if escaped {
851 current.push(ch);
852 escaped = false;
853 continue;
854 }
855 if ch == '\\' {
856 current.push(ch);
857 escaped = true;
858 continue;
859 }
860 if ch == '"' && depth == 0 {
861 in_str = !in_str;
862 current.push(ch);
863 continue;
864 }
865 if in_str {
866 current.push(ch);
867 continue;
868 }
869 match ch {
870 '(' | '[' | '{' => {
871 depth += 1;
872 current.push(ch);
873 }
874 ')' | ']' | '}' => {
875 depth -= 1;
876 current.push(ch);
877 }
878 ',' if depth == 0 => {
879 parts.push(std::mem::take(&mut current));
880 }
881 _ => {
882 current.push(ch);
883 }
884 }
885 }
886 parts.push(current);
887 parts
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893 use crate::data::{HDict, HGrid};
894 use chrono::{Datelike, FixedOffset, NaiveDate, NaiveTime, TimeZone};
895
896 fn round_trip(kind: &Kind) -> Kind {
899 let encoded = crate::codecs::zinc::encode_scalar(kind).unwrap();
900 decode_scalar(&encoded).unwrap()
901 }
902
903 #[test]
904 fn parse_null() {
905 assert_eq!(decode_scalar("N").unwrap(), Kind::Null);
906 }
907
908 #[test]
909 fn parse_true() {
910 assert_eq!(decode_scalar("T").unwrap(), Kind::Bool(true));
911 }
912
913 #[test]
914 fn parse_false() {
915 assert_eq!(decode_scalar("F").unwrap(), Kind::Bool(false));
916 }
917
918 #[test]
919 fn parse_marker() {
920 assert_eq!(decode_scalar("M").unwrap(), Kind::Marker);
921 }
922
923 #[test]
924 fn parse_na() {
925 assert_eq!(decode_scalar("NA").unwrap(), Kind::NA);
926 }
927
928 #[test]
929 fn parse_remove() {
930 assert_eq!(decode_scalar("R").unwrap(), Kind::Remove);
931 }
932
933 #[test]
934 fn roundtrip_null() {
935 assert_eq!(round_trip(&Kind::Null), Kind::Null);
936 }
937
938 #[test]
939 fn roundtrip_bool_true() {
940 assert_eq!(round_trip(&Kind::Bool(true)), Kind::Bool(true));
941 }
942
943 #[test]
944 fn roundtrip_bool_false() {
945 assert_eq!(round_trip(&Kind::Bool(false)), Kind::Bool(false));
946 }
947
948 #[test]
949 fn roundtrip_marker() {
950 assert_eq!(round_trip(&Kind::Marker), Kind::Marker);
951 }
952
953 #[test]
954 fn roundtrip_na() {
955 assert_eq!(round_trip(&Kind::NA), Kind::NA);
956 }
957
958 #[test]
959 fn roundtrip_remove() {
960 assert_eq!(round_trip(&Kind::Remove), Kind::Remove);
961 }
962
963 #[test]
966 fn parse_number_zero() {
967 assert_eq!(
968 decode_scalar("0").unwrap(),
969 Kind::Number(Number::unitless(0.0))
970 );
971 }
972
973 #[test]
974 fn parse_number_integer() {
975 assert_eq!(
976 decode_scalar("42").unwrap(),
977 Kind::Number(Number::unitless(42.0))
978 );
979 }
980
981 #[test]
982 fn parse_number_float() {
983 assert_eq!(
984 decode_scalar("72.5").unwrap(),
985 Kind::Number(Number::unitless(72.5))
986 );
987 }
988
989 #[test]
990 fn parse_number_negative() {
991 assert_eq!(
992 decode_scalar("-23.45").unwrap(),
993 Kind::Number(Number::unitless(-23.45))
994 );
995 }
996
997 #[test]
998 fn parse_number_scientific() {
999 let k = decode_scalar("5.4e8").unwrap();
1000 if let Kind::Number(n) = &k {
1001 assert!((n.val - 5.4e8).abs() < 1.0);
1002 } else {
1003 panic!("expected Number, got {k:?}");
1004 }
1005 }
1006
1007 #[test]
1008 fn parse_number_inf() {
1009 let k = decode_scalar("INF").unwrap();
1010 if let Kind::Number(n) = &k {
1011 assert!(n.val.is_infinite() && n.val > 0.0);
1012 } else {
1013 panic!("expected Number(INF)");
1014 }
1015 }
1016
1017 #[test]
1018 fn parse_number_neg_inf() {
1019 let k = decode_scalar("-INF").unwrap();
1020 if let Kind::Number(n) = &k {
1021 assert!(n.val.is_infinite() && n.val < 0.0);
1022 } else {
1023 panic!("expected Number(-INF)");
1024 }
1025 }
1026
1027 #[test]
1028 fn parse_number_nan() {
1029 let k = decode_scalar("NaN").unwrap();
1030 if let Kind::Number(n) = &k {
1031 assert!(n.val.is_nan());
1032 } else {
1033 panic!("expected Number(NaN)");
1034 }
1035 }
1036
1037 #[test]
1038 fn parse_number_with_unit() {
1039 let k = decode_scalar("72.5\u{00B0}F").unwrap();
1040 if let Kind::Number(n) = &k {
1041 assert_eq!(n.val, 72.5);
1042 assert_eq!(n.unit.as_deref(), Some("\u{00B0}F"));
1043 } else {
1044 panic!("expected Number with unit");
1045 }
1046 }
1047
1048 #[test]
1049 fn roundtrip_number_zero() {
1050 assert_eq!(
1051 round_trip(&Kind::Number(Number::unitless(0.0))),
1052 Kind::Number(Number::unitless(0.0))
1053 );
1054 }
1055
1056 #[test]
1057 fn roundtrip_number_integer() {
1058 assert_eq!(
1059 round_trip(&Kind::Number(Number::unitless(42.0))),
1060 Kind::Number(Number::unitless(42.0))
1061 );
1062 }
1063
1064 #[test]
1065 fn roundtrip_number_float() {
1066 assert_eq!(
1067 round_trip(&Kind::Number(Number::unitless(72.5))),
1068 Kind::Number(Number::unitless(72.5))
1069 );
1070 }
1071
1072 #[test]
1073 fn roundtrip_number_negative() {
1074 assert_eq!(
1075 round_trip(&Kind::Number(Number::unitless(-23.45))),
1076 Kind::Number(Number::unitless(-23.45))
1077 );
1078 }
1079
1080 #[test]
1081 fn roundtrip_number_with_unit() {
1082 let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
1083 let rt = round_trip(&k);
1084 if let Kind::Number(n) = &rt {
1085 assert_eq!(n.val, 72.5);
1086 assert_eq!(n.unit.as_deref(), Some("\u{00B0}F"));
1087 } else {
1088 panic!("expected Number");
1089 }
1090 }
1091
1092 #[test]
1093 fn roundtrip_inf() {
1094 let k = Kind::Number(Number::unitless(f64::INFINITY));
1095 let rt = round_trip(&k);
1096 if let Kind::Number(n) = &rt {
1097 assert!(n.val.is_infinite() && n.val > 0.0);
1098 } else {
1099 panic!("expected Number(INF)");
1100 }
1101 }
1102
1103 #[test]
1104 fn roundtrip_neg_inf() {
1105 let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
1106 let rt = round_trip(&k);
1107 if let Kind::Number(n) = &rt {
1108 assert!(n.val.is_infinite() && n.val < 0.0);
1109 } else {
1110 panic!("expected Number(-INF)");
1111 }
1112 }
1113
1114 #[test]
1115 fn roundtrip_nan() {
1116 let k = Kind::Number(Number::unitless(f64::NAN));
1117 let rt = round_trip(&k);
1118 if let Kind::Number(n) = &rt {
1119 assert!(n.val.is_nan());
1120 } else {
1121 panic!("expected Number(NaN)");
1122 }
1123 }
1124
1125 #[test]
1128 fn parse_string_empty() {
1129 assert_eq!(decode_scalar("\"\"").unwrap(), Kind::Str(String::new()));
1130 }
1131
1132 #[test]
1133 fn parse_string_simple() {
1134 assert_eq!(
1135 decode_scalar("\"hello\"").unwrap(),
1136 Kind::Str("hello".into())
1137 );
1138 }
1139
1140 #[test]
1141 fn parse_string_escapes() {
1142 assert_eq!(
1143 decode_scalar("\"line1\\nline2\"").unwrap(),
1144 Kind::Str("line1\nline2".into())
1145 );
1146 assert_eq!(
1147 decode_scalar("\"tab\\there\"").unwrap(),
1148 Kind::Str("tab\there".into())
1149 );
1150 assert_eq!(
1151 decode_scalar("\"back\\\\slash\"").unwrap(),
1152 Kind::Str("back\\slash".into())
1153 );
1154 assert_eq!(
1155 decode_scalar("\"q\\\"uote\"").unwrap(),
1156 Kind::Str("q\"uote".into())
1157 );
1158 assert_eq!(
1159 decode_scalar("\"dollar\\$sign\"").unwrap(),
1160 Kind::Str("dollar$sign".into())
1161 );
1162 }
1163
1164 #[test]
1165 fn parse_string_unicode_escape() {
1166 assert_eq!(decode_scalar("\"\\u0041\"").unwrap(), Kind::Str("A".into()));
1167 }
1168
1169 #[test]
1170 fn roundtrip_string_empty() {
1171 assert_eq!(
1172 round_trip(&Kind::Str(String::new())),
1173 Kind::Str(String::new())
1174 );
1175 }
1176
1177 #[test]
1178 fn roundtrip_string_escapes() {
1179 let s = "line1\nline2\ttab\\slash\"quote$dollar";
1180 assert_eq!(round_trip(&Kind::Str(s.into())), Kind::Str(s.into()));
1181 }
1182
1183 #[test]
1186 fn parse_ref_simple() {
1187 let k = decode_scalar("@site-1").unwrap();
1188 if let Kind::Ref(r) = &k {
1189 assert_eq!(r.val, "site-1");
1190 assert_eq!(r.dis, None);
1191 } else {
1192 panic!("expected Ref");
1193 }
1194 }
1195
1196 #[test]
1197 fn parse_ref_with_dis() {
1198 let k = decode_scalar("@site-1 \"Main Site\"").unwrap();
1199 if let Kind::Ref(r) = &k {
1200 assert_eq!(r.val, "site-1");
1201 assert_eq!(r.dis, Some("Main Site".into()));
1202 } else {
1203 panic!("expected Ref");
1204 }
1205 }
1206
1207 #[test]
1208 fn roundtrip_ref_simple() {
1209 let k = Kind::Ref(HRef::from_val("site-1"));
1210 let rt = round_trip(&k);
1211 if let Kind::Ref(r) = &rt {
1212 assert_eq!(r.val, "site-1");
1213 } else {
1214 panic!("expected Ref");
1215 }
1216 }
1217
1218 #[test]
1219 fn roundtrip_ref_with_dis() {
1220 let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
1221 let rt = round_trip(&k);
1222 if let Kind::Ref(r) = &rt {
1223 assert_eq!(r.val, "site-1");
1224 assert_eq!(r.dis, Some("Main Site".into()));
1225 } else {
1226 panic!("expected Ref");
1227 }
1228 }
1229
1230 #[test]
1233 fn parse_uri_simple() {
1234 let k = decode_scalar("`http://example.com`").unwrap();
1235 assert_eq!(k, Kind::Uri(Uri::new("http://example.com")));
1236 }
1237
1238 #[test]
1239 fn parse_uri_with_special() {
1240 let k = decode_scalar("`http://ex.com/path?q=1&b=2`").unwrap();
1241 assert_eq!(k, Kind::Uri(Uri::new("http://ex.com/path?q=1&b=2")));
1242 }
1243
1244 #[test]
1245 fn roundtrip_uri() {
1246 let k = Kind::Uri(Uri::new("http://example.com/path"));
1247 assert_eq!(round_trip(&k), k);
1248 }
1249
1250 #[test]
1253 fn parse_symbol_simple() {
1254 let k = decode_scalar("^site").unwrap();
1255 assert_eq!(k, Kind::Symbol(Symbol::new("site")));
1256 }
1257
1258 #[test]
1259 fn parse_symbol_compound() {
1260 let k = decode_scalar("^hot-water").unwrap();
1261 assert_eq!(k, Kind::Symbol(Symbol::new("hot-water")));
1262 }
1263
1264 #[test]
1265 fn roundtrip_symbol() {
1266 let k = Kind::Symbol(Symbol::new("hot-water"));
1267 assert_eq!(round_trip(&k), k);
1268 }
1269
1270 #[test]
1273 fn parse_date() {
1274 let k = decode_scalar("2024-03-13").unwrap();
1275 assert_eq!(k, Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap()));
1276 }
1277
1278 #[test]
1279 fn roundtrip_date() {
1280 let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
1281 assert_eq!(round_trip(&k), k);
1282 }
1283
1284 #[test]
1287 fn parse_time() {
1288 let k = decode_scalar("08:12:05").unwrap();
1289 assert_eq!(k, Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap()));
1290 }
1291
1292 #[test]
1293 fn parse_time_with_frac() {
1294 let k = decode_scalar("14:30:00.123").unwrap();
1295 assert_eq!(
1296 k,
1297 Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap())
1298 );
1299 }
1300
1301 #[test]
1302 fn roundtrip_time() {
1303 let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
1304 assert_eq!(round_trip(&k), k);
1305 }
1306
1307 #[test]
1308 fn roundtrip_time_frac() {
1309 let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
1310 assert_eq!(round_trip(&k), k);
1311 }
1312
1313 #[test]
1316 fn parse_datetime() {
1317 let k = decode_scalar("2024-01-01T08:12:05-05:00 New_York").unwrap();
1318 if let Kind::DateTime(hdt) = &k {
1319 assert_eq!(hdt.tz_name, "New_York");
1320 assert_eq!(hdt.dt.year(), 2024);
1321 } else {
1322 panic!("expected DateTime");
1323 }
1324 }
1325
1326 #[test]
1327 fn parse_datetime_utc() {
1328 let k = decode_scalar("2024-06-15T12:00:00+00:00 UTC").unwrap();
1329 if let Kind::DateTime(hdt) = &k {
1330 assert_eq!(hdt.tz_name, "UTC");
1331 } else {
1332 panic!("expected DateTime");
1333 }
1334 }
1335
1336 #[test]
1337 fn parse_datetime_z() {
1338 let k = decode_scalar("2024-06-15T12:00:00Z UTC").unwrap();
1339 if let Kind::DateTime(hdt) = &k {
1340 assert_eq!(hdt.tz_name, "UTC");
1341 assert_eq!(hdt.dt.offset(), &FixedOffset::east_opt(0).unwrap());
1342 } else {
1343 panic!("expected DateTime");
1344 }
1345 }
1346
1347 #[test]
1348 fn roundtrip_datetime() {
1349 let offset = FixedOffset::west_opt(5 * 3600).unwrap();
1350 let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
1351 let k = Kind::DateTime(HDateTime::new(dt, "New_York"));
1352 let rt = round_trip(&k);
1353 if let Kind::DateTime(hdt) = &rt {
1354 assert_eq!(hdt.tz_name, "New_York");
1355 assert_eq!(hdt.dt, dt);
1356 } else {
1357 panic!("expected DateTime");
1358 }
1359 }
1360
1361 #[test]
1362 fn roundtrip_datetime_utc() {
1363 let offset = FixedOffset::east_opt(0).unwrap();
1364 let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
1365 let k = Kind::DateTime(HDateTime::new(dt, "UTC"));
1366 let rt = round_trip(&k);
1367 if let Kind::DateTime(hdt) = &rt {
1368 assert_eq!(hdt.tz_name, "UTC");
1369 assert_eq!(hdt.dt, dt);
1370 } else {
1371 panic!("expected DateTime");
1372 }
1373 }
1374
1375 #[test]
1378 fn parse_coord() {
1379 let k = decode_scalar("C(37.5458266,-77.4491888)").unwrap();
1380 assert_eq!(k, Kind::Coord(Coord::new(37.5458266, -77.4491888)));
1381 }
1382
1383 #[test]
1384 fn parse_coord_negative() {
1385 let k = decode_scalar("C(-33.8688,151.2093)").unwrap();
1386 assert_eq!(k, Kind::Coord(Coord::new(-33.8688, 151.2093)));
1387 }
1388
1389 #[test]
1390 fn roundtrip_coord() {
1391 let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
1392 assert_eq!(round_trip(&k), k);
1393 }
1394
1395 #[test]
1398 fn parse_xstr() {
1399 let k = decode_scalar("Color(\"red\")").unwrap();
1400 assert_eq!(k, Kind::XStr(XStr::new("Color", "red")));
1401 }
1402
1403 #[test]
1404 fn roundtrip_xstr() {
1405 let k = Kind::XStr(XStr::new("Color", "red"));
1406 assert_eq!(round_trip(&k), k);
1407 }
1408
1409 #[test]
1412 fn parse_list_empty() {
1413 assert_eq!(decode_scalar("[]").unwrap(), Kind::List(vec![]));
1414 }
1415
1416 #[test]
1417 fn parse_list_mixed() {
1418 let k = decode_scalar("[1, \"two\", M]").unwrap();
1419 assert_eq!(
1420 k,
1421 Kind::List(vec![
1422 Kind::Number(Number::unitless(1.0)),
1423 Kind::Str("two".into()),
1424 Kind::Marker,
1425 ])
1426 );
1427 }
1428
1429 #[test]
1430 fn parse_list_nested() {
1431 let k = decode_scalar("[[1, 2], [3, 4]]").unwrap();
1432 assert_eq!(
1433 k,
1434 Kind::List(vec![
1435 Kind::List(vec![
1436 Kind::Number(Number::unitless(1.0)),
1437 Kind::Number(Number::unitless(2.0)),
1438 ]),
1439 Kind::List(vec![
1440 Kind::Number(Number::unitless(3.0)),
1441 Kind::Number(Number::unitless(4.0)),
1442 ]),
1443 ])
1444 );
1445 }
1446
1447 #[test]
1448 fn roundtrip_list_empty() {
1449 assert_eq!(round_trip(&Kind::List(vec![])), Kind::List(vec![]));
1450 }
1451
1452 #[test]
1453 fn roundtrip_list_mixed() {
1454 let k = Kind::List(vec![
1455 Kind::Number(Number::unitless(1.0)),
1456 Kind::Str("two".into()),
1457 Kind::Marker,
1458 ]);
1459 assert_eq!(round_trip(&k), k);
1460 }
1461
1462 #[test]
1465 fn parse_dict_empty() {
1466 let k = decode_scalar("{}").unwrap();
1467 assert_eq!(k, Kind::Dict(Box::new(HDict::new())));
1468 }
1469
1470 #[test]
1471 fn parse_dict_with_marker() {
1472 let k = decode_scalar("{site}").unwrap();
1473 if let Kind::Dict(d) = &k {
1474 assert_eq!(d.get("site"), Some(&Kind::Marker));
1475 } else {
1476 panic!("expected Dict");
1477 }
1478 }
1479
1480 #[test]
1481 fn parse_dict_with_values() {
1482 let k = decode_scalar("{dis:\"Main\" area:42}").unwrap();
1483 if let Kind::Dict(d) = &k {
1484 assert_eq!(d.get("dis"), Some(&Kind::Str("Main".into())));
1485 assert_eq!(d.get("area"), Some(&Kind::Number(Number::unitless(42.0))));
1486 } else {
1487 panic!("expected Dict");
1488 }
1489 }
1490
1491 #[test]
1492 fn parse_dict_mixed() {
1493 let k = decode_scalar("{site dis:\"Main\" area:42}").unwrap();
1494 if let Kind::Dict(d) = &k {
1495 assert_eq!(d.get("site"), Some(&Kind::Marker));
1496 assert_eq!(d.get("dis"), Some(&Kind::Str("Main".into())));
1497 assert_eq!(d.get("area"), Some(&Kind::Number(Number::unitless(42.0))));
1498 } else {
1499 panic!("expected Dict");
1500 }
1501 }
1502
1503 #[test]
1504 fn roundtrip_dict_empty() {
1505 let k = Kind::Dict(Box::new(HDict::new()));
1506 assert_eq!(round_trip(&k), k);
1507 }
1508
1509 #[test]
1510 fn roundtrip_dict_with_values() {
1511 let mut d = HDict::new();
1512 d.set("dis", Kind::Str("Main".into()));
1513 d.set("site", Kind::Marker);
1514 let k = Kind::Dict(Box::new(d));
1515 let rt = round_trip(&k);
1516 if let Kind::Dict(d) = &rt {
1517 assert_eq!(d.get("dis"), Some(&Kind::Str("Main".into())));
1518 assert_eq!(d.get("site"), Some(&Kind::Marker));
1519 } else {
1520 panic!("expected Dict");
1521 }
1522 }
1523
1524 #[test]
1527 fn decode_grid_empty() {
1528 let zinc = "ver:\"3.0\"\nempty\n";
1529 let g = decode_grid(zinc).unwrap();
1530 assert!(g.cols.is_empty());
1531 assert!(g.rows.is_empty());
1532 }
1533
1534 #[test]
1535 fn decode_grid_simple() {
1536 let zinc = "ver:\"3.0\"\ndis,area\n\"Site One\",4500\n\"Site Two\",3200\n";
1537 let g = decode_grid(zinc).unwrap();
1538 assert_eq!(g.num_cols(), 2);
1539 assert_eq!(g.cols[0].name, "dis");
1540 assert_eq!(g.cols[1].name, "area");
1541 assert_eq!(g.len(), 2);
1542
1543 let r0 = g.row(0).unwrap();
1544 assert_eq!(r0.get("dis"), Some(&Kind::Str("Site One".into())));
1545 assert_eq!(
1546 r0.get("area"),
1547 Some(&Kind::Number(Number::unitless(4500.0)))
1548 );
1549
1550 let r1 = g.row(1).unwrap();
1551 assert_eq!(r1.get("dis"), Some(&Kind::Str("Site Two".into())));
1552 assert_eq!(
1553 r1.get("area"),
1554 Some(&Kind::Number(Number::unitless(3200.0)))
1555 );
1556 }
1557
1558 #[test]
1559 fn decode_grid_with_meta() {
1560 let zinc = "ver:\"3.0\" err dis:\"some error\"\nempty\n";
1561 let g = decode_grid(zinc).unwrap();
1562 assert!(g.is_err());
1563 assert_eq!(g.meta.get("dis"), Some(&Kind::Str("some error".into())));
1564 }
1565
1566 #[test]
1567 fn decode_grid_with_col_meta() {
1568 let zinc = "ver:\"3.0\"\nname,power unit:\"kW\"\n\"AHU-1\",75\n";
1569 let g = decode_grid(zinc).unwrap();
1570 assert_eq!(g.num_cols(), 2);
1571 assert_eq!(g.cols[0].name, "name");
1572 assert_eq!(g.cols[1].name, "power");
1573 assert_eq!(g.cols[1].meta.get("unit"), Some(&Kind::Str("kW".into())));
1574 }
1575
1576 #[test]
1577 fn decode_grid_with_null_cells() {
1578 let zinc = "ver:\"3.0\"\na,b\n1,N\nN,2\n";
1579 let g = decode_grid(zinc).unwrap();
1580 assert_eq!(g.len(), 2);
1581 let r0 = g.row(0).unwrap();
1582 assert_eq!(r0.get("a"), Some(&Kind::Number(Number::unitless(1.0))));
1583 assert!(r0.missing("b"));
1584
1585 let r1 = g.row(1).unwrap();
1586 assert!(r1.missing("a"));
1587 assert_eq!(r1.get("b"), Some(&Kind::Number(Number::unitless(2.0))));
1588 }
1589
1590 #[test]
1591 fn decode_grid_with_comments() {
1592 let zinc = "// comment\nver:\"3.0\"\nempty\n";
1593 let g = decode_grid(zinc).unwrap();
1594 assert!(g.cols.is_empty());
1595 }
1596
1597 #[test]
1600 fn grid_roundtrip_empty() {
1601 let g = HGrid::new();
1602 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1603 let decoded = decode_grid(&encoded).unwrap();
1604 assert!(decoded.cols.is_empty());
1605 assert!(decoded.rows.is_empty());
1606 }
1607
1608 #[test]
1609 fn grid_roundtrip_with_data() {
1610 let cols = vec![HCol::new("dis"), HCol::new("area")];
1611 let mut row1 = HDict::new();
1612 row1.set("dis", Kind::Str("Site One".into()));
1613 row1.set("area", Kind::Number(Number::unitless(4500.0)));
1614 let mut row2 = HDict::new();
1615 row2.set("dis", Kind::Str("Site Two".into()));
1616 row2.set("area", Kind::Number(Number::unitless(3200.0)));
1617 let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
1618
1619 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1620 let decoded = decode_grid(&encoded).unwrap();
1621 assert_eq!(decoded.num_cols(), 2);
1622 assert_eq!(decoded.len(), 2);
1623 assert_eq!(
1624 decoded.row(0).unwrap().get("dis"),
1625 Some(&Kind::Str("Site One".into()))
1626 );
1627 assert_eq!(
1628 decoded.row(0).unwrap().get("area"),
1629 Some(&Kind::Number(Number::unitless(4500.0)))
1630 );
1631 }
1632
1633 #[test]
1634 fn grid_roundtrip_with_meta() {
1635 let mut meta = HDict::new();
1636 meta.set("err", Kind::Marker);
1637 meta.set("dis", Kind::Str("some error".into()));
1638 let g = HGrid::from_parts(meta, vec![], vec![]);
1639
1640 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1641 let decoded = decode_grid(&encoded).unwrap();
1642 assert!(decoded.is_err());
1643 assert_eq!(
1644 decoded.meta.get("dis"),
1645 Some(&Kind::Str("some error".into()))
1646 );
1647 }
1648
1649 #[test]
1650 fn grid_roundtrip_error_grid() {
1651 let mut meta = HDict::new();
1652 meta.set("err", Kind::Marker);
1653 meta.set("dis", Kind::Str("Error occurred".into()));
1654 meta.set("errTrace", Kind::Str("stack trace here".into()));
1655 let g = HGrid::from_parts(meta, vec![], vec![]);
1656
1657 let encoded = crate::codecs::zinc::encode_grid(&g).unwrap();
1658 let decoded = decode_grid(&encoded).unwrap();
1659 assert!(decoded.is_err());
1660 assert_eq!(
1661 decoded.meta.get("errTrace"),
1662 Some(&Kind::Str("stack trace here".into()))
1663 );
1664 }
1665
1666 #[test]
1669 fn split_csv_simple() {
1670 let parts = split_csv_aware("a,b,c");
1671 assert_eq!(parts, vec!["a", "b", "c"]);
1672 }
1673
1674 #[test]
1675 fn split_csv_with_quotes() {
1676 let parts = split_csv_aware("\"a,b\",c");
1677 assert_eq!(parts, vec!["\"a,b\"", "c"]);
1678 }
1679
1680 #[test]
1681 fn split_csv_with_nested() {
1682 let parts = split_csv_aware("[1,2],3");
1683 assert_eq!(parts, vec!["[1,2]", "3"]);
1684 }
1685
1686 #[test]
1689 fn parse_neg_inf_standalone() {
1690 let k = decode_scalar("-INF").unwrap();
1691 if let Kind::Number(n) = &k {
1692 assert!(n.val.is_infinite() && n.val < 0.0);
1693 } else {
1694 panic!("expected Number(-INF)");
1695 }
1696 }
1697
1698 #[test]
1701 fn zinc_codec_trait() {
1702 use crate::codecs::Codec;
1703 let codec = crate::codecs::zinc::ZincCodec;
1704 assert_eq!(codec.mime_type(), "text/zinc");
1705
1706 let encoded = codec.encode_scalar(&Kind::Bool(true)).unwrap();
1707 assert_eq!(encoded, "T");
1708
1709 let decoded = codec.decode_scalar("T").unwrap();
1710 assert_eq!(decoded, Kind::Bool(true));
1711
1712 let g = HGrid::new();
1713 let grid_str = codec.encode_grid(&g).unwrap();
1714 let decoded_grid = codec.decode_grid(&grid_str).unwrap();
1715 assert!(decoded_grid.cols.is_empty());
1716 }
1717
1718 #[test]
1721 fn parse_scalar_rejects_trailing_input() {
1722 assert!(decode_scalar("T extra garbage").is_err());
1723 assert!(decode_scalar("42 xyz").is_err());
1724 assert!(decode_scalar("\"hello\" world").is_err());
1725 assert!(decode_scalar("M extra").is_err());
1726 }
1727
1728 #[test]
1729 fn parse_scalar_allows_trailing_whitespace() {
1730 assert_eq!(decode_scalar("T ").unwrap(), Kind::Bool(true));
1731 assert_eq!(decode_scalar("M ").unwrap(), Kind::Marker);
1732 assert_eq!(
1733 decode_scalar("42 ").unwrap(),
1734 Kind::Number(Number::unitless(42.0))
1735 );
1736 }
1737
1738 #[test]
1741 fn parse_string_rejects_unknown_escapes() {
1742 assert!(decode_scalar("\"bad\\x\"").is_err());
1743 assert!(decode_scalar("\"bad\\a\"").is_err());
1744 assert!(decode_scalar("\"bad\\z\"").is_err());
1745 }
1746
1747 #[test]
1748 fn parse_string_accepts_valid_escapes() {
1749 assert!(decode_scalar("\"\\n\"").is_ok());
1750 assert!(decode_scalar("\"\\r\"").is_ok());
1751 assert!(decode_scalar("\"\\t\"").is_ok());
1752 assert!(decode_scalar("\"\\\\\"").is_ok());
1753 assert!(decode_scalar("\"\\\"\"").is_ok());
1754 assert!(decode_scalar("\"\\$\"").is_ok());
1755 assert!(decode_scalar("\"\\b\"").is_ok());
1756 assert!(decode_scalar("\"\\f\"").is_ok());
1757 assert!(decode_scalar("\"\\u0041\"").is_ok());
1758 }
1759}