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(k_str) = &col.key {
71 let h_key = if headers.contains(&k_str.to_string()) {
72 to_padded_col_suffix(k_str, h_index, num_cols)
73 } else {
74 k_str.to_string()
75 };
76 headers.push(h_key);
77 has_override = true;
78 }
79 }
80 if !has_override {
81 if keep_headers && !sn.is_empty() {
82 let sn_key = if headers.contains(&sn) {
83 to_padded_col_suffix(&sn, h_index, num_cols)
84 } else {
85 sn
86 };
87 headers.push(sn_key);
88 } else {
89 headers.push(to_head_key(h_index, field_mode, num_cols));
90 }
91 }
92 }
93 headers
94}
95
96pub fn natural_column_keys(first_row: &[String], field_mode: &FieldNameMode) -> Vec<String> {
100 build_header_keys(first_row, &[], field_mode)
101}
102
103pub fn resolve_columns(columns: &[Column], natural_keys: &[String]) -> Vec<Column> {
113 let mut resolved: Vec<Column> = natural_keys.iter().map(|_| Column::new(None)).collect();
114 for (i, col) in columns.iter().enumerate() {
115 if col.source_key.is_none() {
116 if let Some(slot) = resolved.get_mut(i) {
117 *slot = col.clone();
118 }
119 }
120 }
121 for col in columns {
122 if let Some(src) = &col.source_key {
123 let target = src.to_snake_case();
124 if let Some(idx) = natural_keys.iter().position(|k| k.to_snake_case() == target) {
125 resolved[idx] = col.clone();
126 }
127 }
128 }
129 resolved
130}
131
132pub fn build_a1_headers(first_row: &[String]) -> Vec<String> {
134 build_header_keys(first_row, &[], &FieldNameMode::A1)
135}
136
137pub fn build_c01_headers(first_row: &[String]) -> Vec<String> {
139 build_header_keys(first_row, &[], &FieldNameMode::NumPadded)
140}
141
142pub(crate) fn is_not_header_row(
151 raw_values: &[String],
152 row_index: usize,
153 headers: &[String],
154) -> bool {
155 if row_index > 0 {
156 return true;
157 }
158 let mut num_matched: usize = 0;
159 for (h_index, hk) in headers.iter().enumerate() {
160 let sn = hk.to_snake_case();
161 if let Some(val) = raw_values.get(h_index) {
162 if val.to_snake_case() == sn || sn.is_empty() {
163 num_matched += 1;
164 }
165 }
166 }
167 num_matched < headers.len()
168}
169
170#[cfg(test)]
171mod tests {
172
173 use crate::Format;
174
175 use super::*;
176
177 #[test]
178 fn test_cell_letters_1() {
179 assert_eq!(to_a1_col_key(26), "aa");
180 }
181
182 #[test]
183 fn test_cell_letters_2() {
184 assert_eq!(to_a1_col_key(701), "zz");
185 }
186
187 #[test]
188 fn test_cell_letters_3() {
189 assert_eq!(to_a1_col_key(702), "aaa");
190 }
191
192 #[test]
193 fn test_cell_letters_4() {
194 assert_eq!(to_c01_col_key(8, 60), "c09");
195 }
196
197 #[test]
198 fn test_cell_letters_5() {
199 assert_eq!(to_c01_col_key(20, 750), "c021");
200 }
201
202 #[test]
203 fn test_cell_letters_6() {
204 assert_eq!(to_c01_col_key(20, 2000), "c0021");
205 }
206
207 #[test]
208 fn test_is_not_header_row_uses_raw_text_not_coerced_values() {
209 let headers = vec!["sku".to_string(), "weight".to_string()];
215 let header_row_raw = vec!["sku".to_string(), "weight".to_string()];
217 assert!(!is_not_header_row(&header_row_raw, 0, &headers));
218
219 let data_row_raw = vec!["SKU001".to_string(), "58.2".to_string()];
221 assert!(is_not_header_row(&data_row_raw, 0, &headers));
222
223 assert!(is_not_header_row(&header_row_raw, 1, &headers));
225 }
226
227 #[test]
228 fn test_resolve_columns_matches_by_source_key_regardless_of_position() {
229 let first_row = ["full_name", "height_cm", "weight_kg"].map(|s| s.to_string());
232 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
233 assert_eq!(natural_keys, vec!["full_name", "height_cm", "weight_kg"]);
234
235 let overrides = vec![
236 Column::from_source_key_with_format("weight_kg", Some("weight"), Format::Integer, None, false, false),
237 ];
238 let resolved = resolve_columns(&overrides, &natural_keys);
239 assert_eq!(resolved.len(), 3);
240 assert!(resolved[0].key.is_none());
242 assert!(resolved[1].key.is_none());
243 assert_eq!(resolved[2].key_name(), "weight");
245 assert_eq!(resolved[2].format.to_string(), "integer");
246
247 let headers = build_header_keys(&first_row, &resolved, &FieldNameMode::AutoA1);
248 assert_eq!(headers, vec!["full_name", "height_cm", "weight"]);
249 }
250
251 #[test]
252 fn test_resolve_columns_source_key_match_is_snake_cased() {
253 let first_row = ["Weight (Kg)".to_string()];
256 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
257 assert_eq!(natural_keys, vec!["weight_kg"]);
258
259 let overrides = vec![
260 Column::from_source_key_with_format("Weight Kg", Some("weight"), Format::Auto, None, false, false),
261 ];
262 let resolved = resolve_columns(&overrides, &natural_keys);
263 assert_eq!(resolved[0].key_name(), "weight");
264 }
265
266 #[test]
267 fn test_resolve_columns_unmatched_source_key_is_a_no_op() {
268 let first_row = ["full_name", "height_cm"].map(|s| s.to_string());
269 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
270 let overrides = vec![
271 Column::from_source_key_with_format("nonexistent_field", Some("oops"), Format::Auto, None, false, false),
272 ];
273 let resolved = resolve_columns(&overrides, &natural_keys);
274 assert!(resolved[0].key.is_none());
276 assert!(resolved[1].key.is_none());
277 }
278
279 #[test]
280 fn test_resolve_columns_still_supports_positional_overrides() {
281 let first_row = ["a", "b", "c"].map(|s| s.to_string());
284 let natural_keys = natural_column_keys(&first_row, &FieldNameMode::AutoA1);
285 let overrides = vec![
286 Column::new(Some("first")),
287 Column::new(Some("second")),
288 ];
289 let resolved = resolve_columns(&overrides, &natural_keys);
290 assert_eq!(resolved[0].key_name(), "first");
291 assert_eq!(resolved[1].key_name(), "second");
292 assert!(resolved[2].key.is_none());
293 }
294
295 #[test]
296 fn test_first_row() {
297 let first_row = ["Viscosity", "Rating", "", ""].map(|s| s.to_string());
299 let cols = vec![
300 Column::from_key_ref_with_format(None, Format::Float, None, false, false),
301 Column::from_key_ref_with_format(
302 Some("points"),
303 Format::Decimal(3),
304 None,
305 false,
306 false,
307 ),
308 Column::from_key_ref_with_format(Some("adjusted"), Format::Float, None, false, false),
309 ];
310 let headers = build_header_keys(&first_row, &cols, &FieldNameMode::AutoA1);
311 assert_eq!(headers.first().unwrap(), "viscosity");
313 assert_eq!(headers.get(1).unwrap(), "points");
315 assert_eq!(headers.get(2).unwrap(), "adjusted");
317 assert_eq!(headers.get(3).unwrap(), "d");
319 }
320
321 #[test]
322 fn test_headers_a1_override() {
323 let first_row = ["Viscosity", "Rating", "Weighted", "Class"].map(|s| s.to_string());
325
326 let headers = build_a1_headers(&first_row);
327 assert_eq!(headers.first().unwrap(), "a");
329 assert_eq!(headers.get(3).unwrap(), "d");
331 }
332
333 #[test]
334 fn test_headers_c01_override() {
335 let first_row: Vec<String> = (0..200)
337 .map(|x| {
338 [
339 char::from_u32(65 + (x % 26)).unwrap_or('_').to_string(),
340 (x * 3).to_string(),
341 ]
342 .concat()
343 })
344 .collect();
345
346 let headers = build_c01_headers(&first_row);
347 assert_eq!(headers.first().unwrap(), "c001");
349 assert_eq!(headers.get(3).unwrap(), "c004");
351 }
352}