csv-utils 0.5.0 → 0.6.0
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/CHANGELOG.md +37 -0
- data/README.md +92 -17
- data/bin/csv-change-eol +4 -8
- data/bin/csv-diff +19 -35
- data/bin/csv-duplicate-finder +17 -14
- data/bin/csv-find-error +7 -5
- data/bin/csv-readline +3 -14
- data/bin/csv-splitter +7 -5
- data/bin/csv-validator +10 -6
- data/lib/csv-utils.rb +3 -1
- data/lib/csv_utils/byte_order_mark.rb +34 -0
- data/lib/csv_utils/csv_compare.rb +89 -65
- data/lib/csv_utils/csv_extender.rb +23 -2
- data/lib/csv_utils/csv_iterator.rb +49 -22
- data/lib/csv_utils/csv_options.rb +65 -27
- data/lib/csv_utils/csv_report.rb +26 -3
- data/lib/csv_utils/csv_row.rb +30 -0
- data/lib/csv_utils/csv_row_matcher.rb +16 -2
- data/lib/csv_utils/csv_sort.rb +44 -18
- data/lib/csv_utils/csv_transformer.rb +79 -35
- data/lib/csv_utils/csv_wrapper.rb +24 -4
- data/lib/csv_utils/version.rb +6 -0
- metadata +27 -23
- data/.github/workflows/ci.yml +0 -53
- data/.gitignore +0 -52
- data/.rubocop.yml +0 -81
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/ARCHITECTURE.md +0 -154
- data/CLAUDE.md +0 -63
- data/Gemfile +0 -14
- data/Gemfile.lock +0 -77
- data/csv-utils.gemspec +0 -19
- data/script/console +0 -7
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CSVUtils
|
|
4
|
+
# Matches rows (hashes of header to value) whose values match a regular expression.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# matcher = CSVUtils::CSVRowMatcher.new(/error/i, ['status', 'message'])
|
|
8
|
+
# CSVUtils::CSVIterator.new('logs.csv').select(&matcher)
|
|
4
9
|
class CSVRowMatcher
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
# @return [Regexp]
|
|
11
|
+
attr_accessor :regex
|
|
12
|
+
# @return [Array<String>, :all] headers to search, or :all for every value
|
|
13
|
+
attr_accessor :columns
|
|
7
14
|
|
|
15
|
+
# @param regex [Regexp]
|
|
16
|
+
# @param columns [Array<String>, :all] headers to search
|
|
8
17
|
def initialize(regex, columns = :all)
|
|
9
18
|
self.regex = regex
|
|
10
19
|
self.columns = columns
|
|
11
20
|
end
|
|
12
21
|
|
|
22
|
+
# Whether any searched value matches. Missing and nil values never match.
|
|
23
|
+
# @param row [Hash{String => String}]
|
|
24
|
+
# @return [Boolean]
|
|
13
25
|
def match?(row)
|
|
14
26
|
if columns == :all
|
|
15
27
|
row.each_value do |value|
|
|
@@ -25,6 +37,8 @@ module CSVUtils
|
|
|
25
37
|
false
|
|
26
38
|
end
|
|
27
39
|
|
|
40
|
+
# Lets the matcher be passed to Enumerable methods as a block, as in rows.select(&matcher).
|
|
41
|
+
# @return [Proc]
|
|
28
42
|
def to_proc
|
|
29
43
|
proc do |row|
|
|
30
44
|
match?(row)
|
data/lib/csv_utils/csv_sort.rb
CHANGED
|
@@ -2,32 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
|
|
5
|
-
# Utility class for sorting the rows for a csv file
|
|
6
5
|
module CSVUtils
|
|
6
|
+
# Sorts a CSV file too large to load into memory with an external merge sort: batches of rows are
|
|
7
|
+
# sorted into temporary part files next to the new file, which are then merged in pairs.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# CSVUtils::CSVSort.new('input.csv', 'sorted.csv').sort { |a, b| a[0].to_i <=> b[0].to_i }
|
|
7
11
|
class CSVSort
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# @return [String] path of the file to sort
|
|
13
|
+
attr_reader :csv_file
|
|
14
|
+
# @return [String] path of the sorted file to write
|
|
15
|
+
attr_reader :new_csv_file
|
|
16
|
+
# @return [Boolean] whether the first row is a header row, kept at the top
|
|
17
|
+
attr_reader :has_headers
|
|
18
|
+
# @return [Hash] options passed to CSV.open
|
|
19
|
+
attr_reader :csv_options
|
|
20
|
+
# @return [Array<String>, nil] the header row, once {#sort} has read it
|
|
21
|
+
attr_reader :headers
|
|
22
|
+
|
|
23
|
+
# @param csv_file [String] path of the file to sort
|
|
24
|
+
# @param new_csv_file [String] path of the sorted file to write
|
|
25
|
+
# @param has_headers [Boolean] whether the first row is a header row
|
|
26
|
+
# @param csv_options [Hash] options passed to CSV.open
|
|
14
27
|
def initialize(csv_file, new_csv_file, has_headers = true, csv_options = {})
|
|
15
28
|
@csv_file = csv_file
|
|
16
29
|
@new_csv_file = new_csv_file
|
|
17
30
|
@has_headers = has_headers
|
|
18
31
|
@csv_options = csv_options
|
|
19
32
|
@csv_part_files = []
|
|
20
|
-
@files_to_delete = []
|
|
21
33
|
end
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
# Writes the sorted file. Temporary files are removed even when sorting fails.
|
|
36
|
+
# @param batch_size [Integer] rows held in memory and sorted at a time
|
|
37
|
+
# @yieldparam row1 [Array<String>]
|
|
38
|
+
# @yieldparam row2 [Array<String>]
|
|
39
|
+
# @yieldreturn [Integer] negative, 0 or positive like <=>; without a block rows are compared as arrays
|
|
40
|
+
# @return [void]
|
|
41
|
+
def sort(batch_size = 100_000, &block)
|
|
42
|
+
block ||= proc { |row1, row2| row1 <=> row2 }
|
|
43
|
+
@csv_part_files = []
|
|
44
|
+
create_sorted_csv_part_files(batch_size, &block)
|
|
45
|
+
merge_csv_part_files(&block)
|
|
46
|
+
ensure
|
|
47
|
+
delete_csv_part_files
|
|
26
48
|
end
|
|
27
49
|
|
|
28
50
|
private
|
|
29
51
|
|
|
30
|
-
# rubocop:disable Metrics/MethodLength
|
|
52
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
31
53
|
def merge_sort_csv_files(src_csv_file1, src_csv_file2, dest_csv_file)
|
|
32
54
|
src1 = CSV.open(src_csv_file1, 'rb', **csv_options)
|
|
33
55
|
begin
|
|
@@ -75,7 +97,6 @@ module CSVUtils
|
|
|
75
97
|
src1.close
|
|
76
98
|
end
|
|
77
99
|
end
|
|
78
|
-
# rubocop:enable Metrics/MethodLength
|
|
79
100
|
|
|
80
101
|
def create_sorted_csv_part_files(batch_size, &block)
|
|
81
102
|
src = CSV.open(csv_file, 'rb', **csv_options)
|
|
@@ -110,21 +131,26 @@ module CSVUtils
|
|
|
110
131
|
while @csv_part_files.size > 1
|
|
111
132
|
file_merge_cnt += 1
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
csv_part_file2 = @csv_part_files.
|
|
134
|
+
# inputs stay in the list until merged, so a failed merge still cleans them up
|
|
135
|
+
csv_part_file1, csv_part_file2 = @csv_part_files.first(2)
|
|
115
136
|
@csv_part_files << "#{new_csv_file}.merge.#{file_merge_cnt}"
|
|
116
137
|
|
|
117
138
|
merge_sort_csv_files(csv_part_file1, csv_part_file2, @csv_part_files.last, &)
|
|
118
139
|
|
|
119
|
-
File.unlink(
|
|
120
|
-
File.unlink(csv_part_file2)
|
|
140
|
+
@csv_part_files.shift(2).each { |file| File.unlink(file) }
|
|
121
141
|
end
|
|
122
142
|
|
|
123
143
|
if @csv_part_files.size.positive?
|
|
124
|
-
FileUtils.mv(@csv_part_files.
|
|
144
|
+
FileUtils.mv(@csv_part_files.pop, new_csv_file)
|
|
125
145
|
else
|
|
126
146
|
FileUtils.cp(@csv_file, new_csv_file)
|
|
127
147
|
end
|
|
128
148
|
end
|
|
149
|
+
|
|
150
|
+
# removes the temporary files left behind when sorting fails part way through
|
|
151
|
+
def delete_csv_part_files
|
|
152
|
+
@csv_part_files.each { |file| FileUtils.rm_f(file) }
|
|
153
|
+
@csv_part_files = []
|
|
154
|
+
end
|
|
129
155
|
end
|
|
130
156
|
end
|
|
@@ -1,63 +1,106 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Transforms a CSV given a series of steps
|
|
4
3
|
module CSVUtils
|
|
4
|
+
# Copies a CSV file through a pipeline of steps that filter, change and add to its rows in batches.
|
|
5
|
+
# Each step method returns the transformer so they can be chained, and {#process} runs them.
|
|
6
|
+
#
|
|
7
|
+
# Step blocks receive the row, the headers as they were when the step was added, and the value
|
|
8
|
+
# computed by the last {#additional_data} step for the current batch.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# CSVUtils::CSVTransformer.new('input.csv', 'output.csv')
|
|
12
|
+
# .read_headers
|
|
13
|
+
# .select { |row, _headers, _data| row[0].to_i > 100 }
|
|
14
|
+
# .append(['Email']) { |row, _headers, _data| ["#{row[1].downcase}@example.com"] }
|
|
15
|
+
# .process
|
|
5
16
|
class CSVTransformer
|
|
17
|
+
# @return [Array<String>, nil] the headers the output will have after the steps added so far
|
|
6
18
|
attr_reader :headers
|
|
7
19
|
|
|
20
|
+
# @param src_csv [String, CSV, #shift] path or CSV to read; paths are opened and closed by the transformer
|
|
21
|
+
# @param dest_csv [String, CSV, #<<] path or CSV to write
|
|
22
|
+
# @param csv_options [Hash] options passed to CSV.open for paths
|
|
8
23
|
def initialize(src_csv, dest_csv, csv_options = {})
|
|
9
24
|
@src_csv = CSVUtils::CSVWrapper.new(src_csv, 'rb', csv_options)
|
|
10
25
|
@dest_csv = CSVUtils::CSVWrapper.new(dest_csv, 'wb', csv_options)
|
|
11
26
|
end
|
|
12
27
|
|
|
28
|
+
# Reads the first row as the headers. Without it, every row is data and the output has no header row.
|
|
29
|
+
# @return [self]
|
|
13
30
|
def read_headers
|
|
14
31
|
@headers = @src_csv.shift
|
|
15
32
|
self
|
|
16
33
|
end
|
|
17
34
|
|
|
35
|
+
# Computes a value once per batch, passed to the steps that follow as their third argument.
|
|
36
|
+
# @yieldparam batch [Array<Array<String>>]
|
|
37
|
+
# @yieldparam headers [Array<String>, nil]
|
|
38
|
+
# @return [self]
|
|
18
39
|
def additional_data(&block)
|
|
19
40
|
steps << [:additional_data, @headers, block]
|
|
20
41
|
self
|
|
21
42
|
end
|
|
22
43
|
|
|
44
|
+
# Keeps the rows the block returns true for.
|
|
45
|
+
# @yieldparam row [Array<String>]
|
|
46
|
+
# @yieldparam headers [Array<String>, nil]
|
|
47
|
+
# @yieldparam additional_data [Object]
|
|
48
|
+
# @return [self]
|
|
23
49
|
def select(&block)
|
|
24
50
|
steps << [:select, @headers, block]
|
|
25
51
|
self
|
|
26
52
|
end
|
|
27
53
|
|
|
54
|
+
# Drops the rows the block returns true for.
|
|
55
|
+
# @yieldparam (see #select)
|
|
56
|
+
# @return [self]
|
|
28
57
|
def reject(&block)
|
|
29
58
|
steps << [:reject, @headers, block]
|
|
30
59
|
self
|
|
31
60
|
end
|
|
32
61
|
|
|
62
|
+
# Replaces each row with the block's result.
|
|
63
|
+
# @param new_headers [Array<String>, nil] the headers after this step
|
|
64
|
+
# @yieldparam (see #select)
|
|
65
|
+
# @yieldreturn [Array] the new row
|
|
66
|
+
# @return [self]
|
|
33
67
|
def map(new_headers, &block)
|
|
34
68
|
steps << [:map, @headers, block]
|
|
35
69
|
@headers = new_headers
|
|
36
70
|
self
|
|
37
71
|
end
|
|
38
72
|
|
|
73
|
+
# Adds the block's values to the end of each row.
|
|
74
|
+
# @param additional_headers [Array<String>, nil] headers for the new values; nil drops the header row
|
|
75
|
+
# @yieldparam (see #select)
|
|
76
|
+
# @yieldreturn [Array] values to append
|
|
77
|
+
# @return [self]
|
|
39
78
|
def append(additional_headers, &block)
|
|
40
79
|
steps << [:append, @headers, block]
|
|
41
|
-
|
|
42
|
-
if additional_headers
|
|
43
|
-
@headers += additional_headers
|
|
44
|
-
else
|
|
45
|
-
@headers = nil
|
|
46
|
-
end
|
|
47
|
-
|
|
80
|
+
# without headers (read_headers not called) the output has no header row to extend
|
|
81
|
+
@headers = (@headers + additional_headers if @headers && additional_headers)
|
|
48
82
|
self
|
|
49
83
|
end
|
|
50
84
|
|
|
85
|
+
# Calls the block for each row without changing it.
|
|
86
|
+
# @yieldparam (see #select)
|
|
87
|
+
# @return [self]
|
|
51
88
|
def each(&block)
|
|
52
89
|
steps << [:each, @headers, block]
|
|
53
90
|
self
|
|
54
91
|
end
|
|
55
92
|
|
|
93
|
+
# Replaces the headers written to the output.
|
|
94
|
+
# @param headers [Array<String>, nil]
|
|
95
|
+
# @return [self]
|
|
56
96
|
def set_headers(headers)
|
|
57
97
|
@headers = headers
|
|
58
98
|
self
|
|
59
99
|
end
|
|
60
100
|
|
|
101
|
+
# Runs the steps over every row, writes the output and closes the files it opened, even when a step raises.
|
|
102
|
+
# @param batch_size [Integer] rows read and run through the steps at a time
|
|
103
|
+
# @return [void]
|
|
61
104
|
def process(batch_size = 10_000)
|
|
62
105
|
batch = []
|
|
63
106
|
|
|
@@ -79,7 +122,7 @@ module CSVUtils
|
|
|
79
122
|
end
|
|
80
123
|
|
|
81
124
|
steps_proc.call if batch.size.positive?
|
|
82
|
-
|
|
125
|
+
ensure
|
|
83
126
|
@src_csv.close
|
|
84
127
|
@dest_csv.close
|
|
85
128
|
end
|
|
@@ -90,33 +133,34 @@ module CSVUtils
|
|
|
90
133
|
@steps ||= []
|
|
91
134
|
end
|
|
92
135
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
batch.select! do |row|
|
|
97
|
-
block.call row, current_headers, @additional_data
|
|
98
|
-
end
|
|
99
|
-
when :reject
|
|
100
|
-
batch.reject! do |row|
|
|
101
|
-
block.call row, current_headers, @additional_data
|
|
102
|
-
end
|
|
103
|
-
when :map
|
|
104
|
-
batch.map! do |row|
|
|
105
|
-
block.call row, current_headers, @additional_data
|
|
106
|
-
end
|
|
107
|
-
when :append
|
|
108
|
-
batch.map! do |row|
|
|
109
|
-
row + block.call(row, current_headers, @additional_data)
|
|
110
|
-
end
|
|
111
|
-
when :additional_data
|
|
112
|
-
@additional_data = block.call(batch, current_headers)
|
|
113
|
-
when :each
|
|
114
|
-
batch.each do |row|
|
|
115
|
-
block.call(row, current_headers, @additional_data)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
136
|
+
# each step type has a process_<type>_step method that changes the batch in place
|
|
137
|
+
def process_step(step_type, current_headers, batch, &)
|
|
138
|
+
send(:"process_#{step_type}_step", current_headers, batch, &)
|
|
119
139
|
batch
|
|
120
140
|
end
|
|
141
|
+
|
|
142
|
+
def process_select_step(current_headers, batch)
|
|
143
|
+
batch.select! { |row| yield row, current_headers, @additional_data }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def process_reject_step(current_headers, batch)
|
|
147
|
+
batch.reject! { |row| yield row, current_headers, @additional_data }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def process_map_step(current_headers, batch)
|
|
151
|
+
batch.map! { |row| yield row, current_headers, @additional_data }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def process_append_step(current_headers, batch)
|
|
155
|
+
batch.map! { |row| row + yield(row, current_headers, @additional_data) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def process_additional_data_step(current_headers, batch)
|
|
159
|
+
@additional_data = yield batch, current_headers
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def process_each_step(current_headers, batch)
|
|
163
|
+
batch.each { |row| yield row, current_headers, @additional_data }
|
|
164
|
+
end
|
|
121
165
|
end
|
|
122
166
|
end
|
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Wraps a CSV object, if wrapper opens the csv file it will close it
|
|
4
3
|
module CSVUtils
|
|
4
|
+
# Wraps a path or an open CSV so callers can read, write and close it the same way.
|
|
5
|
+
# It closes only the files it opened itself.
|
|
5
6
|
class CSVWrapper
|
|
7
|
+
# @return [CSV, #shift, #<<] the CSV being wrapped
|
|
6
8
|
attr_reader :csv
|
|
7
9
|
|
|
10
|
+
# @param csv [String, CSV, #shift, #<<] path to open, or a CSV (or array of rows) to use as is
|
|
11
|
+
# @param mode [String] file mode for a path
|
|
12
|
+
# @param csv_options [Hash] options passed to CSV.open for a path
|
|
8
13
|
def initialize(csv, mode, csv_options)
|
|
9
14
|
open(csv, mode, csv_options)
|
|
10
15
|
end
|
|
11
16
|
|
|
17
|
+
# Wraps a path or CSV; with a block, yields the wrapper and closes it afterwards, even when the block raises.
|
|
18
|
+
# @param file [String, CSV] path or CSV
|
|
19
|
+
# @param mode [String] file mode for a path
|
|
20
|
+
# @param csv_options [Hash] options passed to CSV.open for a path
|
|
21
|
+
# @yieldparam csv [CSVWrapper]
|
|
22
|
+
# @return [CSVWrapper, Object] the wrapper, or the block's value
|
|
12
23
|
def self.open(file, mode, csv_options = {})
|
|
13
24
|
csv = new(file, mode, csv_options)
|
|
25
|
+
return csv unless block_given?
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
begin
|
|
16
28
|
yield csv
|
|
29
|
+
ensure
|
|
17
30
|
csv.close
|
|
18
|
-
else
|
|
19
|
-
csv
|
|
20
31
|
end
|
|
21
32
|
end
|
|
22
33
|
|
|
34
|
+
# @api private
|
|
35
|
+
# @return [CSV, #shift, #<<]
|
|
23
36
|
def open(csv, mode, csv_options)
|
|
24
37
|
if csv.is_a?(String)
|
|
25
38
|
@close_when_done = true
|
|
@@ -30,18 +43,25 @@ module CSVUtils
|
|
|
30
43
|
end
|
|
31
44
|
end
|
|
32
45
|
|
|
46
|
+
# @param row [Array]
|
|
47
|
+
# @return [CSV, #<<]
|
|
33
48
|
def <<(row)
|
|
34
49
|
csv << row
|
|
35
50
|
end
|
|
36
51
|
|
|
52
|
+
# @return [Array<String>, nil] the next row, nil at the end
|
|
37
53
|
def shift
|
|
38
54
|
csv.shift
|
|
39
55
|
end
|
|
40
56
|
|
|
57
|
+
# Moves back to the first row.
|
|
58
|
+
# @return [void]
|
|
41
59
|
def rewind
|
|
42
60
|
csv.rewind
|
|
43
61
|
end
|
|
44
62
|
|
|
63
|
+
# Closes the CSV if the wrapper opened it.
|
|
64
|
+
# @return [void]
|
|
45
65
|
def close
|
|
46
66
|
csv.close if close_when_done?
|
|
47
67
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: csv-utils
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: csv
|
|
@@ -15,31 +15,39 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
18
|
+
version: '3.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
25
|
+
version: '3.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: inheritance-helper
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
32
|
+
version: '0.2'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
33
36
|
type: :runtime
|
|
34
37
|
prerelease: false
|
|
35
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
39
|
requirements:
|
|
37
40
|
- - ">="
|
|
38
41
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
version: '0.2'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
46
|
+
description: Streaming tools for CSV files too big or too broken to load into memory.
|
|
47
|
+
Compare two sorted files into create, update and delete actions, sort with an external
|
|
48
|
+
merge sort, transform and extend rows in batches, and build reports from Ruby objects.
|
|
49
|
+
Detects separators, byte order marks and encodings, and ships command line tools
|
|
50
|
+
that pinpoint malformed rows, diff, grep, split and validate CSV files.
|
|
43
51
|
email: dougyouch@gmail.com
|
|
44
52
|
executables:
|
|
45
53
|
- csv-change-eol
|
|
@@ -54,15 +62,7 @@ executables:
|
|
|
54
62
|
extensions: []
|
|
55
63
|
extra_rdoc_files: []
|
|
56
64
|
files:
|
|
57
|
-
-
|
|
58
|
-
- ".gitignore"
|
|
59
|
-
- ".rubocop.yml"
|
|
60
|
-
- ".ruby-gemset"
|
|
61
|
-
- ".ruby-version"
|
|
62
|
-
- ARCHITECTURE.md
|
|
63
|
-
- CLAUDE.md
|
|
64
|
-
- Gemfile
|
|
65
|
-
- Gemfile.lock
|
|
65
|
+
- CHANGELOG.md
|
|
66
66
|
- LICENSE
|
|
67
67
|
- README.md
|
|
68
68
|
- bin/csv-change-eol
|
|
@@ -74,8 +74,8 @@ files:
|
|
|
74
74
|
- bin/csv-readline
|
|
75
75
|
- bin/csv-splitter
|
|
76
76
|
- bin/csv-validator
|
|
77
|
-
- csv-utils.gemspec
|
|
78
77
|
- lib/csv-utils.rb
|
|
78
|
+
- lib/csv_utils/byte_order_mark.rb
|
|
79
79
|
- lib/csv_utils/csv_compare.rb
|
|
80
80
|
- lib/csv_utils/csv_extender.rb
|
|
81
81
|
- lib/csv_utils/csv_iterator.rb
|
|
@@ -86,12 +86,16 @@ files:
|
|
|
86
86
|
- lib/csv_utils/csv_sort.rb
|
|
87
87
|
- lib/csv_utils/csv_transformer.rb
|
|
88
88
|
- lib/csv_utils/csv_wrapper.rb
|
|
89
|
-
-
|
|
89
|
+
- lib/csv_utils/version.rb
|
|
90
90
|
homepage: https://github.com/dougyouch/csv-utils
|
|
91
91
|
licenses:
|
|
92
92
|
- MIT
|
|
93
93
|
metadata:
|
|
94
94
|
rubygems_mfa_required: 'true'
|
|
95
|
+
source_code_uri: https://github.com/dougyouch/csv-utils
|
|
96
|
+
changelog_uri: https://github.com/dougyouch/csv-utils/blob/master/CHANGELOG.md
|
|
97
|
+
bug_tracker_uri: https://github.com/dougyouch/csv-utils/issues
|
|
98
|
+
documentation_uri: https://rubydoc.info/gems/csv-utils
|
|
95
99
|
rdoc_options: []
|
|
96
100
|
require_paths:
|
|
97
101
|
- lib
|
|
@@ -99,14 +103,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
99
103
|
requirements:
|
|
100
104
|
- - ">="
|
|
101
105
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '
|
|
106
|
+
version: '3.3'
|
|
103
107
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
108
|
requirements:
|
|
105
109
|
- - ">="
|
|
106
110
|
- !ruby/object:Gem::Version
|
|
107
111
|
version: '0'
|
|
108
112
|
requirements: []
|
|
109
|
-
rubygems_version:
|
|
113
|
+
rubygems_version: 4.0.20
|
|
110
114
|
specification_version: 4
|
|
111
|
-
summary:
|
|
115
|
+
summary: Compare, sort, transform and debug large or malformed CSV files
|
|
112
116
|
test_files: []
|
data/.github/workflows/ci.yml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [master]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
lint:
|
|
11
|
-
name: RuboCop
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
|
|
16
|
-
- name: Set up Ruby
|
|
17
|
-
uses: ruby/setup-ruby@v1
|
|
18
|
-
with:
|
|
19
|
-
ruby-version: '3.4'
|
|
20
|
-
bundler-cache: true
|
|
21
|
-
|
|
22
|
-
- name: Run RuboCop
|
|
23
|
-
run: bundle exec rubocop --format github
|
|
24
|
-
|
|
25
|
-
test:
|
|
26
|
-
name: Tests (Ruby ${{ matrix.ruby }})
|
|
27
|
-
runs-on: ubuntu-latest
|
|
28
|
-
strategy:
|
|
29
|
-
fail-fast: false
|
|
30
|
-
matrix:
|
|
31
|
-
ruby: ['3.2', '3.3', '3.4']
|
|
32
|
-
|
|
33
|
-
steps:
|
|
34
|
-
- uses: actions/checkout@v4
|
|
35
|
-
|
|
36
|
-
- name: Set up Ruby ${{ matrix.ruby }}
|
|
37
|
-
uses: ruby/setup-ruby@v1
|
|
38
|
-
with:
|
|
39
|
-
ruby-version: ${{ matrix.ruby }}
|
|
40
|
-
bundler-cache: true
|
|
41
|
-
|
|
42
|
-
- name: Run tests
|
|
43
|
-
run: bundle exec rspec
|
|
44
|
-
|
|
45
|
-
- name: Upload coverage to Codecov
|
|
46
|
-
if: matrix.ruby == '3.4'
|
|
47
|
-
uses: codecov/codecov-action@v4
|
|
48
|
-
with:
|
|
49
|
-
files: coverage/coverage.xml
|
|
50
|
-
fail_ci_if_error: false
|
|
51
|
-
verbose: true
|
|
52
|
-
env:
|
|
53
|
-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
data/.gitignore
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
*.gem
|
|
2
|
-
*.rbc
|
|
3
|
-
/.config
|
|
4
|
-
/coverage/
|
|
5
|
-
/InstalledFiles
|
|
6
|
-
/pkg/
|
|
7
|
-
/spec/reports/
|
|
8
|
-
/spec/examples.txt
|
|
9
|
-
/test/tmp/
|
|
10
|
-
/test/version_tmp/
|
|
11
|
-
/tmp/
|
|
12
|
-
|
|
13
|
-
# Used by dotenv library to load environment variables.
|
|
14
|
-
# .env
|
|
15
|
-
|
|
16
|
-
## Specific to RubyMotion:
|
|
17
|
-
.dat*
|
|
18
|
-
.repl_history
|
|
19
|
-
build/
|
|
20
|
-
*.bridgesupport
|
|
21
|
-
build-iPhoneOS/
|
|
22
|
-
build-iPhoneSimulator/
|
|
23
|
-
|
|
24
|
-
## Specific to RubyMotion (use of CocoaPods):
|
|
25
|
-
#
|
|
26
|
-
# We recommend against adding the Pods directory to your .gitignore. However
|
|
27
|
-
# you should judge for yourself, the pros and cons are mentioned at:
|
|
28
|
-
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
29
|
-
#
|
|
30
|
-
# vendor/Pods/
|
|
31
|
-
|
|
32
|
-
## Documentation cache and generated files:
|
|
33
|
-
/.yardoc/
|
|
34
|
-
/_yardoc/
|
|
35
|
-
/doc/
|
|
36
|
-
/rdoc/
|
|
37
|
-
|
|
38
|
-
## Environment normalization:
|
|
39
|
-
/.bundle/
|
|
40
|
-
/vendor/bundle
|
|
41
|
-
/lib/bundler/man/
|
|
42
|
-
|
|
43
|
-
# for a library or gem, you might want to ignore these files since the code is
|
|
44
|
-
# intended to run in multiple environments; otherwise, check them in:
|
|
45
|
-
# Gemfile.lock
|
|
46
|
-
# .ruby-version
|
|
47
|
-
# .ruby-gemset
|
|
48
|
-
|
|
49
|
-
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
50
|
-
.rvmrc
|
|
51
|
-
|
|
52
|
-
*~
|