1use crate::error::ExtractorError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ExtractionConfig {
12 pub col_guides: Vec<f64>,
14 pub col_mappings: Vec<String>,
16 pub y_tolerance: f64,
18 pub merge_multi_line: bool,
20 pub skip_header_rows: usize,
22 pub skip_footer_rows: usize,
24 pub filter_only_date: bool,
26 pub filter_only_amount: bool,
28 pub y_top_trim: f64,
30 pub y_bottom_trim: f64,
32 #[serde(default)]
34 pub manual_edits: HashMap<String, HashMap<String, HashMap<String, String>>>,
35 #[serde(default)]
37 pub deleted_rows: HashMap<String, HashMap<String, bool>>,
38 #[serde(default)]
40 pub password: Option<String>,
41 #[serde(default)]
43 pub categorize: bool,
44 #[serde(default)]
46 pub from_date: Option<String>,
47 #[serde(default)]
49 pub to_date: Option<String>,
50}
51
52impl Default for ExtractionConfig {
53 fn default() -> Self {
54 Self {
55 col_guides: Vec::new(),
56 col_mappings: Vec::new(),
57 y_tolerance: 6.0,
58 merge_multi_line: true,
59 skip_header_rows: 0,
60 skip_footer_rows: 0,
61 filter_only_date: false,
62 filter_only_amount: false,
63 y_top_trim: 0.0,
64 y_bottom_trim: 1.0,
65 manual_edits: HashMap::new(),
66 deleted_rows: HashMap::new(),
67 password: None,
68 categorize: false,
69 from_date: None,
70 to_date: None,
71 }
72 }
73}
74
75impl ExtractionConfig {
76 #[must_use]
99 pub fn builder() -> ExtractionConfigBuilder {
100 ExtractionConfigBuilder::new()
101 }
102
103 pub fn validate(&self) -> Result<(), ExtractorError> {
108 if (!self.col_guides.is_empty() || !self.col_mappings.is_empty())
109 && self.col_mappings.len() != self.col_guides.len() + 1
110 {
111 return Err(ExtractorError::InvalidConfig(format!(
112 "Column mappings length ({}) must be equal to column guides length ({}) + 1",
113 self.col_mappings.len(),
114 self.col_guides.len()
115 )));
116 }
117 for (i, &g) in self.col_guides.iter().enumerate() {
118 if !(0.0..=1.0).contains(&g) || g.is_nan() {
119 return Err(ExtractorError::InvalidConfig(format!(
120 "Column guide at index {} has invalid value {}: must be between 0.0 and 1.0",
121 i, g
122 )));
123 }
124 if i > 0 && g < self.col_guides[i - 1] {
125 return Err(ExtractorError::InvalidConfig(format!(
126 "Column guides must be sorted in ascending order (found {} after {})",
127 g,
128 self.col_guides[i - 1]
129 )));
130 }
131 }
132 if self.y_tolerance <= 0.0 || self.y_tolerance.is_nan() {
133 return Err(ExtractorError::InvalidConfig(format!(
134 "y_tolerance must be a positive number, found {}",
135 self.y_tolerance
136 )));
137 }
138 if self.y_top_trim < 0.0 || self.y_top_trim > 1.0 || self.y_top_trim.is_nan() {
139 return Err(ExtractorError::InvalidConfig(format!(
140 "y_top_trim must be between 0.0 and 1.0, found {}",
141 self.y_top_trim
142 )));
143 }
144 if self.y_bottom_trim < 0.0 || self.y_bottom_trim > 1.0 || self.y_bottom_trim.is_nan() {
145 return Err(ExtractorError::InvalidConfig(format!(
146 "y_bottom_trim must be between 0.0 and 1.0, found {}",
147 self.y_bottom_trim
148 )));
149 }
150 if self.y_top_trim > self.y_bottom_trim {
151 return Err(ExtractorError::InvalidConfig(format!(
152 "y_top_trim ({}) cannot be greater than y_bottom_trim ({})",
153 self.y_top_trim, self.y_bottom_trim
154 )));
155 }
156 if let Some(from_str) = &self.from_date
157 && parse_date(from_str).is_none()
158 {
159 return Err(ExtractorError::InvalidConfig(format!(
160 "Invalid from_date '{}': expected YYYY-MM-DD or DD-MM-YYYY format",
161 from_str
162 )));
163 }
164 if let Some(to_str) = &self.to_date
165 && parse_date(to_str).is_none()
166 {
167 return Err(ExtractorError::InvalidConfig(format!(
168 "Invalid to_date '{}': expected YYYY-MM-DD or DD-MM-YYYY format",
169 to_str
170 )));
171 }
172 if let (Some(from_str), Some(to_str)) = (&self.from_date, &self.to_date)
173 && let (Some(from_d), Some(to_d)) = (parse_date(from_str), parse_date(to_str))
174 && from_d > to_d
175 {
176 return Err(ExtractorError::InvalidConfig(format!(
177 "from_date ({}) cannot be after to_date ({})",
178 from_str, to_str
179 )));
180 }
181 Ok(())
182 }
183}
184
185pub fn parse_date(s: &str) -> Option<chrono::NaiveDate> {
187 let clean = s.trim();
188 if clean.is_empty() {
189 return None;
190 }
191 if let Ok(d) = chrono::NaiveDate::parse_from_str(clean, "%Y-%m-%d") {
192 return Some(d);
193 }
194 if let Ok(d) = chrono::NaiveDate::parse_from_str(clean, "%d-%m-%Y") {
195 return Some(d);
196 }
197 if let Ok(d) = chrono::NaiveDate::parse_from_str(clean, "%d/%m/%Y") {
198 return Some(d);
199 }
200 if let Ok(d) = chrono::NaiveDate::parse_from_str(clean, "%Y/%m/%d") {
201 return Some(d);
202 }
203 if let Ok(d) = chrono::NaiveDate::parse_from_str(clean, "%d-%b-%Y") {
204 return Some(d);
205 }
206 if let Ok(d) = chrono::NaiveDate::parse_from_str(clean, "%d %b %Y") {
207 return Some(d);
208 }
209 None
210}
211
212pub struct ExtractionConfigBuilder {
214 config: ExtractionConfig,
215}
216
217impl Default for ExtractionConfigBuilder {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl ExtractionConfigBuilder {
224 #[must_use]
226 pub fn new() -> Self {
227 Self {
228 config: ExtractionConfig::default(),
229 }
230 }
231
232 #[must_use]
235 pub fn col_guides(mut self, mut guides: Vec<f64>) -> Self {
236 guides.sort_by(f64::total_cmp);
237 self.config.col_guides = guides;
238 self
239 }
240
241 #[must_use]
244 pub fn col_mappings(mut self, mappings: Vec<String>) -> Self {
245 self.config.col_mappings = mappings;
246 self
247 }
248
249 #[must_use]
251 pub fn y_tolerance(mut self, tolerance: f64) -> Self {
252 self.config.y_tolerance = tolerance;
253 self
254 }
255
256 #[must_use]
258 pub fn merge_multi_line(mut self, merge: bool) -> Self {
259 self.config.merge_multi_line = merge;
260 self
261 }
262
263 #[must_use]
265 pub fn skip_header_rows(mut self, count: usize) -> Self {
266 self.config.skip_header_rows = count;
267 self
268 }
269
270 #[must_use]
272 pub fn skip_footer_rows(mut self, count: usize) -> Self {
273 self.config.skip_footer_rows = count;
274 self
275 }
276
277 #[must_use]
279 pub fn filter_only_date(mut self, filter: bool) -> Self {
280 self.config.filter_only_date = filter;
281 self
282 }
283
284 #[must_use]
286 pub fn filter_only_amount(mut self, filter: bool) -> Self {
287 self.config.filter_only_amount = filter;
288 self
289 }
290
291 #[must_use]
293 pub fn y_top_trim(mut self, trim: f64) -> Self {
294 self.config.y_top_trim = trim;
295 self
296 }
297
298 #[must_use]
300 pub fn y_bottom_trim(mut self, trim: f64) -> Self {
301 self.config.y_bottom_trim = trim;
302 self
303 }
304
305 #[must_use]
307 pub fn manual_edits(
308 mut self,
309 edits: HashMap<String, HashMap<String, HashMap<String, String>>>,
310 ) -> Self {
311 self.config.manual_edits = edits;
312 self
313 }
314
315 #[must_use]
317 pub fn deleted_rows(mut self, deleted: HashMap<String, HashMap<String, bool>>) -> Self {
318 self.config.deleted_rows = deleted;
319 self
320 }
321
322 #[must_use]
324 pub fn password(mut self, password: Option<String>) -> Self {
325 self.config.password = password;
326 self
327 }
328
329 #[must_use]
331 pub fn categorize(mut self, categorize: bool) -> Self {
332 self.config.categorize = categorize;
333 self
334 }
335
336 #[must_use]
338 pub fn from_date(mut self, from_date: Option<String>) -> Self {
339 self.config.from_date = from_date;
340 self
341 }
342
343 #[must_use]
345 pub fn to_date(mut self, to_date: Option<String>) -> Self {
346 self.config.to_date = to_date;
347 self
348 }
349
350 pub fn build(self) -> Result<ExtractionConfig, ExtractorError> {
367 self.config.validate()?;
368 Ok(self.config)
369 }
370}