Skip to main content

spreadsheet_kit/tools/
param_enums.rs

1use schemars::JsonSchema;
2use serde::de;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6fn normalize_literal(value: &str) -> String {
7    value
8        .chars()
9        .filter(|ch| ch.is_ascii_alphanumeric())
10        .flat_map(|ch| ch.to_lowercase())
11        .collect()
12}
13
14fn levenshtein_distance(left: &str, right: &str) -> usize {
15    if left.is_empty() {
16        return right.chars().count();
17    }
18    if right.is_empty() {
19        return left.chars().count();
20    }
21
22    let right_chars: Vec<char> = right.chars().collect();
23    let mut previous: Vec<usize> = (0..=right_chars.len()).collect();
24    let mut current = vec![0; right_chars.len() + 1];
25
26    for (i, left_ch) in left.chars().enumerate() {
27        current[0] = i + 1;
28        for (j, right_ch) in right_chars.iter().enumerate() {
29            let substitution_cost = if left_ch == *right_ch { 0 } else { 1 };
30            current[j + 1] = (previous[j + 1] + 1)
31                .min(current[j] + 1)
32                .min(previous[j] + substitution_cost);
33        }
34        std::mem::swap(&mut previous, &mut current);
35    }
36
37    previous[right_chars.len()]
38}
39
40fn suggest_literal<'a>(input: &str, valid: &'a [&'a str]) -> Option<&'a str> {
41    let normalized_input = normalize_literal(input);
42    let mut best: Option<(&str, usize)> = None;
43
44    for candidate in valid {
45        let distance = levenshtein_distance(&normalized_input, &normalize_literal(candidate));
46        match best {
47            Some((_, best_distance)) if distance >= best_distance => {}
48            _ => best = Some((candidate, distance)),
49        }
50    }
51
52    match best {
53        Some((candidate, distance)) if distance <= 6 => Some(candidate),
54        _ => None,
55    }
56}
57
58fn enum_value_error(label: &str, input: &str, valid: &[&str], suggestion: Option<&str>) -> String {
59    let valid_list = valid.join("|");
60    match suggestion {
61        Some(candidate) if !candidate.eq_ignore_ascii_case(input) => {
62            format!("invalid {label} '{input}'. Did you mean '{candidate}'? valid: {valid_list}")
63        }
64        _ => format!("invalid {label} '{input}'. valid: {valid_list}"),
65    }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
69#[serde(rename_all = "snake_case")]
70#[derive(Default)]
71pub enum BatchMode {
72    #[default]
73    Apply,
74    Preview,
75}
76
77impl BatchMode {
78    pub fn as_str(self) -> &'static str {
79        match self {
80            Self::Apply => "apply",
81            Self::Preview => "preview",
82        }
83    }
84
85    pub fn is_preview(self) -> bool {
86        matches!(self, Self::Preview)
87    }
88}
89
90impl fmt::Display for BatchMode {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_str(self.as_str())
93    }
94}
95
96impl<'de> Deserialize<'de> for BatchMode {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98    where
99        D: de::Deserializer<'de>,
100    {
101        let s = String::deserialize(deserializer)?;
102        match s.to_ascii_lowercase().as_str() {
103            "apply" => Ok(Self::Apply),
104            "preview" => Ok(Self::Preview),
105            other => {
106                let valid = ["apply", "preview"];
107                let message =
108                    enum_value_error("batch_mode", other, &valid, suggest_literal(other, &valid));
109                Err(de::Error::custom(message))
110            }
111        }
112    }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
116#[serde(rename_all = "snake_case")]
117#[derive(Default)]
118pub enum ReplaceMatchMode {
119    #[default]
120    Exact,
121    Contains,
122}
123
124impl ReplaceMatchMode {
125    pub fn as_str(self) -> &'static str {
126        match self {
127            Self::Exact => "exact",
128            Self::Contains => "contains",
129        }
130    }
131}
132
133impl<'de> Deserialize<'de> for ReplaceMatchMode {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: de::Deserializer<'de>,
137    {
138        let s = String::deserialize(deserializer)?;
139        match s.to_ascii_lowercase().as_str() {
140            "exact" => Ok(Self::Exact),
141            "contains" => Ok(Self::Contains),
142            other => {
143                let valid = ["exact", "contains"];
144                let message =
145                    enum_value_error("match_mode", other, &valid, suggest_literal(other, &valid));
146                Err(de::Error::custom(message))
147            }
148        }
149    }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
153#[serde(rename_all = "snake_case")]
154#[derive(Default)]
155pub enum FillDirection {
156    Down,
157    Right,
158    #[default]
159    Both,
160}
161
162impl FillDirection {
163    pub fn as_str(self) -> &'static str {
164        match self {
165            Self::Down => "down",
166            Self::Right => "right",
167            Self::Both => "both",
168        }
169    }
170}
171
172impl<'de> Deserialize<'de> for FillDirection {
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: de::Deserializer<'de>,
176    {
177        let s = String::deserialize(deserializer)?;
178        match s.to_ascii_lowercase().as_str() {
179            "down" => Ok(Self::Down),
180            "right" => Ok(Self::Right),
181            "both" => Ok(Self::Both),
182            other => {
183                let valid = ["down", "right", "both"];
184                let message = enum_value_error(
185                    "fill_direction",
186                    other,
187                    &valid,
188                    suggest_literal(other, &valid),
189                );
190                Err(de::Error::custom(message))
191            }
192        }
193    }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
197#[serde(rename_all = "snake_case")]
198#[derive(Default)]
199pub enum FormulaRelativeMode {
200    #[default]
201    Excel,
202    AbsCols,
203    AbsRows,
204}
205
206impl<'de> Deserialize<'de> for FormulaRelativeMode {
207    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
208    where
209        D: de::Deserializer<'de>,
210    {
211        let s = String::deserialize(deserializer)?;
212        match s.to_ascii_lowercase().as_str() {
213            "excel" => Ok(Self::Excel),
214            "abs_cols" | "abscols" | "columns_absolute" => Ok(Self::AbsCols),
215            "abs_rows" | "absrows" | "rows_absolute" => Ok(Self::AbsRows),
216            other => {
217                let valid = ["excel", "abs_cols", "abs_rows"];
218                let direct_suggestion = match other {
219                    "fully_relative" | "fullyrelative" => Some("excel"),
220                    _ => None,
221                };
222                let message = enum_value_error(
223                    "relative_mode",
224                    other,
225                    &valid,
226                    direct_suggestion.or_else(|| suggest_literal(other, &valid)),
227                );
228                Err(de::Error::custom(message))
229            }
230        }
231    }
232}
233
234impl From<FormulaRelativeMode> for crate::formula::pattern::RelativeMode {
235    fn from(value: FormulaRelativeMode) -> Self {
236        match value {
237            FormulaRelativeMode::Excel => Self::Excel,
238            FormulaRelativeMode::AbsCols => Self::AbsCols,
239            FormulaRelativeMode::AbsRows => Self::AbsRows,
240        }
241    }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
245#[serde(rename_all = "snake_case")]
246pub enum PageOrientation {
247    Portrait,
248    Landscape,
249}
250
251impl<'de> Deserialize<'de> for PageOrientation {
252    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253    where
254        D: de::Deserializer<'de>,
255    {
256        let s = String::deserialize(deserializer)?;
257        match s.to_ascii_lowercase().as_str() {
258            "portrait" => Ok(Self::Portrait),
259            "landscape" => Ok(Self::Landscape),
260            other => {
261                let valid = ["portrait", "landscape"];
262                let message = enum_value_error(
263                    "page_orientation",
264                    other,
265                    &valid,
266                    suggest_literal(other, &valid),
267                );
268                Err(de::Error::custom(message))
269            }
270        }
271    }
272}
273
274impl PageOrientation {
275    pub fn to_umya(self) -> umya_spreadsheet::OrientationValues {
276        match self {
277            Self::Portrait => umya_spreadsheet::OrientationValues::Portrait,
278            Self::Landscape => umya_spreadsheet::OrientationValues::Landscape,
279        }
280    }
281}