roo 1.13.2 → 2.10.1
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 +5 -5
- data/.codeclimate.yml +17 -0
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +14 -0
- data/.github/workflows/pull-request.yml +15 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +186 -0
- data/.simplecov +4 -0
- data/CHANGELOG.md +702 -0
- data/Gemfile +18 -12
- data/Guardfile +23 -0
- data/LICENSE +5 -1
- data/README.md +328 -0
- data/Rakefile +23 -23
- data/examples/roo_soap_client.rb +28 -31
- data/examples/roo_soap_server.rb +4 -6
- data/examples/write_me.rb +9 -10
- data/lib/roo/base.rb +317 -504
- data/lib/roo/constants.rb +7 -0
- data/lib/roo/csv.rb +141 -113
- data/lib/roo/errors.rb +11 -0
- data/lib/roo/excelx/cell/base.rb +108 -0
- data/lib/roo/excelx/cell/boolean.rb +30 -0
- data/lib/roo/excelx/cell/date.rb +28 -0
- data/lib/roo/excelx/cell/datetime.rb +107 -0
- data/lib/roo/excelx/cell/empty.rb +20 -0
- data/lib/roo/excelx/cell/number.rb +99 -0
- data/lib/roo/excelx/cell/string.rb +19 -0
- data/lib/roo/excelx/cell/time.rb +44 -0
- data/lib/roo/excelx/cell.rb +110 -0
- data/lib/roo/excelx/comments.rb +55 -0
- data/lib/roo/excelx/coordinate.rb +19 -0
- data/lib/roo/excelx/extractor.rb +39 -0
- data/lib/roo/excelx/format.rb +71 -0
- data/lib/roo/excelx/images.rb +26 -0
- data/lib/roo/excelx/relationships.rb +33 -0
- data/lib/roo/excelx/shared.rb +39 -0
- data/lib/roo/excelx/shared_strings.rb +151 -0
- data/lib/roo/excelx/sheet.rb +151 -0
- data/lib/roo/excelx/sheet_doc.rb +257 -0
- data/lib/roo/excelx/styles.rb +64 -0
- data/lib/roo/excelx/workbook.rb +64 -0
- data/lib/roo/excelx.rb +407 -601
- data/lib/roo/font.rb +17 -0
- data/lib/roo/formatters/base.rb +15 -0
- data/lib/roo/formatters/csv.rb +84 -0
- data/lib/roo/formatters/matrix.rb +23 -0
- data/lib/roo/formatters/xml.rb +31 -0
- data/lib/roo/formatters/yaml.rb +40 -0
- data/lib/roo/helpers/default_attr_reader.rb +20 -0
- data/lib/roo/helpers/weak_instance_cache.rb +41 -0
- data/lib/roo/libre_office.rb +4 -0
- data/lib/roo/link.rb +34 -0
- data/lib/roo/open_office.rb +631 -0
- data/lib/roo/spreadsheet.rb +28 -23
- data/lib/roo/tempdir.rb +24 -0
- data/lib/roo/utils.rb +128 -0
- data/lib/roo/version.rb +3 -0
- data/lib/roo.rb +26 -24
- data/roo.gemspec +29 -203
- data/spec/helpers.rb +5 -0
- data/spec/lib/roo/base_spec.rb +291 -3
- data/spec/lib/roo/csv_spec.rb +38 -11
- data/spec/lib/roo/excelx/cell/time_spec.rb +15 -0
- data/spec/lib/roo/excelx/format_spec.rb +7 -6
- data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
- data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
- data/spec/lib/roo/excelx_spec.rb +672 -11
- data/spec/lib/roo/libreoffice_spec.rb +16 -6
- data/spec/lib/roo/openoffice_spec.rb +30 -8
- data/spec/lib/roo/spreadsheet_spec.rb +60 -12
- data/spec/lib/roo/strict_spec.rb +43 -0
- data/spec/lib/roo/utils_spec.rb +119 -0
- data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
- data/spec/lib/roo_spec.rb +0 -0
- data/spec/spec_helper.rb +7 -6
- data/test/all_ss.rb +12 -11
- data/test/excelx/cell/test_attr_reader_default.rb +72 -0
- data/test/excelx/cell/test_base.rb +68 -0
- data/test/excelx/cell/test_boolean.rb +36 -0
- data/test/excelx/cell/test_date.rb +38 -0
- data/test/excelx/cell/test_datetime.rb +45 -0
- data/test/excelx/cell/test_empty.rb +18 -0
- data/test/excelx/cell/test_number.rb +90 -0
- data/test/excelx/cell/test_string.rb +48 -0
- data/test/excelx/cell/test_time.rb +30 -0
- data/test/excelx/test_coordinate.rb +51 -0
- data/test/formatters/test_csv.rb +136 -0
- data/test/formatters/test_matrix.rb +76 -0
- data/test/formatters/test_xml.rb +78 -0
- data/test/formatters/test_yaml.rb +20 -0
- data/test/helpers/test_accessing_files.rb +81 -0
- data/test/helpers/test_comments.rb +43 -0
- data/test/helpers/test_formulas.rb +9 -0
- data/test/helpers/test_labels.rb +103 -0
- data/test/helpers/test_sheets.rb +55 -0
- data/test/helpers/test_styles.rb +62 -0
- data/test/roo/test_base.rb +182 -0
- data/test/roo/test_csv.rb +88 -0
- data/test/roo/test_excelx.rb +360 -0
- data/test/roo/test_libre_office.rb +9 -0
- data/test/roo/test_open_office.rb +289 -0
- data/test/test_helper.rb +123 -59
- data/test/test_roo.rb +392 -2292
- metadata +153 -298
- data/CHANGELOG +0 -417
- data/Gemfile.lock +0 -78
- data/README.markdown +0 -126
- data/VERSION +0 -1
- data/lib/roo/excel.rb +0 -355
- data/lib/roo/excel2003xml.rb +0 -300
- data/lib/roo/google.rb +0 -292
- data/lib/roo/openoffice.rb +0 -496
- data/lib/roo/roo_rails_helper.rb +0 -83
- data/lib/roo/worksheet.rb +0 -18
- data/scripts/txt2html +0 -67
- data/spec/lib/roo/excel2003xml_spec.rb +0 -15
- data/spec/lib/roo/excel_spec.rb +0 -17
- data/spec/lib/roo/google_spec.rb +0 -64
- data/test/files/1900_base.xls +0 -0
- data/test/files/1900_base.xlsx +0 -0
- data/test/files/1904_base.xls +0 -0
- data/test/files/1904_base.xlsx +0 -0
- data/test/files/Bibelbund.csv +0 -3741
- data/test/files/Bibelbund.ods +0 -0
- data/test/files/Bibelbund.xls +0 -0
- data/test/files/Bibelbund.xlsx +0 -0
- data/test/files/Bibelbund.xml +0 -62518
- data/test/files/Bibelbund1.ods +0 -0
- data/test/files/Pfand_from_windows_phone.xlsx +0 -0
- data/test/files/bad_excel_date.xls +0 -0
- data/test/files/bbu.ods +0 -0
- data/test/files/bbu.xls +0 -0
- data/test/files/bbu.xlsx +0 -0
- data/test/files/bbu.xml +0 -152
- data/test/files/bode-v1.ods.zip +0 -0
- data/test/files/bode-v1.xls.zip +0 -0
- data/test/files/boolean.csv +0 -2
- data/test/files/boolean.ods +0 -0
- data/test/files/boolean.xls +0 -0
- data/test/files/boolean.xlsx +0 -0
- data/test/files/boolean.xml +0 -112
- data/test/files/borders.ods +0 -0
- data/test/files/borders.xls +0 -0
- data/test/files/borders.xlsx +0 -0
- data/test/files/borders.xml +0 -144
- data/test/files/bug-numbered-sheet-names.xlsx +0 -0
- data/test/files/bug-row-column-fixnum-float.xls +0 -0
- data/test/files/bug-row-column-fixnum-float.xml +0 -127
- data/test/files/comments.ods +0 -0
- data/test/files/comments.xls +0 -0
- data/test/files/comments.xlsx +0 -0
- data/test/files/csvtypes.csv +0 -1
- data/test/files/datetime.ods +0 -0
- data/test/files/datetime.xls +0 -0
- data/test/files/datetime.xlsx +0 -0
- data/test/files/datetime.xml +0 -142
- data/test/files/datetime_floatconv.xls +0 -0
- data/test/files/datetime_floatconv.xml +0 -148
- data/test/files/dreimalvier.ods +0 -0
- data/test/files/emptysheets.ods +0 -0
- data/test/files/emptysheets.xls +0 -0
- data/test/files/emptysheets.xlsx +0 -0
- data/test/files/emptysheets.xml +0 -105
- data/test/files/excel2003.xml +0 -21140
- data/test/files/false_encoding.xls +0 -0
- data/test/files/false_encoding.xml +0 -132
- data/test/files/file_item_error.xlsx +0 -0
- data/test/files/formula.ods +0 -0
- data/test/files/formula.xls +0 -0
- data/test/files/formula.xlsx +0 -0
- data/test/files/formula.xml +0 -134
- data/test/files/formula_parse_error.xls +0 -0
- data/test/files/formula_parse_error.xml +0 -1833
- data/test/files/formula_string_error.xlsx +0 -0
- data/test/files/html-escape.ods +0 -0
- data/test/files/link.xls +0 -0
- data/test/files/link.xlsx +0 -0
- data/test/files/matrix.ods +0 -0
- data/test/files/matrix.xls +0 -0
- data/test/files/named_cells.ods +0 -0
- data/test/files/named_cells.xls +0 -0
- data/test/files/named_cells.xlsx +0 -0
- data/test/files/no_spreadsheet_file.txt +0 -1
- data/test/files/numbers1.csv +0 -18
- data/test/files/numbers1.ods +0 -0
- data/test/files/numbers1.xls +0 -0
- data/test/files/numbers1.xlsx +0 -0
- data/test/files/numbers1.xml +0 -312
- data/test/files/numeric-link.xlsx +0 -0
- data/test/files/only_one_sheet.ods +0 -0
- data/test/files/only_one_sheet.xls +0 -0
- data/test/files/only_one_sheet.xlsx +0 -0
- data/test/files/only_one_sheet.xml +0 -67
- data/test/files/paragraph.ods +0 -0
- data/test/files/paragraph.xls +0 -0
- data/test/files/paragraph.xlsx +0 -0
- data/test/files/paragraph.xml +0 -127
- data/test/files/prova.xls +0 -0
- data/test/files/ric.ods +0 -0
- data/test/files/simple_spreadsheet.ods +0 -0
- data/test/files/simple_spreadsheet.xls +0 -0
- data/test/files/simple_spreadsheet.xlsx +0 -0
- data/test/files/simple_spreadsheet.xml +0 -225
- data/test/files/simple_spreadsheet_from_italo.ods +0 -0
- data/test/files/simple_spreadsheet_from_italo.xls +0 -0
- data/test/files/simple_spreadsheet_from_italo.xml +0 -242
- data/test/files/so_datetime.csv +0 -7
- data/test/files/style.ods +0 -0
- data/test/files/style.xls +0 -0
- data/test/files/style.xlsx +0 -0
- data/test/files/style.xml +0 -154
- data/test/files/time-test.csv +0 -2
- data/test/files/time-test.ods +0 -0
- data/test/files/time-test.xls +0 -0
- data/test/files/time-test.xlsx +0 -0
- data/test/files/time-test.xml +0 -131
- data/test/files/type_excel.ods +0 -0
- data/test/files/type_excel.xlsx +0 -0
- data/test/files/type_excelx.ods +0 -0
- data/test/files/type_excelx.xls +0 -0
- data/test/files/type_openoffice.xls +0 -0
- data/test/files/type_openoffice.xlsx +0 -0
- data/test/files/whitespace.ods +0 -0
- data/test/files/whitespace.xls +0 -0
- data/test/files/whitespace.xlsx +0 -0
- data/test/files/whitespace.xml +0 -184
- data/test/rm_sub_test.rb +0 -12
- data/test/rm_test.rb +0 -7
- data/test/test_generic_spreadsheet.rb +0 -259
- data/website/index.html +0 -385
- data/website/index.txt +0 -423
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -130
- data/website/template.rhtml +0 -48
data/lib/roo/base.rb
CHANGED
|
@@ -1,51 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# For rubyzip >= 1.0.0
|
|
11
|
-
require 'zip/filesystem'
|
|
12
|
-
Roo::ZipFile = Zip::File
|
|
13
|
-
end
|
|
1
|
+
require "tmpdir"
|
|
2
|
+
require "stringio"
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "roo/utils"
|
|
5
|
+
require "roo/formatters/base"
|
|
6
|
+
require "roo/formatters/csv"
|
|
7
|
+
require "roo/formatters/matrix"
|
|
8
|
+
require "roo/formatters/xml"
|
|
9
|
+
require "roo/formatters/yaml"
|
|
14
10
|
|
|
15
11
|
# Base class for all other types of spreadsheets
|
|
16
12
|
class Roo::Base
|
|
17
13
|
include Enumerable
|
|
14
|
+
include Roo::Formatters::Base
|
|
15
|
+
include Roo::Formatters::CSV
|
|
16
|
+
include Roo::Formatters::Matrix
|
|
17
|
+
include Roo::Formatters::XML
|
|
18
|
+
include Roo::Formatters::YAML
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
MAX_ROW_COL = 999_999
|
|
21
|
+
MIN_ROW_COL = 0
|
|
20
22
|
|
|
21
|
-
attr_reader :
|
|
23
|
+
attr_reader :headers
|
|
22
24
|
|
|
23
25
|
# sets the line with attribute names (default: 1)
|
|
24
26
|
attr_accessor :header_line
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
letter,number = Roo::Base.split_coord(str)
|
|
30
|
-
x = letter_to_number(letter)
|
|
31
|
-
y = number
|
|
32
|
-
return y, x
|
|
28
|
+
def self.TEMP_PREFIX
|
|
29
|
+
warn "[DEPRECATION] please access TEMP_PREFIX via Roo::TEMP_PREFIX"
|
|
30
|
+
Roo::TEMP_PREFIX
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
def self.
|
|
36
|
-
|
|
37
|
-
letter = $1
|
|
38
|
-
number = $2.to_i
|
|
39
|
-
else
|
|
40
|
-
raise ArgumentError
|
|
41
|
-
end
|
|
42
|
-
return letter, number
|
|
33
|
+
def self.finalize(object_id)
|
|
34
|
+
proc { finalize_tempdirs(object_id) }
|
|
43
35
|
end
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
public
|
|
47
|
-
|
|
48
|
-
def initialize(filename, options={}, file_warning=:error, tmpdir=nil)
|
|
37
|
+
def initialize(filename, options = {}, _file_warning = :error, _tmpdir = nil)
|
|
49
38
|
@filename = filename
|
|
50
39
|
@options = options
|
|
51
40
|
|
|
@@ -59,158 +48,81 @@ class Roo::Base
|
|
|
59
48
|
@last_column = {}
|
|
60
49
|
|
|
61
50
|
@header_line = 1
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def close
|
|
54
|
+
if self.class.respond_to?(:finalize_tempdirs)
|
|
55
|
+
self.class.finalize_tempdirs(object_id)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
instance_variables.each do |instance_variable|
|
|
59
|
+
instance_variable_set(instance_variable, nil)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def default_sheet
|
|
66
|
+
@default_sheet ||= sheets.first
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
# sets the working sheet in the document
|
|
67
|
-
# 'sheet' can be a number (
|
|
70
|
+
# 'sheet' can be a number (0 = first sheet) or the name of a sheet.
|
|
68
71
|
def default_sheet=(sheet)
|
|
69
72
|
validate_sheet!(sheet)
|
|
70
|
-
@default_sheet = sheet
|
|
73
|
+
@default_sheet = sheet.is_a?(String) ? sheet : sheets[sheet]
|
|
71
74
|
@first_row[sheet] = @last_row[sheet] = @first_column[sheet] = @last_column[sheet] = nil
|
|
72
75
|
@cells_read[sheet] = false
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
# first non-empty column as a letter
|
|
76
|
-
def first_column_as_letter(sheet=
|
|
77
|
-
Roo::
|
|
79
|
+
def first_column_as_letter(sheet = default_sheet)
|
|
80
|
+
::Roo::Utils.number_to_letter(first_column(sheet))
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
# last non-empty column as a letter
|
|
81
|
-
def last_column_as_letter(sheet=
|
|
82
|
-
Roo::
|
|
84
|
+
def last_column_as_letter(sheet = default_sheet)
|
|
85
|
+
::Roo::Utils.number_to_letter(last_column(sheet))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Set first/last row/column for sheet
|
|
89
|
+
def first_last_row_col_for_sheet(sheet)
|
|
90
|
+
@first_last_row_cols ||= {}
|
|
91
|
+
@first_last_row_cols[sheet] ||= begin
|
|
92
|
+
result = collect_last_row_col_for_sheet(sheet)
|
|
93
|
+
{
|
|
94
|
+
first_row: result[:first_row] == MAX_ROW_COL ? nil : result[:first_row],
|
|
95
|
+
first_column: result[:first_column] == MAX_ROW_COL ? nil : result[:first_column],
|
|
96
|
+
last_row: result[:last_row] == MIN_ROW_COL ? nil : result[:last_row],
|
|
97
|
+
last_column: result[:last_column] == MIN_ROW_COL ? nil : result[:last_column]
|
|
98
|
+
}
|
|
99
|
+
end
|
|
83
100
|
end
|
|
84
101
|
|
|
85
|
-
#
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} if @cell[sheet]
|
|
98
|
-
result = nil if result == impossible_value
|
|
99
|
-
@first_row[sheet] = result
|
|
100
|
-
result
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# returns the number of the last non-empty row
|
|
104
|
-
def last_row(sheet=nil)
|
|
105
|
-
sheet ||= @default_sheet
|
|
106
|
-
read_cells(sheet)
|
|
107
|
-
if @last_row[sheet]
|
|
108
|
-
return @last_row[sheet]
|
|
109
|
-
end
|
|
110
|
-
impossible_value = 0
|
|
111
|
-
result = impossible_value
|
|
112
|
-
@cell[sheet].each_pair {|key,value|
|
|
113
|
-
y = key.first.to_i # _to_string(key).split(',')
|
|
114
|
-
result = [result, y].max if value
|
|
115
|
-
} if @cell[sheet]
|
|
116
|
-
result = nil if result == impossible_value
|
|
117
|
-
@last_row[sheet] = result
|
|
118
|
-
result
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# returns the number of the first non-empty column
|
|
122
|
-
def first_column(sheet=nil)
|
|
123
|
-
sheet ||= @default_sheet
|
|
124
|
-
read_cells(sheet)
|
|
125
|
-
if @first_column[sheet]
|
|
126
|
-
return @first_column[sheet]
|
|
127
|
-
end
|
|
128
|
-
impossible_value = 999_999 # more than a spreadsheet can hold
|
|
129
|
-
result = impossible_value
|
|
130
|
-
@cell[sheet].each_pair {|key,value|
|
|
131
|
-
x = key.last.to_i # _to_string(key).split(',')
|
|
132
|
-
result = [result, x].min if value
|
|
133
|
-
} if @cell[sheet]
|
|
134
|
-
result = nil if result == impossible_value
|
|
135
|
-
@first_column[sheet] = result
|
|
136
|
-
result
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# returns the number of the last non-empty column
|
|
140
|
-
def last_column(sheet=nil)
|
|
141
|
-
sheet ||= @default_sheet
|
|
142
|
-
read_cells(sheet)
|
|
143
|
-
if @last_column[sheet]
|
|
144
|
-
return @last_column[sheet]
|
|
145
|
-
end
|
|
146
|
-
impossible_value = 0
|
|
147
|
-
result = impossible_value
|
|
148
|
-
@cell[sheet].each_pair {|key,value|
|
|
149
|
-
x = key.last.to_i # _to_string(key).split(',')
|
|
150
|
-
result = [result, x].max if value
|
|
151
|
-
} if @cell[sheet]
|
|
152
|
-
result = nil if result == impossible_value
|
|
153
|
-
@last_column[sheet] = result
|
|
154
|
-
result
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# returns a rectangular area (default: all cells) as yaml-output
|
|
158
|
-
# you can add additional attributes with the prefix parameter like:
|
|
159
|
-
# oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
|
|
160
|
-
def to_yaml(prefix={}, from_row=nil, from_column=nil, to_row=nil, to_column=nil,sheet=nil)
|
|
161
|
-
sheet ||= @default_sheet
|
|
162
|
-
result = "--- \n"
|
|
163
|
-
return '' unless first_row # empty result if there is no first_row in a sheet
|
|
164
|
-
|
|
165
|
-
(from_row||first_row(sheet)).upto(to_row||last_row(sheet)) do |row|
|
|
166
|
-
(from_column||first_column(sheet)).upto(to_column||last_column(sheet)) do |col|
|
|
167
|
-
unless empty?(row,col,sheet)
|
|
168
|
-
result << "cell_#{row}_#{col}: \n"
|
|
169
|
-
prefix.each {|k,v|
|
|
170
|
-
result << " #{k}: #{v} \n"
|
|
171
|
-
}
|
|
172
|
-
result << " row: #{row} \n"
|
|
173
|
-
result << " col: #{col} \n"
|
|
174
|
-
result << " celltype: #{self.celltype(row,col,sheet)} \n"
|
|
175
|
-
if self.celltype(row,col,sheet) == :time
|
|
176
|
-
result << " value: #{Roo::Base.integer_to_timestring( self.cell(row,col,sheet))} \n"
|
|
177
|
-
else
|
|
178
|
-
result << " value: #{self.cell(row,col,sheet)} \n"
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
result
|
|
102
|
+
# Collect first/last row/column from sheet
|
|
103
|
+
def collect_last_row_col_for_sheet(sheet)
|
|
104
|
+
first_row = first_column = MAX_ROW_COL
|
|
105
|
+
last_row = last_column = MIN_ROW_COL
|
|
106
|
+
@cell[sheet].each_pair do |key, value|
|
|
107
|
+
next unless value
|
|
108
|
+
first_row = [first_row, key.first.to_i].min
|
|
109
|
+
last_row = [last_row, key.first.to_i].max
|
|
110
|
+
first_column = [first_column, key.last.to_i].min
|
|
111
|
+
last_column = [last_column, key.last.to_i].max
|
|
112
|
+
end if @cell[sheet]
|
|
113
|
+
{ first_row: first_row, first_column: first_column, last_row: last_row, last_column: last_column }
|
|
184
114
|
end
|
|
185
115
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
sheet
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
write_csv_content(file,sheet)
|
|
192
|
-
end
|
|
193
|
-
return true
|
|
194
|
-
else
|
|
195
|
-
sio = StringIO.new
|
|
196
|
-
write_csv_content(sio,sheet)
|
|
197
|
-
sio.rewind
|
|
198
|
-
return sio.read
|
|
116
|
+
%i(first_row last_row first_column last_column).each do |key|
|
|
117
|
+
ivar = "@#{key}".to_sym
|
|
118
|
+
define_method(key) do |sheet = default_sheet|
|
|
119
|
+
read_cells(sheet)
|
|
120
|
+
instance_variable_get(ivar)[sheet] ||= first_last_row_col_for_sheet(sheet)[key]
|
|
199
121
|
end
|
|
200
122
|
end
|
|
201
123
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
require 'matrix'
|
|
205
|
-
|
|
206
|
-
sheet ||= @default_sheet
|
|
207
|
-
return Matrix.empty unless first_row
|
|
208
|
-
|
|
209
|
-
Matrix.rows((from_row||first_row(sheet)).upto(to_row||last_row(sheet)).map do |row|
|
|
210
|
-
(from_column||first_column(sheet)).upto(to_column||last_column(sheet)).map do |col|
|
|
211
|
-
cell(row,col,sheet)
|
|
212
|
-
end
|
|
213
|
-
end)
|
|
124
|
+
def inspect
|
|
125
|
+
"<##{self.class}:#{object_id.to_s(8)} #{instance_variables.join(' ')}>"
|
|
214
126
|
end
|
|
215
127
|
|
|
216
128
|
# find a row either by row number or a condition
|
|
@@ -218,98 +130,68 @@ class Roo::Base
|
|
|
218
130
|
# (experimental. see examples in the test_roo.rb file)
|
|
219
131
|
def find(*args) # :nodoc
|
|
220
132
|
options = (args.last.is_a?(Hash) ? args.pop : {})
|
|
221
|
-
result_array = options[:array]
|
|
222
|
-
header_for = Hash[1.upto(last_column).map do |col|
|
|
223
|
-
[col, cell(@header_line,col)]
|
|
224
|
-
end]
|
|
225
|
-
#-- id
|
|
226
|
-
if args[0].class == Fixnum
|
|
227
|
-
rownum = args[0]
|
|
228
|
-
if @header_line
|
|
229
|
-
[Hash[1.upto(self.row().size).map {|j|
|
|
230
|
-
[header_for.fetch(j), cell(rownum,j)]
|
|
231
|
-
}]]
|
|
232
|
-
else
|
|
233
|
-
self.row(rownum).size.times.map {|j|
|
|
234
|
-
cell(rownum,j + 1)
|
|
235
|
-
}
|
|
236
|
-
end
|
|
237
|
-
#-- :all
|
|
238
|
-
elsif args[0] == :all
|
|
239
|
-
rows = first_row.upto(last_row)
|
|
240
|
-
|
|
241
|
-
# are all conditions met?
|
|
242
|
-
if (conditions = options[:conditions]) && !conditions.empty?
|
|
243
|
-
column_with = header_for.invert
|
|
244
|
-
rows = rows.select do |i|
|
|
245
|
-
conditions.all? { |key,val| cell(i,column_with[key]) == val }
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
133
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
end
|
|
257
|
-
end
|
|
134
|
+
case args[0]
|
|
135
|
+
when Integer
|
|
136
|
+
find_by_row(args[0])
|
|
137
|
+
when :all
|
|
138
|
+
find_by_conditions(options)
|
|
139
|
+
else
|
|
140
|
+
fail ArgumentError, "unexpected arg #{args[0].inspect}, pass a row index or :all"
|
|
258
141
|
end
|
|
259
142
|
end
|
|
260
143
|
|
|
261
144
|
# returns all values in this row as an array
|
|
262
145
|
# row numbers are 1,2,3,... like in the spreadsheet
|
|
263
|
-
def row(
|
|
264
|
-
sheet ||= @default_sheet
|
|
146
|
+
def row(row_number, sheet = default_sheet)
|
|
265
147
|
read_cells(sheet)
|
|
266
148
|
first_column(sheet).upto(last_column(sheet)).map do |col|
|
|
267
|
-
cell(
|
|
149
|
+
cell(row_number, col, sheet)
|
|
268
150
|
end
|
|
269
151
|
end
|
|
270
152
|
|
|
271
153
|
# returns all values in this column as an array
|
|
272
154
|
# column numbers are 1,2,3,... like in the spreadsheet
|
|
273
|
-
def column(
|
|
274
|
-
if
|
|
275
|
-
|
|
155
|
+
def column(column_number, sheet = default_sheet)
|
|
156
|
+
if column_number.is_a?(::String)
|
|
157
|
+
column_number = ::Roo::Utils.letter_to_number(column_number)
|
|
276
158
|
end
|
|
277
|
-
sheet ||= @default_sheet
|
|
278
159
|
read_cells(sheet)
|
|
279
160
|
first_row(sheet).upto(last_row(sheet)).map do |row|
|
|
280
|
-
cell(row,
|
|
161
|
+
cell(row, column_number, sheet)
|
|
281
162
|
end
|
|
282
163
|
end
|
|
283
164
|
|
|
284
165
|
# set a cell to a certain value
|
|
285
166
|
# (this will not be saved back to the spreadsheet file!)
|
|
286
|
-
def set(row,col,value,sheet=
|
|
287
|
-
sheet ||= @default_sheet
|
|
167
|
+
def set(row, col, value, sheet = default_sheet) #:nodoc:
|
|
288
168
|
read_cells(sheet)
|
|
289
|
-
row, col = normalize(row,col)
|
|
290
|
-
cell_type =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
raise ArgumentError, "Type for #{value} not set"
|
|
295
|
-
end
|
|
169
|
+
row, col = normalize(row, col)
|
|
170
|
+
cell_type = cell_type_by_value(value)
|
|
171
|
+
set_value(row, col, value, sheet)
|
|
172
|
+
set_type(row, col, cell_type, sheet)
|
|
173
|
+
end
|
|
296
174
|
|
|
297
|
-
|
|
298
|
-
|
|
175
|
+
def cell_type_by_value(value)
|
|
176
|
+
case value
|
|
177
|
+
when Integer then :float
|
|
178
|
+
when String, Float then :string
|
|
179
|
+
else
|
|
180
|
+
fail ArgumentError, "Type for #{value} not set"
|
|
181
|
+
end
|
|
299
182
|
end
|
|
300
183
|
|
|
301
184
|
# reopens and read a spreadsheet document
|
|
302
185
|
def reload
|
|
303
|
-
ds =
|
|
186
|
+
ds = default_sheet
|
|
304
187
|
reinitialize
|
|
305
188
|
self.default_sheet = ds
|
|
306
189
|
end
|
|
307
190
|
|
|
308
191
|
# true if cell is empty
|
|
309
|
-
def empty?(row, col, sheet=
|
|
310
|
-
sheet ||= @default_sheet
|
|
192
|
+
def empty?(row, col, sheet = default_sheet)
|
|
311
193
|
read_cells(sheet)
|
|
312
|
-
row,col = normalize(row,col)
|
|
194
|
+
row, col = normalize(row, col)
|
|
313
195
|
contents = cell(row, col, sheet)
|
|
314
196
|
!contents || (celltype(row, col, sheet) == :string && contents.empty?) \
|
|
315
197
|
|| (row < first_row(sheet) || row > last_row(sheet) || col < first_column(sheet) || col > last_column(sheet))
|
|
@@ -319,66 +201,40 @@ class Roo::Base
|
|
|
319
201
|
# this document.
|
|
320
202
|
def info
|
|
321
203
|
without_changing_default_sheet do
|
|
322
|
-
result = "File: #{File.basename(@filename)}\n"
|
|
323
|
-
"Number of sheets: #{sheets.size}\n"
|
|
204
|
+
result = "File: #{File.basename(@filename)}\n"\
|
|
205
|
+
"Number of sheets: #{sheets.size}\n"\
|
|
324
206
|
"Sheets: #{sheets.join(', ')}\n"
|
|
325
207
|
n = 1
|
|
326
|
-
sheets.each
|
|
208
|
+
sheets.each do |sheet|
|
|
327
209
|
self.default_sheet = sheet
|
|
328
210
|
result << "Sheet " + n.to_s + ":\n"
|
|
329
|
-
|
|
330
|
-
result << " - empty -"
|
|
331
|
-
else
|
|
211
|
+
if first_row
|
|
332
212
|
result << " First row: #{first_row}\n"
|
|
333
213
|
result << " Last row: #{last_row}\n"
|
|
334
|
-
result << " First column: #{Roo::
|
|
335
|
-
result << " Last column: #{Roo::
|
|
214
|
+
result << " First column: #{::Roo::Utils.number_to_letter(first_column)}\n"
|
|
215
|
+
result << " Last column: #{::Roo::Utils.number_to_letter(last_column)}"
|
|
216
|
+
else
|
|
217
|
+
result << " - empty -"
|
|
336
218
|
end
|
|
337
219
|
result << "\n" if sheet != sheets.last
|
|
338
220
|
n += 1
|
|
339
|
-
|
|
221
|
+
end
|
|
340
222
|
result
|
|
341
223
|
end
|
|
342
224
|
end
|
|
343
225
|
|
|
344
|
-
# returns an XML representation of all sheets of a spreadsheet file
|
|
345
|
-
def to_xml
|
|
346
|
-
Nokogiri::XML::Builder.new do |xml|
|
|
347
|
-
xml.spreadsheet {
|
|
348
|
-
self.sheets.each do |sheet|
|
|
349
|
-
self.default_sheet = sheet
|
|
350
|
-
xml.sheet(:name => sheet) { |x|
|
|
351
|
-
if first_row and last_row and first_column and last_column
|
|
352
|
-
# sonst gibt es Fehler bei leeren Blaettern
|
|
353
|
-
first_row.upto(last_row) do |row|
|
|
354
|
-
first_column.upto(last_column) do |col|
|
|
355
|
-
unless empty?(row,col)
|
|
356
|
-
x.cell(cell(row,col),
|
|
357
|
-
:row =>row,
|
|
358
|
-
:column => col,
|
|
359
|
-
:type => celltype(row,col))
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
}
|
|
365
|
-
end
|
|
366
|
-
}
|
|
367
|
-
end.to_xml
|
|
368
|
-
end
|
|
369
|
-
|
|
370
226
|
# when a method like spreadsheet.a42 is called
|
|
371
227
|
# convert it to a call of spreadsheet.cell('a',42)
|
|
372
228
|
def method_missing(m, *args)
|
|
373
229
|
# #aa42 => #cell('aa',42)
|
|
374
230
|
# #aa42('Sheet1') => #cell('aa',42,'Sheet1')
|
|
375
|
-
if m =~ /^([a-z]+)(\d)$/
|
|
376
|
-
col = Roo::
|
|
377
|
-
row =
|
|
231
|
+
if m =~ /^([a-z]+)(\d+)$/
|
|
232
|
+
col = ::Roo::Utils.letter_to_number(Regexp.last_match[1])
|
|
233
|
+
row = Regexp.last_match[2].to_i
|
|
378
234
|
if args.empty?
|
|
379
|
-
cell(row,col)
|
|
235
|
+
cell(row, col)
|
|
380
236
|
else
|
|
381
|
-
cell(row,col,args.first)
|
|
237
|
+
cell(row, col, args.first)
|
|
382
238
|
end
|
|
383
239
|
else
|
|
384
240
|
super
|
|
@@ -387,15 +243,17 @@ class Roo::Base
|
|
|
387
243
|
|
|
388
244
|
# access different worksheets by calling spreadsheet.sheet(1)
|
|
389
245
|
# or spreadsheet.sheet('SHEETNAME')
|
|
390
|
-
def sheet(index,name=false)
|
|
391
|
-
|
|
392
|
-
name ? [
|
|
246
|
+
def sheet(index, name = false)
|
|
247
|
+
self.default_sheet = index.is_a?(::String) ? index : sheets[index]
|
|
248
|
+
name ? [default_sheet, self] : self
|
|
393
249
|
end
|
|
394
250
|
|
|
395
251
|
# iterate through all worksheets of a document
|
|
396
252
|
def each_with_pagename
|
|
397
|
-
|
|
398
|
-
|
|
253
|
+
return to_enum(:each_with_pagename) { sheets.size } unless block_given?
|
|
254
|
+
|
|
255
|
+
sheets.each do |s|
|
|
256
|
+
yield sheet(s, true)
|
|
399
257
|
end
|
|
400
258
|
end
|
|
401
259
|
|
|
@@ -409,7 +267,6 @@ class Roo::Base
|
|
|
409
267
|
# such as :price => '^(Cost|Price)'
|
|
410
268
|
# case insensitive by default
|
|
411
269
|
|
|
412
|
-
|
|
413
270
|
# by using the :header_search option, you can query for headers
|
|
414
271
|
# and return a hash of every row with the keys set to the header result
|
|
415
272
|
# for example:
|
|
@@ -420,116 +277,85 @@ class Roo::Base
|
|
|
420
277
|
# * is the wildcard character
|
|
421
278
|
|
|
422
279
|
# you can also pass in a :clean => true option to strip the sheet of
|
|
423
|
-
#
|
|
280
|
+
# control characters and white spaces around columns
|
|
281
|
+
|
|
282
|
+
def each(options = {})
|
|
283
|
+
return to_enum(:each, options) unless block_given?
|
|
424
284
|
|
|
425
|
-
def each(options={})
|
|
426
285
|
if options.empty?
|
|
427
286
|
1.upto(last_row) do |line|
|
|
428
287
|
yield row(line)
|
|
429
288
|
end
|
|
430
289
|
else
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
@cleaned ||= {}
|
|
434
|
-
@cleaned[@default_sheet] || clean_sheet(@default_sheet)
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
if options[:header_search]
|
|
438
|
-
@headers = nil
|
|
439
|
-
@header_line = row_with(options[:header_search])
|
|
440
|
-
elsif [:first_row,true].include?(options[:headers])
|
|
441
|
-
@headers = []
|
|
442
|
-
row(first_row).each_with_index {|x,i| @headers << [x,i + 1]}
|
|
443
|
-
else
|
|
444
|
-
set_headers(options)
|
|
445
|
-
end
|
|
446
|
-
|
|
290
|
+
clean_sheet_if_need(options)
|
|
291
|
+
search_or_set_header(options)
|
|
447
292
|
headers = @headers ||
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
293
|
+
(first_column..last_column).each_with_object({}) do |col, hash|
|
|
294
|
+
hash[cell(@header_line, col)] = col
|
|
295
|
+
end
|
|
451
296
|
|
|
452
297
|
@header_line.upto(last_row) do |line|
|
|
453
|
-
yield(
|
|
298
|
+
yield(headers.each_with_object({}) { |(k, v), hash| hash[k] = cell(line, v) })
|
|
454
299
|
end
|
|
455
300
|
end
|
|
456
301
|
end
|
|
457
302
|
|
|
458
|
-
def parse(options={})
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
each(options) {|row| ary << yield(row)}
|
|
462
|
-
else
|
|
463
|
-
each(options) {|row| ary << row}
|
|
303
|
+
def parse(options = {})
|
|
304
|
+
results = each(options).map do |row|
|
|
305
|
+
block_given? ? yield(row) : row
|
|
464
306
|
end
|
|
465
|
-
|
|
307
|
+
|
|
308
|
+
options[:headers] == true ? results : results.drop(1)
|
|
466
309
|
end
|
|
467
310
|
|
|
468
|
-
def row_with(query,return_headers=false)
|
|
469
|
-
query.map! {|x| Array(x.split('*'))}
|
|
311
|
+
def row_with(query, return_headers = false)
|
|
470
312
|
line_no = 0
|
|
313
|
+
closest_mismatched_headers = []
|
|
471
314
|
each do |row|
|
|
472
315
|
line_no += 1
|
|
473
|
-
|
|
474
|
-
# ex. if UPC and SKU exist for UPC*SKU search, UPC takes the cake
|
|
475
|
-
headers = query.map do |q|
|
|
476
|
-
q.map {|i| row.grep(/#{i}/i)[0]}.compact[0]
|
|
477
|
-
end.compact
|
|
478
|
-
|
|
316
|
+
headers = query.map { |q| row.grep(q)[0] }.compact
|
|
479
317
|
if headers.length == query.length
|
|
480
318
|
@header_line = line_no
|
|
481
319
|
return return_headers ? headers : line_no
|
|
482
|
-
|
|
483
|
-
|
|
320
|
+
else
|
|
321
|
+
closest_mismatched_headers = headers if headers.length > closest_mismatched_headers.length
|
|
322
|
+
if line_no > 100
|
|
323
|
+
break
|
|
324
|
+
end
|
|
484
325
|
end
|
|
485
326
|
end
|
|
327
|
+
missing_headers = query.select { |q| closest_mismatched_headers.grep(q).empty? }
|
|
328
|
+
raise Roo::HeaderRowNotFoundError, missing_headers
|
|
486
329
|
end
|
|
487
330
|
|
|
488
331
|
protected
|
|
489
332
|
|
|
490
|
-
def
|
|
491
|
-
|
|
492
|
-
|
|
333
|
+
def file_type_check(filename, exts, name, warning_level, packed = nil)
|
|
334
|
+
if packed == :zip
|
|
335
|
+
# spreadsheet.ods.zip => spreadsheet.ods
|
|
336
|
+
# Decompression is not performed here, only the 'zip' extension
|
|
337
|
+
# is removed from the file.
|
|
338
|
+
filename = File.basename(filename, File.extname(filename))
|
|
493
339
|
end
|
|
494
|
-
end
|
|
495
340
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
correct_class = "use #{new_expression[ext]} to handle #{ext} spreadsheet files. This has #{File.extname(filename).downcase}"
|
|
341
|
+
if uri?(filename) && (qs_begin = filename.rindex("?"))
|
|
342
|
+
filename = filename[0..qs_begin - 1]
|
|
343
|
+
end
|
|
344
|
+
exts = Array(exts)
|
|
345
|
+
|
|
346
|
+
return if exts.include?(File.extname(filename).downcase)
|
|
347
|
+
|
|
348
|
+
case warning_level
|
|
349
|
+
when :error
|
|
350
|
+
warn file_type_warning_message(filename, exts)
|
|
351
|
+
fail TypeError, "#{filename} is not #{name} file"
|
|
352
|
+
when :warning
|
|
353
|
+
warn "are you sure, this is #{name} spreadsheet file?"
|
|
354
|
+
warn file_type_warning_message(filename, exts)
|
|
355
|
+
when :ignore
|
|
356
|
+
# ignore
|
|
513
357
|
else
|
|
514
|
-
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
if uri?(filename) && qs_begin = filename.rindex('?')
|
|
518
|
-
filename = filename[0..qs_begin-1]
|
|
519
|
-
end
|
|
520
|
-
if File.extname(filename).downcase != ext
|
|
521
|
-
case warning_level
|
|
522
|
-
when :error
|
|
523
|
-
warn correct_class
|
|
524
|
-
raise TypeError, "#{filename} is not #{name} file"
|
|
525
|
-
when :warning
|
|
526
|
-
warn "are you sure, this is #{name} spreadsheet file?"
|
|
527
|
-
warn correct_class
|
|
528
|
-
when :ignore
|
|
529
|
-
# ignore
|
|
530
|
-
else
|
|
531
|
-
raise "#{warning_level} illegal state of file_warning"
|
|
532
|
-
end
|
|
358
|
+
fail "#{warning_level} illegal state of file_warning"
|
|
533
359
|
end
|
|
534
360
|
end
|
|
535
361
|
|
|
@@ -538,8 +364,8 @@ class Roo::Base
|
|
|
538
364
|
# Diese Methode ist eine temp. Loesung, um zu erforschen, ob der
|
|
539
365
|
# Zugriff mit numerischen Keys schneller ist.
|
|
540
366
|
def key_to_num(str)
|
|
541
|
-
r,c = str.split(
|
|
542
|
-
[r.to_i,c.to_i]
|
|
367
|
+
r, c = str.split(",")
|
|
368
|
+
[r.to_i, c.to_i]
|
|
543
369
|
end
|
|
544
370
|
|
|
545
371
|
# see: key_to_num
|
|
@@ -547,8 +373,83 @@ class Roo::Base
|
|
|
547
373
|
"#{arr[0]},#{arr[1]}"
|
|
548
374
|
end
|
|
549
375
|
|
|
376
|
+
def is_stream?(filename_or_stream)
|
|
377
|
+
filename_or_stream.respond_to?(:seek)
|
|
378
|
+
end
|
|
379
|
+
|
|
550
380
|
private
|
|
551
381
|
|
|
382
|
+
def clean_sheet_if_need(options)
|
|
383
|
+
return unless options[:clean]
|
|
384
|
+
options.delete(:clean)
|
|
385
|
+
@cleaned ||= {}
|
|
386
|
+
clean_sheet(default_sheet) unless @cleaned[default_sheet]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def search_or_set_header(options)
|
|
390
|
+
if options[:header_search]
|
|
391
|
+
@headers = nil
|
|
392
|
+
@header_line = row_with(options[:header_search])
|
|
393
|
+
elsif [:first_row, true].include?(options[:headers])
|
|
394
|
+
@headers = []
|
|
395
|
+
row(first_row).each_with_index { |x, i| @headers << [x, i + 1] }
|
|
396
|
+
else
|
|
397
|
+
set_headers(options)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def local_filename(filename, tmpdir, packed)
|
|
402
|
+
return if is_stream?(filename)
|
|
403
|
+
filename = download_uri(filename, tmpdir) if uri?(filename)
|
|
404
|
+
filename = unzip(filename, tmpdir) if packed == :zip
|
|
405
|
+
|
|
406
|
+
fail IOError, "file #{filename} does not exist" unless File.file?(filename)
|
|
407
|
+
|
|
408
|
+
filename
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def file_type_warning_message(filename, exts)
|
|
412
|
+
*rest, last_ext = exts
|
|
413
|
+
ext_list = rest.any? ? "#{rest.join(', ')} or #{last_ext}" : last_ext
|
|
414
|
+
"use #{Roo::CLASS_FOR_EXTENSION.fetch(last_ext.sub('.', '').to_sym)}.new to handle #{ext_list} spreadsheet files. This has #{File.extname(filename).downcase}"
|
|
415
|
+
rescue KeyError
|
|
416
|
+
raise "unknown file types: #{ext_list}"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def find_by_row(row_index)
|
|
420
|
+
row_index += (header_line - 1) if @header_line
|
|
421
|
+
|
|
422
|
+
row(row_index).size.times.map do |cell_index|
|
|
423
|
+
cell(row_index, cell_index + 1)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def find_by_conditions(options)
|
|
428
|
+
rows = first_row.upto(last_row)
|
|
429
|
+
header_for = 1.upto(last_column).each_with_object({}) do |col, hash|
|
|
430
|
+
hash[col] = cell(@header_line, col)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# are all conditions met?
|
|
434
|
+
conditions = options[:conditions]
|
|
435
|
+
if conditions && !conditions.empty?
|
|
436
|
+
column_with = header_for.invert
|
|
437
|
+
rows = rows.select do |i|
|
|
438
|
+
conditions.all? { |key, val| cell(i, column_with[key]) == val }
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
if options[:array]
|
|
443
|
+
rows.map { |i| row(i) }
|
|
444
|
+
else
|
|
445
|
+
rows.map do |i|
|
|
446
|
+
1.upto(row(i).size).each_with_object({}) do |j, hash|
|
|
447
|
+
hash[header_for.fetch(j)] = cell(i, j)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
552
453
|
def without_changing_default_sheet
|
|
553
454
|
original_default_sheet = default_sheet
|
|
554
455
|
yield
|
|
@@ -560,77 +461,94 @@ class Roo::Base
|
|
|
560
461
|
initialize(@filename)
|
|
561
462
|
end
|
|
562
463
|
|
|
563
|
-
def
|
|
564
|
-
|
|
565
|
-
|
|
464
|
+
def find_basename(filename)
|
|
465
|
+
if uri?(filename)
|
|
466
|
+
require "uri"
|
|
467
|
+
uri = URI.parse filename
|
|
468
|
+
File.basename(uri.path)
|
|
469
|
+
elsif !is_stream?(filename)
|
|
470
|
+
File.basename(filename)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def make_tmpdir(prefix = nil, root = nil, &block)
|
|
475
|
+
warn "[DEPRECATION] extend Roo::Tempdir and use its .make_tempdir instead"
|
|
476
|
+
prefix = "#{Roo::TEMP_PREFIX}#{prefix}"
|
|
477
|
+
root ||= ENV["ROO_TMP"]
|
|
478
|
+
|
|
479
|
+
if block_given?
|
|
480
|
+
# folder is deleted at end of block
|
|
481
|
+
::Dir.mktmpdir(prefix, root, &block)
|
|
482
|
+
else
|
|
483
|
+
self.class.make_tempdir(self, prefix, root)
|
|
566
484
|
end
|
|
567
485
|
end
|
|
568
486
|
|
|
569
487
|
def clean_sheet(sheet)
|
|
570
488
|
read_cells(sheet)
|
|
571
|
-
@cell[sheet].each_pair do |coord,value|
|
|
572
|
-
|
|
573
|
-
@cell[sheet][coord] = sanitize_value(value)
|
|
574
|
-
end
|
|
489
|
+
@cell[sheet].each_pair do |coord, value|
|
|
490
|
+
@cell[sheet][coord] = sanitize_value(value) if value.is_a?(::String)
|
|
575
491
|
end
|
|
576
492
|
@cleaned[sheet] = true
|
|
577
493
|
end
|
|
578
494
|
|
|
579
495
|
def sanitize_value(v)
|
|
580
|
-
v.
|
|
496
|
+
v.gsub(/[[:cntrl:]]|^[\p{Space}]+|[\p{Space}]+$/, "")
|
|
581
497
|
end
|
|
582
498
|
|
|
583
|
-
def set_headers(hash={})
|
|
499
|
+
def set_headers(hash = {})
|
|
584
500
|
# try to find header row with all values or give an error
|
|
585
501
|
# then create new hash by indexing strings and keeping integers for header array
|
|
586
|
-
|
|
587
|
-
@headers =
|
|
502
|
+
header_row = row_with(hash.values, true)
|
|
503
|
+
@headers = {}
|
|
504
|
+
hash.each_with_index do |(key, _), index|
|
|
505
|
+
@headers[key] = header_index(header_row[index])
|
|
506
|
+
end
|
|
588
507
|
end
|
|
589
508
|
|
|
590
509
|
def header_index(query)
|
|
591
510
|
row(@header_line).index(query) + first_column
|
|
592
511
|
end
|
|
593
512
|
|
|
594
|
-
def set_value(row,col,value,sheet=
|
|
595
|
-
sheet
|
|
596
|
-
@cell[sheet][[row,col]] = value
|
|
513
|
+
def set_value(row, col, value, sheet = default_sheet)
|
|
514
|
+
@cell[sheet][[row, col]] = value
|
|
597
515
|
end
|
|
598
516
|
|
|
599
|
-
def set_type(row,col,type,sheet=
|
|
600
|
-
sheet
|
|
601
|
-
@cell_type[sheet][[row,col]] = type
|
|
517
|
+
def set_type(row, col, type, sheet = default_sheet)
|
|
518
|
+
@cell_type[sheet][[row, col]] = type
|
|
602
519
|
end
|
|
603
520
|
|
|
604
521
|
# converts cell coordinate to numeric values of row,col
|
|
605
|
-
def normalize(row,col)
|
|
606
|
-
if row.
|
|
607
|
-
if col.
|
|
522
|
+
def normalize(row, col)
|
|
523
|
+
if row.is_a?(::String)
|
|
524
|
+
if col.is_a?(::Integer)
|
|
608
525
|
# ('A',1):
|
|
609
526
|
# ('B', 5) -> (5, 2)
|
|
610
527
|
row, col = col, row
|
|
611
528
|
else
|
|
612
|
-
|
|
529
|
+
fail ArgumentError
|
|
613
530
|
end
|
|
614
531
|
end
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
532
|
+
|
|
533
|
+
col = ::Roo::Utils.letter_to_number(col) if col.is_a?(::String)
|
|
534
|
+
|
|
535
|
+
[row, col]
|
|
619
536
|
end
|
|
620
537
|
|
|
621
538
|
def uri?(filename)
|
|
622
|
-
filename.start_with?("http://", "https://")
|
|
539
|
+
filename.start_with?("http://", "https://", "ftp://")
|
|
540
|
+
rescue
|
|
541
|
+
false
|
|
623
542
|
end
|
|
624
543
|
|
|
625
544
|
def download_uri(uri, tmpdir)
|
|
626
|
-
require
|
|
627
|
-
tempfilename = File.join(tmpdir,
|
|
628
|
-
response = ''
|
|
545
|
+
require "open-uri"
|
|
546
|
+
tempfilename = File.join(tmpdir, find_basename(uri))
|
|
629
547
|
begin
|
|
630
|
-
File.open(tempfilename,"wb") do |file|
|
|
631
|
-
open(uri, "User-Agent" => "Ruby/#{RUBY_VERSION}")
|
|
548
|
+
File.open(tempfilename, "wb") do |file|
|
|
549
|
+
URI.open(uri, "User-Agent" => "Ruby/#{RUBY_VERSION}") do |net|
|
|
632
550
|
file.write(net.read)
|
|
633
|
-
|
|
551
|
+
end
|
|
634
552
|
end
|
|
635
553
|
rescue OpenURI::HTTPError
|
|
636
554
|
raise "could not open #{uri}"
|
|
@@ -640,49 +558,16 @@ class Roo::Base
|
|
|
640
558
|
|
|
641
559
|
def open_from_stream(stream, tmpdir)
|
|
642
560
|
tempfilename = File.join(tmpdir, "spreadsheet")
|
|
643
|
-
File.open(tempfilename,"wb") do |file|
|
|
561
|
+
File.open(tempfilename, "wb") do |file|
|
|
644
562
|
file.write(stream[7..-1])
|
|
645
563
|
end
|
|
646
564
|
File.join(tmpdir, "spreadsheet")
|
|
647
565
|
end
|
|
648
566
|
|
|
649
|
-
LETTERS = %w{A B C D E F G H I J K L M N O P Q R S T U V W X Y Z}
|
|
650
|
-
|
|
651
|
-
# convert a number to something like 'AB' (1 => 'A', 2 => 'B', ...)
|
|
652
|
-
def self.number_to_letter(n)
|
|
653
|
-
letters=""
|
|
654
|
-
if n > 26
|
|
655
|
-
while n % 26 == 0 && n != 0
|
|
656
|
-
letters << 'Z'
|
|
657
|
-
n = (n - 26) / 26
|
|
658
|
-
end
|
|
659
|
-
while n > 0
|
|
660
|
-
num = n%26
|
|
661
|
-
letters = LETTERS[num-1] + letters
|
|
662
|
-
n = (n / 26)
|
|
663
|
-
end
|
|
664
|
-
else
|
|
665
|
-
letters = LETTERS[n-1]
|
|
666
|
-
end
|
|
667
|
-
letters
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
# convert letters like 'AB' to a number ('A' => 1, 'B' => 2, ...)
|
|
671
|
-
def self.letter_to_number(letters)
|
|
672
|
-
result = 0
|
|
673
|
-
while letters && letters.length > 0
|
|
674
|
-
character = letters[0,1].upcase
|
|
675
|
-
num = LETTERS.index(character)
|
|
676
|
-
raise ArgumentError, "invalid column character '#{letters[0,1]}'" if num == nil
|
|
677
|
-
num += 1
|
|
678
|
-
result = result * 26 + num
|
|
679
|
-
letters = letters[1..-1]
|
|
680
|
-
end
|
|
681
|
-
result
|
|
682
|
-
end
|
|
683
|
-
|
|
684
567
|
def unzip(filename, tmpdir)
|
|
685
|
-
|
|
568
|
+
require "zip/filesystem"
|
|
569
|
+
|
|
570
|
+
Zip::File.open(filename) do |zip|
|
|
686
571
|
process_zipfile_packed(zip, tmpdir)
|
|
687
572
|
end
|
|
688
573
|
end
|
|
@@ -691,106 +576,34 @@ class Roo::Base
|
|
|
691
576
|
def validate_sheet!(sheet)
|
|
692
577
|
case sheet
|
|
693
578
|
when nil
|
|
694
|
-
|
|
695
|
-
when
|
|
696
|
-
|
|
697
|
-
|
|
579
|
+
fail ArgumentError, "Error: sheet 'nil' not valid"
|
|
580
|
+
when Integer
|
|
581
|
+
sheets.fetch(sheet) do
|
|
582
|
+
fail RangeError, "sheet index #{sheet} not found"
|
|
698
583
|
end
|
|
699
584
|
when String
|
|
700
|
-
|
|
701
|
-
|
|
585
|
+
unless sheets.include?(sheet)
|
|
586
|
+
fail RangeError, "sheet '#{sheet}' not found"
|
|
702
587
|
end
|
|
703
588
|
else
|
|
704
|
-
|
|
589
|
+
fail TypeError, "not a valid sheet type: #{sheet.inspect}"
|
|
705
590
|
end
|
|
706
591
|
end
|
|
707
592
|
|
|
708
|
-
def process_zipfile_packed(zip, tmpdir, path=
|
|
593
|
+
def process_zipfile_packed(zip, tmpdir, path = "")
|
|
709
594
|
if zip.file.file? path
|
|
710
595
|
# extract and return filename
|
|
711
|
-
File.open(File.join(tmpdir, path),"wb") do |file|
|
|
596
|
+
File.open(File.join(tmpdir, path), "wb") do |file|
|
|
712
597
|
file.write(zip.read(path))
|
|
713
598
|
end
|
|
714
599
|
File.join(tmpdir, path)
|
|
715
600
|
else
|
|
716
|
-
ret=nil
|
|
717
|
-
path +=
|
|
601
|
+
ret = nil
|
|
602
|
+
path += "/" unless path.empty?
|
|
718
603
|
zip.dir.foreach(path) do |filename|
|
|
719
604
|
ret = process_zipfile_packed(zip, tmpdir, path + filename)
|
|
720
605
|
end
|
|
721
606
|
ret
|
|
722
607
|
end
|
|
723
608
|
end
|
|
724
|
-
|
|
725
|
-
# Write all cells to the csv file. File can be a filename or nil. If the this
|
|
726
|
-
# parameter is nil the output goes to STDOUT
|
|
727
|
-
def write_csv_content(file=nil,sheet=nil)
|
|
728
|
-
file ||= STDOUT
|
|
729
|
-
if first_row(sheet) # sheet is not empty
|
|
730
|
-
1.upto(last_row(sheet)) do |row|
|
|
731
|
-
1.upto(last_column(sheet)) do |col|
|
|
732
|
-
file.print(",") if col > 1
|
|
733
|
-
file.print cell_to_csv(row,col,sheet)
|
|
734
|
-
end
|
|
735
|
-
file.print("\n")
|
|
736
|
-
end # sheet not empty
|
|
737
|
-
end
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
# The content of a cell in the csv output
|
|
741
|
-
def cell_to_csv(row, col, sheet)
|
|
742
|
-
if empty?(row,col,sheet)
|
|
743
|
-
''
|
|
744
|
-
else
|
|
745
|
-
onecell = cell(row,col,sheet)
|
|
746
|
-
|
|
747
|
-
case celltype(row,col,sheet)
|
|
748
|
-
when :string
|
|
749
|
-
unless onecell.empty?
|
|
750
|
-
%{"#{onecell.gsub(/"/,'""')}"}
|
|
751
|
-
end
|
|
752
|
-
when :boolean
|
|
753
|
-
%{"#{onecell.gsub(/"/,'""').downcase}"}
|
|
754
|
-
when :float, :percentage
|
|
755
|
-
if onecell == onecell.to_i
|
|
756
|
-
onecell.to_i.to_s
|
|
757
|
-
else
|
|
758
|
-
onecell.to_s
|
|
759
|
-
end
|
|
760
|
-
when :formula
|
|
761
|
-
case onecell
|
|
762
|
-
when String
|
|
763
|
-
unless onecell.empty?
|
|
764
|
-
%{"#{onecell.gsub(/"/,'""')}"}
|
|
765
|
-
end
|
|
766
|
-
when Float
|
|
767
|
-
if onecell == onecell.to_i
|
|
768
|
-
onecell.to_i.to_s
|
|
769
|
-
else
|
|
770
|
-
onecell.to_s
|
|
771
|
-
end
|
|
772
|
-
when DateTime
|
|
773
|
-
onecell.to_s
|
|
774
|
-
else
|
|
775
|
-
raise "unhandled onecell-class #{onecell.class}"
|
|
776
|
-
end
|
|
777
|
-
when :date, :datetime
|
|
778
|
-
onecell.to_s
|
|
779
|
-
when :time
|
|
780
|
-
Roo::Base.integer_to_timestring(onecell)
|
|
781
|
-
else
|
|
782
|
-
raise "unhandled celltype #{celltype(row,col,sheet)}"
|
|
783
|
-
end || ""
|
|
784
|
-
end
|
|
785
|
-
end
|
|
786
|
-
|
|
787
|
-
# converts an integer value to a time string like '02:05:06'
|
|
788
|
-
def self.integer_to_timestring(content)
|
|
789
|
-
h = (content/3600.0).floor
|
|
790
|
-
content = content - h*3600
|
|
791
|
-
m = (content/60.0).floor
|
|
792
|
-
content = content - m*60
|
|
793
|
-
s = content
|
|
794
|
-
sprintf("%02d:%02d:%02d",h,m,s)
|
|
795
|
-
end
|
|
796
609
|
end
|