roo 1.13.1 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (235) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +17 -0
  3. data/.github/issue_template.md +16 -0
  4. data/.github/pull_request_template.md +14 -0
  5. data/.github/workflows/pull-request.yml +15 -0
  6. data/.github/workflows/ruby.yml +34 -0
  7. data/.gitignore +11 -0
  8. data/.rubocop.yml +186 -0
  9. data/.simplecov +4 -0
  10. data/CHANGELOG.md +702 -0
  11. data/Gemfile +18 -12
  12. data/Guardfile +23 -0
  13. data/LICENSE +5 -1
  14. data/README.md +328 -0
  15. data/Rakefile +23 -23
  16. data/examples/roo_soap_client.rb +28 -31
  17. data/examples/roo_soap_server.rb +4 -6
  18. data/examples/write_me.rb +9 -10
  19. data/lib/roo/base.rb +317 -504
  20. data/lib/roo/constants.rb +7 -0
  21. data/lib/roo/csv.rb +141 -113
  22. data/lib/roo/errors.rb +11 -0
  23. data/lib/roo/excelx/cell/base.rb +108 -0
  24. data/lib/roo/excelx/cell/boolean.rb +30 -0
  25. data/lib/roo/excelx/cell/date.rb +28 -0
  26. data/lib/roo/excelx/cell/datetime.rb +107 -0
  27. data/lib/roo/excelx/cell/empty.rb +20 -0
  28. data/lib/roo/excelx/cell/number.rb +99 -0
  29. data/lib/roo/excelx/cell/string.rb +19 -0
  30. data/lib/roo/excelx/cell/time.rb +44 -0
  31. data/lib/roo/excelx/cell.rb +110 -0
  32. data/lib/roo/excelx/comments.rb +55 -0
  33. data/lib/roo/excelx/coordinate.rb +19 -0
  34. data/lib/roo/excelx/extractor.rb +39 -0
  35. data/lib/roo/excelx/format.rb +71 -0
  36. data/lib/roo/excelx/images.rb +26 -0
  37. data/lib/roo/excelx/relationships.rb +33 -0
  38. data/lib/roo/excelx/shared.rb +39 -0
  39. data/lib/roo/excelx/shared_strings.rb +151 -0
  40. data/lib/roo/excelx/sheet.rb +151 -0
  41. data/lib/roo/excelx/sheet_doc.rb +257 -0
  42. data/lib/roo/excelx/styles.rb +64 -0
  43. data/lib/roo/excelx/workbook.rb +64 -0
  44. data/lib/roo/excelx.rb +407 -601
  45. data/lib/roo/font.rb +17 -0
  46. data/lib/roo/formatters/base.rb +15 -0
  47. data/lib/roo/formatters/csv.rb +84 -0
  48. data/lib/roo/formatters/matrix.rb +23 -0
  49. data/lib/roo/formatters/xml.rb +31 -0
  50. data/lib/roo/formatters/yaml.rb +40 -0
  51. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  52. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  53. data/lib/roo/libre_office.rb +4 -0
  54. data/lib/roo/link.rb +34 -0
  55. data/lib/roo/open_office.rb +631 -0
  56. data/lib/roo/spreadsheet.rb +28 -23
  57. data/lib/roo/tempdir.rb +24 -0
  58. data/lib/roo/utils.rb +128 -0
  59. data/lib/roo/version.rb +3 -0
  60. data/lib/roo.rb +26 -24
  61. data/roo.gemspec +29 -202
  62. data/spec/helpers.rb +5 -0
  63. data/spec/lib/roo/base_spec.rb +291 -3
  64. data/spec/lib/roo/csv_spec.rb +38 -11
  65. data/spec/lib/roo/excelx/cell/time_spec.rb +15 -0
  66. data/spec/lib/roo/excelx/format_spec.rb +7 -6
  67. data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
  68. data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
  69. data/spec/lib/roo/excelx_spec.rb +682 -6
  70. data/spec/lib/roo/libreoffice_spec.rb +16 -6
  71. data/spec/lib/roo/openoffice_spec.rb +30 -8
  72. data/spec/lib/roo/spreadsheet_spec.rb +60 -12
  73. data/spec/lib/roo/strict_spec.rb +43 -0
  74. data/spec/lib/roo/utils_spec.rb +119 -0
  75. data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
  76. data/spec/lib/roo_spec.rb +0 -0
  77. data/spec/spec_helper.rb +7 -6
  78. data/test/all_ss.rb +12 -11
  79. data/test/excelx/cell/test_attr_reader_default.rb +72 -0
  80. data/test/excelx/cell/test_base.rb +68 -0
  81. data/test/excelx/cell/test_boolean.rb +36 -0
  82. data/test/excelx/cell/test_date.rb +38 -0
  83. data/test/excelx/cell/test_datetime.rb +45 -0
  84. data/test/excelx/cell/test_empty.rb +18 -0
  85. data/test/excelx/cell/test_number.rb +90 -0
  86. data/test/excelx/cell/test_string.rb +48 -0
  87. data/test/excelx/cell/test_time.rb +30 -0
  88. data/test/excelx/test_coordinate.rb +51 -0
  89. data/test/formatters/test_csv.rb +136 -0
  90. data/test/formatters/test_matrix.rb +76 -0
  91. data/test/formatters/test_xml.rb +78 -0
  92. data/test/formatters/test_yaml.rb +20 -0
  93. data/test/helpers/test_accessing_files.rb +81 -0
  94. data/test/helpers/test_comments.rb +43 -0
  95. data/test/helpers/test_formulas.rb +9 -0
  96. data/test/helpers/test_labels.rb +103 -0
  97. data/test/helpers/test_sheets.rb +55 -0
  98. data/test/helpers/test_styles.rb +62 -0
  99. data/test/roo/test_base.rb +182 -0
  100. data/test/roo/test_csv.rb +88 -0
  101. data/test/roo/test_excelx.rb +360 -0
  102. data/test/roo/test_libre_office.rb +9 -0
  103. data/test/roo/test_open_office.rb +289 -0
  104. data/test/test_helper.rb +123 -59
  105. data/test/test_roo.rb +392 -2292
  106. metadata +153 -296
  107. data/CHANGELOG +0 -412
  108. data/Gemfile.lock +0 -78
  109. data/README.markdown +0 -126
  110. data/VERSION +0 -1
  111. data/lib/roo/excel.rb +0 -355
  112. data/lib/roo/excel2003xml.rb +0 -300
  113. data/lib/roo/google.rb +0 -292
  114. data/lib/roo/openoffice.rb +0 -496
  115. data/lib/roo/roo_rails_helper.rb +0 -83
  116. data/lib/roo/worksheet.rb +0 -18
  117. data/scripts/txt2html +0 -67
  118. data/spec/lib/roo/excel2003xml_spec.rb +0 -15
  119. data/spec/lib/roo/excel_spec.rb +0 -17
  120. data/spec/lib/roo/google_spec.rb +0 -64
  121. data/test/files/1900_base.xls +0 -0
  122. data/test/files/1900_base.xlsx +0 -0
  123. data/test/files/1904_base.xls +0 -0
  124. data/test/files/1904_base.xlsx +0 -0
  125. data/test/files/Bibelbund.csv +0 -3741
  126. data/test/files/Bibelbund.ods +0 -0
  127. data/test/files/Bibelbund.xls +0 -0
  128. data/test/files/Bibelbund.xlsx +0 -0
  129. data/test/files/Bibelbund.xml +0 -62518
  130. data/test/files/Bibelbund1.ods +0 -0
  131. data/test/files/Pfand_from_windows_phone.xlsx +0 -0
  132. data/test/files/bad_excel_date.xls +0 -0
  133. data/test/files/bbu.ods +0 -0
  134. data/test/files/bbu.xls +0 -0
  135. data/test/files/bbu.xlsx +0 -0
  136. data/test/files/bbu.xml +0 -152
  137. data/test/files/bode-v1.ods.zip +0 -0
  138. data/test/files/bode-v1.xls.zip +0 -0
  139. data/test/files/boolean.csv +0 -2
  140. data/test/files/boolean.ods +0 -0
  141. data/test/files/boolean.xls +0 -0
  142. data/test/files/boolean.xlsx +0 -0
  143. data/test/files/boolean.xml +0 -112
  144. data/test/files/borders.ods +0 -0
  145. data/test/files/borders.xls +0 -0
  146. data/test/files/borders.xlsx +0 -0
  147. data/test/files/borders.xml +0 -144
  148. data/test/files/bug-numbered-sheet-names.xlsx +0 -0
  149. data/test/files/bug-row-column-fixnum-float.xls +0 -0
  150. data/test/files/bug-row-column-fixnum-float.xml +0 -127
  151. data/test/files/comments.ods +0 -0
  152. data/test/files/comments.xls +0 -0
  153. data/test/files/comments.xlsx +0 -0
  154. data/test/files/csvtypes.csv +0 -1
  155. data/test/files/datetime.ods +0 -0
  156. data/test/files/datetime.xls +0 -0
  157. data/test/files/datetime.xlsx +0 -0
  158. data/test/files/datetime.xml +0 -142
  159. data/test/files/datetime_floatconv.xls +0 -0
  160. data/test/files/datetime_floatconv.xml +0 -148
  161. data/test/files/dreimalvier.ods +0 -0
  162. data/test/files/emptysheets.ods +0 -0
  163. data/test/files/emptysheets.xls +0 -0
  164. data/test/files/emptysheets.xlsx +0 -0
  165. data/test/files/emptysheets.xml +0 -105
  166. data/test/files/excel2003.xml +0 -21140
  167. data/test/files/false_encoding.xls +0 -0
  168. data/test/files/false_encoding.xml +0 -132
  169. data/test/files/file_item_error.xlsx +0 -0
  170. data/test/files/formula.ods +0 -0
  171. data/test/files/formula.xls +0 -0
  172. data/test/files/formula.xlsx +0 -0
  173. data/test/files/formula.xml +0 -134
  174. data/test/files/formula_parse_error.xls +0 -0
  175. data/test/files/formula_parse_error.xml +0 -1833
  176. data/test/files/formula_string_error.xlsx +0 -0
  177. data/test/files/html-escape.ods +0 -0
  178. data/test/files/link.xls +0 -0
  179. data/test/files/link.xlsx +0 -0
  180. data/test/files/matrix.ods +0 -0
  181. data/test/files/matrix.xls +0 -0
  182. data/test/files/named_cells.ods +0 -0
  183. data/test/files/named_cells.xls +0 -0
  184. data/test/files/named_cells.xlsx +0 -0
  185. data/test/files/no_spreadsheet_file.txt +0 -1
  186. data/test/files/numbers1.csv +0 -18
  187. data/test/files/numbers1.ods +0 -0
  188. data/test/files/numbers1.xls +0 -0
  189. data/test/files/numbers1.xlsx +0 -0
  190. data/test/files/numbers1.xml +0 -312
  191. data/test/files/only_one_sheet.ods +0 -0
  192. data/test/files/only_one_sheet.xls +0 -0
  193. data/test/files/only_one_sheet.xlsx +0 -0
  194. data/test/files/only_one_sheet.xml +0 -67
  195. data/test/files/paragraph.ods +0 -0
  196. data/test/files/paragraph.xls +0 -0
  197. data/test/files/paragraph.xlsx +0 -0
  198. data/test/files/paragraph.xml +0 -127
  199. data/test/files/prova.xls +0 -0
  200. data/test/files/ric.ods +0 -0
  201. data/test/files/simple_spreadsheet.ods +0 -0
  202. data/test/files/simple_spreadsheet.xls +0 -0
  203. data/test/files/simple_spreadsheet.xlsx +0 -0
  204. data/test/files/simple_spreadsheet.xml +0 -225
  205. data/test/files/simple_spreadsheet_from_italo.ods +0 -0
  206. data/test/files/simple_spreadsheet_from_italo.xls +0 -0
  207. data/test/files/simple_spreadsheet_from_italo.xml +0 -242
  208. data/test/files/so_datetime.csv +0 -7
  209. data/test/files/style.ods +0 -0
  210. data/test/files/style.xls +0 -0
  211. data/test/files/style.xlsx +0 -0
  212. data/test/files/style.xml +0 -154
  213. data/test/files/time-test.csv +0 -2
  214. data/test/files/time-test.ods +0 -0
  215. data/test/files/time-test.xls +0 -0
  216. data/test/files/time-test.xlsx +0 -0
  217. data/test/files/time-test.xml +0 -131
  218. data/test/files/type_excel.ods +0 -0
  219. data/test/files/type_excel.xlsx +0 -0
  220. data/test/files/type_excelx.ods +0 -0
  221. data/test/files/type_excelx.xls +0 -0
  222. data/test/files/type_openoffice.xls +0 -0
  223. data/test/files/type_openoffice.xlsx +0 -0
  224. data/test/files/whitespace.ods +0 -0
  225. data/test/files/whitespace.xls +0 -0
  226. data/test/files/whitespace.xlsx +0 -0
  227. data/test/files/whitespace.xml +0 -184
  228. data/test/rm_sub_test.rb +0 -12
  229. data/test/rm_test.rb +0 -7
  230. data/test/test_generic_spreadsheet.rb +0 -259
  231. data/website/index.html +0 -385
  232. data/website/index.txt +0 -423
  233. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  234. data/website/stylesheets/screen.css +0 -130
  235. data/website/template.rhtml +0 -48
data/lib/roo/base.rb CHANGED
@@ -1,51 +1,40 @@
1
- # encoding: utf-8
2
-
3
- require 'tmpdir'
4
- require 'stringio'
5
-
6
- begin
7
- require 'zip/zipfilesystem'
8
- Roo::ZipFile = Zip::ZipFile
9
- rescue LoadError
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
- TEMP_PREFIX = "oo_"
20
+ MAX_ROW_COL = 999_999
21
+ MIN_ROW_COL = 0
20
22
 
21
- attr_reader :default_sheet, :headers
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
- protected
27
-
28
- def self.split_coordinate(str)
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.split_coord(s)
36
- if s =~ /([a-zA-Z]+)([0-9]+)/
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
- @default_sheet = self.sheets.first
63
- @header_line = 1
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 (1 = first sheet) or the name of a sheet.
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=nil)
77
- Roo::Base.number_to_letter(first_column(sheet))
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=nil)
82
- Roo::Base.number_to_letter(last_column(sheet))
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
- # returns the number of the first non-empty row
86
- def first_row(sheet=nil)
87
- sheet ||= @default_sheet
88
- read_cells(sheet)
89
- if @first_row[sheet]
90
- return @first_row[sheet]
91
- end
92
- impossible_value = 999_999 # more than a spreadsheet can hold
93
- result = impossible_value
94
- @cell[sheet].each_pair {|key,value|
95
- y = key.first.to_i # _to_string(key).split(',')
96
- result = [result, y].min if value
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
- # write the current spreadsheet to stdout or into a file
187
- def to_csv(filename=nil,sheet=nil)
188
- sheet ||= @default_sheet
189
- if filename
190
- File.open(filename,"w") do |file|
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
- # returns a matrix object from the whole sheet or a rectangular area of a sheet
203
- def to_matrix(from_row=nil, from_column=nil, to_row=nil, to_column=nil,sheet=nil)
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
- rows.map do |i|
250
- if result_array
251
- self.row(i)
252
- else
253
- Hash[1.upto(self.row(i).size).map do |j|
254
- [header_for.fetch(j), cell(i,j)]
255
- end]
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(rownumber,sheet=nil)
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(rownumber,col,sheet)
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(columnnumber,sheet=nil)
274
- if columnnumber.class == String
275
- columnnumber = Roo::Excel.letter_to_number(columnnumber)
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,columnnumber,sheet)
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=nil) #:nodoc:
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 = case value
291
- when Fixnum then :float
292
- when String, Float then :string
293
- else
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
- set_value(row,col,value,sheet)
298
- set_type(row,col,cell_type,sheet)
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 = @default_sheet
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=nil)
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 {|sheet|
208
+ sheets.each do |sheet|
327
209
  self.default_sheet = sheet
328
210
  result << "Sheet " + n.to_s + ":\n"
329
- unless first_row
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::Base.number_to_letter(first_column)}\n"
335
- result << " Last column: #{Roo::Base.number_to_letter(last_column)}"
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::Base.letter_to_number($1)
377
- row = $2.to_i
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
- @default_sheet = String === index ? index : self.sheets[index]
392
- name ? [@default_sheet,self] : self
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
- self.sheets.each do |s|
398
- yield sheet(s,true)
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
- # odd unicode characters and white spaces around columns
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
- if options[:clean]
432
- options.delete(:clean)
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
- Hash[(first_column..last_column).map do |col|
449
- [cell(@header_line,col), col]
450
- end]
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(Hash[headers.map {|k,v| [k,cell(line,v)]}])
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
- ary = []
460
- if block_given?
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
- ary
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
- # makes sure headers is the first part of wildcard search for priority
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
- elsif line_no > 100
483
- raise "Couldn't find header row."
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 load_xml(path)
491
- File.open(path) do |file|
492
- Nokogiri::XML(file)
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
- def file_type_check(filename, ext, name, warning_level, packed=nil)
497
- new_expression = {
498
- '.ods' => 'Roo::OpenOffice.new',
499
- '.xls' => 'Roo::Excel.new',
500
- '.xlsx' => 'Roo::Excelx.new',
501
- '.csv' => 'Roo::CSV.new',
502
- '.xml' => 'Roo::Excel2003XML.new',
503
- }
504
- if packed == :zip
505
- # lalala.ods.zip => lalala.ods
506
- # hier wird KEIN unzip gemacht, sondern nur der Name der Datei
507
- # getestet, falls es eine gepackte Datei ist.
508
- filename = File.basename(filename,File.extname(filename))
509
- end
510
- case ext
511
- when '.ods', '.xls', '.xlsx', '.csv', '.xml'
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
- raise "unknown file type: #{ext}"
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 make_tmpdir(tmp_root = nil)
564
- Dir.mktmpdir(TEMP_PREFIX, tmp_root || ENV['ROO_TMP']) do |tmpdir|
565
- yield tmpdir
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
- if String === value
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.strip.unpack('U*').select {|b| b < 127}.pack('U*')
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
- @headers = row_with(hash.values,true)
587
- @headers = Hash[hash.keys.zip(@headers.map {|x| header_index(x)})]
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=nil)
595
- sheet ||= @default_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=nil)
600
- sheet ||= @default_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.class == String
607
- if col.class == Fixnum
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
- raise ArgumentError
529
+ fail ArgumentError
613
530
  end
614
531
  end
615
- if col.class == String
616
- col = Roo::Base.letter_to_number(col)
617
- end
618
- return row,col
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 'open-uri'
627
- tempfilename = File.join(tmpdir, File.basename(uri))
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}") { |net|
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
- Roo::ZipFile.open(filename) do |zip|
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
- raise ArgumentError, "Error: sheet 'nil' not valid"
695
- when Fixnum
696
- self.sheets.fetch(sheet-1) do
697
- raise RangeError, "sheet index #{sheet} not found"
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
- if !sheets.include? sheet
701
- raise RangeError, "sheet '#{sheet}' not found"
585
+ unless sheets.include?(sheet)
586
+ fail RangeError, "sheet '#{sheet}' not found"
702
587
  end
703
588
  else
704
- raise TypeError, "not a valid sheet type: #{sheet.inspect}"
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 += '/' unless path.empty?
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