roo 1.10.1 → 1.10.2

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.
Files changed (136) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +38 -0
  4. data/History.txt +4 -4
  5. data/License.txt +20 -0
  6. data/Manifest.txt +68 -0
  7. data/README.markdown +109 -0
  8. data/Rakefile +5 -4
  9. data/bin/roo +0 -0
  10. data/examples/roo_soap_client.rb +53 -0
  11. data/examples/roo_soap_server.rb +29 -0
  12. data/examples/write_me.rb +33 -0
  13. data/lib/roo.rb +20 -61
  14. data/lib/roo/csv.rb +13 -11
  15. data/lib/roo/excel.rb +108 -219
  16. data/lib/roo/excel2003xml.rb +312 -0
  17. data/lib/roo/excelx.rb +205 -341
  18. data/lib/roo/generic_spreadsheet.rb +371 -268
  19. data/lib/roo/google.rb +64 -54
  20. data/lib/roo/openoffice.rb +101 -156
  21. data/lib/roo/roo_rails_helper.rb +5 -5
  22. data/lib/roo/worksheet.rb +18 -0
  23. data/roo.gemspec +43 -0
  24. data/scripts/txt2html +67 -0
  25. data/test/all_ss.rb +8 -10
  26. data/test/{1900_base.xls → files/1900_base.xls} +0 -0
  27. data/test/{1904_base.xls → files/1904_base.xls} +0 -0
  28. data/test/{Bibelbund.csv → files/Bibelbund.csv} +0 -0
  29. data/test/{Bibelbund.ods → files/Bibelbund.ods} +0 -0
  30. data/test/{Bibelbund.xls → files/Bibelbund.xls} +0 -0
  31. data/test/{Bibelbund.xlsx → files/Bibelbund.xlsx} +0 -0
  32. data/test/files/Bibelbund.xml +62518 -0
  33. data/test/{Bibelbund1.ods → files/Bibelbund1.ods} +0 -0
  34. data/test/{Pfand_from_windows_phone.xlsx → files/Pfand_from_windows_phone.xlsx} +0 -0
  35. data/test/files/bad_excel_date.xls +0 -0
  36. data/test/{bbu.ods → files/bbu.ods} +0 -0
  37. data/test/{bbu.xls → files/bbu.xls} +0 -0
  38. data/test/{bbu.xlsx → files/bbu.xlsx} +0 -0
  39. data/test/files/bbu.xml +152 -0
  40. data/test/{bode-v1.ods.zip → files/bode-v1.ods.zip} +0 -0
  41. data/test/{bode-v1.xls.zip → files/bode-v1.xls.zip} +0 -0
  42. data/test/{boolean.ods → files/boolean.ods} +0 -0
  43. data/test/{boolean.xls → files/boolean.xls} +0 -0
  44. data/test/{boolean.xlsx → files/boolean.xlsx} +0 -0
  45. data/test/files/boolean.xml +112 -0
  46. data/test/{borders.ods → files/borders.ods} +0 -0
  47. data/test/{borders.xls → files/borders.xls} +0 -0
  48. data/test/{borders.xlsx → files/borders.xlsx} +0 -0
  49. data/test/files/borders.xml +144 -0
  50. data/test/{bug-row-column-fixnum-float.xls → files/bug-row-column-fixnum-float.xls} +0 -0
  51. data/test/files/bug-row-column-fixnum-float.xml +127 -0
  52. data/test/{comments.ods → files/comments.ods} +0 -0
  53. data/test/{comments.xls → files/comments.xls} +0 -0
  54. data/test/{comments.xlsx → files/comments.xlsx} +0 -0
  55. data/test/{csvtypes.csv → files/csvtypes.csv} +0 -0
  56. data/test/{datetime.ods → files/datetime.ods} +0 -0
  57. data/test/{datetime.xls → files/datetime.xls} +0 -0
  58. data/test/{datetime.xlsx → files/datetime.xlsx} +0 -0
  59. data/test/files/datetime.xml +142 -0
  60. data/test/{datetime_floatconv.xls → files/datetime_floatconv.xls} +0 -0
  61. data/test/files/datetime_floatconv.xml +148 -0
  62. data/test/{dreimalvier.ods → files/dreimalvier.ods} +0 -0
  63. data/test/{emptysheets.ods → files/emptysheets.ods} +0 -0
  64. data/test/{emptysheets.xls → files/emptysheets.xls} +0 -0
  65. data/test/{emptysheets.xlsx → files/emptysheets.xlsx} +0 -0
  66. data/test/files/emptysheets.xml +105 -0
  67. data/test/files/excel2003.xml +21140 -0
  68. data/test/{false_encoding.xls → files/false_encoding.xls} +0 -0
  69. data/test/files/false_encoding.xml +132 -0
  70. data/test/{formula.ods → files/formula.ods} +0 -0
  71. data/test/{formula.xls → files/formula.xls} +0 -0
  72. data/test/{formula.xlsx → files/formula.xlsx} +0 -0
  73. data/test/files/formula.xml +134 -0
  74. data/test/files/formula_parse_error.xls +0 -0
  75. data/test/files/formula_parse_error.xml +1833 -0
  76. data/test/{formula_string_error.xlsx → files/formula_string_error.xlsx} +0 -0
  77. data/test/{html-escape.ods → files/html-escape.ods} +0 -0
  78. data/test/{matrix.ods → files/matrix.ods} +0 -0
  79. data/test/{matrix.xls → files/matrix.xls} +0 -0
  80. data/test/{named_cells.ods → files/named_cells.ods} +0 -0
  81. data/test/{named_cells.xls → files/named_cells.xls} +0 -0
  82. data/test/{named_cells.xlsx → files/named_cells.xlsx} +0 -0
  83. data/test/{no_spreadsheet_file.txt → files/no_spreadsheet_file.txt} +0 -0
  84. data/test/{numbers1.csv → files/numbers1.csv} +0 -0
  85. data/test/{numbers1.ods → files/numbers1.ods} +0 -0
  86. data/test/{numbers1.xls → files/numbers1.xls} +0 -0
  87. data/test/{numbers1.xlsx → files/numbers1.xlsx} +0 -0
  88. data/test/files/numbers1.xml +312 -0
  89. data/test/{only_one_sheet.ods → files/only_one_sheet.ods} +0 -0
  90. data/test/{only_one_sheet.xls → files/only_one_sheet.xls} +0 -0
  91. data/test/{only_one_sheet.xlsx → files/only_one_sheet.xlsx} +0 -0
  92. data/test/files/only_one_sheet.xml +67 -0
  93. data/test/{paragraph.ods → files/paragraph.ods} +0 -0
  94. data/test/{paragraph.xls → files/paragraph.xls} +0 -0
  95. data/test/{paragraph.xlsx → files/paragraph.xlsx} +0 -0
  96. data/test/files/paragraph.xml +127 -0
  97. data/test/{prova.xls → files/prova.xls} +0 -0
  98. data/test/{ric.ods → files/ric.ods} +0 -0
  99. data/test/{simple_spreadsheet.ods → files/simple_spreadsheet.ods} +0 -0
  100. data/test/{simple_spreadsheet.xls → files/simple_spreadsheet.xls} +0 -0
  101. data/test/{simple_spreadsheet.xlsx → files/simple_spreadsheet.xlsx} +0 -0
  102. data/test/files/simple_spreadsheet.xml +225 -0
  103. data/test/{simple_spreadsheet_from_italo.ods → files/simple_spreadsheet_from_italo.ods} +0 -0
  104. data/test/{simple_spreadsheet_from_italo.xls → files/simple_spreadsheet_from_italo.xls} +0 -0
  105. data/test/files/simple_spreadsheet_from_italo.xml +242 -0
  106. data/test/{so_datetime.csv → files/so_datetime.csv} +0 -0
  107. data/test/{style.ods → files/style.ods} +0 -0
  108. data/test/{style.xls → files/style.xls} +0 -0
  109. data/test/{style.xlsx → files/style.xlsx} +0 -0
  110. data/test/files/style.xml +154 -0
  111. data/test/{time-test.csv → files/time-test.csv} +0 -0
  112. data/test/{time-test.ods → files/time-test.ods} +0 -0
  113. data/test/{time-test.xls → files/time-test.xls} +0 -0
  114. data/test/{time-test.xlsx → files/time-test.xlsx} +0 -0
  115. data/test/files/time-test.xml +131 -0
  116. data/test/{type_excel.ods → files/type_excel.ods} +0 -0
  117. data/test/{type_excel.xlsx → files/type_excel.xlsx} +0 -0
  118. data/test/{type_excelx.ods → files/type_excelx.ods} +0 -0
  119. data/test/{type_excelx.xls → files/type_excelx.xls} +0 -0
  120. data/test/{type_openoffice.xls → files/type_openoffice.xls} +0 -0
  121. data/test/{type_openoffice.xlsx → files/type_openoffice.xlsx} +0 -0
  122. data/test/{whitespace.ods → files/whitespace.ods} +0 -0
  123. data/test/{whitespace.xls → files/whitespace.xls} +0 -0
  124. data/test/{whitespace.xlsx → files/whitespace.xlsx} +0 -0
  125. data/test/files/whitespace.xml +184 -0
  126. data/test/test_generic_spreadsheet.rb +257 -0
  127. data/test/test_helper.rb +167 -27
  128. data/test/test_roo.rb +1178 -930
  129. data/website/index.html +385 -0
  130. data/website/index.txt +423 -0
  131. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  132. data/website/stylesheets/screen.css +130 -0
  133. data/website/template.rhtml +48 -0
  134. metadata +151 -121
  135. data/README.txt +0 -110
  136. data/lib/roo/.csv.rb.swp +0 -0
@@ -1,39 +1,23 @@
1
1
  # encoding: utf-8
2
- require 'matrix'
2
+
3
+ require 'tmpdir'
4
+ require 'stringio'
3
5
 
4
6
  # Base class for all other types of spreadsheets
5
- class GenericSpreadsheet
7
+ class Roo::GenericSpreadsheet
8
+ include Enumerable
9
+
10
+ TEMP_PREFIX = "oo_"
6
11
 
7
- attr_reader :default_sheet
12
+ attr_reader :default_sheet, :headers
8
13
 
9
14
  # sets the line with attribute names (default: 1)
10
15
  attr_accessor :header_line
11
16
 
12
17
  protected
13
18
 
14
- # Helper function for development
15
- def fremdrechner? #nodoc
16
- eigener = [
17
- 'C:\Users\thopre',
18
- 'c:/Users/thopre',
19
- '/c/Users/thopre',
20
- '/home/tp',
21
- ].include? ENV['HOME']
22
- # if eigener
23
- # puts "fremdrechner? ==> false"
24
- # else
25
- # puts "fremdrechner? ==> true"
26
- # end
27
- ! eigener
28
- end
29
-
30
- def self.next_tmpdir
31
- tmpdir = "oo_"+$$.to_s+"_"+sprintf("%010d",rand(10_000_000_000))
32
- tmpdir
33
- end
34
-
35
19
  def self.split_coordinate(str)
36
- letter,number = GenericSpreadsheet.split_coord(str)
20
+ letter,number = Roo::GenericSpreadsheet.split_coord(str)
37
21
  x = letter_to_number(letter)
38
22
  y = number
39
23
  return y, x
@@ -52,41 +36,47 @@ class GenericSpreadsheet
52
36
 
53
37
  public
54
38
 
39
+ def initialize(filename, packed=nil, file_warning=:error, tmpdir=nil)
40
+ @cell = Hash.new{|h,k| h[k] = {}}
41
+ @cell_type = Hash.new{|h,k| h[k] = {}}
42
+ @cells_read = {}
43
+
44
+ @first_row = {}
45
+ @last_row = {}
46
+ @first_column = {}
47
+ @last_column = {}
48
+
49
+ @style = {}
50
+ @style_defaults = Hash.new { |h,k| h[k] = [] }
51
+ @style_definitions = {}
52
+
53
+ @default_sheet = self.sheets.first
54
+ @formula = {}
55
+ @header_line = 1
56
+ end
57
+
55
58
  # sets the working sheet in the document
56
59
  # 'sheet' can be a number (1 = first sheet) or the name of a sheet.
57
60
  def default_sheet=(sheet)
58
- if sheet.kind_of? Fixnum
59
- if sheet > 0 and sheet <= sheets.length
60
- sheet = self.sheets[sheet-1]
61
- else
62
- raise RangeError
63
- end
64
- elsif sheet.kind_of?(String)
65
- raise RangeError if ! self.sheets.include?(sheet)
66
- else
67
- raise TypeError, "what are you trying to set as default sheet?"
68
- end
61
+ validate_sheet!(sheet)
69
62
  @default_sheet = sheet
70
- check_default_sheet
71
63
  @first_row[sheet] = @last_row[sheet] = @first_column[sheet] = @last_column[sheet] = nil
72
64
  @cells_read[sheet] = false
73
65
  end
74
66
 
75
67
  # first non-empty column as a letter
76
68
  def first_column_as_letter(sheet=nil)
77
- GenericSpreadsheet.number_to_letter(first_column(sheet))
69
+ Roo::GenericSpreadsheet.number_to_letter(first_column(sheet))
78
70
  end
79
71
 
80
72
  # last non-empty column as a letter
81
73
  def last_column_as_letter(sheet=nil)
82
- GenericSpreadsheet.number_to_letter(last_column(sheet))
74
+ Roo::GenericSpreadsheet.number_to_letter(last_column(sheet))
83
75
  end
84
76
 
85
77
  # returns the number of the first non-empty row
86
78
  def first_row(sheet=nil)
87
- if sheet == nil
88
- sheet = @default_sheet
89
- end
79
+ sheet ||= @default_sheet
90
80
  read_cells(sheet) unless @cells_read[sheet]
91
81
  if @first_row[sheet]
92
82
  return @first_row[sheet]
@@ -94,8 +84,7 @@ class GenericSpreadsheet
94
84
  impossible_value = 999_999 # more than a spreadsheet can hold
95
85
  result = impossible_value
96
86
  @cell[sheet].each_pair {|key,value|
97
- y,x = key # _to_string(key).split(',')
98
- y = y.to_i
87
+ y = key.first.to_i # _to_string(key).split(',')
99
88
  result = [result, y].min if value
100
89
  } if @cell[sheet]
101
90
  result = nil if result == impossible_value
@@ -105,7 +94,7 @@ class GenericSpreadsheet
105
94
 
106
95
  # returns the number of the last non-empty row
107
96
  def last_row(sheet=nil)
108
- sheet = @default_sheet unless sheet
97
+ sheet ||= @default_sheet
109
98
  read_cells(sheet) unless @cells_read[sheet]
110
99
  if @last_row[sheet]
111
100
  return @last_row[sheet]
@@ -113,8 +102,7 @@ class GenericSpreadsheet
113
102
  impossible_value = 0
114
103
  result = impossible_value
115
104
  @cell[sheet].each_pair {|key,value|
116
- y,x = key # _to_string(key).split(',')
117
- y = y.to_i
105
+ y = key.first.to_i # _to_string(key).split(',')
118
106
  result = [result, y].max if value
119
107
  } if @cell[sheet]
120
108
  result = nil if result == impossible_value
@@ -124,9 +112,7 @@ class GenericSpreadsheet
124
112
 
125
113
  # returns the number of the first non-empty column
126
114
  def first_column(sheet=nil)
127
- if sheet == nil
128
- sheet = @default_sheet
129
- end
115
+ sheet ||= @default_sheet
130
116
  read_cells(sheet) unless @cells_read[sheet]
131
117
  if @first_column[sheet]
132
118
  return @first_column[sheet]
@@ -134,8 +120,7 @@ class GenericSpreadsheet
134
120
  impossible_value = 999_999 # more than a spreadsheet can hold
135
121
  result = impossible_value
136
122
  @cell[sheet].each_pair {|key,value|
137
- y,x = key # _to_string(key).split(',')
138
- x = x # .to_i
123
+ x = key.last.to_i # _to_string(key).split(',')
139
124
  result = [result, x].min if value
140
125
  } if @cell[sheet]
141
126
  result = nil if result == impossible_value
@@ -145,7 +130,7 @@ class GenericSpreadsheet
145
130
 
146
131
  # returns the number of the last non-empty column
147
132
  def last_column(sheet=nil)
148
- sheet = @default_sheet unless sheet
133
+ sheet ||= @default_sheet
149
134
  read_cells(sheet) unless @cells_read[sheet]
150
135
  if @last_column[sheet]
151
136
  return @last_column[sheet]
@@ -153,8 +138,7 @@ class GenericSpreadsheet
153
138
  impossible_value = 0
154
139
  result = impossible_value
155
140
  @cell[sheet].each_pair {|key,value|
156
- y,x = key # _to_string(key).split(',')
157
- x = x.to_i
141
+ x = key.last.to_i # _to_string(key).split(',')
158
142
  result = [result, x].max if value
159
143
  } if @cell[sheet]
160
144
  result = nil if result == impossible_value
@@ -166,7 +150,7 @@ class GenericSpreadsheet
166
150
  # you can add additional attributes with the prefix parameter like:
167
151
  # oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
168
152
  def to_yaml(prefix={}, from_row=nil, from_column=nil, to_row=nil, to_column=nil,sheet=nil)
169
- sheet = @default_sheet unless sheet
153
+ sheet ||= @default_sheet
170
154
  result = "--- \n"
171
155
  return '' unless first_row # empty result if there is no first_row in a sheet
172
156
 
@@ -181,7 +165,7 @@ class GenericSpreadsheet
181
165
  result << " col: #{col} \n"
182
166
  result << " celltype: #{self.celltype(row,col,sheet)} \n"
183
167
  if self.celltype(row,col,sheet) == :time
184
- result << " value: #{GenericSpreadsheet.integer_to_timestring( self.cell(row,col,sheet))} \n"
168
+ result << " value: #{Roo::GenericSpreadsheet.integer_to_timestring( self.cell(row,col,sheet))} \n"
185
169
  else
186
170
  result << " value: #{self.cell(row,col,sheet)} \n"
187
171
  end
@@ -193,152 +177,117 @@ class GenericSpreadsheet
193
177
 
194
178
  # write the current spreadsheet to stdout or into a file
195
179
  def to_csv(filename=nil,sheet=nil)
196
- sheet = @default_sheet unless sheet
180
+ sheet ||= @default_sheet
197
181
  if filename
198
- file = File.open(filename,"w") # do |file|
199
- write_csv_content(file,sheet)
200
- file.close
182
+ File.open(filename,"w") do |file|
183
+ write_csv_content(file,sheet)
184
+ end
185
+ return true
201
186
  else
202
- write_csv_content(STDOUT,sheet)
187
+ sio = StringIO.new
188
+ write_csv_content(sio,sheet)
189
+ sio.rewind
190
+ return sio.read
203
191
  end
204
- true
205
192
  end
206
193
 
207
194
  # returns a matrix object from the whole sheet or a rectangular area of a sheet
208
195
  def to_matrix(from_row=nil, from_column=nil, to_row=nil, to_column=nil,sheet=nil)
209
- sheet = @default_sheet unless sheet
210
- arr = []
211
- pos = 0
212
- return Matrix.rows([]) unless first_row
196
+ require 'matrix'
213
197
 
214
- (from_row||first_row(sheet)).upto(to_row||last_row(sheet)) do |row|
215
- line = []
216
- (from_column||first_column(sheet)).upto(to_column||last_column(sheet)) do |col|
198
+ sheet ||= @default_sheet
199
+ return Matrix.empty unless first_row
217
200
 
218
- line << cell(row,col)
201
+ Matrix.rows((from_row||first_row(sheet)).upto(to_row||last_row(sheet)).map do |row|
202
+ (from_column||first_column(sheet)).upto(to_column||last_column(sheet)).map do |col|
203
+ cell(row,col)
219
204
  end
220
- arr[pos] = line
221
- pos += 1
222
- end
223
- Matrix.rows(arr)
205
+ end)
224
206
  end
225
207
 
226
208
  # find a row either by row number or a condition
227
209
  # Caution: this works only within the default sheet -> set default_sheet before you call this method
228
210
  # (experimental. see examples in the test_roo.rb file)
229
211
  def find(*args) # :nodoc
230
- result_array = false
231
- args.each {|arg,val|
232
- if arg.class == Hash
233
- arg.each { |hkey,hval|
234
- if hkey == :array and hval == true
235
- result_array = true
236
- end
237
- }
238
- end
239
- }
240
- column_with = {}
241
- 1.upto(last_column) do |col|
242
- column_with[cell(@header_line,col)] = col
243
- end
244
- result = Array.new
212
+ options = (args.last.is_a?(Hash) ? args.pop : {})
213
+ result_array = options[:array]
214
+ header_for = Hash[1.upto(last_column).map do |col|
215
+ [col, cell(@header_line,col)]
216
+ end]
245
217
  #-- id
246
218
  if args[0].class == Fixnum
247
219
  rownum = args[0]
248
220
  if @header_line
249
- tmp = {}
221
+ [Hash[1.upto(self.row().size).map {|j|
222
+ [header_for.fetch(j), cell(rownum,j)]
223
+ }]]
250
224
  else
251
- tmp = []
252
- end
253
- 1.upto(self.row(rownum).size) {|j|
254
- x = ''
255
- column_with.each { |key,val|
256
- if val == j
257
- x = key
258
- end
225
+ self.row(rownum).size.times.map {|j|
226
+ cell(rownum,j + 1)
259
227
  }
260
- if @header_line
261
- tmp[x] = cell(rownum,j)
262
- else
263
- tmp[j-1] = cell(rownum,j)
264
- end
265
-
266
- }
267
- if @header_line
268
- result = [ tmp ]
269
- else
270
- result = tmp
271
228
  end
272
- #-- :all
229
+ #-- :all
273
230
  elsif args[0] == :all
274
- if args[1].class == Hash
275
- args[1].each {|key,val|
276
- if key == :conditions
277
- column_with = {}
278
- 1.upto(last_column) do |col|
279
- column_with[cell(@header_line,col)] = col
280
- end
281
- conditions = val
282
- first_row.upto(last_row) do |i|
283
- # are all conditions met?
284
- found = 1
285
- conditions.each { |key,val|
286
- if cell(i,column_with[key]) == val
287
- found *= 1
288
- else
289
- found *= 0
290
- end
291
- }
292
- if found > 0
293
- tmp = {}
294
- 1.upto(self.row(i).size) {|j|
295
- x = ''
296
- column_with.each { |key,val|
297
- if val == j
298
- x = key
299
- end
300
- }
301
- tmp[x] = cell(i,j)
302
- }
303
- if result_array
304
- result << self.row(i)
305
- else
306
- result << tmp
307
- end
308
- end
309
- end
310
- end # :conditions
311
- }
231
+ rows = first_row.upto(last_row)
232
+
233
+ # are all conditions met?
234
+ if (conditions = options[:conditions]) && !conditions.empty?
235
+ column_with = header_for.invert
236
+ rows = rows.select do |i|
237
+ conditions.all? { |key,val| cell(i,column_with[key]) == val }
238
+ end
239
+ end
240
+
241
+ rows.map do |i|
242
+ if result_array
243
+ self.row(i)
244
+ else
245
+ Hash[1.upto(self.row(i).size).map do |j|
246
+ [header_for.fetch(j), cell(i,j)]
247
+ end]
248
+ end
312
249
  end
313
250
  end
314
- result
315
251
  end
316
252
 
317
253
  # returns all values in this row as an array
318
254
  # row numbers are 1,2,3,... like in the spreadsheet
319
255
  def row(rownumber,sheet=nil)
320
- sheet = @default_sheet unless sheet
256
+ sheet ||= @default_sheet
321
257
  read_cells(sheet) unless @cells_read[sheet]
322
- result = []
323
- first_column(sheet).upto(last_column(sheet)) do |col|
324
- result << cell(rownumber,col,sheet)
258
+ first_column(sheet).upto(last_column(sheet)).map do |col|
259
+ cell(rownumber,col,sheet)
325
260
  end
326
- result
327
261
  end
328
262
 
329
263
  # returns all values in this column as an array
330
264
  # column numbers are 1,2,3,... like in the spreadsheet
331
265
  def column(columnnumber,sheet=nil)
332
266
  if columnnumber.class == String
333
- columnnumber = Excel.letter_to_number(columnnumber)
267
+ columnnumber = Roo::Excel.letter_to_number(columnnumber)
334
268
  end
335
- sheet = @default_sheet unless sheet
269
+ sheet ||= @default_sheet
336
270
  read_cells(sheet) unless @cells_read[sheet]
337
- result = []
338
- first_row(sheet).upto(last_row(sheet)) do |row|
339
- result << cell(row,columnnumber,sheet)
271
+ first_row(sheet).upto(last_row(sheet)).map do |row|
272
+ cell(row,columnnumber,sheet)
340
273
  end
341
- result
274
+ end
275
+
276
+ # set a cell to a certain value
277
+ # (this will not be saved back to the spreadsheet file!)
278
+ def set(row,col,value,sheet=nil) #:nodoc:
279
+ sheet ||= @default_sheet
280
+ read_cells(sheet) unless @cells_read[sheet]
281
+ row, col = normalize(row,col)
282
+ cell_type = case value
283
+ when Fixnum then :float
284
+ when String, Float then :string
285
+ else
286
+ raise ArgumentError, "Type for #{value} not set"
287
+ end
288
+
289
+ set_value(row,col,value,sheet)
290
+ set_type(row,col,cell_type,sheet)
342
291
  end
343
292
 
344
293
  # reopens and read a spreadsheet document
@@ -356,21 +305,12 @@ class GenericSpreadsheet
356
305
 
357
306
  # true if cell is empty
358
307
  def empty?(row, col, sheet=nil)
359
- sheet = @default_sheet unless sheet
360
- read_cells(sheet) unless @cells_read[sheet] or self.class == Excel
308
+ sheet ||= @default_sheet
309
+ read_cells(sheet) unless @cells_read[sheet] or self.class == Roo::Excel
361
310
  row,col = normalize(row,col)
362
- return true unless cell(row, col, sheet)
363
- return true if celltype(row, col, sheet) == :string && cell(row, col, sheet).empty?
364
- return true if row < first_row(sheet) || row > last_row(sheet) || col < first_column(sheet) || col > last_column(sheet)
365
- false
366
- end
367
-
368
- # recursively removes the current temporary directory
369
- # this is only needed if you work with zipped files or files via the web
370
- def remove_tmp
371
- if File.exists?(@tmpdir)
372
- FileUtils::rm_r(@tmpdir)
373
- end
311
+ contents = cell(row, col, sheet)
312
+ !contents || (celltype(row, col, sheet) == :string && contents.empty?) \
313
+ || (row < first_row(sheet) || row > last_row(sheet) || col < first_column(sheet) || col > last_column(sheet))
374
314
  end
375
315
 
376
316
  # returns information of the spreadsheet document and all sheets within
@@ -388,8 +328,8 @@ class GenericSpreadsheet
388
328
  else
389
329
  result << " First row: #{first_row}\n"
390
330
  result << " Last row: #{last_row}\n"
391
- result << " First column: #{GenericSpreadsheet.number_to_letter(first_column)}\n"
392
- result << " Last column: #{GenericSpreadsheet.number_to_letter(last_column)}"
331
+ result << " First column: #{Roo::GenericSpreadsheet.number_to_letter(first_column)}\n"
332
+ result << " Last column: #{Roo::GenericSpreadsheet.number_to_letter(last_column)}"
393
333
  end
394
334
  result << "\n" if sheet != sheets.last
395
335
  n += 1
@@ -399,7 +339,7 @@ class GenericSpreadsheet
399
339
 
400
340
  # returns an XML representation of all sheets of a spreadsheet file
401
341
  def to_xml
402
- builder = Nokogiri::XML::Builder.new do |xml|
342
+ Nokogiri::XML::Builder.new do |xml|
403
343
  xml.spreadsheet {
404
344
  self.sheets.each do |sheet|
405
345
  self.default_sheet = sheet
@@ -420,8 +360,7 @@ class GenericSpreadsheet
420
360
  }
421
361
  end
422
362
  }
423
- end
424
- return builder.to_xml
363
+ end.to_xml
425
364
  end
426
365
 
427
366
  # when a method like spreadsheet.a42 is called
@@ -430,12 +369,12 @@ class GenericSpreadsheet
430
369
  # #aa42 => #cell('aa',42)
431
370
  # #aa42('Sheet1') => #cell('aa',42,'Sheet1')
432
371
  if m =~ /^([a-z]+)(\d)$/
433
- col = GenericSpreadsheet.letter_to_number($1)
372
+ col = Roo::GenericSpreadsheet.letter_to_number($1)
434
373
  row = $2.to_i
435
- if args.size > 0
436
- return cell(row,col,args[0])
374
+ if args.empty?
375
+ cell(row,col)
437
376
  else
438
- return cell(row,col)
377
+ cell(row,col,args.first)
439
378
  end
440
379
  else
441
380
  super
@@ -448,7 +387,7 @@ class GenericSpreadsheet
448
387
  # [row, col, formula]
449
388
  def formulas(sheet=nil)
450
389
  theformulas = Array.new
451
- sheet = @default_sheet unless sheet
390
+ sheet ||= @default_sheet
452
391
  read_cells(sheet) unless @cells_read[sheet]
453
392
  return theformulas unless first_row(sheet) # if there is no first row then
454
393
  # there can't be formulas
@@ -463,14 +402,127 @@ class GenericSpreadsheet
463
402
  end
464
403
  =end
465
404
 
405
+
406
+
407
+ # FestivalBobcats fork changes begin here
408
+
409
+
410
+
411
+ # access different worksheets by calling spreadsheet.sheet(1)
412
+ # or spreadsheet.sheet('SHEETNAME')
413
+ def sheet(index,name=false)
414
+ @default_sheet = String === index ? index : self.sheets[index]
415
+ name ? [@default_sheet,self] : self
416
+ end
417
+
418
+ # iterate through all worksheets of a document
419
+ def each_with_pagename
420
+ self.sheets.each do |s|
421
+ yield sheet(s,true)
422
+ end
423
+ end
424
+
425
+ # by passing in headers as options, this method returns
426
+ # specific columns from your header assignment
427
+ # for example:
428
+ # xls.sheet('New Prices').parse(:upc => 'UPC', :price => 'Price') would return:
429
+ # [{:upc => 123456789012, :price => 35.42},..]
430
+
431
+ # the queries are matched with regex, so regex options can be passed in
432
+ # such as :price => '^(Cost|Price)'
433
+ # case insensitive by default
434
+
435
+
436
+ # by using the :header_search option, you can query for headers
437
+ # and return a hash of every row with the keys set to the header result
438
+ # for example:
439
+ # xls.sheet('New Prices').parse(:header_search => ['UPC*SKU','^Price*\sCost\s'])
440
+
441
+ # that example searches for a column titled either UPC or SKU and another
442
+ # column titled either Price or Cost (regex characters allowed)
443
+ # * is the wildcard character
444
+
445
+ # you can also pass in a :clean => true option to strip the sheet of
446
+ # odd unicode characters and white spaces around columns
447
+
448
+ def each(options={})
449
+ if options.empty?
450
+ 1.upto(last_row) do |line|
451
+ yield row(line)
452
+ end
453
+ else
454
+ if options[:clean]
455
+ options.delete(:clean)
456
+ @cleaned ||= {}
457
+ @cleaned[@default_sheet] || clean_sheet(@default_sheet)
458
+ end
459
+
460
+ if options[:header_search]
461
+ @headers = nil
462
+ @header_line = row_with(options[:header_search])
463
+ elsif [:first_row,true].include?(options[:headers])
464
+ @headers = []
465
+ row(first_row).each_with_index {|x,i| @headers << [x,i + 1]}
466
+ else
467
+ set_headers(options)
468
+ end
469
+
470
+ headers = @headers ||
471
+ Hash[(first_column..last_column).map do |col|
472
+ [cell(@header_line,col), col]
473
+ end]
474
+
475
+ @header_line.upto(last_row) do |line|
476
+ yield(Hash[headers.map {|k,v| [k,cell(line,v)]}])
477
+ end
478
+ end
479
+ end
480
+
481
+ def parse(options={})
482
+ ary = []
483
+ if block_given?
484
+ each(options) {|row| ary << yield(row)}
485
+ else
486
+ each(options) {|row| ary << row}
487
+ end
488
+ ary
489
+ end
490
+
491
+ def row_with(query,return_headers=false)
492
+ query.map! {|x| Array(x.split('*'))}
493
+ line_no = 0
494
+ each do |row|
495
+ line_no += 1
496
+ # makes sure headers is the first part of wildcard search for priority
497
+ # ex. if UPC and SKU exist for UPC*SKU search, UPC takes the cake
498
+ headers = query.map do |q|
499
+ q.map {|i| row.grep(/#{i}/i)[0]}.compact[0]
500
+ end.compact
501
+
502
+ if headers.length == query.length
503
+ @header_line = line_no
504
+ return return_headers ? headers : line_no
505
+ elsif line_no > 100
506
+ raise "Couldn't find header row."
507
+ end
508
+ end
509
+ end
510
+
511
+ # this method lets you find the worksheet with the most data
512
+ def longest_sheet
513
+ sheet(@workbook.worksheets.inject {|m,o|
514
+ o.row_count > m.row_count ? o : m
515
+ }.name)
516
+ end
517
+
466
518
  protected
467
519
 
468
- def file_type_check(filename, ext, name, packed=nil)
520
+ def file_type_check(filename, ext, name, warning_level, packed=nil)
469
521
  new_expression = {
470
- '.ods' => 'Openoffice.new',
471
- '.xls' => 'Excel.new',
472
- '.xlsx' => 'Excelx.new',
473
- '.csv' => 'Csv.new',
522
+ '.ods' => 'Roo::Openoffice.new',
523
+ '.xls' => 'Roo::Excel.new',
524
+ '.xlsx' => 'Roo::Excelx.new',
525
+ '.csv' => 'Roo::Csv.new',
474
526
  }
475
527
  if packed == :zip
476
528
  # lalala.ods.zip => lalala.ods
@@ -480,12 +532,12 @@ class GenericSpreadsheet
480
532
  end
481
533
  case ext
482
534
  when '.ods', '.xls', '.xlsx', '.csv'
483
- correct_class = "use #{new_expression[ext]} to handle #{ext} spreadsheet files"
535
+ correct_class = "use #{new_expression[ext]} to handle #{ext} spreadsheet files. This has #{File.extname(filename).downcase}"
484
536
  else
485
537
  raise "unknown file type: #{ext}"
486
538
  end
487
539
  if File.extname(filename).downcase != ext
488
- case @file_warning
540
+ case warning_level
489
541
  when :error
490
542
  warn correct_class
491
543
  raise TypeError, "#{filename} is not #{name} file"
@@ -495,7 +547,7 @@ class GenericSpreadsheet
495
547
  when :ignore
496
548
  # ignore
497
549
  else
498
- raise "#{@file_warning} illegal state of file_warning"
550
+ raise "#{warning_level} illegal state of file_warning"
499
551
  end
500
552
  end
501
553
  end
@@ -506,9 +558,7 @@ class GenericSpreadsheet
506
558
  # Zugriff mit numerischen Keys schneller ist.
507
559
  def key_to_num(str)
508
560
  r,c = str.split(',')
509
- r = r.to_i
510
- c = c.to_i
511
- [r,c]
561
+ [r.to_i,c.to_i]
512
562
  end
513
563
 
514
564
  # see: key_to_num
@@ -518,6 +568,47 @@ class GenericSpreadsheet
518
568
 
519
569
  private
520
570
 
571
+ def make_tmpdir(tmp_root = nil)
572
+ Dir.mktmpdir(TEMP_PREFIX, tmp_root || ENV['ROO_TMP']) do |tmpdir|
573
+ yield tmpdir
574
+ end
575
+ end
576
+
577
+ def clean_sheet(sheet)
578
+ read_cells(sheet) unless @cells_read[sheet]
579
+ @cell[sheet].each_pair do |coord,value|
580
+ if String === value
581
+ @cell[sheet][coord] = sanitize_value(value)
582
+ end
583
+ end
584
+ @cleaned[sheet] = true
585
+ end
586
+
587
+ def sanitize_value(v)
588
+ v.strip.unpack('U*').select {|b| b < 127}.pack('U*')
589
+ end
590
+
591
+ def set_headers(hash={})
592
+ # try to find header row with all values or give an error
593
+ # then create new hash by indexing strings and keeping integers for header array
594
+ @headers = row_with(hash.values,true)
595
+ @headers = Hash[hash.keys.zip(@headers.map {|x| header_index(x)})]
596
+ end
597
+
598
+ def header_index(query)
599
+ row(@header_line).index(query) + first_column
600
+ end
601
+
602
+ def set_value(row,col,value,sheet=nil)
603
+ sheet ||= @default_sheet
604
+ @cell[sheet][[row,col]] = value
605
+ end
606
+
607
+ def set_type(row,col,type,sheet=nil)
608
+ sheet ||= @default_sheet
609
+ @cell_type[sheet][[row,col]] = type
610
+ end
611
+
521
612
  # converts cell coordinate to numeric values of row,col
522
613
  def normalize(row,col)
523
614
  if row.class == String
@@ -530,43 +621,57 @@ class GenericSpreadsheet
530
621
  end
531
622
  end
532
623
  if col.class == String
533
- col = GenericSpreadsheet.letter_to_number(col)
624
+ col = Roo::GenericSpreadsheet.letter_to_number(col)
534
625
  end
535
626
  return row,col
536
627
  end
537
628
 
538
- def open_from_uri(uri)
629
+ def uri?(filename)
630
+ filename.start_with?("http://", "https://")
631
+ end
632
+
633
+ def open_from_uri(uri, tmpdir)
539
634
  require 'open-uri'
540
635
  response = ''
541
636
  begin
542
637
  open(uri, "User-Agent" => "Ruby/#{RUBY_VERSION}") { |net|
543
638
  response = net.read
544
- tempfilename = File.join(@tmpdir, File.basename(uri))
545
- f = File.open(tempfilename,"wb")
546
- f.write(response)
547
- f.close
639
+ tempfilename = File.join(tmpdir, File.basename(uri))
640
+ File.open(tempfilename,"wb") do |file|
641
+ file.write(response)
642
+ end
548
643
  }
549
644
  rescue OpenURI::HTTPError
550
645
  raise "could not open #{uri}"
551
646
  end
552
- File.join(@tmpdir, File.basename(uri))
647
+ File.join(tmpdir, File.basename(uri))
553
648
  end
554
-
555
- def open_from_stream(stream)
556
- tempfilename = File.join(@tmpdir, "spreadsheet")
557
- f = File.open(tempfilename,"wb")
558
- f.write(stream[7..-1])
559
- f.close
560
- File.join(@tmpdir, "spreadsheet")
649
+
650
+ def open_from_stream(stream, tmpdir)
651
+ tempfilename = File.join(tmpdir, "spreadsheet")
652
+ File.open(tempfilename,"wb") do |file|
653
+ file.write(stream[7..-1])
654
+ end
655
+ File.join(tmpdir, "spreadsheet")
561
656
  end
562
657
 
658
+ 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}
659
+
563
660
  # convert a number to something like 'AB' (1 => 'A', 2 => 'B', ...)
564
661
  def self.number_to_letter(n)
565
662
  letters=""
566
- while n > 0
567
- num = n%26
568
- letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num-1,1] + letters
569
- n = n.div(26)
663
+ if n > 26
664
+ while n % 26 == 0 && n != 0
665
+ letters << 'Z'
666
+ n = (n - 26) / 26
667
+ end
668
+ while n > 0
669
+ num = n%26
670
+ letters = LETTERS[num-1] + letters
671
+ n = (n / 26)
672
+ end
673
+ else
674
+ letters = LETTERS[n-1]
570
675
  end
571
676
  letters
572
677
  end
@@ -576,7 +681,7 @@ class GenericSpreadsheet
576
681
  result = 0
577
682
  while letters && letters.length > 0
578
683
  character = letters[0,1].upcase
579
- num = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".index(character)
684
+ num = LETTERS.index(character)
580
685
  raise ArgumentError, "invalid column character '#{letters[0,1]}'" if num == nil
581
686
  num += 1
582
687
  result = result * 26 + num
@@ -585,106 +690,105 @@ class GenericSpreadsheet
585
690
  result
586
691
  end
587
692
 
588
- def unzip(filename)
589
- ret = nil
693
+ def unzip(filename, tmpdir)
590
694
  Zip::ZipFile.open(filename) do |zip|
591
- ret = process_zipfile_packed zip
695
+ process_zipfile_packed(zip, tmpdir)
592
696
  end
593
- ret
594
697
  end
595
698
 
596
699
  # check if default_sheet was set and exists in sheets-array
597
- def check_default_sheet
598
- sheet_found = false
599
- raise ArgumentError, "Error: default_sheet not set" if @default_sheet == nil
600
- if sheets.index(@default_sheet)
601
- sheet_found = true
602
- end
603
- if ! sheet_found
604
- raise RangeError, "sheet '#{@default_sheet}' not found"
700
+ def validate_sheet!(sheet)
701
+ case sheet
702
+ when nil
703
+ raise ArgumentError, "Error: sheet 'nil' not valid"
704
+ when Fixnum
705
+ self.sheets.fetch(sheet-1) do
706
+ raise RangeError, "sheet index #{sheet} not found"
707
+ end
708
+ when String
709
+ if !sheets.include? sheet
710
+ raise RangeError, "sheet '#{sheet}' not found"
711
+ end
712
+ else
713
+ raise TypeError, "not a valid sheet type: #{sheet.inspect}"
605
714
  end
606
715
  end
607
716
 
608
- def process_zipfile_packed(zip, path='')
609
- ret=nil
717
+ def process_zipfile_packed(zip, tmpdir, path='')
610
718
  if zip.file.file? path
611
719
  # extract and return filename
612
- file = File.open(File.join(@tmpdir, path),"wb")
613
- file.write(zip.read(path))
614
- file.close
615
- return File.join(@tmpdir, path)
616
- else
617
- unless path.empty?
618
- path += '/'
720
+ File.open(File.join(tmpdir, path),"wb") do |file|
721
+ file.write(zip.read(path))
619
722
  end
723
+ File.join(tmpdir, path)
724
+ else
725
+ ret=nil
726
+ path += '/' unless path.empty?
620
727
  zip.dir.foreach(path) do |filename|
621
- ret = process_zipfile_packed(zip, path + filename)
728
+ ret = process_zipfile_packed(zip, tmpdir, path + filename)
622
729
  end
730
+ ret
623
731
  end
624
- ret
625
732
  end
626
733
 
627
734
  # Write all cells to the csv file. File can be a filename or nil. If the this
628
735
  # parameter is nil the output goes to STDOUT
629
736
  def write_csv_content(file=nil,sheet=nil)
630
- file = STDOUT unless file
737
+ file ||= STDOUT
631
738
  if first_row(sheet) # sheet is not empty
632
739
  1.upto(last_row(sheet)) do |row|
633
740
  1.upto(last_column(sheet)) do |col|
634
741
  file.print(",") if col > 1
635
- onecell = cell(row,col,sheet)
636
- onecelltype = celltype(row,col,sheet)
637
- file.print one_cell_output(onecelltype,onecell,empty?(row,col,sheet))
742
+ file.print cell_to_csv(row,col,sheet)
638
743
  end
639
744
  file.print("\n")
640
745
  end # sheet not empty
641
746
  end
642
747
  end
643
748
 
644
- # The content of a cell in the csv output
645
- def one_cell_output(onecelltype, onecell, empty)
646
- str = ""
647
- if empty
648
- str += ''
749
+ # The content of a cell in the csv output
750
+ def cell_to_csv(row, col, sheet)
751
+ if empty?(row,col,sheet)
752
+ ''
649
753
  else
650
- case onecelltype
754
+ onecell = cell(row,col,sheet)
755
+
756
+ case celltype(row,col,sheet)
651
757
  when :string
652
758
  unless onecell.empty?
653
- one = onecell.gsub(/"/,'""')
654
- str << ('"'+one+'"')
759
+ %{"#{onecell.gsub(/"/,'""')}"}
655
760
  end
656
761
  when :float, :percentage
657
762
  if onecell == onecell.to_i
658
- str << onecell.to_i.to_s
763
+ onecell.to_i.to_s
659
764
  else
660
- str << onecell.to_s
765
+ onecell.to_s
661
766
  end
662
767
  when :formula
663
- if onecell.class == String
768
+ case onecell
769
+ when String
664
770
  unless onecell.empty?
665
- one = onecell.gsub(/"/,'""')
666
- str << '"'+one+'"'
771
+ %{"#{onecell.gsub(/"/,'""')}"}
667
772
  end
668
- elsif onecell.class == Float
773
+ when Float
669
774
  if onecell == onecell.to_i
670
- str << onecell.to_i.to_s
775
+ onecell.to_i.to_s
671
776
  else
672
- str << onecell.to_s
777
+ onecell.to_s
673
778
  end
779
+ when DateTime
780
+ onecell.to_s
674
781
  else
675
- raise "unhandled onecell-class "+onecell.class.to_s
782
+ raise "unhandled onecell-class #{onecell.class}"
676
783
  end
677
- when :date
678
- str << onecell.to_s
784
+ when :date, :datetime
785
+ onecell.to_s
679
786
  when :time
680
- str << GenericSpreadsheet.integer_to_timestring(onecell)
681
- when :datetime
682
- str << onecell.to_s
787
+ Roo::GenericSpreadsheet.integer_to_timestring(onecell)
683
788
  else
684
- raise "unhandled celltype "+onecelltype.to_s
685
- end
789
+ raise "unhandled celltype #{celltype(row,col,sheet)}"
790
+ end || ""
686
791
  end
687
- str
688
792
  end
689
793
 
690
794
  # converts an integer value to a time string like '02:05:06'
@@ -696,5 +800,4 @@ class GenericSpreadsheet
696
800
  s = content
697
801
  sprintf("%02d:%02d:%02d",h,m,s)
698
802
  end
699
-
700
803
  end