patchwork_csv_utils 0.1.25-aarch64-linux → 0.1.26-aarch64-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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/ext/csv_utils/src/utils/csv.rs +14 -5
- data/ext/csv_utils/src/utils/dedup.rs +2 -2
- data/ext/csv_utils/src/utils/mod.rs +5 -5
- data/ext/csv_utils/src/utils/shared/dropped_rows.rs +25 -0
- data/ext/csv_utils/src/utils/shared/filters.rs +172 -5
- data/ext/csv_utils/src/utils/shared/mod.rs +1 -0
- data/ext/csv_utils/src/utils/xls.rs +16 -7
- data/lib/csv_utils/2.7/csv_utils.so +0 -0
- data/lib/csv_utils/3.0/csv_utils.so +0 -0
- data/lib/csv_utils/3.1/csv_utils.so +0 -0
- data/lib/csv_utils/3.2/csv_utils.so +0 -0
- data/lib/csv_utils/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48b29376c2100ab16451b75344c24acf4e5262436c9063ca37938ddc7c6f82ec
|
|
4
|
+
data.tar.gz: 1a1a0e368e425b2dbc65fcdfecfa4c871c51914b5c97ee6511f60691f5edfb42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be279537c6c0f636394bb8743ea910865a424a369bcdc868f3df536669b678e0bfed456bd89e0560a60dbb3ca22cc3d23bf5f20b1b0e5f8a2aa58162fa327022
|
|
7
|
+
data.tar.gz: e66000aa0bb16e83fb2dc9fdc5dd6145865e27a4d5808591b3ce4c107e2ecbf7100478781d335939c7ff4a26933861abc3447002fb0e026dfce8a875d0bfb191
|
data/Gemfile.lock
CHANGED
|
@@ -5,7 +5,8 @@ use std::collections::HashMap;
|
|
|
5
5
|
use std::fs::File;
|
|
6
6
|
|
|
7
7
|
use crate::utils::shared::datetime::DateTimeProcessor;
|
|
8
|
-
use crate::utils::shared::
|
|
8
|
+
use crate::utils::shared::dropped_rows::dropped_to_ruby;
|
|
9
|
+
use crate::utils::shared::filters::{FilterableRecord, RowFilters, SkipReason};
|
|
9
10
|
use crate::utils::shared::types::{HeaderConfig, MandatoryColumn, ProcessingConfig};
|
|
10
11
|
use crate::utils::shared::validation::TrustValidator;
|
|
11
12
|
use crate::utils::{
|
|
@@ -25,7 +26,7 @@ pub fn transform_csv(
|
|
|
25
26
|
expected_trust_name: String,
|
|
26
27
|
is_streamed_file: bool,
|
|
27
28
|
earliest_start_date: Option<String>,
|
|
28
|
-
) -> magnus::error::Result<
|
|
29
|
+
) -> magnus::error::Result<RArray> {
|
|
29
30
|
if !csv_path.has_extension(&["csv"]) {
|
|
30
31
|
return Err(Error::new(
|
|
31
32
|
ruby.exception_standard_error(),
|
|
@@ -55,7 +56,7 @@ pub fn transform_csv(
|
|
|
55
56
|
if let Some(value) =
|
|
56
57
|
check_mandatory_headers(ruby, &headers_list, &config.mandatory_headers, "csv")
|
|
57
58
|
{
|
|
58
|
-
return value;
|
|
59
|
+
return Err(value);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
let header_map = create_header_map(&config.mandatory_headers);
|
|
@@ -80,13 +81,21 @@ pub fn transform_csv(
|
|
|
80
81
|
let mandatory_records =
|
|
81
82
|
get_mandatory_records(ruby, &mut csv, &headers_list, &config.mandatory_headers)?;
|
|
82
83
|
|
|
84
|
+
let mut dropped: Vec<(String, SkipReason)> = Vec::new();
|
|
85
|
+
|
|
83
86
|
for (ri, record) in mandatory_records.iter().enumerate() {
|
|
84
|
-
if filters.
|
|
87
|
+
if let Some(reason) = filters.skip_reason(
|
|
85
88
|
record,
|
|
86
89
|
header_config.request_id,
|
|
87
90
|
header_config.status,
|
|
88
91
|
header_config.date,
|
|
89
92
|
) {
|
|
93
|
+
dropped.push((
|
|
94
|
+
record
|
|
95
|
+
.get_request_id(header_config.request_id)
|
|
96
|
+
.unwrap_or_default(),
|
|
97
|
+
reason,
|
|
98
|
+
));
|
|
90
99
|
continue;
|
|
91
100
|
}
|
|
92
101
|
|
|
@@ -149,7 +158,7 @@ pub fn transform_csv(
|
|
|
149
158
|
|
|
150
159
|
wtr.flush().map_err(|e| magnus_err(ruby, e, "flush"))?;
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
dropped_to_ruby(ruby, dropped)
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
fn get_mandatory_records(
|
|
@@ -48,13 +48,13 @@ pub fn dedup(
|
|
|
48
48
|
&mandatory_headers,
|
|
49
49
|
"previous_csv",
|
|
50
50
|
) {
|
|
51
|
-
return err;
|
|
51
|
+
return Err(err);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if let Some(err) =
|
|
55
55
|
check_mandatory_headers(ruby, &new_headers_list, &mandatory_headers, "new_csv")
|
|
56
56
|
{
|
|
57
|
-
return err;
|
|
57
|
+
return Err(err);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
wtr.write_byte_record(&headers_as_byte_record(mandatory_headers.clone()))
|
|
@@ -49,14 +49,14 @@ fn check_mandatory_headers(
|
|
|
49
49
|
headers: &[String],
|
|
50
50
|
mandatory_headers: &[String],
|
|
51
51
|
message: &str,
|
|
52
|
-
) -> Option<magnus::
|
|
52
|
+
) -> Option<magnus::Error> {
|
|
53
53
|
let csv_mandatory_headers = filter_headers(headers, mandatory_headers);
|
|
54
54
|
|
|
55
55
|
if csv_mandatory_headers.is_empty() {
|
|
56
|
-
return Some(
|
|
56
|
+
return Some(magnus::Error::new(
|
|
57
57
|
ruby.exception_standard_error(),
|
|
58
58
|
format!("{} has no mandatory headers", message),
|
|
59
|
-
))
|
|
59
|
+
));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
let csv_mandatory_headers = csv_mandatory_headers.to_owned().clone();
|
|
@@ -71,14 +71,14 @@ fn check_mandatory_headers(
|
|
|
71
71
|
.iter()
|
|
72
72
|
.map(|h| h.to_string())
|
|
73
73
|
.collect::<Vec<String>>();
|
|
74
|
-
return Some(
|
|
74
|
+
return Some(magnus::Error::new(
|
|
75
75
|
ruby.exception_standard_error(),
|
|
76
76
|
format!(
|
|
77
77
|
"{} is missing mandatory headers: {}",
|
|
78
78
|
message,
|
|
79
79
|
missing_headers.join(", ")
|
|
80
80
|
),
|
|
81
|
-
))
|
|
81
|
+
));
|
|
82
82
|
}
|
|
83
83
|
None
|
|
84
84
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
use magnus::{RArray, Ruby};
|
|
2
|
+
|
|
3
|
+
use crate::utils::shared::filters::SkipReason;
|
|
4
|
+
|
|
5
|
+
/// Converts dropped rows into `[[request_id, reason], ...]` for Ruby.
|
|
6
|
+
///
|
|
7
|
+
/// A flat array of pairs rather than a hash: a Request Id can legitimately appear more
|
|
8
|
+
/// than once in an export, and keying by it would silently collapse those into one.
|
|
9
|
+
///
|
|
10
|
+
/// The id is empty for `empty_row` and `empty_request_id`, because those rows have no
|
|
11
|
+
/// id to report. Consumers should treat those two reasons as counts rather than as
|
|
12
|
+
/// identifiable rows.
|
|
13
|
+
pub fn dropped_to_ruby(
|
|
14
|
+
ruby: &Ruby,
|
|
15
|
+
dropped: Vec<(String, SkipReason)>,
|
|
16
|
+
) -> magnus::error::Result<RArray> {
|
|
17
|
+
let out = ruby.ary_new_capa(dropped.len());
|
|
18
|
+
for (request_id, reason) in dropped {
|
|
19
|
+
let pair = ruby.ary_new_capa(2);
|
|
20
|
+
pair.push(request_id)?;
|
|
21
|
+
pair.push(reason.as_str())?;
|
|
22
|
+
out.push(pair)?;
|
|
23
|
+
}
|
|
24
|
+
Ok(out)
|
|
25
|
+
}
|
|
@@ -10,6 +10,32 @@ pub trait FilterableRecord {
|
|
|
10
10
|
fn get_date(&self, index: usize) -> Option<NaiveDateTime>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/// Why a row was dropped.
|
|
14
|
+
///
|
|
15
|
+
/// The filter used to return a bare bool, so when a shift went missing there was no way
|
|
16
|
+
/// to tell whether Allocate never sent it or Patchwork discarded it, and if discarded,
|
|
17
|
+
/// on which rule. See PLT-2487.
|
|
18
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
19
|
+
pub enum SkipReason {
|
|
20
|
+
EmptyRow,
|
|
21
|
+
EmptyRequestId,
|
|
22
|
+
PendingTaskExclusion,
|
|
23
|
+
StatusExclusion,
|
|
24
|
+
EarliestStartDate,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl SkipReason {
|
|
28
|
+
pub fn as_str(self) -> &'static str {
|
|
29
|
+
match self {
|
|
30
|
+
SkipReason::EmptyRow => "empty_row",
|
|
31
|
+
SkipReason::EmptyRequestId => "empty_request_id",
|
|
32
|
+
SkipReason::PendingTaskExclusion => "pending_task_exclusion",
|
|
33
|
+
SkipReason::StatusExclusion => "status_exclusion",
|
|
34
|
+
SkipReason::EarliestStartDate => "earliest_start_date",
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
pub struct RowFilters {
|
|
14
40
|
exclusions: Vec<String>,
|
|
15
41
|
status_exclusions: Vec<String>,
|
|
@@ -50,11 +76,36 @@ impl RowFilters {
|
|
|
50
76
|
status_index: Option<usize>,
|
|
51
77
|
date_index: usize,
|
|
52
78
|
) -> bool {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
self.skip_reason(record, request_id_index, status_index, date_index)
|
|
80
|
+
.is_some()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// First match wins, preserving the original short-circuit order. A row can satisfy
|
|
84
|
+
/// several rules at once; reporting the first is enough to explain the drop and
|
|
85
|
+
/// avoids evaluating predicates whose answer cannot change the outcome.
|
|
86
|
+
pub fn skip_reason<R: FilterableRecord>(
|
|
87
|
+
&self,
|
|
88
|
+
record: &R,
|
|
89
|
+
request_id_index: usize,
|
|
90
|
+
status_index: Option<usize>,
|
|
91
|
+
date_index: usize,
|
|
92
|
+
) -> Option<SkipReason> {
|
|
93
|
+
if record.is_empty() {
|
|
94
|
+
return Some(SkipReason::EmptyRow);
|
|
95
|
+
}
|
|
96
|
+
if record.has_empty_first_column() {
|
|
97
|
+
return Some(SkipReason::EmptyRequestId);
|
|
98
|
+
}
|
|
99
|
+
if self.should_skip_by_exclusion(record, request_id_index, status_index) {
|
|
100
|
+
return Some(SkipReason::PendingTaskExclusion);
|
|
101
|
+
}
|
|
102
|
+
if self.should_skip_by_status(record, status_index) {
|
|
103
|
+
return Some(SkipReason::StatusExclusion);
|
|
104
|
+
}
|
|
105
|
+
if self.should_skip_by_date(record, date_index) {
|
|
106
|
+
return Some(SkipReason::EarliestStartDate);
|
|
107
|
+
}
|
|
108
|
+
None
|
|
58
109
|
}
|
|
59
110
|
|
|
60
111
|
fn should_skip_by_exclusion<R: FilterableRecord>(
|
|
@@ -195,4 +246,120 @@ mod tests {
|
|
|
195
246
|
2
|
|
196
247
|
));
|
|
197
248
|
}
|
|
249
|
+
|
|
250
|
+
fn reason_for(filters: &RowFilters, cells: &[&str]) -> Option<SkipReason> {
|
|
251
|
+
filters.skip_reason(&row(cells), 0, Some(1), 2)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn reports_which_rule_dropped_the_row() {
|
|
256
|
+
let filters = RowFilters::new(
|
|
257
|
+
vec!["0726154700".to_string()],
|
|
258
|
+
vec!["Cancelled".to_string()],
|
|
259
|
+
Some(
|
|
260
|
+
NaiveDateTime::parse_from_str("2026-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
assert_eq!(
|
|
265
|
+
reason_for(&filters, &["", "", ""]),
|
|
266
|
+
Some(SkipReason::EmptyRow)
|
|
267
|
+
);
|
|
268
|
+
assert_eq!(
|
|
269
|
+
reason_for(&filters, &["", "UnFilled Bank", "2026-07-30 00:00:00"]),
|
|
270
|
+
Some(SkipReason::EmptyRequestId)
|
|
271
|
+
);
|
|
272
|
+
assert_eq!(
|
|
273
|
+
reason_for(
|
|
274
|
+
&filters,
|
|
275
|
+
&["0726154700", "UnFilled Bank", "2026-07-30 00:00:00"]
|
|
276
|
+
),
|
|
277
|
+
Some(SkipReason::PendingTaskExclusion)
|
|
278
|
+
);
|
|
279
|
+
assert_eq!(
|
|
280
|
+
reason_for(
|
|
281
|
+
&filters,
|
|
282
|
+
&["0726154999", "Cancelled", "2026-07-30 00:00:00"]
|
|
283
|
+
),
|
|
284
|
+
Some(SkipReason::StatusExclusion)
|
|
285
|
+
);
|
|
286
|
+
assert_eq!(
|
|
287
|
+
reason_for(
|
|
288
|
+
&filters,
|
|
289
|
+
&["0726154999", "UnFilled Bank", "2025-06-01 00:00:00"]
|
|
290
|
+
),
|
|
291
|
+
Some(SkipReason::EarliestStartDate)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn reports_no_reason_for_a_row_that_survives() {
|
|
297
|
+
let filters = excluding("0726154700");
|
|
298
|
+
|
|
299
|
+
assert_eq!(
|
|
300
|
+
reason_for(
|
|
301
|
+
&filters,
|
|
302
|
+
&["0726154999", "UnFilled Bank", "2026-07-30 00:00:00"]
|
|
303
|
+
),
|
|
304
|
+
None
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[test]
|
|
309
|
+
fn skip_reason_and_should_skip_always_agree() {
|
|
310
|
+
let filters = RowFilters::new(
|
|
311
|
+
vec!["0726154700".to_string()],
|
|
312
|
+
vec!["Cancelled".to_string()],
|
|
313
|
+
None,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
for cells in [
|
|
317
|
+
vec!["", "", ""],
|
|
318
|
+
vec!["", "UnFilled Bank", "2026-07-30 00:00:00"],
|
|
319
|
+
vec!["0726154700", "UnFilled Bank", "2026-07-30 00:00:00"],
|
|
320
|
+
vec!["0726154999", "Cancelled", "2026-07-30 00:00:00"],
|
|
321
|
+
vec!["0726154999", "UnFilled Bank", "2026-07-30 00:00:00"],
|
|
322
|
+
] {
|
|
323
|
+
let record = row(&cells);
|
|
324
|
+
assert_eq!(
|
|
325
|
+
filters.should_skip(&record, 0, Some(1), 2),
|
|
326
|
+
filters.skip_reason(&record, 0, Some(1), 2).is_some(),
|
|
327
|
+
"disagreement on {cells:?}"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn reports_the_first_matching_rule_when_several_apply() {
|
|
334
|
+
// Excluded AND status-excluded. The exclusion check runs first, and reporting one
|
|
335
|
+
// reason is enough to explain the drop.
|
|
336
|
+
let filters = RowFilters::new(
|
|
337
|
+
vec!["0726154700".to_string()],
|
|
338
|
+
vec!["Cancelled".to_string()],
|
|
339
|
+
None,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
assert_eq!(
|
|
343
|
+
reason_for(
|
|
344
|
+
&filters,
|
|
345
|
+
&["0726154700", "Cancelled", "2026-07-30 00:00:00"]
|
|
346
|
+
),
|
|
347
|
+
Some(SkipReason::PendingTaskExclusion)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#[test]
|
|
352
|
+
fn reason_names_are_stable_for_consumers() {
|
|
353
|
+
assert_eq!(SkipReason::EmptyRow.as_str(), "empty_row");
|
|
354
|
+
assert_eq!(SkipReason::EmptyRequestId.as_str(), "empty_request_id");
|
|
355
|
+
assert_eq!(
|
|
356
|
+
SkipReason::PendingTaskExclusion.as_str(),
|
|
357
|
+
"pending_task_exclusion"
|
|
358
|
+
);
|
|
359
|
+
assert_eq!(SkipReason::StatusExclusion.as_str(), "status_exclusion");
|
|
360
|
+
assert_eq!(
|
|
361
|
+
SkipReason::EarliestStartDate.as_str(),
|
|
362
|
+
"earliest_start_date"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
198
365
|
}
|
|
@@ -7,7 +7,8 @@ use chrono::{NaiveDateTime, Timelike, Utc};
|
|
|
7
7
|
use magnus::{RArray, Ruby};
|
|
8
8
|
|
|
9
9
|
use crate::utils::shared::datetime::DateTimeProcessor;
|
|
10
|
-
use crate::utils::shared::
|
|
10
|
+
use crate::utils::shared::dropped_rows::dropped_to_ruby;
|
|
11
|
+
use crate::utils::shared::filters::{FilterableRecord, RowFilters, SkipReason};
|
|
11
12
|
use crate::utils::shared::types::{HeaderConfig, MandatoryColumn, ProcessingConfig};
|
|
12
13
|
use crate::utils::shared::validation::TrustValidator;
|
|
13
14
|
use crate::utils::{
|
|
@@ -26,7 +27,7 @@ pub fn to_csv(
|
|
|
26
27
|
expected_trust_name: String,
|
|
27
28
|
is_streamed_file: bool,
|
|
28
29
|
earliest_start_date: Option<String>,
|
|
29
|
-
) -> magnus::error::Result<
|
|
30
|
+
) -> magnus::error::Result<RArray> {
|
|
30
31
|
if !xls_path.has_extension(&["xls", "xlsx"]) {
|
|
31
32
|
return Err(magnus::Error::new(
|
|
32
33
|
ruby.exception_standard_error(),
|
|
@@ -72,7 +73,7 @@ pub fn to_csv(
|
|
|
72
73
|
if let Some(value) =
|
|
73
74
|
check_mandatory_headers(ruby, &headers_list, &config.mandatory_headers, "csv")
|
|
74
75
|
{
|
|
75
|
-
return value;
|
|
76
|
+
return Err(value);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
let csv_out_file = File::create(&target_path).map_err(|e| {
|
|
@@ -84,7 +85,8 @@ pub fn to_csv(
|
|
|
84
85
|
})?;
|
|
85
86
|
let mut dest = BufWriter::new(csv_out_file);
|
|
86
87
|
|
|
87
|
-
write_csv(ruby, &mut dest, &range, config, headers_list)
|
|
88
|
+
let dropped = write_csv(ruby, &mut dest, &range, config, headers_list)?;
|
|
89
|
+
dropped_to_ruby(ruby, dropped)
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
fn write_csv<W: Write>(
|
|
@@ -93,7 +95,7 @@ fn write_csv<W: Write>(
|
|
|
93
95
|
range: &Range<Data>,
|
|
94
96
|
config: ProcessingConfig,
|
|
95
97
|
headers_list: Vec<String>,
|
|
96
|
-
) -> magnus::error::Result<()
|
|
98
|
+
) -> magnus::error::Result<Vec<(String, SkipReason)>> {
|
|
97
99
|
let n = config.mandatory_headers.len() - 1;
|
|
98
100
|
let header_map: HashMap<String, usize> = config
|
|
99
101
|
.mandatory_headers
|
|
@@ -113,13 +115,20 @@ fn write_csv<W: Write>(
|
|
|
113
115
|
let mandatory_rows =
|
|
114
116
|
get_mandatory_records(ruby, range, &headers_list, &config.mandatory_headers)?;
|
|
115
117
|
|
|
118
|
+
let mut dropped: Vec<(String, SkipReason)> = Vec::new();
|
|
119
|
+
|
|
116
120
|
for (ri, r) in mandatory_rows.into_iter().enumerate() {
|
|
117
|
-
if filters.
|
|
121
|
+
if let Some(reason) = filters.skip_reason(
|
|
118
122
|
&r,
|
|
119
123
|
header_config.request_id,
|
|
120
124
|
header_config.status,
|
|
121
125
|
header_config.date,
|
|
122
126
|
) {
|
|
127
|
+
dropped.push((
|
|
128
|
+
r.get_request_id(header_config.request_id)
|
|
129
|
+
.unwrap_or_default(),
|
|
130
|
+
reason,
|
|
131
|
+
));
|
|
123
132
|
continue;
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -184,7 +193,7 @@ fn write_csv<W: Write>(
|
|
|
184
193
|
)
|
|
185
194
|
})?;
|
|
186
195
|
}
|
|
187
|
-
Ok(
|
|
196
|
+
Ok(dropped)
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
fn handle_datetime_iso<W: Write>(
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/csv_utils/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.1.26
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- kingsley.hendrickse
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Deduplication of CSV files and XLS to CSV conversion.
|
|
14
14
|
email:
|
|
@@ -33,6 +33,7 @@ files:
|
|
|
33
33
|
- ext/csv_utils/src/utils/dedup.rs
|
|
34
34
|
- ext/csv_utils/src/utils/mod.rs
|
|
35
35
|
- ext/csv_utils/src/utils/shared/datetime.rs
|
|
36
|
+
- ext/csv_utils/src/utils/shared/dropped_rows.rs
|
|
36
37
|
- ext/csv_utils/src/utils/shared/filters.rs
|
|
37
38
|
- ext/csv_utils/src/utils/shared/mod.rs
|
|
38
39
|
- ext/csv_utils/src/utils/shared/types.rs
|