csv-utils 0.2.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '083044bd714b955ff9d5f6a44cf6d4cb3344cf7f649b105d81a94b3ddb1c9425'
4
- data.tar.gz: 5fa1cd9acacf10c275a23176e36722e189f8e6d8a85e1bfc7faf18eb45a1ca31
3
+ metadata.gz: 7b3f6bdded232bf3be2009d4bbb5ab99e083cd9f48dff5fabe89324f5217550a
4
+ data.tar.gz: 86b130f177d173a74bc1611c5677cc6bad5053e953a1a5fabfeabee494e372c8
5
5
  SHA512:
6
- metadata.gz: de36fb6c80a68b33c92f3c943a665c419ac1287f5b4b2901c1a60f45964d4b13c2199f53e8dd779b2947d6c86439fa1b1b781cc5ba2225656b2d8b50f690e4cc
7
- data.tar.gz: 0a6b5a9301f2386c2ad5bf3009ca77f949a92510ebb606844a1e6bf9fcc573f524966e68d1d8cec7db9778be3c93fb58a006fee9008e531c53a2c5391af6c0c3
6
+ metadata.gz: dd0a299cbe4b153f122d605bbb1b2ab08726194d58010e909090048f1cef0e384d08c8a3a59683b8108c1dc3766deacac46a9063af35e57502f6fe3c92b855ff
7
+ data.tar.gz: e4233e0c38338d24a6105d465745092146a65e03f297442fabada13ecf380db70740196726ec2d40a8f92828b6cfbc6945c0274701091cb2687af549126e34fa
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'csv'
4
+ begin
5
+ require 'rchardet'
6
+ rescue LoadError
7
+ $stderr.puts 'gem install rchardet'
8
+ exit 1
9
+ end
10
+
11
+ def utf8?(str)
12
+ str
13
+ .force_encoding('utf-8')
14
+ .valid_encoding?
15
+ end
16
+
17
+ def convert_to_utf8(str, current_encoding)
18
+ str.force_encoding(current_encoding)
19
+ return nil unless str.valid_encoding?
20
+
21
+ str.encode('utf-8')
22
+ end
23
+
24
+ def detect_encoding(col)
25
+ CharDet.detect(col)['encoding']
26
+ end
27
+
28
+ csv = CSV.open(ARGV[0], 'rb')
29
+ out = CSV.open(ARGV[1], 'wb') if ARGV[1]
30
+
31
+ headers = csv.shift
32
+ out << headers if out
33
+ csv_lineno = 1
34
+
35
+ while (row = csv.shift)
36
+ csv_lineno += 1
37
+
38
+ unless row.size == headers.size
39
+ $stderr.puts "row(#{csv_lineno}): invalid number of columns, expected #{headers.size} got #{row.size}"
40
+ end
41
+
42
+ converted = false
43
+ row.each_with_index do |col, idx|
44
+ next if utf8?(col)
45
+
46
+ $stderr.puts "row(#{csv_lineno}),col(#{idx + 1}): none UTF-8 characters found in \"#{col}\""
47
+ if (col_utf8_encoded = convert_to_utf8(col, detect_encoding(col)))
48
+ converted = true
49
+ puts "row(#{csv_lineno}),col(#{idx + 1}): converted to UTF-8 from #{detect_encoding(col)} \"#{col_utf8_encoded}\""
50
+ row[idx] = col_utf8_encoded
51
+ else
52
+ $stderr.puts "row(#{csv_lineno}),col(#{idx + 1}): unknown character encoding"
53
+ end
54
+ end
55
+
56
+ out << row if out && converted
57
+ end
58
+
59
+ csv.close
60
+ out.close if out
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'csv-utils'
5
- s.version = '0.2.1'
5
+ s.version = '0.3.2'
6
6
  s.licenses = ['MIT']
7
7
  s.summary = 'CSV Utils'
8
8
  s.description = 'Tools for debugging malformed CSV files'
@@ -6,4 +6,7 @@ module CSVUtils
6
6
  autoload :CSVOptions, 'csv_utils/csv_options'
7
7
  autoload :CSVReport, 'csv_utils/csv_report'
8
8
  autoload :CSVRow, 'csv_utils/csv_row'
9
+ autoload :CSVSort, 'csv_utils/csv_sort'
10
+ autoload :CSVTransformer, 'csv_utils/csv_transformer'
11
+ autoload :CSVWrapper, 'csv_utils/csv_wrapper'
9
12
  end
@@ -1,20 +1,15 @@
1
1
  # Utility class for appending data to a csv file.
2
2
  class CSVUtils::CSVExtender
3
- attr_reader :csv_file,
4
- :new_csv_file,
5
- :csv_options
6
-
7
- def initialize(csv_file, new_csv_file, csv_options = {})
8
- @csv_file = csv_file
9
- @new_csv_file = new_csv_file
10
- @csv_options = csv_options
3
+ def initialize(src_csv, dest_csv, csv_options = {})
4
+ @src_csv = CSVUtils::CSVWrapper.new(src_csv, 'rb', csv_options)
5
+ @dest_csv = CSVUtils::CSVWrapper.new(dest_csv, 'wb', csv_options)
11
6
  end
12
7
 
13
8
  def append(additional_headers)
14
9
  process(additional_headers) do |current_headers|
15
- while (row = src.shift)
10
+ while (row = @src_csv.shift)
16
11
  additional_columns = yield row, current_headers
17
- dest << (row + additional_columns)
12
+ @dest_csv << (row + additional_columns)
18
13
  end
19
14
  end
20
15
  end
@@ -27,13 +22,13 @@ class CSVUtils::CSVExtender
27
22
  additional_rows = yield batch, current_headers
28
23
 
29
24
  batch.each_with_index do |row, idx|
30
- dest << (row + additional_rows[idx])
25
+ @dest_csv << (row + additional_rows[idx])
31
26
  end
32
27
 
33
28
  batch = []
34
29
  end
35
30
 
36
- while (row = src.shift)
31
+ while (row = @src_csv.shift)
37
32
  batch << row
38
33
 
39
34
  process_batch_proc.call if batch.size >= batch_size
@@ -43,6 +38,8 @@ class CSVUtils::CSVExtender
43
38
  end
44
39
  end
45
40
 
41
+ private
42
+
46
43
  def process(additional_headers)
47
44
  current_headers = append_headers(additional_headers)
48
45
 
@@ -51,26 +48,16 @@ class CSVUtils::CSVExtender
51
48
  close
52
49
  end
53
50
 
54
- def src
55
- @src ||= CSV.open(csv_file, 'rb', csv_options)
56
- end
57
-
58
- def dest
59
- @dest ||= CSV.open(new_csv_file, 'wb', csv_options)
60
- end
61
-
62
51
  def close
63
- src.close
64
- dest.close
52
+ @src_csv.close
53
+ @dest_csv.close
65
54
  end
66
55
 
67
- private
68
-
69
56
  def append_headers(additional_headers)
70
57
  return nil unless additional_headers
71
58
 
72
- current_headers = src.shift
73
- dest << (current_headers + additional_headers)
59
+ current_headers = @src_csv.shift
60
+ @dest_csv << (current_headers + additional_headers)
74
61
  current_headers
75
62
  end
76
63
  end
@@ -4,7 +4,7 @@ module CSVUtils
4
4
  attr_reader :csv,
5
5
  :must_close
6
6
 
7
- def initialize(csv, csv_options = {}, &block)
7
+ def initialize(csv, headers = nil, csv_options = {}, &block)
8
8
  @csv =
9
9
  if csv.is_a?(String)
10
10
  @must_close = true
@@ -15,10 +15,11 @@ module CSVUtils
15
15
  csv
16
16
  end
17
17
 
18
- generate(&block) if block
18
+ generate(headers, &block) if block
19
19
  end
20
20
 
21
- def generate
21
+ def generate(headers = nil)
22
+ add_headers(headers) if headers
22
23
  yield self
23
24
  @csv.close if @must_close
24
25
  end
@@ -23,10 +23,16 @@ module CSVUtils
23
23
 
24
24
  add_value_to_class_method(:csv_columns, header => options)
25
25
  end
26
- end
27
26
 
28
- def csv_headers
29
- self.class.csv_columns.values.map { |column_options| csv_column_header(column_options) }
27
+ def csv_headers
28
+ csv_columns.values.map { |column_options| csv_column_header(column_options) }
29
+ end
30
+
31
+ private
32
+
33
+ def csv_column_header(column_options)
34
+ column_options[:header]
35
+ end
30
36
  end
31
37
 
32
38
  def csv_row
@@ -34,12 +40,12 @@ module CSVUtils
34
40
  end
35
41
  alias_method :to_a, :csv_row
36
42
 
37
- private
38
-
39
- def csv_column_header(column_options)
40
- column_options[:header]
43
+ def csv_headers
44
+ self.class.csv_headers
41
45
  end
42
46
 
47
+ private
48
+
43
49
  def csv_column_value(column_options)
44
50
  if column_options[:proc]
45
51
  instance_eval(&column_options[:proc])
@@ -0,0 +1,112 @@
1
+ require 'fileutils'
2
+
3
+ # Utility class for sorting the rows for a csv file
4
+ class CSVUtils::CSVSort
5
+ attr_reader :csv_file,
6
+ :new_csv_file,
7
+ :has_headers,
8
+ :csv_options,
9
+ :headers
10
+
11
+ def initialize(csv_file, new_csv_file, has_headers = true, csv_options = {})
12
+ @csv_file = csv_file
13
+ @new_csv_file = new_csv_file
14
+ @has_headers = has_headers
15
+ @csv_options = csv_options
16
+ @csv_part_files = []
17
+ @files_to_delete = []
18
+ end
19
+
20
+ def sort(batch_size = 100_000, &block)
21
+ create_sorted_csv_part_files(batch_size, &block)
22
+ merge_csv_part_files(&block)
23
+ end
24
+
25
+ private
26
+
27
+ def merge_sort_csv_files(src_csv_file1, src_csv_file2, dest_csv_file)
28
+ src1 = CSV.open(src_csv_file1, 'rb', csv_options)
29
+ src2 = CSV.open(src_csv_file2, 'rb', csv_options)
30
+ dest = CSV.open(dest_csv_file, 'wb', csv_options)
31
+
32
+ if @headers
33
+ dest << @headers
34
+ src1.shift
35
+ src2.shift
36
+ end
37
+
38
+ row1 = src1.shift
39
+ row2 = src2.shift
40
+
41
+ append_row1_proc = Proc.new do
42
+ dest << row1
43
+ row1 = src1.shift
44
+ end
45
+
46
+ append_row2_proc = Proc.new do
47
+ dest << row2
48
+ row2 = src2.shift
49
+ end
50
+
51
+ while row1 || row2
52
+ if row1.nil?
53
+ append_row2_proc.call
54
+ elsif row2.nil?
55
+ append_row1_proc.call
56
+ elsif yield(row1, row2) <= 0
57
+ append_row1_proc.call
58
+ else
59
+ append_row2_proc.call
60
+ end
61
+ end
62
+
63
+ src1.close
64
+ src2.close
65
+ dest.close
66
+ end
67
+
68
+ def create_sorted_csv_part_files(batch_size, &block)
69
+ src = CSV.open(csv_file, 'rb', csv_options)
70
+
71
+ @headers = src.shift if has_headers
72
+
73
+ batch = []
74
+ create_batch_part_proc = Proc.new do
75
+ batch.sort!(&block)
76
+ @csv_part_files << "#{new_csv_file}.part.#{@csv_part_files.size}"
77
+ CSV.open(@csv_part_files.last, 'wb', csv_options) do |csv|
78
+ csv << @headers if @headers
79
+ batch.each { |row| csv << row }
80
+ end
81
+ batch = []
82
+ end
83
+
84
+ while (row = src.shift)
85
+ batch << row
86
+ create_batch_part_proc.call if batch.size >= batch_size
87
+ end
88
+
89
+ create_batch_part_proc.call if batch.size > 0
90
+
91
+ src.close
92
+ end
93
+
94
+ def merge_csv_part_files(&block)
95
+ file_merge_cnt = 0
96
+
97
+ while @csv_part_files.size > 1
98
+ file_merge_cnt += 1
99
+
100
+ csv_part_file1 = @csv_part_files.shift
101
+ csv_part_file2 = @csv_part_files.shift
102
+ @csv_part_files << "#{new_csv_file}.merge.#{file_merge_cnt}"
103
+
104
+ merge_sort_csv_files(csv_part_file1, csv_part_file2, @csv_part_files.last, &block)
105
+
106
+ File.unlink(csv_part_file1)
107
+ File.unlink(csv_part_file2)
108
+ end
109
+
110
+ FileUtils.mv(@csv_part_files.last, new_csv_file)
111
+ end
112
+ end
@@ -0,0 +1,119 @@
1
+ # Transforms a CSV given a series of steps
2
+ class CSVUtils::CSVTransformer
3
+ attr_reader :headers
4
+
5
+ def initialize(src_csv, dest_csv, csv_options = {})
6
+ @src_csv = CSVUtils::CSVWrapper.new(src_csv, 'rb', csv_options)
7
+ @dest_csv = CSVUtils::CSVWrapper.new(dest_csv, 'wb', csv_options)
8
+ end
9
+
10
+ def read_headers
11
+ @headers = @src_csv.shift
12
+ self
13
+ end
14
+
15
+ def additional_data(&block)
16
+ steps << [:additional_data, @headers, block]
17
+ self
18
+ end
19
+
20
+ def select(&block)
21
+ steps << [:select, @headers, block]
22
+ self
23
+ end
24
+
25
+ def reject(&block)
26
+ steps << [:reject, @headers, block]
27
+ self
28
+ end
29
+
30
+ def map(new_headers, &block)
31
+ steps << [:map, @headers, block]
32
+ @headers = new_headers
33
+ self
34
+ end
35
+
36
+ def append(additional_headers, &block)
37
+ steps << [:append, @headers, block]
38
+
39
+ if additional_headers
40
+ @headers += additional_headers
41
+ else
42
+ @headers = nil
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ def each(&block)
49
+ steps << [:each, @headers, block]
50
+ self
51
+ end
52
+
53
+ def set_headers(headers)
54
+ @headers = headers
55
+ self
56
+ end
57
+
58
+ def process(batch_size = 10_000, &block)
59
+ batch = []
60
+
61
+ @dest_csv << @headers if @headers
62
+
63
+ steps_proc = Proc.new do
64
+ steps.each do |step_type, current_headers, proc|
65
+ batch = process_step(step_type, current_headers, batch, &proc)
66
+ end
67
+
68
+ batch.each { |row| @dest_csv << row }
69
+
70
+ batch = []
71
+ end
72
+
73
+ while (row = @src_csv.shift)
74
+ batch << row
75
+ steps_proc.call if batch.size >= batch_size
76
+ end
77
+
78
+ steps_proc.call if batch.size > 0
79
+
80
+ @src_csv.close
81
+ @dest_csv.close
82
+ end
83
+
84
+ private
85
+
86
+ def steps
87
+ @steps ||= []
88
+ end
89
+
90
+
91
+ def process_step(step_type, current_headers, batch, &block)
92
+ case step_type
93
+ when :select
94
+ batch.select! do |row|
95
+ block.call row, current_headers, @additional_data
96
+ end
97
+ when :reject
98
+ batch.reject! do |row|
99
+ block.call row, current_headers, @additional_data
100
+ end
101
+ when :map
102
+ batch.map! do |row|
103
+ block.call row, current_headers, @additional_data
104
+ end
105
+ when :append
106
+ batch.map! do |row|
107
+ row + block.call(row, current_headers, @additional_data)
108
+ end
109
+ when :additional_data
110
+ @additional_data = block.call(batch, current_headers)
111
+ when :each
112
+ batch.each do |row|
113
+ block.call(row, current_headers, @additional_data)
114
+ end
115
+ end
116
+
117
+ batch
118
+ end
119
+ end
@@ -0,0 +1,47 @@
1
+ # Wraps a CSV object, if wrapper opens the csv file it will close it
2
+ class CSVUtils::CSVWrapper
3
+ attr_reader :csv
4
+
5
+ def initialize(csv, mode, csv_options)
6
+ open(csv, mode, csv_options)
7
+ end
8
+
9
+ def self.open(file, mode, csv_options = {})
10
+ csv = new(file, mode, csv_options)
11
+
12
+ if block_given?
13
+ yield csv
14
+ csv.close
15
+ else
16
+ csv
17
+ end
18
+ end
19
+
20
+ def open(csv, mode, csv_options)
21
+ if csv.is_a?(String)
22
+ @close_when_done = true
23
+ @csv = CSV.open(csv, mode, csv_options)
24
+ else
25
+ @close_when_done = false
26
+ @csv = csv
27
+ end
28
+ end
29
+
30
+ def <<(row)
31
+ csv << row
32
+ end
33
+
34
+ def shift
35
+ csv.shift
36
+ end
37
+
38
+ def close
39
+ csv.close if close_when_done?
40
+ end
41
+
42
+ private
43
+
44
+ def close_when_done?
45
+ @close_when_done
46
+ end
47
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csv-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-04 00:00:00.000000000 Z
11
+ date: 2020-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inheritance-helper
@@ -30,6 +30,7 @@ executables:
30
30
  - csv-change-eol
31
31
  - csv-find-error
32
32
  - csv-readline
33
+ - csv-validator
33
34
  extensions: []
34
35
  extra_rdoc_files: []
35
36
  files:
@@ -43,12 +44,16 @@ files:
43
44
  - bin/csv-change-eol
44
45
  - bin/csv-find-error
45
46
  - bin/csv-readline
47
+ - bin/csv-validator
46
48
  - csv-utils.gemspec
47
49
  - lib/csv-utils.rb
48
50
  - lib/csv_utils/csv_extender.rb
49
51
  - lib/csv_utils/csv_options.rb
50
52
  - lib/csv_utils/csv_report.rb
51
53
  - lib/csv_utils/csv_row.rb
54
+ - lib/csv_utils/csv_sort.rb
55
+ - lib/csv_utils/csv_transformer.rb
56
+ - lib/csv_utils/csv_wrapper.rb
52
57
  - script/console
53
58
  homepage: https://github.com/dougyouch/csv-utils
54
59
  licenses: