Skip to main content

spreadsheet_to_json/
headers.rs

1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use serde_json::Value;
4
5use crate::{Column, FieldNameMode};
6
7pub fn to_a1_col_key(index: usize) -> String {
8    let mut result = String::new();
9    let mut n = index as i32; // Work with i32 to handle potential negative values
10
11    while n >= 0 {
12        let remainder = (n % 26) as u8;
13        result.push((b'a' + remainder) as char);
14        n = (n / 26) - 1;
15    }
16    result.chars().rev().collect()
17}
18
19pub fn to_padded_col_key(prefix: &str, index: usize, num_cols: usize) -> String {
20    build_padded_col_key(prefix, false, index, num_cols)
21}
22
23pub fn to_padded_col_suffix(prefix: &str, index: usize, num_cols: usize) -> String {
24    build_padded_col_key(prefix, true, index, num_cols)
25}
26
27fn build_padded_col_key(prefix: &str, underscore: bool, index: usize, num_cols: usize) -> String {
28    let width = if num_cols < 100 {
29        2
30    } else if num_cols < 1000 {
31        3
32    } else if num_cols < 10000 {
33        4
34    } else {
35        5
36    };
37    let num = index + 1;
38    let separator = if underscore { "_" } else { "" };
39    format!("{}{}{:0width$}", prefix, separator, num, width = width)
40}
41
42pub fn to_c01_col_key(index: usize, num_cols: usize) -> String {
43    to_padded_col_key("c", index, num_cols)
44}
45
46pub fn to_head_key(index: usize, field_mode: &FieldNameMode, num_cols: usize) -> String {
47    if field_mode.use_c01() {
48        to_c01_col_key(index, num_cols)
49    } else {
50        to_a1_col_key(index)
51    }
52}
53
54pub fn to_head_key_default(index: usize) -> String {
55    to_c01_col_key(index, 1000)
56}
57
58/// Build header keys from the first row of a CSV file or headers captured from a spreadsheet
59pub fn build_header_keys(
60    first_row: &[String],
61    columns: &[Column],
62    field_mode: &FieldNameMode,
63) -> Vec<String> {
64    let mut h_index = 0;
65    let mut headers: Vec<String> = vec![];
66    let num_cols = first_row.len();
67    let keep_headers = field_mode.keep_headers();
68    for h_row in first_row.to_owned() {
69        let sn = h_row.to_snake_case();
70        let mut has_override = false;
71        if let Some(col) = columns.get(h_index) {
72            // only apply override if key is not empty
73            if let Some(k_str) = &col.key {
74                let h_key = if headers.contains(&k_str.to_string()) {
75                    to_padded_col_suffix(k_str, h_index, num_cols)
76                } else {
77                    k_str.to_string()
78                };
79                headers.push(h_key);
80                has_override = true;
81            }
82        }
83        if !has_override {
84            if keep_headers && sn.len() > 0 {
85                let sn_key = if headers.contains(&sn) {
86                    to_padded_col_suffix(&sn, h_index, num_cols)
87                } else {
88                    sn
89                };
90                headers.push(sn_key);
91            } else {
92                headers.push(to_head_key(h_index, field_mode, num_cols));
93            }
94        }
95        h_index += 1;
96    }
97    headers
98}
99
100/// Assign keys with A1+ notation
101pub fn build_a1_headers(first_row: &[String]) -> Vec<String> {
102    build_header_keys(first_row, &[], &FieldNameMode::A1)
103}
104
105/// Assign keys as c + zero-padded number
106pub fn build_c01_headers(first_row: &[String]) -> Vec<String> {
107    build_header_keys(first_row, &[], &FieldNameMode::NumPadded)
108}
109
110/// check if the row is not a header row. Always return true if row_index is greater than 0
111pub(crate) fn is_not_header_row(
112    row_map: &IndexMap<String, Value>,
113    row_index: usize,
114    headers: &[String],
115) -> bool {
116    if row_index > 0 {
117        return true;
118    }
119    let mut h_index = 0;
120    let mut num_matched: usize = 0;
121    for (_key, value) in row_map.iter() {
122        let ref_key = value.to_string().to_snake_case();
123        if let Some(hk) = headers.get(h_index) {
124            let sn = hk.to_snake_case();
125            if sn == ref_key || sn.len() == 0 {
126                num_matched += 1;
127            }
128        }
129        h_index += 1;
130    }
131    num_matched < headers.len()
132}
133
134#[cfg(test)]
135mod tests {
136
137    use crate::Format;
138
139    use super::*;
140
141    #[test]
142    fn test_cell_letters_1() {
143        assert_eq!(to_a1_col_key(26), "aa");
144    }
145
146    #[test]
147    fn test_cell_letters_2() {
148        assert_eq!(to_a1_col_key(701), "zz");
149    }
150
151    #[test]
152    fn test_cell_letters_3() {
153        assert_eq!(to_a1_col_key(702), "aaa");
154    }
155
156    #[test]
157    fn test_cell_letters_4() {
158        assert_eq!(to_c01_col_key(8, 60), "c09");
159    }
160
161    #[test]
162    fn test_cell_letters_5() {
163        assert_eq!(to_c01_col_key(20, 750), "c021");
164    }
165
166    #[test]
167    fn test_cell_letters_6() {
168        assert_eq!(to_c01_col_key(20, 2000), "c0021");
169    }
170
171    #[test]
172    fn test_first_row() {
173        // header labels as captured from the top row
174        let first_row = ["Viscosity", "Rating", "", ""].map(|s| s.to_string());
175        let cols = vec![
176            Column::from_key_ref_with_format(None, Format::Float, None, false, false),
177            Column::from_key_ref_with_format(
178                Some("points"),
179                Format::Decimal(3),
180                None,
181                false,
182                false,
183            ),
184            Column::from_key_ref_with_format(Some("adjusted"), Format::Float, None, false, false),
185        ];
186        let headers = build_header_keys(&first_row, &cols, &FieldNameMode::AutoA1);
187        // should be lower-cased as `viscosity`
188        assert_eq!(headers.get(0).unwrap(), "viscosity");
189        // should be overridden as `points`
190        assert_eq!(headers.get(1).unwrap(), "points");
191        // should be labelled `adjusted`
192        assert_eq!(headers.get(2).unwrap(), "adjusted");
193        // fourth column  with empty heading should be assigned an A1-style key of `d`
194        assert_eq!(headers.get(3).unwrap(), "d");
195    }
196
197    #[test]
198    fn test_headers_a1_override() {
199        // header labels as captured from the top row
200        let first_row = ["Viscosity", "Rating", "Weighted", "Class"].map(|s| s.to_string());
201
202        let headers = build_a1_headers(&first_row);
203        // should be lower-cased as `viscosity`
204        assert_eq!(headers.get(0).unwrap(), "a");
205        // the column should be d.
206        assert_eq!(headers.get(3).unwrap(), "d");
207    }
208
209    #[test]
210    fn test_headers_c01_override() {
211        // build header row with 200 sequential alphanumeric values
212        let first_row: Vec<String> = (0..200)
213            .map(|x| {
214                [
215                    char::from_u32(65 + (x % 26)).unwrap_or('_').to_string(),
216                    (x * 3 * 1).to_string(),
217                ]
218                .concat()
219            })
220            .collect();
221
222        let headers = build_c01_headers(&first_row);
223        // the column should be c0001
224        assert_eq!(headers.get(0).unwrap(), "c001");
225        // the column should be c0004
226        assert_eq!(headers.get(3).unwrap(), "c004");
227    }
228}