roo 1.10.1 → 1.10.2

Sign up to get free protection for your applications and to get access to all the features.
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