1use heck::ToSnakeCase;
2
3use crate::{Column, FieldNameMode};
4
5pub fn to_a1_col_key(index: usize) -> String {
6 let mut result = String::new();
7 let mut n = index as i32; while n >= 0 {
10 let remainder = (n % 26) as u8;
11 result.push((b'a' + remainder) as char);
12 n = (n / 26) - 1;
13 }
14 result.chars().rev().collect()
15}
16
17pub fn to_padded_col_key(prefix: &str, index: usize, num_cols: usize) -> String {
18 build_padded_col_key(prefix, false, index, num_cols)
19}
20
21pub fn to_padded_col_suffix(prefix: &str, index: usize, num_cols: usize) -> String {
22 build_padded_col_key(prefix, true, index, num_cols)
23}
24
25fn build_padded_col_key(prefix: &str, underscore: bool, index: usize, num_cols: usize) -> String {
26 let width = if num_cols < 100 {
27 2
28 } else if num_cols < 1000 {
29 3
30 } else if num_cols < 10000 {
31 4
32 } else {
33 5
34 };
35 let num = index + 1;
36 let separator = if underscore { "_" } else { "" };
37 format!("{}{}{:0width$}", prefix, separator, num, width = width)
38}
39
40pub fn to_c01_col_key(index: usize, num_cols: usize) -> String {
41 to_padded_col_key("c", index, num_cols)
42}
43
44pub fn to_head_key(index: usize, field_mode: &FieldNameMode, num_cols: usize) -> String {
45 if field_mode.use_c01() {
46 to_c01_col_key(index, num_cols)
47 } else {
48 to_a1_col_key(index)
49 }
50}
51
52pub fn to_head_key_default(index: usize) -> String {
53 to_c01_col_key(index, 1000)
54}
55
56pub fn build_header_keys(
58 first_row: &[String],
59 columns: &[Column],
60 field_mode: &FieldNameMode,
61) -> Vec<String> {
62 let mut h_index = 0;
63 let mut headers: Vec<String> = vec![];
64 let num_cols = first_row.len();
65 let keep_headers = field_mode.keep_headers();
66 for h_row in first_row.to_owned() {
67 let sn = h_row.to_snake_case();
68 let mut has_override = false;
69 if let Some(col) = columns.get(h_index) {
70 if let Some(k_str) = &col.key {
72 let h_key = if headers.contains(&k_str.to_string()) {
73 to_padded_col_suffix(k_str, h_index, num_cols)
74 } else {
75 k_str.to_string()
76 };
77 headers.push(h_key);
78 has_override = true;
79 }
80 }
81 if !has_override {
82 if keep_headers && sn.len() > 0 {
83 let sn_key = if headers.contains(&sn) {
84 to_padded_col_suffix(&sn, h_index, num_cols)
85 } else {
86 sn
87 };
88 headers.push(sn_key);
89 } else {
90 headers.push(to_head_key(h_index, field_mode, num_cols));
91 }
92 }
93 h_index += 1;
94 }
95 headers
96}
97
98pub fn natural_column_keys(first_row: &[String], field_mode: &FieldNameMode) -> Vec<String> {
102 build_header_keys(first_row, &[], field_mode)
103}
104
105pub fn resolve_columns(columns: &[Column], natural_keys: &[String]) -> Vec<Column> {
115 let mut resolved: Vec<Column> = natural_keys.iter().map(|_| Column::new(None)).collect();
116 for (i, col) in columns.iter().enumerate() {
117 if col.source_key.is_none() {
118 if let Some(slot) = resolved.get_mut(i) {
119 *slot = col.clone();
120 }
121 }
122 }
123 for col in columns {
124 if let Some(src) = &col.source_key {
125 let target = src.to_snake_case();
126 if let Some(idx) = natural_keys.iter().position(|k| k.to_snake_case() == target) {
127 resolved[idx] = col.clone();
128 }
129 }
130 }
131 resolved
132}
133
134pub fn build_a1_headers(first_row: &[String]) -> Vec<String> {
136 build_header_keys(first_row, &[], &FieldNameMode::A1)
137}
138
139pub fn build_c01_headers(first_row: &[String]) -> Vec<String> {
141 build_header_keys(first_row, &[], &FieldNameMode::NumPadded)
142}
143
144pub(crate) fn is_not_header_row(
153 raw_values: &[String],
154 row_index: usize,
155 headers: &[String],
156) -> bool {
157 if row_index > 0 {
158 return true;
159 }
160 let mut num_matched: usize = 0;
161 for (h_index, hk) in headers.iter().enumerate() {
162 let sn = hk.to_snake_case();
163 if let Some(val) = raw_values.get(h_index) {
164 if val.to_snake_case() == sn || sn.len() == 0 {
165 num_matched += 1;
166 }
167 }
168 }
169 num_matched < headers.len()
170}
171
172#[cfg(test)]
173mod tests {
174
175 use crate::Format;
176
177 use super::*;
178
179 #[test]
180 fn test_cell_letters_1() {
181 assert_eq!(to_a1_col_key(26), "aa");
182 }
183
184 #[test]
185 fn test_cell_letters_2() {
186 assert_eq!(to_a1_col_key(701), "zz");
187 }
188
189 #[test]
190 fn test_cell_letters_3() {
191 assert_eq!(to_a1_col_key(702), "aaa");
192 }
193
194 #[test]
195 fn test_cell_letters_4() {
196 assert_eq!(to_c01_col_key(8, 60), "c09");
197 }
198
199 #[test]
200 fn test_cell_letters_5() {
201 assert_eq!(to_c01_col_key(20, 750), "c021");
202 }
203
204 #[test]
205 fn test_cell_letters_6() {
206 assert_eq!(to_c01_col_key(20, 2000), "c0021");
207 }
208
209 #[test]
210 fn test_is_not_header_row_uses_raw_text_not_coerced_values() {
211 let headers = vec!["sku".to_string(), "weight".to_string()];
217 let header_row_raw = vec!["sku".to_string(), "weight".to_string()];
219 assert_eq!(is_not_header_row(&header_row_raw, 0, &headers), false);
220
221 let data_row_raw = vec!["SKU001".to_string(), "58.2".to_string()];
223 assert_eq!(is_not_header_row(&data_row_raw, 0, &headers), true);
224
225 assert_eq!(is_not_header_row(&header_row_raw, 1, &headers), true);
227 }
228
229 #[test]
230 fn test_resolve_columns_matches_by_source_key_regardless_of_position() {
231 let first_row = ["full_name", "height_cm", "weight_kg"].map(|s| s.to_string());
234 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
235 assert_eq!(natural_keys, vec!["full_name", "height_cm", "weight_kg"]);
236
237 let overrides = vec![
238 Column::from_source_key_with_format("weight_kg", Some("weight"), Format::Integer, None, false, false),
239 ];
240 let resolved = resolve_columns(&overrides, &natural_keys);
241 assert_eq!(resolved.len(), 3);
242 assert!(resolved[0].key.is_none());
244 assert!(resolved[1].key.is_none());
245 assert_eq!(resolved[2].key_name(), "weight");
247 assert_eq!(resolved[2].format.to_string(), "integer");
248
249 let headers = build_header_keys(&first_row, &resolved, &FieldNameMode::AutoA1);
250 assert_eq!(headers, vec!["full_name", "height_cm", "weight"]);
251 }
252
253 #[test]
254 fn test_resolve_columns_source_key_match_is_snake_cased() {
255 let first_row = ["Weight (Kg)".to_string()];
258 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
259 assert_eq!(natural_keys, vec!["weight_kg"]);
260
261 let overrides = vec![
262 Column::from_source_key_with_format("Weight Kg", Some("weight"), Format::Auto, None, false, false),
263 ];
264 let resolved = resolve_columns(&overrides, &natural_keys);
265 assert_eq!(resolved[0].key_name(), "weight");
266 }
267
268 #[test]
269 fn test_resolve_columns_unmatched_source_key_is_a_no_op() {
270 let first_row = ["full_name", "height_cm"].map(|s| s.to_string());
271 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
272 let overrides = vec![
273 Column::from_source_key_with_format("nonexistent_field", Some("oops"), Format::Auto, None, false, false),
274 ];
275 let resolved = resolve_columns(&overrides, &natural_keys);
276 assert!(resolved[0].key.is_none());
278 assert!(resolved[1].key.is_none());
279 }
280
281 #[test]
282 fn test_resolve_columns_still_supports_positional_overrides() {
283 let first_row = ["a", "b", "c"].map(|s| s.to_string());
286 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
287 let overrides = vec![
288 Column::new(Some("first")),
289 Column::new(Some("second")),
290 ];
291 let resolved = resolve_columns(&overrides, &natural_keys);
292 assert_eq!(resolved[0].key_name(), "first");
293 assert_eq!(resolved[1].key_name(), "second");
294 assert!(resolved[2].key.is_none());
295 }
296
297 #[test]
298 fn test_first_row() {
299 let first_row = ["Viscosity", "Rating", "", ""].map(|s| s.to_string());
301 let cols = vec![
302 Column::from_key_ref_with_format(None, Format::Float, None, false, false),
303 Column::from_key_ref_with_format(
304 Some("points"),
305 Format::Decimal(3),
306 None,
307 false,
308 false,
309 ),
310 Column::from_key_ref_with_format(Some("adjusted"), Format::Float, None, false, false),
311 ];
312 let headers = build_header_keys(&first_row, &cols, &FieldNameMode::AutoA1);
313 assert_eq!(headers.get(0).unwrap(), "viscosity");
315 assert_eq!(headers.get(1).unwrap(), "points");
317 assert_eq!(headers.get(2).unwrap(), "adjusted");
319 assert_eq!(headers.get(3).unwrap(), "d");
321 }
322
323 #[test]
324 fn test_headers_a1_override() {
325 let first_row = ["Viscosity", "Rating", "Weighted", "Class"].map(|s| s.to_string());
327
328 let headers = build_a1_headers(&first_row);
329 assert_eq!(headers.get(0).unwrap(), "a");
331 assert_eq!(headers.get(3).unwrap(), "d");
333 }
334
335 #[test]
336 fn test_headers_c01_override() {
337 let first_row: Vec<String> = (0..200)
339 .map(|x| {
340 [
341 char::from_u32(65 + (x % 26)).unwrap_or('_').to_string(),
342 (x * 3 * 1).to_string(),
343 ]
344 .concat()
345 })
346 .collect();
347
348 let headers = build_c01_headers(&first_row);
349 assert_eq!(headers.get(0).unwrap(), "c001");
351 assert_eq!(headers.get(3).unwrap(), "c004");
353 }
354}