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 headers: Vec<String> = vec![];
63 let num_cols = first_row.len();
64 let keep_headers = field_mode.keep_headers();
65 for (h_index, h_row) in first_row.iter().enumerate() {
66 let sn = h_row.to_snake_case();
67 let mut has_override = false;
68 if let Some(col) = columns.get(h_index) {
69 if let Some(segment) = &col.key {
71 let k_str = segment.to_string();
72 let h_key = if headers.contains(&k_str) {
73 to_padded_col_suffix(&k_str, h_index, num_cols)
74 } else {
75 k_str
76 };
77 headers.push(h_key);
78 has_override = true;
79 }
80 }
81 if !has_override {
82 if keep_headers && !sn.is_empty() {
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 }
94 headers
95}
96
97pub fn combine_header_rows(rows: &[Vec<String>]) -> Vec<String> {
117 let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
118 let filled: Vec<Vec<String>> = rows.iter().map(|row| forward_fill_row(row, num_cols)).collect();
119 (0..num_cols)
120 .map(|col_index| {
121 filled
122 .iter()
123 .filter_map(|row| row.get(col_index))
124 .filter(|v| !v.is_empty())
125 .cloned()
126 .collect::<Vec<String>>()
127 .join("_")
128 })
129 .collect()
130}
131
132fn forward_fill_row(row: &[String], num_cols: usize) -> Vec<String> {
133 let mut result = Vec::with_capacity(num_cols);
134 let mut carry: Option<&str> = None;
135 for i in 0..num_cols {
136 let cell = row.get(i).map(|s| s.trim()).unwrap_or("");
137 if !cell.is_empty() {
138 carry = Some(cell);
139 result.push(cell.to_string());
140 } else {
141 result.push(carry.unwrap_or("").to_string());
142 }
143 }
144 result
145}
146
147pub fn natural_column_keys(first_row: &[String], field_mode: &FieldNameMode) -> Vec<String> {
151 build_header_keys(first_row, &[], field_mode)
152}
153
154pub fn resolve_columns(columns: &[Column], natural_keys: &[String]) -> Vec<Column> {
164 let mut resolved: Vec<Column> = natural_keys.iter().map(|_| Column::new(None)).collect();
165 for (i, col) in columns.iter().enumerate() {
166 if col.source_key.is_none() {
167 if let Some(slot) = resolved.get_mut(i) {
168 *slot = col.clone();
169 }
170 }
171 }
172 for col in columns {
173 if let Some(src) = &col.source_key {
174 let target = src.to_snake_case();
175 if let Some(idx) = natural_keys.iter().position(|k| k.to_snake_case() == target) {
176 resolved[idx] = col.clone();
177 }
178 }
179 }
180 resolved
181}
182
183pub fn build_a1_headers(first_row: &[String]) -> Vec<String> {
185 build_header_keys(first_row, &[], &FieldNameMode::A1)
186}
187
188pub fn build_c01_headers(first_row: &[String]) -> Vec<String> {
190 build_header_keys(first_row, &[], &FieldNameMode::NumPadded)
191}
192
193pub(crate) fn is_not_header_row(
202 raw_values: &[String],
203 row_index: usize,
204 headers: &[String],
205) -> bool {
206 if row_index > 0 {
207 return true;
208 }
209 let mut num_matched: usize = 0;
210 for (h_index, hk) in headers.iter().enumerate() {
211 let sn = hk.to_snake_case();
212 if let Some(val) = raw_values.get(h_index) {
213 if val.to_snake_case() == sn || sn.is_empty() {
214 num_matched += 1;
215 }
216 }
217 }
218 num_matched < headers.len()
219}
220
221#[cfg(test)]
222mod tests {
223
224 use crate::{DateTimeMode, Format};
225
226 use super::*;
227
228 #[test]
229 fn test_cell_letters_1() {
230 assert_eq!(to_a1_col_key(26), "aa");
231 }
232
233 fn strs(vals: &[&str]) -> Vec<String> {
234 vals.iter().map(|s| s.to_string()).collect()
235 }
236
237 #[test]
238 fn test_combine_header_rows_forward_fills_a_single_merged_block() {
239 let rows = vec![strs(&["2015", "", ""]), strs(&["North", "Midlands", "South"])];
241 assert_eq!(
242 combine_header_rows(&rows),
243 strs(&["2015_North", "2015_Midlands", "2015_South"])
244 );
245 }
246
247 #[test]
248 fn test_combine_header_rows_handles_two_merged_blocks_in_the_same_row() {
249 let rows = vec![
250 strs(&["2015", "", "", "2025", "", ""]),
251 strs(&["North", "Midlands", "South", "North", "Midlands", "South"]),
252 ];
253 assert_eq!(
254 combine_header_rows(&rows),
255 strs(&[
256 "2015_North", "2015_Midlands", "2015_South",
257 "2025_North", "2025_Midlands", "2025_South"
258 ])
259 );
260 }
261
262 #[test]
263 fn test_combine_header_rows_leaves_a_leading_blank_with_nothing_to_inherit_empty() {
264 let rows = vec![
267 strs(&["", "2015", ""]),
268 strs(&["", "North", "South"]),
269 ];
270 assert_eq!(combine_header_rows(&rows), strs(&["", "2015_North", "2015_South"]));
271 }
272
273 #[test]
274 fn test_combine_header_rows_skips_a_row_that_contributes_nothing_for_one_column() {
275 let rows = vec![strs(&["country code", "2015", ""]), strs(&["", "North", "South"])];
278 assert_eq!(combine_header_rows(&rows), strs(&["country code", "2015_North", "2015_South"]));
279 }
280
281 #[test]
282 fn test_combine_header_rows_with_a_single_row_is_a_no_op() {
283 let rows = vec![strs(&["id", "name", "score"])];
285 assert_eq!(combine_header_rows(&rows), strs(&["id", "name", "score"]));
286 }
287
288 #[test]
289 fn test_cell_letters_2() {
290 assert_eq!(to_a1_col_key(701), "zz");
291 }
292
293 #[test]
294 fn test_cell_letters_3() {
295 assert_eq!(to_a1_col_key(702), "aaa");
296 }
297
298 #[test]
299 fn test_cell_letters_4() {
300 assert_eq!(to_c01_col_key(8, 60), "c09");
301 }
302
303 #[test]
304 fn test_cell_letters_5() {
305 assert_eq!(to_c01_col_key(20, 750), "c021");
306 }
307
308 #[test]
309 fn test_cell_letters_6() {
310 assert_eq!(to_c01_col_key(20, 2000), "c0021");
311 }
312
313 #[test]
314 fn test_is_not_header_row_uses_raw_text_not_coerced_values() {
315 let headers = vec!["sku".to_string(), "weight".to_string()];
321 let header_row_raw = vec!["sku".to_string(), "weight".to_string()];
323 assert!(!is_not_header_row(&header_row_raw, 0, &headers));
324
325 let data_row_raw = vec!["SKU001".to_string(), "58.2".to_string()];
327 assert!(is_not_header_row(&data_row_raw, 0, &headers));
328
329 assert!(is_not_header_row(&header_row_raw, 1, &headers));
331 }
332
333 #[test]
334 fn test_resolve_columns_matches_by_source_key_regardless_of_position() {
335 let first_row = ["full_name", "height_cm", "weight_kg"].map(|s| s.to_string());
338 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
339 assert_eq!(natural_keys, vec!["full_name", "height_cm", "weight_kg"]);
340
341 let overrides = vec![
342 Column::from_source_key_with_format("weight_kg", Some("weight"), Format::Integer, None, DateTimeMode::Full, false),
343 ];
344 let resolved = resolve_columns(&overrides, &natural_keys);
345 assert_eq!(resolved.len(), 3);
346 assert!(resolved[0].key.is_none());
348 assert!(resolved[1].key.is_none());
349 assert_eq!(resolved[2].key_name(), "weight");
351 assert_eq!(resolved[2].format.to_string(), "integer");
352
353 let headers = build_header_keys(&first_row, &resolved, &FieldNameMode::AutoA1);
354 assert_eq!(headers, vec!["full_name", "height_cm", "weight"]);
355 }
356
357 #[test]
358 fn test_resolve_columns_source_key_match_is_snake_cased() {
359 let first_row = ["Weight (Kg)".to_string()];
362 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
363 assert_eq!(natural_keys, vec!["weight_kg"]);
364
365 let overrides = vec![
366 Column::from_source_key_with_format("Weight Kg", Some("weight"), Format::Auto, None, DateTimeMode::Full, false),
367 ];
368 let resolved = resolve_columns(&overrides, &natural_keys);
369 assert_eq!(resolved[0].key_name(), "weight");
370 }
371
372 #[test]
373 fn test_resolve_columns_unmatched_source_key_is_a_no_op() {
374 let first_row = ["full_name", "height_cm"].map(|s| s.to_string());
375 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
376 let overrides = vec![
377 Column::from_source_key_with_format("nonexistent_field", Some("oops"), Format::Auto, None, DateTimeMode::Full, false),
378 ];
379 let resolved = resolve_columns(&overrides, &natural_keys);
380 assert!(resolved[0].key.is_none());
382 assert!(resolved[1].key.is_none());
383 }
384
385 #[test]
386 fn test_resolve_columns_still_supports_positional_overrides() {
387 let first_row = ["a", "b", "c"].map(|s| s.to_string());
390 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
391 let overrides = vec![
392 Column::new(Some("first")),
393 Column::new(Some("second")),
394 ];
395 let resolved = resolve_columns(&overrides, &natural_keys);
396 assert_eq!(resolved[0].key_name(), "first");
397 assert_eq!(resolved[1].key_name(), "second");
398 assert!(resolved[2].key.is_none());
399 }
400
401 #[test]
402 fn test_first_row() {
403 let first_row = ["Viscosity", "Rating", "", ""].map(|s| s.to_string());
405 let cols = vec![
406 Column::from_key_ref_with_format(None, Format::Float, None, DateTimeMode::Full, false),
407 Column::from_key_ref_with_format(
408 Some("points"),
409 Format::Decimal(3),
410 None,
411 DateTimeMode::Full,
412 false,
413 ),
414 Column::from_key_ref_with_format(Some("adjusted"), Format::Float, None, DateTimeMode::Full, false),
415 ];
416 let headers = build_header_keys(&first_row, &cols, &FieldNameMode::AutoA1);
417 assert_eq!(headers.first().unwrap(), "viscosity");
419 assert_eq!(headers.get(1).unwrap(), "points");
421 assert_eq!(headers.get(2).unwrap(), "adjusted");
423 assert_eq!(headers.get(3).unwrap(), "d");
425 }
426
427 #[test]
428 fn test_headers_a1_override() {
429 let first_row = ["Viscosity", "Rating", "Weighted", "Class"].map(|s| s.to_string());
431
432 let headers = build_a1_headers(&first_row);
433 assert_eq!(headers.first().unwrap(), "a");
435 assert_eq!(headers.get(3).unwrap(), "d");
437 }
438
439 #[test]
440 fn test_headers_c01_override() {
441 let first_row: Vec<String> = (0..200)
443 .map(|x| {
444 [
445 char::from_u32(65 + (x % 26)).unwrap_or('_').to_string(),
446 (x * 3).to_string(),
447 ]
448 .concat()
449 })
450 .collect();
451
452 let headers = build_c01_headers(&first_row);
453 assert_eq!(headers.first().unwrap(), "c001");
455 assert_eq!(headers.get(3).unwrap(), "c004");
457 }
458}