1use std::cmp::max;
23use std::collections::{BTreeMap, HashMap};
24
25use crate::parsing::*;
26
27use nom::character::complete as ncc;
28use nom::error::VerboseError;
29use nom::IResult;
30
31use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Deserialize, Serialize)]
34struct TwoDARepr {
35 file_type: FixedSizeString<4>,
36 file_version: FixedSizeString<4>,
37 default_value: Option<String>,
38 columns: Vec<String>,
39 rows: Vec<Vec<Option<String>>>,
40}
41impl From<TwoDARepr> for TwoDA {
42 fn from(repr: TwoDARepr) -> Self {
43 let column_lookup = repr
44 .columns
45 .iter()
46 .enumerate()
47 .map(|(i, col)| (col.to_lowercase(), i))
48 .collect::<HashMap<_, _>>();
49 let data = repr.rows.into_iter().flatten().collect(); Self {
51 file_type: repr.file_type,
52 file_version: repr.file_version,
53 default_value: repr.default_value,
54 columns: repr.columns.into_iter().collect(),
55 column_lookup,
56 data,
57 merge_meta: None,
58 }
59 }
60}
61impl From<TwoDA> for TwoDARepr {
62 fn from(twoda: TwoDA) -> Self {
63 let rows: Vec<Vec<Option<String>>> = twoda.iter().map(|row| row.to_vec()).collect();
64 Self {
65 file_type: twoda.file_type,
66 file_version: twoda.file_version,
67 default_value: twoda.default_value,
68 columns: twoda.columns,
69 rows,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Deserialize, Serialize)]
76#[serde(from = "TwoDARepr", into = "TwoDARepr")]
77pub struct TwoDA {
78 pub file_type: FixedSizeString<4>,
80 pub file_version: FixedSizeString<4>,
82 pub default_value: Option<String>,
83 columns: Vec<String>,
84 column_lookup: HashMap<String, usize>,
85 data: Vec<Option<String>>,
86 merge_meta: Option<MergeMetadata>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum MergeAction {
91 Ignore,
92 Expect,
93 Set,
94}
95
96#[derive(Debug, Clone)]
97struct MergeMetadata {
98 merge_actions: BTreeMap<usize, Vec<MergeAction>>,
100}
101
102impl TwoDA {
103 pub fn from_str(input: &str, repair: bool) -> IResult<&str, Self, VerboseError<&str>> {
109 let (input, twoda_header) = ncc::not_line_ending(input)?;
116 let (input, _) = ncc::line_ending(input)?;
117 let file_type = FixedSizeString::<4>::from_str(
118 twoda_header
119 .get(0..4)
120 .ok_or(nom_context_error("Missing file type", input))?,
121 )
122 .unwrap(); let file_version = FixedSizeString::<4>::from_str(
124 twoda_header
125 .get(4..8)
126 .ok_or(nom_context_error("Missing file version", input))?,
127 )
128 .unwrap(); let mut merge_meta = if file_type == "2DAM" {
131 Some(MergeMetadata {
132 merge_actions: BTreeMap::new(),
133 })
134 } else {
135 None
136 };
137
138 enum ParserState {
139 Defaults,
140 Headers,
141 Rows,
142 }
143
144 let mut twoda_data = vec![];
146 let mut state = ParserState::Defaults;
147 let mut default_value = None;
148 let mut columns = vec![];
149 let mut input = input;
150 while input.len() > 0 {
151 let line;
153 (input, line) = ncc::not_line_ending(input)?;
154 if input.len() > 0 {
155 (input, _) = ncc::line_ending(input)?;
156 }
157
158 if line.is_empty() || line.chars().all(|c| c.is_whitespace()) {
160 continue;
161 }
162
163 if let ParserState::Defaults = state {
165 if line.starts_with("DEFAULT:") {
166 (_, default_value) = Self::field_parser(line)?;
167 state = ParserState::Headers;
168 continue;
169 } else {
170 state = ParserState::Headers;
171 }
173 }
174
175 match state {
176 ParserState::Defaults => panic!("handled previously"),
177 ParserState::Headers => {
178 columns = Self::parse_row(line)?
179 .1
180 .into_iter()
181 .map(|c| c.unwrap_or("".to_string()))
182 .collect::<Vec<_>>();
183 if columns.len() == 0 {
184 return Err(nom_context_error("no columns detected", input));
185 }
186 state = ParserState::Rows;
187 }
188 ParserState::Rows => {
189 let (mut row_data, row_merge_actions): (_, Option<Vec<_>>) =
190 if merge_meta.is_none() {
191 (Self::parse_row(line)?.1, None)
192 } else {
193 Self::parse_merge_row(line)?.1
194 };
195
196 if row_data.is_empty() {
197 return Err(nom_context_error("missing row number", line));
198 }
199
200 let row_index_res = row_data.remove(0).map(|s| s.parse::<usize>());
201
202 let row_index = if let Some(Ok(idx)) = row_index_res {
203 idx
204 } else if repair {
205 0
206 } else {
207 return Err(nom_context_error("failed to parse row number", line));
208 };
209
210 if row_data.len() != columns.len() {
211 if repair {
212 row_data.resize(columns.len(), None);
213 } else {
214 return Err(nom_context_error("Invalid number of columns", line));
215 }
216 }
217
218 if let Some(ref mut merge_meta) = merge_meta {
219 let start = row_index * columns.len();
221 let end = start + columns.len();
222 if twoda_data.len() < end {
223 twoda_data.resize(end, None);
224 }
225
226 twoda_data[start..end].clone_from_slice(&row_data);
227
228 if let Some(mut row_merge_actions) = row_merge_actions {
230 row_merge_actions.remove(0);
232
233 if row_merge_actions.len() != row_data.len() {
234 row_merge_actions.resize(row_data.len(), MergeAction::Ignore);
235 }
236 merge_meta
237 .merge_actions
238 .insert(row_index, row_merge_actions);
239 }
240 } else {
241 if row_index != twoda_data.len() / columns.len() && !repair {
242 return Err(nom_context_error("Inconsistent row index", line));
243 }
244
245 twoda_data.extend_from_slice(&row_data);
247 }
248 }
249 }
250 }
251
252 let column_lookup = columns
253 .iter()
254 .enumerate()
255 .map(|(i, col)| (col.to_lowercase(), i))
256 .collect::<HashMap<_, _>>();
257
258 Ok((
259 input,
260 Self {
261 file_type,
262 file_version,
263 default_value,
264 columns,
265 column_lookup,
266 data: twoda_data,
267 merge_meta,
268 },
269 ))
270 }
271 pub fn to_string(&self, compact: bool) -> String {
278 let mut ret = String::new();
279 ret.push_str("2DA V2.0\n\n");
280 ret.push_str(&Self::encode_rows(
281 &self.iter().enumerate().collect::<Vec<_>>(),
282 Some(&self.columns),
283 compact,
284 ));
285
286 ret
287 }
288
289 pub fn get_columns(&self) -> &Vec<String> {
291 &self.columns
292 }
293
294 pub fn len_cols(&self) -> usize {
296 self.columns.len()
297 }
298
299 pub fn len_rows(&self) -> usize {
301 self.data.len() / self.columns.len()
302 }
303
304 pub fn resize_rows(&mut self, len: usize) {
306 self.data.resize(len * self.len_cols(), None)
307 }
308
309 pub fn get_row(&self, row: usize) -> Option<&[Option<String>]> {
311 let start = row * self.columns.len();
312 let end = start + self.columns.len();
313 self.data.get(start..end)
314 }
315
316 pub fn get_row_mut(&mut self, row: usize) -> Option<&mut [Option<String>]> {
318 let start = row * self.columns.len();
319 let end = start + self.columns.len();
320 self.data.get_mut(start..end)
321 }
322
323 pub fn get_row_col(&self, row: usize, column: usize) -> &Option<String> {
325 let index = row * self.columns.len() + column;
326 if let Some(field) = self.data.get(index) {
327 field
328 } else {
329 &None
330 }
331 }
332
333 pub fn get_row_label(&self, row: usize, label: &str) -> &Option<String> {
335 if let Some(col) = self.column_lookup.get(&label.to_string().to_lowercase()) {
337 self.get_row_col(row, *col)
338 } else {
339 &None
340 }
341 }
342
343 fn str_encoded_len<S: AsRef<str>>(s: S) -> usize {
344 let s = s.as_ref();
345 if s == "****" || s.chars().any(char::is_whitespace) {
346 let mut len = 2usize; for c in s.chars() {
349 match c {
350 '\\' => len += 1,
351 '\n' => len += 1,
352 '\t' => len += 1,
353 '"' => len += 1,
354 _ => {}
355 }
356 len += c.len_utf8();
357 }
358 len
359 } else {
360 s.len()
362 }
363 }
364 pub fn field_encoded_len<S: AsRef<str>>(field: &Option<S>) -> usize {
366 match field.as_ref() {
367 None => 4,
368 Some(s) => Self::str_encoded_len(s),
369 }
370 }
371
372 pub fn encode_field<S: AsRef<str>>(field: &Option<S>) -> String {
374 match field {
376 None => "****".to_string(),
377 Some(s) => {
378 let s = s.as_ref();
379 if s == "****" || s.chars().any(char::is_whitespace) {
380 let mut res = "\"".to_string();
382 for c in s.chars() {
383 match c {
384 '\\' => res.push_str(r"\\"),
385 '\n' => res.push_str(r"\n"),
386 '\t' => res.push_str(r"\t"),
387 '"' => res.push_str("\\\""),
388 _ => res.push(c),
389 }
390 }
391 res.push('\"');
392 res
393 } else {
394 s.to_string()
396 }
397 }
398 }
399 }
400
401 fn field_parser(input: &str) -> IResult<&str, Option<String>, VerboseError<&str>> {
402 if input.is_empty() || input.chars().all(char::is_whitespace) {
403 return Err(nom_context_error("End of input", input));
404 }
405
406 if let Some(input) = input.strip_prefix('"') {
407 let mut res = String::new();
408 let mut escaped = false;
409 let mut input_end = 0usize;
410 for c in input.chars() {
411 input_end += c.len_utf8();
412 if escaped {
413 escaped = false;
414 match c {
415 '\\' => res.push('\\'),
416 'n' => res.push('\n'),
417 't' => res.push('\t'),
418 '"' => res.push('"'),
419 c => res.extend(&['\\', c]),
420 }
421 } else {
422 match c {
423 '\\' => escaped = true,
424 '"' => break,
425 c => res.push(c),
426 }
427 }
428 }
429 let input = &input[input_end..];
430 Ok((input, Some(res)))
432 } else {
433 let end = input.find(char::is_whitespace);
434 let (input, res) = if let Some(end) = end {
435 (&input[end..], &input[..end])
436 } else {
437 (&input[input.len()..], input)
438 };
439
440 if res == "****" {
442 Ok((input, None))
443 } else {
444 Ok((input, Some(res.to_string())))
445 }
446 }
447 }
448
449 pub fn parse_row(input: &str) -> IResult<&str, Vec<Option<String>>, VerboseError<&str>> {
451 let (input, _) = ncc::space0(input)?;
452 nom::multi::separated_list0(ncc::space1, Self::field_parser)(input)
453 }
454
455 fn parse_merge_row(
457 input: &str,
458 ) -> IResult<&str, (Vec<Option<String>>, Option<Vec<MergeAction>>), VerboseError<&str>> {
459 let (input, _) = ncc::space0(input)?;
460
461 if input.starts_with('?') {
462 let (input, fields_meta) = nom::multi::separated_list0(
463 ncc::space1,
464 nom::sequence::tuple((ncc::one_of("?=-"), Self::field_parser)),
465 )(input)?;
466
467 let (mda, fields) = fields_meta
468 .into_iter()
469 .map(|(c, field)| {
470 let action = match c {
471 '?' => MergeAction::Expect,
472 '-' => MergeAction::Ignore,
473 '=' => MergeAction::Set,
474 _ => panic!(),
475 };
476 (action, field)
477 })
478 .unzip();
479
480 Ok((input, (fields, Some(mda))))
481
482 } else {
484 let (input, fields) = Self::parse_row(input)?;
485 Ok((input, (fields, None)))
486 }
487 }
488
489 pub fn encode_row<S: AsRef<str>>(
497 row_index: usize,
498 row_data: &[Option<S>],
499 column_sizes: Option<&[usize]>,
500 compact: bool,
501 ) -> String {
502 let get_col_size = |i: usize| {
503 if let Some(column_sizes) = column_sizes {
504 assert_eq!(row_data.len() + 1, column_sizes.len());
505 column_sizes[i]
506 } else {
507 0
508 }
509 };
510
511 let mut ret = String::new();
512 ret.push_str(&format!("{:<w$}", row_index, w = get_col_size(0)));
513
514 let end = if compact {
515 let last_column = row_data
516 .iter()
517 .enumerate()
518 .rfind(|&(_, field)| field.is_some());
519 if let Some((i, _)) = last_column {
520 i + 1
521 } else {
522 1
523 }
524 } else {
525 row_data.len()
526 };
527
528 ret.push_str(
529 &row_data[..end]
530 .iter()
531 .enumerate()
532 .map(|(i, field)| {
533 format!("{:<w$}", Self::encode_field(field), w = get_col_size(i + 1))
534 })
535 .fold(String::new(), |mut res, s| {
536 res.push(' ');
537 res.push_str(&s);
538 res
539 }),
540 );
541 ret
542 }
543 pub fn encode_rows<S1: AsRef<str>, S2: AsRef<str>>(
550 rows: &[(usize, &[Option<S1>])],
551 columns: Option<&[S2]>,
552 compact: bool,
553 ) -> String {
554 let mut column_sizes = vec![
555 0usize;
556 rows.iter()
557 .map(|(_, cols)| cols.len())
558 .max()
559 .expect("no rows to encode")
560 + 1
561 ];
562
563 if !compact {
564 column_sizes[0] = rows
565 .iter()
566 .map(|(i, _)| i)
567 .max()
568 .expect("no rows to encode")
569 .ilog10() as usize
570 + 1;
571 if let Some(columns) = columns {
572 for (i, column) in columns.iter().enumerate() {
573 if i + 1 == columns.len() {
574 continue; }
576 column_sizes[i + 1] = max(column_sizes[i + 1], Self::str_encoded_len(column));
577 }
578 }
579
580 for (_irow, row) in rows {
581 for (ifield, field) in row.iter().enumerate() {
582 if ifield + 2 == column_sizes.len() {
583 continue; }
585 column_sizes[ifield + 1] =
586 max(column_sizes[ifield + 1], Self::field_encoded_len(field));
587 }
588 }
589 }
590
591 let mut ret = String::new();
592
593 if let Some(columns) = columns {
594 ret.push_str(&" ".repeat(column_sizes[0]));
595 ret.push_str(
596 &columns
597 .iter()
598 .enumerate()
599 .map(|(i, col)| format!("{:<w$}", col.as_ref(), w = column_sizes[i + 1]))
600 .fold(String::new(), |mut res, s| {
601 res.push(' ');
602 res.push_str(&s);
603 res
604 }),
605 );
606 ret.push('\n');
607 }
608
609 for (i, (irow, row)) in rows.iter().enumerate() {
610 ret.push_str(&Self::encode_row(*irow, row, Some(&column_sizes), compact));
611 if i + 1 < rows.len() {
612 ret.push('\n');
613 }
614 }
615 ret
616 }
617
618 pub fn iter(&self) -> std::slice::Chunks<'_, Option<String>> {
620 self.data.chunks(self.columns.len())
621 }
622
623 pub fn is_merge(&self) -> bool {
625 self.merge_meta.is_some()
626 }
627
628 pub fn get_row_merge_actions(&self, row_index: usize) -> Option<&[MergeAction]> {
630 if let Some(meta) = self.merge_meta.as_ref() {
631 meta.merge_actions.get(&row_index).map(Vec::as_slice)
632 } else {
633 None
634 }
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn test_twoda() {
644 let twoda_bytes = include_str!("../unittest/restsys_wm_tables.2da");
645
646 let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
647 assert_eq!(
648 twoda.get_row_label(0, "TableName"),
649 &Some("INVALID_TABLE".to_string())
650 );
651 assert_eq!(
652 twoda.get_row_label(0, "Comment"),
653 &Some(",start w/ comma (makes excel add quotes!) Dont_change this row!".to_string())
654 );
655 assert_eq!(
656 twoda.get_row_label(0, "FeedBackStrRefSuccess"),
657 &Some("83306".to_string())
658 );
659 assert_eq!(twoda.get_row_label(0, "Enc3_ResRefs"), &None);
660 assert_eq!(
661 twoda.get_row_label(21, "TableName"),
662 &Some("b_wells_of_lurue".to_string())
663 );
664 assert_eq!(
665 twoda.get_row_label(21, "Enc3_ResRefs"),
666 &Some("b10_earth_elemental,b10_orglash".to_string())
667 );
668
669 assert!(TwoDA::from_str(twoda_bytes, false).is_err());
672 assert_eq!(
673 twoda.get_row_label(10, "TableName"),
674 &Some("c_cave".to_string())
675 );
676 assert_eq!(
677 twoda.get_row_label(10, "Comment"),
678 &Some(",beasts".to_string())
679 );
680 assert_eq!(
681 twoda.get_row_label(10, "Enc2_Prob"),
682 &Some("50".to_string())
683 );
684 assert_eq!(twoda.get_row_label(10, "Enc3_ResRefs"), &None);
685 }
686 #[test]
687 fn test_bloodmagus() {
688 let twoda_bytes = include_str!("../unittest/cls_feat_bloodmagus.2DA");
690 let twoda = TwoDA::from_str(twoda_bytes, false).unwrap().1;
691
692 assert_eq!(twoda.columns.len(), 5);
693 assert_eq!(twoda.len_rows(), 22);
694
695 assert_eq!(twoda.columns[0], "FeatLabel");
696 assert_eq!(
697 twoda.get_row_label(0, "FeatLabel"),
698 &Some("FEAT_EPIC_SPELL_DRAGON_KNIGHT".to_string())
699 );
700 assert_eq!(twoda.columns[1], "FeatIndex");
701 assert_eq!(
702 twoda.get_row_label(8, "FeatIndex"),
703 &Some("2784".to_string())
704 );
705 }
706 #[test]
707 fn test_savthr_nw9a() {
708 let twoda_bytes = include_str!("../unittest/cls_savthr_nw9a.2DA");
710 let twoda = TwoDA::from_str(twoda_bytes, false).unwrap().1;
711
712 assert_eq!(twoda.columns.len(), 4);
713 assert_eq!(twoda.len_rows(), 5);
714
715 assert_eq!(twoda.columns[0], "Level");
716
717 assert_eq!(
718 twoda.get_row(0).unwrap(),
719 &[
720 Some("1".to_string()),
721 Some("2".to_string()),
722 Some("0".to_string()),
723 Some("2".to_string())
724 ]
725 );
726 assert_eq!(
727 twoda.get_row(1).unwrap(),
728 &[
729 Some("2".to_string()),
730 Some("3".to_string()),
731 Some("0".to_string()),
732 Some("3".to_string())
733 ]
734 );
735 assert_eq!(
736 twoda.get_row(3).unwrap(),
737 &[
738 Some("4".to_string()),
739 Some("4".to_string()),
740 Some("1".to_string()),
741 Some("4".to_string())
742 ]
743 );
744 }
745 #[test]
746 fn test_nwn_2_colors() {
747 let twoda_bytes = include_str!("../unittest/NWN2_Colors.2DA");
749 let twoda = TwoDA::from_str(twoda_bytes, false).unwrap().1;
750
751 assert_eq!(twoda.columns.as_slice(), &["ColorName", "ColorHEX"]);
752 assert_eq!(twoda.len_rows(), 153);
753
754 assert_eq!(
755 twoda.get_row(0).unwrap(),
756 &[Some("AliceBlue".to_string()), Some("F0F8FF".to_string()),]
757 );
758 assert_eq!(
759 twoda.get_row(152).unwrap(),
760 &[
761 Some("HotbarItmNoUse".to_string()),
762 Some("FF0000".to_string()),
763 ]
764 );
765 }
766 #[test]
767 fn test_cls_bfeat_dwdef() {
768 let twoda_bytes = include_str!("../unittest/cls_bfeat_dwdef.2da");
769 let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
770
771 assert_eq!(twoda.columns.as_slice(), &["Bonus"]);
772 assert_eq!(twoda.len_rows(), 60);
773 }
774 #[test]
775 fn test_combatmodes() {
776 let twoda_bytes = include_str!("../unittest/combatmodes.2da");
778 let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
779
780 assert_eq!(
781 twoda.columns.as_slice(),
782 &[
783 "CombatMode",
784 "ActivatedName",
785 "DeactivatedName",
786 "NeedsTarget"
787 ]
788 );
789 assert_eq!(twoda.len_rows(), 19);
790 assert_eq!(
791 twoda.get_row(0).unwrap(),
792 &[
793 Some("Detect".to_string()),
794 Some("58275".to_string()),
795 Some("58276".to_string()),
796 Some("0".to_string())
797 ]
798 );
799 assert_eq!(
800 twoda.get_row(13).unwrap(),
801 &[
802 Some("Taunt".to_string()),
803 Some("11468".to_string()),
804 Some("11468".to_string()),
805 Some("0".to_string())
806 ]
807 );
808 assert_eq!(
809 twoda.get_row(14).unwrap(),
810 &[Some("Attack".to_string()), None, None, None]
811 );
812 assert_eq!(
813 twoda.get_row(15).unwrap(),
814 &[Some("Power_Attack".to_string()), None, None, None]
815 );
816 assert_eq!(
817 twoda.get_row(18).unwrap(),
818 &[
819 Some("stealth,".to_string()),
820 Some("counterspell,".to_string()),
821 Some("defensive_Cast,".to_string()),
822 Some("taunt".to_string())
823 ]
824 );
825 }
826 #[test]
827 fn test_creaturespeed() {
828 let twoda_bytes = include_str!("../unittest/creaturespeed.2da");
830 let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
831
832 assert_eq!(
833 twoda.columns.as_slice(),
834 &["Label", "Name", "2DAName", "WALKRATE", "RUNRATE"]
835 );
836 assert_eq!(twoda.len_rows(), 9);
837
838 assert_eq!(
839 twoda.get_row(0).unwrap(),
840 &[
841 Some("PC_Movement".to_string()),
842 None,
843 Some("PLAYER".to_string()),
844 Some("2.00".to_string()),
845 Some("4.00".to_string())
846 ]
847 );
848 }
849 #[test]
850 fn test_des_treas_items() {
851 let twoda_bytes = include_str!("../unittest/des_treas_items.2da");
853 let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
854
855 assert_eq!(twoda.len_cols(), 59);
856 assert_eq!(twoda.len_rows(), 25);
857
858 assert_eq!(twoda.get_row_col(0, 0), &Some("2".to_string()));
859 assert_eq!(twoda.get_row_col(1, 0), &Some("NW_WSWMGS003".to_string()));
860 }
861 #[test]
869 fn test_twodam() {
870 let twoda_bytes = include_str!("../unittest/restsys_wm_tables_merge.2da");
871 let twoda = TwoDA::from_str(twoda_bytes, true).unwrap().1;
872 assert_eq!(
873 twoda.get_row_label(11, "TableName"),
874 &Some("conflicting".to_string())
875 );
876 assert_eq!(twoda.get_row_merge_actions(11), None);
877
878 assert_eq!(
879 twoda.get_row_label(12, "TableName"),
880 &Some("force".to_string())
881 );
882 assert_eq!(
883 twoda.get_row_merge_actions(12),
884 Some(
885 [
886 MergeAction::Expect,
887 MergeAction::Set,
888 MergeAction::Set,
889 MergeAction::Ignore,
890 MergeAction::Ignore,
891 MergeAction::Ignore,
892 MergeAction::Ignore,
893 MergeAction::Ignore,
894 MergeAction::Ignore
895 ]
896 .as_slice()
897 )
898 );
899
900 assert_eq!(
901 twoda.get_row_label(19, "Comment"),
902 &Some(",Death Knights, Mummified Priests, Dread Wraiths".to_string())
903 );
904 assert_eq!(
905 twoda.get_row_label(19, "FeedBackStrRefSuccess"),
906 &Some("hello world".to_string())
907 );
908 assert_eq!(
909 twoda.get_row_merge_actions(19),
910 Some(
911 [
912 MergeAction::Ignore,
913 MergeAction::Expect,
914 MergeAction::Set,
915 MergeAction::Ignore,
916 MergeAction::Ignore,
917 MergeAction::Ignore,
918 MergeAction::Ignore,
919 MergeAction::Ignore,
920 MergeAction::Ignore
921 ]
922 .as_slice()
923 )
924 );
925
926 assert_eq!(
927 twoda.get_row_label(20, "TableName"),
928 &Some("b_lower_vault".to_string())
929 );
930 assert_eq!(
931 twoda.get_row_label(20, "FeedBackStrRefFail"),
932 &Some("e1rref".to_string())
933 );
934 assert_eq!(twoda.get_row_label(20, "Enc1_ResRefs"), &None);
935 assert_eq!(
936 twoda.get_row_merge_actions(20),
937 Some(
938 [
939 MergeAction::Expect,
940 MergeAction::Ignore,
941 MergeAction::Ignore,
942 MergeAction::Set,
943 MergeAction::Set,
944 MergeAction::Set,
945 MergeAction::Set,
946 MergeAction::Set,
947 MergeAction::Set
948 ]
949 .as_slice()
950 )
951 );
952
953 assert_eq!(
954 twoda.get_row_label(22, "TableName"),
955 &Some("non".to_string())
956 );
957 assert_eq!(twoda.get_row_label(22, "Enc1_ResRefs"), &None);
958 assert_eq!(twoda.get_row_merge_actions(22), None);
959 }
960}