patchwork_csv_utils 0.1.23-x86_64-linux → 0.1.24-x86_64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,270 +2,331 @@ use std::collections::HashMap;
2
2
  use std::fs::File;
3
3
  use std::io::{BufWriter, Write};
4
4
 
5
- use calamine::{open_workbook_auto, Data, DataType, Range, Reader};
5
+ use calamine::{open_workbook_auto, Data, ExcelDateTime, Range, Reader};
6
6
  use chrono::{NaiveDateTime, Timelike, Utc};
7
7
  use magnus::{RArray, Ruby};
8
8
 
9
- use crate::utils::{check_mandatory_headers, index_of_header_in_mandatory_list, magnus_err, missing_header, missing_value, string_to_datetime, to_datetime_error, FileExtension};
10
-
11
- pub fn to_csv(ruby: &Ruby, xls_path: String,
12
- target_path: String,
13
- exclusions: RArray,
14
- mandatory_headers: RArray,
15
- status_exclusions: RArray,
16
- expected_trust_name: String,
17
- is_streamed_file: bool,
18
- earliest_start_date: Option<String>
9
+ use crate::utils::shared::datetime::DateTimeProcessor;
10
+ use crate::utils::shared::filters::RowFilters;
11
+ use crate::utils::shared::types::{HeaderConfig, MandatoryColumn, ProcessingConfig};
12
+ use crate::utils::shared::validation::TrustValidator;
13
+ use crate::utils::{
14
+ check_mandatory_headers, index_of_header_in_mandatory_list, magnus_err, missing_header,
15
+ missing_value, FileExtension,
16
+ };
17
+
18
+ #[allow(clippy::too_many_arguments)]
19
+ pub fn to_csv(
20
+ ruby: &Ruby,
21
+ xls_path: String,
22
+ target_path: String,
23
+ exclusions: RArray,
24
+ mandatory_headers: RArray,
25
+ status_exclusions: RArray,
26
+ expected_trust_name: String,
27
+ is_streamed_file: bool,
28
+ earliest_start_date: Option<String>,
19
29
  ) -> magnus::error::Result<()> {
20
- if !xls_path.has_extension(&["xls","xlsx"]) {
21
- return Err(magnus::Error::new(ruby.exception_standard_error(), "xls_path must be an xls or xlsx file".to_string()));
30
+ if !xls_path.has_extension(&["xls", "xlsx"]) {
31
+ return Err(magnus::Error::new(
32
+ ruby.exception_standard_error(),
33
+ "xls_path must be an xls or xlsx file".to_string(),
34
+ ));
22
35
  }
23
36
 
24
- let exclusions = RArray::to_vec(exclusions)?;
25
- let mandatory_headers: Vec<String> = RArray::to_vec(mandatory_headers)?;
26
- let status_exclusions = RArray::to_vec(status_exclusions)?;
27
-
28
- let start_date = earliest_start_date
29
- .and_then(|date_str| string_to_datetime(&date_str));
30
-
31
- let mut workbook = open_workbook_auto(&xls_path)
32
- .map_err(|e| magnus_err(ruby, e, format!("could not open workbook: {}", xls_path).as_str()))?;
33
-
34
- let range = workbook.worksheet_range_at(0)
35
- .ok_or(magnus::Error::new(ruby.exception_standard_error(), "no worksheet found in xls".to_string()))
37
+ let config = ProcessingConfig::from_ruby(
38
+ exclusions,
39
+ mandatory_headers,
40
+ status_exclusions,
41
+ expected_trust_name,
42
+ is_streamed_file,
43
+ earliest_start_date,
44
+ )?;
45
+
46
+ let mut workbook = open_workbook_auto(&xls_path).map_err(|e| {
47
+ magnus_err(
48
+ ruby,
49
+ e,
50
+ format!("could not open workbook: {}", xls_path).as_str(),
51
+ )
52
+ })?;
53
+
54
+ let range = workbook
55
+ .worksheet_range_at(0)
56
+ .ok_or_else(|| {
57
+ magnus::Error::new(
58
+ ruby.exception_standard_error(),
59
+ "no worksheet found in xls".to_string(),
60
+ )
61
+ })
36
62
  .and_then(|r| r.map_err(|e| magnus_err(ruby, e, "could not read worksheet range")))?;
37
63
 
38
- let headers = range.headers().ok_or(magnus::Error::new(ruby.exception_standard_error(), "no headers found in xls".to_string()))?;
64
+ let headers = range.headers().ok_or_else(|| {
65
+ magnus::Error::new(
66
+ ruby.exception_standard_error(),
67
+ "no headers found in xls".to_string(),
68
+ )
69
+ })?;
39
70
  let headers_list: Vec<String> = headers.iter().map(|h| h.to_string()).collect();
40
71
 
41
72
  if let Some(value) =
42
- check_mandatory_headers(ruby, &headers_list, &mandatory_headers, "csv") { return value; }
73
+ check_mandatory_headers(ruby, &headers_list, &config.mandatory_headers, "csv")
74
+ {
75
+ return value;
76
+ }
43
77
 
44
- let header_map: HashMap<String, usize> = mandatory_headers.iter().enumerate().map(|(i, h)| (h.to_string(), i)).collect();
45
- let csv_out_file = File::create(target_path.clone()).map_err(|e| magnus_err(ruby, e, format!("could not create csv file: {}", target_path).as_str()))?;
78
+ let csv_out_file = File::create(&target_path).map_err(|e| {
79
+ magnus_err(
80
+ ruby,
81
+ e,
82
+ format!("could not create csv file: {}", target_path).as_str(),
83
+ )
84
+ })?;
46
85
  let mut dest = BufWriter::new(csv_out_file);
47
86
 
48
- write_csv(ruby, &mut dest, &range, header_map, exclusions, mandatory_headers, headers_list, status_exclusions, expected_trust_name, is_streamed_file, start_date)
87
+ write_csv(ruby, &mut dest, &range, config, headers_list)
49
88
  }
50
89
 
51
- fn write_csv<W: Write>(ruby: &Ruby, dest: &mut W, range: &Range<Data>,
52
- header_map: HashMap<String, usize>, exclusions: Vec<String>,
53
- mandatory_headers: Vec<String>,
54
- headers_list: Vec<String>,
55
- status_exclusions: Vec<String>,
56
- expected_trust_name: String,
57
- is_streamed_file: bool,
58
- start_date: Option<NaiveDateTime>) -> magnus::error::Result<()> {
59
- let n = mandatory_headers.len() - 1;
60
- let request_id = header_map.get("Request Id").ok_or(missing_header(ruby, "Request Id"))?;
61
- let date = header_map.get("Date").ok_or(missing_header(ruby, "Date"))?;
62
- let start = header_map.get("Start").ok_or(missing_header(ruby, "Start"))?;
63
- let end = header_map.get("End").ok_or(missing_header(ruby, "End"))?;
64
- let actual_start = header_map.get("Actual Start").ok_or(missing_header(ruby, "Actual Start"))?;
65
- let actual_end = header_map.get("Actual End").ok_or(missing_header(ruby, "Actual End"))?;
66
- let status = header_map.get("Status");
67
- let trust_name = header_map.get("Trust").ok_or(missing_header(ruby, "Trust"))?;
68
-
69
- let mandatory_rows = get_mandatory_records(ruby, range, &headers_list, &mandatory_headers)?;
90
+ fn write_csv<W: Write>(
91
+ ruby: &Ruby,
92
+ dest: &mut W,
93
+ range: &Range<Data>,
94
+ config: ProcessingConfig,
95
+ headers_list: Vec<String>,
96
+ ) -> magnus::error::Result<()> {
97
+ let n = config.mandatory_headers.len() - 1;
98
+ let header_map: HashMap<String, usize> = config
99
+ .mandatory_headers
100
+ .iter()
101
+ .enumerate()
102
+ .map(|(i, h)| (h.to_string(), i))
103
+ .collect();
104
+
105
+ let header_config = HeaderConfig::from_header_map(&header_map, ruby)?;
106
+ let filters = RowFilters::new(
107
+ config.exclusions,
108
+ config.status_exclusions,
109
+ config.earliest_start_date,
110
+ );
111
+ let trust_validator = TrustValidator::new(config.expected_trust_name, config.is_streamed_file);
112
+
113
+ let mandatory_rows =
114
+ get_mandatory_records(ruby, range, &headers_list, &config.mandatory_headers)?;
70
115
 
71
116
  for (ri, r) in mandatory_rows.into_iter().enumerate() {
72
- let mut date_value = Utc::now().naive_utc();
117
+ if filters.should_skip(
118
+ &r,
119
+ header_config.request_id,
120
+ header_config.status,
121
+ header_config.date,
122
+ ) {
123
+ continue;
124
+ }
73
125
 
74
- if skip_rows_before_start_date(&start_date, &r, &date) { continue; }
75
- if skip_excluded_rows(&request_id, &status, &r, &exclusions) { continue; }
76
- if skip_excluded_status_rows(&status, &r, &status_exclusions) { continue; }
77
- if skip_empty_rows(&r) { continue; }
78
- if skip_rows_with_no_request_id(&request_id, &r) { continue; }
79
- if date_value_is_not_present(&date, &r) {
80
- return Err(magnus::Error::new(ruby.exception_standard_error(), format!("Date value is not present in row: {}", ri)));
126
+ if r.get(header_config.date)
127
+ .is_none_or(|d| matches!(d, &Data::Empty))
128
+ {
129
+ return Err(magnus::Error::new(
130
+ ruby.exception_standard_error(),
131
+ format!("Date value is not present in row: {}", ri),
132
+ ));
81
133
  }
82
134
 
83
- if !is_streamed_file {
84
- validate_trust_name(ruby, &expected_trust_name, trust_name, ri, &r)?;
135
+ if ri > 0 {
136
+ if let Some(trust_data) = r.get(header_config.trust_name) {
137
+ trust_validator.validate(ruby, &trust_data.to_string())?;
138
+ }
85
139
  }
86
140
 
87
- for (i, c) in mandatory_headers.iter().enumerate() {
88
- let column_index = header_map.get(c).ok_or(missing_header(ruby, c))?;
89
- let c = r.get(*column_index).ok_or(missing_value(ruby, c))?;
141
+ let mut date_value = Utc::now().naive_utc();
142
+
143
+ for (i, c) in config.mandatory_headers.iter().enumerate() {
144
+ let column_index = *header_map.get(c).ok_or_else(|| missing_header(ruby, c))?;
145
+ let c = r.get(column_index).ok_or_else(|| missing_value(ruby, c))?;
90
146
 
91
147
  match *c {
92
148
  Data::Empty => Ok(()),
93
- Data::String(ref s) | Data::DurationIso(ref s) => {
94
- handle_commas(dest, s)
95
- }
149
+ Data::String(ref s) | Data::DurationIso(ref s) => handle_commas(dest, s),
96
150
  Data::Float(ref f) => write!(dest, "{}", f),
97
151
  Data::DateTimeIso(ref s) => {
98
- // Normalize the string to ensure manageable precision
99
- let normalized_s = if s.contains('.') {
100
- let parts: Vec<&str> = s.split('.').collect();
101
- format!("{}.{}", parts[0], &parts[1][..std::cmp::min(parts[1].len(), 6)]) // Keep up to 6 fractional seconds
102
- } else {
103
- s.to_string()
104
- };
105
-
106
- // Attempt to parse the normalized string as a full datetime
107
- let mut current = NaiveDateTime::parse_from_str(&normalized_s, "%Y-%m-%dT%H:%M:%S%.f")
108
- .or_else(|_| {
109
- // If parsing as datetime fails, try parsing as date-only
110
- NaiveDateTime::parse_from_str(&format!("{}T00:00:00", normalized_s), "%Y-%m-%dT%H:%M:%S%.f")
111
- })
112
- .or_else(|_| {
113
- // If parsing as time-only fails, try parsing as time-only
114
- NaiveDateTime::parse_from_str(&format!("1970-01-01T{}", normalized_s), "%Y-%m-%dT%H:%M:%S%.f")
115
- })
116
- .map_err(|_| to_datetime_error(ruby, s, ri, "Date or Time"))?;
117
-
118
- // Apply the same logic as for Data::DateTime
119
- if i == *date {
120
- date_value = current;
121
- } else if i == *start || i == *end || i == *actual_start || i == *actual_end {
122
- current = transform_time_to_datetime(date_value, current);
123
- }
124
-
125
- // Round up to the next second if we have any fractional seconds
126
- let adjusted_time = if current.nanosecond() > 0 {
127
- current + chrono::Duration::seconds(1) - chrono::Duration::nanoseconds(current.nanosecond() as i64)
128
- } else {
129
- current
130
- };
131
-
132
- // Format the output to ensure consistent precision
133
- let formatted_output = adjusted_time.format("%Y-%m-%d %H:%M:%S").to_string();
134
- write!(dest, "{}", formatted_output)
152
+ handle_datetime_iso(ruby, dest, s, ri, i, &header_config, &mut date_value)
135
153
  }
136
154
  Data::DateTime(ref d) => {
137
- let mut current = d.as_datetime().ok_or(to_datetime_error(ruby, &d.to_string(), ri, "Date"))?;
138
- if i == *date {
139
- date_value = current;
140
- } else if i == *start || i == *end || i == *actual_start || i == *actual_end {
141
- current = transform_time_to_datetime(date_value, current);
142
- }
143
- write!(dest, "{}", current)
155
+ handle_datetime(dest, *d, ri, i, &header_config, &mut date_value)
144
156
  }
145
157
  Data::Int(ref i) => write!(dest, "{}", i),
146
158
  Data::Error(ref e) => write!(dest, "{:?}", e),
147
159
  Data::Bool(ref b) => write!(dest, "{}", b),
148
- }.map_err(|e| magnus_err(ruby, e, format!("error writing xls row: {}, column: {}", ri, i).as_str()))?;
149
- if i != n {
150
- write!(dest, ",").map_err(|e| magnus_err(ruby, e, format!("error writing csv comma for row: {}, column: {}", ri, i).as_str()))?;
151
160
  }
152
- }
153
- write!(dest, "\r\n").map_err(|e| magnus_err(ruby, e, format!("error writing end of line for row: {}", ri).as_str()))?;
154
- }
155
- Ok(())
156
- }
161
+ .map_err(|e| {
162
+ magnus_err(
163
+ ruby,
164
+ e,
165
+ format!("error writing xls row: {}, column: {}", ri, i).as_str(),
166
+ )
167
+ })?;
157
168
 
158
- fn validate_trust_name(ruby: &Ruby, expected_trust_name: &String, trust_name: &usize, ri: usize, r: &Vec<&Data>) -> magnus::error::Result<()> {
159
- if ri > 0 {
160
- let s = r[*trust_name].to_string();
161
- let s = s.trim();
162
- if s != expected_trust_name.clone() {
163
- return Err(magnus::Error::new(ruby.exception_standard_error(), format!("Trust actual name: '{}' is not as expected: '{}'", s, expected_trust_name)));
169
+ if i != n {
170
+ write!(dest, ",").map_err(|e| {
171
+ magnus_err(
172
+ ruby,
173
+ e,
174
+ format!("error writing csv comma for row: {}, column: {}", ri, i).as_str(),
175
+ )
176
+ })?;
177
+ }
164
178
  }
179
+ write!(dest, "\r\n").map_err(|e| {
180
+ magnus_err(
181
+ ruby,
182
+ e,
183
+ format!("error writing end of line for row: {}", ri).as_str(),
184
+ )
185
+ })?;
165
186
  }
166
187
  Ok(())
167
188
  }
168
189
 
169
- fn get_mandatory_records<'a>(ruby: &Ruby, range: &'a Range<Data>, csv_header_list: &Vec<String>, mandatory_headers_list: &Vec<String>) -> magnus::error::Result<Vec<Vec<&'a Data>>> {
170
- let inverse_header_map: HashMap<usize, String> = csv_header_list.iter().enumerate().map(|(i, h)| (i, h.to_string())).collect();
171
-
172
- let mut records = vec![];
173
- for row in range.rows() {
174
- let mut columns = vec![];
175
- for (i, column_value) in row.iter().enumerate() {
176
- let column_name = inverse_header_map.get(&i).ok_or(missing_header(ruby, &i.to_string()))?;
177
- if mandatory_headers_list.contains(column_name) {
178
- let index = index_of_header_in_mandatory_list(mandatory_headers_list.clone(), column_name.to_string()).unwrap();
179
- columns.push(XlsMandatoryColumn::new(column_value, index));
180
- }
181
- }
182
- columns.sort_by(|a, b| a.index.cmp(&b.index));
183
- let columns = columns.iter().map(|c| c.value).collect::<Vec<&Data>>();
184
- records.push(columns);
190
+ fn handle_datetime_iso<W: Write>(
191
+ _ruby: &Ruby,
192
+ dest: &mut W,
193
+ s: &str,
194
+ ri: usize,
195
+ i: usize,
196
+ header_config: &HeaderConfig,
197
+ date_value: &mut NaiveDateTime,
198
+ ) -> std::io::Result<()> {
199
+ let normalized_s = if s.contains('.') {
200
+ let parts: Vec<&str> = s.split('.').collect();
201
+ format!(
202
+ "{}.{}",
203
+ parts[0],
204
+ &parts[1][..std::cmp::min(parts[1].len(), 6)]
205
+ )
206
+ } else {
207
+ s.to_string()
208
+ };
209
+
210
+ let mut current = NaiveDateTime::parse_from_str(&normalized_s, "%Y-%m-%dT%H:%M:%S%.f")
211
+ .or_else(|_| {
212
+ NaiveDateTime::parse_from_str(
213
+ &format!("{}T00:00:00", normalized_s),
214
+ "%Y-%m-%dT%H:%M:%S%.f",
215
+ )
216
+ })
217
+ .or_else(|_| {
218
+ NaiveDateTime::parse_from_str(
219
+ &format!("1970-01-01T{}", normalized_s),
220
+ "%Y-%m-%dT%H:%M:%S%.f",
221
+ )
222
+ })
223
+ .map_err(|_| {
224
+ std::io::Error::new(
225
+ std::io::ErrorKind::InvalidData,
226
+ format!(
227
+ "Could not parse datetime '{}', row: {}, col: Date or Time",
228
+ s, ri
229
+ ),
230
+ )
231
+ })?;
232
+
233
+ if header_config.is_date_column(i) {
234
+ *date_value = current;
235
+ } else if header_config.is_time_column(i) {
236
+ current = DateTimeProcessor::new(*date_value).combine_datetime_parts(current);
185
237
  }
186
238
 
187
- Ok(records)
188
- }
239
+ let adjusted_time = if current.nanosecond() > 0 {
240
+ current + chrono::Duration::seconds(1)
241
+ - chrono::Duration::nanoseconds(current.nanosecond() as i64)
242
+ } else {
243
+ current
244
+ };
189
245
 
190
- fn date_value_is_not_present(date: &usize, r: &Vec<&Data>) -> bool {
191
- r[*date] == &Data::Empty
246
+ write!(dest, "{}", adjusted_time.format("%Y-%m-%d %H:%M:%S"))
192
247
  }
193
248
 
194
- fn skip_excluded_rows(request_id: &usize, status: &Option<&usize>, r: &Vec<&Data>, exclusions: &Vec<String>) -> bool {
195
- if let Some(status_index) = status {
196
- if let Some(status) = r.get(**status_index) {
197
- if let Some(status_str) = status.as_string() {
198
- if status_str.eq("Recalled") {
199
- return false
200
- }
201
- }
202
- }
249
+ fn handle_datetime<W: Write>(
250
+ dest: &mut W,
251
+ d: ExcelDateTime,
252
+ _ri: usize,
253
+ i: usize,
254
+ header_config: &HeaderConfig,
255
+ date_value: &mut NaiveDateTime,
256
+ ) -> std::io::Result<()> {
257
+ let mut current = d.as_datetime().ok_or_else(|| {
258
+ std::io::Error::new(
259
+ std::io::ErrorKind::InvalidData,
260
+ format!(
261
+ "Could not parse datetime '{:?}', row: {}, col: Date",
262
+ d, _ri
263
+ ),
264
+ )
265
+ })?;
266
+
267
+ if header_config.is_date_column(i) {
268
+ *date_value = current;
269
+ } else if header_config.is_time_column(i) {
270
+ current = DateTimeProcessor::new(*date_value).combine_datetime_parts(current);
203
271
  }
204
272
 
205
- let value = r[*request_id].to_string();
206
- exclusions.contains(&value.to_string())
207
- }
208
-
209
- fn skip_excluded_status_rows(status: &Option<&usize>, r: &Vec<&Data>, exclusions: &Vec<String>) -> bool {
210
- status
211
- .map(|index| exclusions.contains(&r[*index].to_string()))
212
- .unwrap_or(false)
213
- }
214
-
215
- fn skip_empty_rows(r: &Vec<&Data>) -> bool {
216
- r.into_iter().all(|c| c == &&Data::Empty)
273
+ write!(dest, "{}", current)
217
274
  }
218
275
 
219
- fn skip_rows_with_no_request_id(request_id: &usize, r: &Vec<&Data>) -> bool {
220
- r[*request_id] == &Data::Empty
221
- }
222
-
223
- fn skip_rows_before_start_date(start_date: &Option<NaiveDateTime>, r: &Vec<&Data>, date_index: &usize) -> bool {
224
- if let Some(start_date) = start_date {
225
- if let Some(date_data) = r.get(*date_index) {
226
- match date_data {
227
- Data::DateTime(d) => {
228
- if let Some(date) = d.as_datetime() {
229
- return date <= *start_date;
230
- }
231
- }
232
- Data::DateTimeIso(s) => {
233
- if let Some(date) = string_to_datetime(s) {
234
- return date <= *start_date;
235
- }
236
- }
237
- _ => {}
238
- }
239
- }
240
- }
241
- false
242
- }
243
-
244
- fn transform_time_to_datetime(t1: NaiveDateTime, t2: NaiveDateTime) -> NaiveDateTime {
245
- NaiveDateTime::new(t1.date(), t2.time())
276
+ fn get_mandatory_records<'a>(
277
+ _ruby: &Ruby,
278
+ range: &'a Range<Data>,
279
+ csv_header_list: &[String],
280
+ mandatory_headers_list: &[String],
281
+ ) -> magnus::error::Result<Vec<Vec<&'a Data>>> {
282
+ let inverse_header_map: HashMap<usize, String> = csv_header_list
283
+ .iter()
284
+ .enumerate()
285
+ .map(|(i, h)| (i, h.to_string()))
286
+ .collect();
287
+
288
+ range
289
+ .rows()
290
+ .map(|row| {
291
+ let mut columns: Vec<MandatoryColumn<&Data>> = row
292
+ .iter()
293
+ .enumerate()
294
+ .filter_map(|(i, column_value)| {
295
+ inverse_header_map.get(&i).and_then(|column_name| {
296
+ if mandatory_headers_list.contains(column_name) {
297
+ index_of_header_in_mandatory_list(
298
+ mandatory_headers_list.to_vec(),
299
+ column_name.to_string(),
300
+ )
301
+ .map(|index| MandatoryColumn::new(column_value, index))
302
+ } else {
303
+ None
304
+ }
305
+ })
306
+ })
307
+ .collect();
308
+
309
+ columns.sort_by_key(|c| c.index);
310
+
311
+ Ok(columns.into_iter().map(|c| c.value).collect())
312
+ })
313
+ .collect()
246
314
  }
247
315
 
248
316
  fn handle_commas<W: Write>(dest: &mut W, s: &str) -> std::io::Result<()> {
249
- if s.contains(",") {
250
- write!(dest, "{:?}", clean_strings(s).trim_end())
317
+ let cleaned = s
318
+ .chars()
319
+ .map(|c| match c {
320
+ '\n' => ' ',
321
+ '\r' | '"' => '\0',
322
+ _ => c,
323
+ })
324
+ .filter(|&c| c != '\0')
325
+ .collect::<String>();
326
+
327
+ if s.contains(',') {
328
+ write!(dest, "{:?}", cleaned.trim_end())
251
329
  } else {
252
- write!(dest, "{}", clean_strings(s).trim_end())
253
- }
254
- }
255
-
256
- fn clean_strings(s: &str) -> String {
257
- s.replace("\n", " ")
258
- .replace("\r", "")
259
- .replace("\"", "")
260
- }
261
-
262
- struct XlsMandatoryColumn<'a> {
263
- value: &'a Data,
264
- index: usize,
265
- }
266
-
267
- impl<'a> XlsMandatoryColumn<'a> {
268
- fn new(value: &'a Data, index: usize) -> Self {
269
- XlsMandatoryColumn { value, index }
330
+ write!(dest, "{}", cleaned.trim_end())
270
331
  }
271
332
  }
Binary file
Binary file
Binary file
Binary file
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CsvUtils
4
- VERSION = '0.1.23'
4
+ VERSION = '0.1.24'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patchwork_csv_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.23
4
+ version: 0.1.24
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - kingsley.hendrickse
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
11
+ date: 2025-11-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Deduplication of CSV files and XLS to CSV conversion.
14
14
  email:
@@ -32,6 +32,11 @@ files:
32
32
  - ext/csv_utils/src/utils/csv.rs
33
33
  - ext/csv_utils/src/utils/dedup.rs
34
34
  - ext/csv_utils/src/utils/mod.rs
35
+ - ext/csv_utils/src/utils/shared/datetime.rs
36
+ - ext/csv_utils/src/utils/shared/filters.rs
37
+ - ext/csv_utils/src/utils/shared/mod.rs
38
+ - ext/csv_utils/src/utils/shared/types.rs
39
+ - ext/csv_utils/src/utils/shared/validation.rs
35
40
  - ext/csv_utils/src/utils/xls.rs
36
41
  - lib/csv_utils.rb
37
42
  - lib/csv_utils/2.7/csv_utils.so
@@ -63,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
68
  - !ruby/object:Gem::Version
64
69
  version: '0'
65
70
  requirements: []
66
- rubygems_version: 3.4.4
71
+ rubygems_version: 3.5.23
67
72
  signing_key:
68
73
  specification_version: 4
69
74
  summary: Fast CSV utils