1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
# frozen_string_literal: true
require "csv"
require "time"
# The CSV class can read csv files (must be separated with commas) which then
# can be handled like spreadsheets. This means you can access cells like A5
# within these files.
# The CSV class provides only string objects. If you want conversions to other
# types you have to do it yourself.
#
# You can pass options to the underlying CSV parse operation, via the
# :csv_options option.
module Roo
class CSV < Roo::Base
attr_reader :filename
# Returns an array with the names of the sheets. In CSV class there is only
# one dummy sheet, because a csv file cannot have more than one sheet.
def sheets
["default"]
end
def cell(row, col, sheet = nil)
sheet ||= default_sheet
read_cells(sheet)
@cell[normalize(row, col)]
end
def celltype(row, col, sheet = nil)
sheet ||= default_sheet
read_cells(sheet)
@cell_type[normalize(row, col)]
end
def cell_postprocessing(_row, _col, value)
value
end
def csv_options
@options[:csv_options] || {}
end
def set_value(row, col, value, _sheet)
@cell[[row, col]] = value
end
def set_type(row, col, type, _sheet)
@cell_type[[row, col]] = type
end
private
TYPE_MAP = {
String => :string,
Float => :float,
Date => :date,
DateTime => :datetime,
}
def celltype_class(value)
TYPE_MAP[value.class]
end
def read_cells(sheet = default_sheet)
sheet ||= default_sheet
return if @cells_read[sheet]
row_num = 0
max_col_num = 0
each_row csv_options do |row|
row_num += 1
col_num = 0
row.each do |elem|
col_num += 1
coordinate = [row_num, col_num]
@cell[coordinate] = elem
@cell_type[coordinate] = celltype_class(elem)
end
max_col_num = col_num if col_num > max_col_num
end
set_row_count(sheet, row_num)
set_column_count(sheet, max_col_num)
@cells_read[sheet] = true
end
def each_row(options, &block)
if uri?(filename)
each_row_using_tempdir(options, &block)
else
csv_foreach(filename_or_stream, options, &block)
end
end
def each_row_using_tempdir(options, &block)
::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV["ROO_TMP"]) do |tmpdir|
tmp_filename = download_uri(filename, tmpdir)
csv_foreach(tmp_filename, options, &block)
end
end
def csv_foreach(path_or_io, options, &block)
if is_stream?(path_or_io)
::CSV.new(path_or_io, **options).each(&block)
else
::CSV.foreach(path_or_io, **options, &block)
end
end
def set_row_count(sheet, last_row)
@first_row[sheet] = 1
@last_row[sheet] = last_row
@last_row[sheet] = @first_row[sheet] if @last_row[sheet].zero?
nil
end
def set_column_count(sheet, last_col)
@first_column[sheet] = 1
@last_column[sheet] = last_col
@last_column[sheet] = @first_column[sheet] if @last_column[sheet].zero?
nil
end
def clean_sheet(sheet)
read_cells(sheet)
@cell.each_pair do |coord, value|
@cell[coord] = sanitize_value(value) if value.is_a?(::String)
end
@cleaned[sheet] = true
end
alias_method :filename_or_stream, :filename
end
end
|