thunderboltlabs-rubyXL 1.2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +197 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/.DS_Store +0 -0
- data/lib/rubyXL/Hash.rb +60 -0
- data/lib/rubyXL/cell.rb +461 -0
- data/lib/rubyXL/color.rb +14 -0
- data/lib/rubyXL/parser.rb +471 -0
- data/lib/rubyXL/private_class.rb +265 -0
- data/lib/rubyXL/workbook.rb +450 -0
- data/lib/rubyXL/worksheet.rb +1493 -0
- data/lib/rubyXL/writer/app_writer.rb +62 -0
- data/lib/rubyXL/writer/calc_chain_writer.rb +33 -0
- data/lib/rubyXL/writer/content_types_writer.rb +77 -0
- data/lib/rubyXL/writer/core_writer.rb +51 -0
- data/lib/rubyXL/writer/root_rels_writer.rb +25 -0
- data/lib/rubyXL/writer/shared_strings_writer.rb +30 -0
- data/lib/rubyXL/writer/styles_writer.rb +407 -0
- data/lib/rubyXL/writer/theme_writer.rb +343 -0
- data/lib/rubyXL/writer/workbook_rels_writer.rb +59 -0
- data/lib/rubyXL/writer/workbook_writer.rb +77 -0
- data/lib/rubyXL/writer/worksheet_writer.rb +230 -0
- data/lib/rubyXL/zip.rb +20 -0
- data/lib/rubyXL.rb +10 -0
- data/rubyXL.gemspec +92 -0
- data/spec/lib/cell_spec.rb +385 -0
- data/spec/lib/color_spec.rb +14 -0
- data/spec/lib/hash_spec.rb +28 -0
- data/spec/lib/parser_spec.rb +66 -0
- data/spec/lib/workbook_spec.rb +51 -0
- data/spec/lib/worksheet_spec.rb +1782 -0
- metadata +179 -0
@@ -0,0 +1,450 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','content_types_writer'))
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','root_rels_writer'))
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','app_writer'))
|
4
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','core_writer'))
|
5
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','theme_writer'))
|
6
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','workbook_rels_writer'))
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','workbook_writer'))
|
8
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','styles_writer'))
|
9
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','shared_strings_writer'))
|
10
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'writer','worksheet_writer'))
|
11
|
+
require 'rubyXL/zip'
|
12
|
+
require 'date'
|
13
|
+
|
14
|
+
module RubyXL
|
15
|
+
class Workbook
|
16
|
+
include Enumerable
|
17
|
+
attr_accessor :worksheets, :filepath, :creator, :modifier, :created_at,
|
18
|
+
:modified_at, :company, :application, :appversion, :num_fmts, :num_fmts_hash, :fonts, :fills,
|
19
|
+
:borders, :cell_xfs, :cell_style_xfs, :cell_styles, :shared_strings, :calc_chain,
|
20
|
+
:num_strings, :size, :date1904, :external_links, :style_corrector, :drawings,
|
21
|
+
:worksheet_rels, :printer_settings, :macros, :colors, :shared_strings_XML, :defined_names, :column_lookup_hash
|
22
|
+
|
23
|
+
|
24
|
+
APPLICATION = 'Microsoft Macintosh Excel'
|
25
|
+
APPVERSION = '12.0000'
|
26
|
+
SHEET_NAME = 'Sheet1'
|
27
|
+
def initialize(worksheets=[], filepath=nil, creator=nil, modifier=nil, created_at=nil,
|
28
|
+
company='', application=APPLICATION,
|
29
|
+
appversion=APPVERSION, date1904=0)
|
30
|
+
if worksheets.nil? || worksheets.empty?
|
31
|
+
@worksheets = [Worksheet.new(self,SHEET_NAME)]
|
32
|
+
else
|
33
|
+
@worksheets = worksheets
|
34
|
+
end
|
35
|
+
@filepath = filepath
|
36
|
+
@creator = creator
|
37
|
+
@modifier = modifier
|
38
|
+
@company = company
|
39
|
+
@application = application
|
40
|
+
@appversion = appversion
|
41
|
+
@num_fmts = nil
|
42
|
+
@num_fmts_hash = nil
|
43
|
+
@fonts = nil
|
44
|
+
@fills = nil
|
45
|
+
@borders = nil
|
46
|
+
@cell_xfs = nil
|
47
|
+
@cell_style_xfs = nil
|
48
|
+
@cell_styles = nil
|
49
|
+
@shared_strings = nil
|
50
|
+
@calc_chain = nil #unnecessary?
|
51
|
+
@num_strings = 0 #num strings total
|
52
|
+
@size = 0 #num strings in shared_strings array
|
53
|
+
@date1904 = date1904 > 0
|
54
|
+
@external_links = nil
|
55
|
+
@style_corrector = nil
|
56
|
+
@drawings = nil
|
57
|
+
@worksheet_rels = nil
|
58
|
+
@printer_settings = nil
|
59
|
+
@macros = nil
|
60
|
+
@colors = nil
|
61
|
+
@shared_strings_XML = nil
|
62
|
+
@defined_names = nil
|
63
|
+
@column_lookup_hash = {}
|
64
|
+
|
65
|
+
begin
|
66
|
+
@created_at = DateTime.parse(created_at).strftime('%Y-%m-%dT%TZ')
|
67
|
+
rescue
|
68
|
+
t = Time.now
|
69
|
+
@created_at = t.strftime('%Y-%m-%dT%TZ')
|
70
|
+
end
|
71
|
+
@modified_at = @created_at
|
72
|
+
|
73
|
+
fill_styles()
|
74
|
+
fill_shared_strings()
|
75
|
+
end
|
76
|
+
|
77
|
+
# allows easier access to worksheets
|
78
|
+
def [](worksheet)
|
79
|
+
return worksheets[worksheet]
|
80
|
+
end
|
81
|
+
|
82
|
+
def each
|
83
|
+
worksheets.each{|i| yield i}
|
84
|
+
end
|
85
|
+
|
86
|
+
def num_fmts_by_id
|
87
|
+
|
88
|
+
return @num_fmts_hash unless @num_fmts_hash.nil?
|
89
|
+
if num_fmts
|
90
|
+
@num_fmts_hash={}
|
91
|
+
num_fmts[:numFmt].each do |num_fmt|
|
92
|
+
@num_fmts_hash[num_fmt[:attributes][:numFmtId]]=num_fmt
|
93
|
+
end
|
94
|
+
@num_fmts_hash
|
95
|
+
else
|
96
|
+
{}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#filepath of xlsx file (including file itself)
|
101
|
+
def write(filepath=@filepath)
|
102
|
+
validate_before_write
|
103
|
+
if !(filepath =~ /(.+)\.xls(x|m)/)
|
104
|
+
raise "Only xlsx and xlsm files are supported. Unsupported type for file: #{filepath}"
|
105
|
+
end
|
106
|
+
dirpath = ''
|
107
|
+
extension = 'xls'
|
108
|
+
if(filepath =~ /((.|\s)*)\.xls(x|m)$/)
|
109
|
+
dirpath = $1.to_s()
|
110
|
+
extension += $3.to_s
|
111
|
+
end
|
112
|
+
filename = ''
|
113
|
+
if(filepath =~ /\/((.|\s)*)\/((.|\s)*)\.xls(x|m)$/)
|
114
|
+
filename = $3.to_s()
|
115
|
+
end
|
116
|
+
|
117
|
+
#creates zip file, writes each type of file to zip folder
|
118
|
+
#zips package and renames it to xlsx.
|
119
|
+
zippath = File.join(dirpath, filename + '.zip')
|
120
|
+
File.unlink(zippath) if File.exists?(zippath)
|
121
|
+
FileUtils.mkdir_p(File.join(dirpath,zippath))
|
122
|
+
Zip::ZipFile.open(zippath, Zip::ZipFile::CREATE) do |zipfile|
|
123
|
+
writer = Writer::ContentTypesWriter.new(dirpath,self)
|
124
|
+
zipfile.get_output_stream('[Content_Types].xml') {|f| f.puts(writer.write())}
|
125
|
+
|
126
|
+
writer = Writer::RootRelsWriter.new(dirpath,self)
|
127
|
+
zipfile.get_output_stream(File.join('_rels','.rels')) {|f| f.puts(writer.write())}
|
128
|
+
|
129
|
+
writer = Writer::AppWriter.new(dirpath,self)
|
130
|
+
zipfile.get_output_stream(File.join('docProps','app.xml')) {|f| f.puts(writer.write())}
|
131
|
+
|
132
|
+
writer = Writer::CoreWriter.new(dirpath,self)
|
133
|
+
zipfile.get_output_stream(File.join('docProps','core.xml')) {|f| f.puts(writer.write())}
|
134
|
+
|
135
|
+
writer = Writer::ThemeWriter.new(dirpath,self)
|
136
|
+
zipfile.get_output_stream(File.join('xl','theme','theme1.xml')) {|f| f.puts(writer.write())}
|
137
|
+
|
138
|
+
writer = Writer::WorkbookRelsWriter.new(dirpath,self)
|
139
|
+
zipfile.get_output_stream(File.join('xl','_rels','workbook.xml.rels')) {|f| f.puts(writer.write())}
|
140
|
+
|
141
|
+
writer = Writer::WorkbookWriter.new(dirpath,self)
|
142
|
+
zipfile.get_output_stream(File.join('xl','workbook.xml')) {|f| f.puts(writer.write())}
|
143
|
+
|
144
|
+
writer = Writer::StylesWriter.new(dirpath,self)
|
145
|
+
zipfile.get_output_stream(File.join('xl','styles.xml')) {|f| f.puts(writer.write())}
|
146
|
+
|
147
|
+
unless @shared_strings.nil?
|
148
|
+
writer = Writer::SharedStringsWriter.new(dirpath,self)
|
149
|
+
zipfile.get_output_stream(File.join('xl','sharedStrings.xml')) {|f| f.puts(writer.write())}
|
150
|
+
end
|
151
|
+
|
152
|
+
#preserves external links (exactly, no modification allowed)
|
153
|
+
unless @external_links.nil?
|
154
|
+
#-1 because of rels
|
155
|
+
1.upto(@external_links.size-1) do |i|
|
156
|
+
zipfile.get_output_stream(
|
157
|
+
File.join('xl','externalLinks',"externalLink#{i}.xml")) {|f|
|
158
|
+
f.puts(@external_links[i])
|
159
|
+
}
|
160
|
+
end
|
161
|
+
@external_links['rels'].each_index do |i|
|
162
|
+
unless @external_links['rels'][i].nil?
|
163
|
+
zipfile.get_output_stream(
|
164
|
+
File.join('xl','externalLinks','_rels',"externalLink#{i}.xml.rels")) {|f|
|
165
|
+
f.puts(@external_links['rels'][i])
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
#preserves drawings (exactly, no modification allowed)
|
172
|
+
unless @drawings.nil?
|
173
|
+
1.upto(@drawings.size) do |i|
|
174
|
+
zipfile.get_output_stream(
|
175
|
+
File.join('xl','drawings',"vmlDrawing#{i}.vml")) {|f|
|
176
|
+
f.puts(@drawings[i])
|
177
|
+
}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
unless @printer_settings.nil?
|
182
|
+
1.upto(@printer_settings.size) do |i|
|
183
|
+
zipfile.get_output_stream(
|
184
|
+
File.join('xl','printerSettings',"printerSettings#{i}.bin")) {|f|
|
185
|
+
f.puts(@printer_settings[i])
|
186
|
+
}
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
unless @worksheet_rels.nil?
|
191
|
+
1.upto(@worksheet_rels.size) do |i|
|
192
|
+
zipfile.get_output_stream(
|
193
|
+
File.join('xl','worksheets','_rels',"sheet#{i}.xml.rels")) {|f|
|
194
|
+
f.puts(@worksheet_rels[i])
|
195
|
+
}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
unless @macros.nil?
|
200
|
+
zipfile.get_output_stream(File.join('xl','vbaProject.bin')) {|f| f.puts(@macros)}
|
201
|
+
end
|
202
|
+
|
203
|
+
@worksheets.each_with_index do |sheet,i|
|
204
|
+
writer = Writer::WorksheetWriter.new(dirpath,self,i)
|
205
|
+
zipfile.get_output_stream(File.join('xl','worksheets',"sheet#{i+1}.xml")) {|f| f.puts(writer.write())}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
FileUtils.cp(zippath,File.join(dirpath,filename+".#{extension}"))
|
210
|
+
FileUtils.cp(File.join(dirpath,filename+".#{extension}"),filepath)
|
211
|
+
if File.exist?(filepath)
|
212
|
+
FileUtils.rm_rf(dirpath)
|
213
|
+
end
|
214
|
+
return filepath
|
215
|
+
end
|
216
|
+
|
217
|
+
def date_to_num(date)
|
218
|
+
return nil if date.nil?
|
219
|
+
if @date1904
|
220
|
+
compare_date = DateTime.parse('December 31, 1903')
|
221
|
+
else
|
222
|
+
compare_date = DateTime.parse('December 31, 1899')
|
223
|
+
end
|
224
|
+
# add one day to compare date for erroneous 1900 leap year compatibility
|
225
|
+
date.ajd + 1 - compare_date.ajd
|
226
|
+
end
|
227
|
+
|
228
|
+
def num_to_date(num)
|
229
|
+
return nil if num.nil?
|
230
|
+
if @date1904
|
231
|
+
compare_date = DateTime.parse('December 31, 1903')
|
232
|
+
else
|
233
|
+
compare_date = DateTime.parse('December 31, 1899')
|
234
|
+
end
|
235
|
+
# subtract one day to compare date for erroneous 1900 leap year compatibility
|
236
|
+
compare_date - 1 + num
|
237
|
+
end
|
238
|
+
|
239
|
+
def date_num_fmt?(num_fmt)
|
240
|
+
@num_fmt_date_hash ||= {}
|
241
|
+
if @num_fmt_date_hash[num_fmt].nil?
|
242
|
+
@num_fmt_date_hash[num_fmt] = is_date_format?(num_fmt)
|
243
|
+
end
|
244
|
+
return @num_fmt_date_hash[num_fmt]
|
245
|
+
end
|
246
|
+
|
247
|
+
def is_date_format?(num_fmt)
|
248
|
+
skip_chars = ['$', '-', '+', '/', '(', ')', ':', ' ']
|
249
|
+
num_chars = ['0', '#', '?']
|
250
|
+
non_date_formats = ['0.00E+00', '##0.0E+0', 'General', 'GENERAL', 'general', '@']
|
251
|
+
date_chars = ['y','m','d','h','s']
|
252
|
+
|
253
|
+
state = 0
|
254
|
+
s = ''
|
255
|
+
num_fmt.split(//).each do |c|
|
256
|
+
if state == 0
|
257
|
+
if c == '"'
|
258
|
+
state = 1
|
259
|
+
elsif ['\\', '_', '*'].include?(c)
|
260
|
+
state = 2
|
261
|
+
elsif skip_chars.include?(c)
|
262
|
+
next
|
263
|
+
else
|
264
|
+
s << c
|
265
|
+
end
|
266
|
+
elsif state == 1
|
267
|
+
if c == '"'
|
268
|
+
state = 0
|
269
|
+
end
|
270
|
+
elsif state == 2
|
271
|
+
state = 0
|
272
|
+
end
|
273
|
+
end
|
274
|
+
s.gsub!(/\[[^\]]*\]/, '')
|
275
|
+
if non_date_formats.include?(s)
|
276
|
+
return false
|
277
|
+
end
|
278
|
+
separator = ';'
|
279
|
+
got_sep = 0
|
280
|
+
date_count = 0
|
281
|
+
num_count = 0
|
282
|
+
s.split(//).each do |c|
|
283
|
+
if date_chars.include?(c)
|
284
|
+
date_count += 1
|
285
|
+
elsif num_chars.include?(c)
|
286
|
+
num_count += 1
|
287
|
+
elsif c == separator
|
288
|
+
got_sep = 1
|
289
|
+
end
|
290
|
+
end
|
291
|
+
if date_count > 0 && num_count == 0
|
292
|
+
return true
|
293
|
+
elsif num_count > 0 && date_count == 0
|
294
|
+
return false
|
295
|
+
elsif date_count
|
296
|
+
# ambiguous result
|
297
|
+
elsif got_sep == 0
|
298
|
+
# constant result
|
299
|
+
end
|
300
|
+
return date_count > num_count
|
301
|
+
end
|
302
|
+
|
303
|
+
#gets style object from style array given index
|
304
|
+
def get_style(style_index)
|
305
|
+
if !@cell_xfs[:xf].is_a?Array
|
306
|
+
@cell_xfs[:xf] = [@cell_xfs[:xf]]
|
307
|
+
end
|
308
|
+
|
309
|
+
xf_obj = @cell_xfs[:xf]
|
310
|
+
if xf_obj.is_a?Array
|
311
|
+
xf_obj = xf_obj[Integer(style_index)]
|
312
|
+
end
|
313
|
+
xf_obj
|
314
|
+
end
|
315
|
+
|
316
|
+
#gets attributes of above style object
|
317
|
+
#necessary because can take the form of hash or array,
|
318
|
+
#based on odd behavior of Nokogiri
|
319
|
+
def get_style_attributes(xf_obj)
|
320
|
+
if xf_obj.is_a?Array
|
321
|
+
xf = xf_obj[1]
|
322
|
+
else
|
323
|
+
xf = xf_obj[:attributes]
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def get_fill_color(xf_attributes)
|
328
|
+
if @fills[xf_attributes[:fillId]].nil? || @fills[xf_attributes[:fillId]][:fill].nil? || @fills[xf_attributes[:fillId]][:fill][:patternFill].nil? || @fills[xf_attributes[:fillId]][:fill][:patternFill][:fgColor].nil?
|
329
|
+
'ffffff' #white
|
330
|
+
else
|
331
|
+
@fills[xf_attributes[:fillId]][:fill][:patternFill][:fgColor][:attributes][:rgb]
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
# Do not change. Excel requires that some of these styles be default,
|
339
|
+
# and will simply assume that the 0 and 1 indexed fonts are the default values.
|
340
|
+
def fill_styles()
|
341
|
+
@fonts = {
|
342
|
+
'0' => {
|
343
|
+
:font => {
|
344
|
+
:sz => { :attributes => { :val => 10 } },
|
345
|
+
:name => { :attributes => { :val => "Verdana" } }
|
346
|
+
},
|
347
|
+
:count=>1
|
348
|
+
},
|
349
|
+
'1' => {
|
350
|
+
:font => {
|
351
|
+
:sz => { :attributes => { :val => 8 } },
|
352
|
+
:name => { :attributes => { :val => "Verdana" } }
|
353
|
+
},
|
354
|
+
:count=>0
|
355
|
+
}
|
356
|
+
}
|
357
|
+
|
358
|
+
@fills = {
|
359
|
+
'0' => {
|
360
|
+
:fill => {
|
361
|
+
:patternFill => { :attributes => { :patternType => "none" } }
|
362
|
+
},
|
363
|
+
:count=>1} ,
|
364
|
+
'1' => {
|
365
|
+
:fill => {
|
366
|
+
:patternFill => { :attributes => { :patternType => "gray125" } }
|
367
|
+
},
|
368
|
+
:count=>0
|
369
|
+
}
|
370
|
+
}
|
371
|
+
|
372
|
+
@borders = {
|
373
|
+
'0' => {
|
374
|
+
:border => {
|
375
|
+
:left => { },
|
376
|
+
:right => { },
|
377
|
+
:top => { },
|
378
|
+
:bottom => { },
|
379
|
+
:diagonal => { }
|
380
|
+
},
|
381
|
+
:count => 1 #count = how many styles reference it
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
@cell_style_xfs = {
|
386
|
+
:attributes => {
|
387
|
+
:count => 1
|
388
|
+
},
|
389
|
+
:xf => {
|
390
|
+
:attributes => { :numFmtId => 0, :fontId => 0, :fillId => 0, :borderId => 0 }
|
391
|
+
}
|
392
|
+
}
|
393
|
+
@cell_xfs = {
|
394
|
+
:attributes => {
|
395
|
+
:count => 1
|
396
|
+
},
|
397
|
+
:xf => {
|
398
|
+
:attributes => { :numFmtId => 0, :fontId => 0, :fillId => 0, :borderId => 0, :xfId => 0 }
|
399
|
+
}
|
400
|
+
}
|
401
|
+
@cell_styles = {
|
402
|
+
:cellStyle => {
|
403
|
+
:attributes => { :builtinId=>0, :name=>"Normal", :xfId=>0 }
|
404
|
+
},
|
405
|
+
:attributes => { :count => 1 }
|
406
|
+
}
|
407
|
+
end
|
408
|
+
|
409
|
+
|
410
|
+
#fills shared strings hash, contains each unique string
|
411
|
+
def fill_shared_strings()
|
412
|
+
if @shared_strings.nil?
|
413
|
+
string_hash = {}
|
414
|
+
string_index = 0
|
415
|
+
@num_strings = 0
|
416
|
+
#fill hash for shared strings
|
417
|
+
@worksheets.each do |sheet|
|
418
|
+
unless sheet.nil?
|
419
|
+
sheet.sheet_data.each do |row|
|
420
|
+
row.each do |cell|
|
421
|
+
unless cell.nil? || cell.value.nil?
|
422
|
+
#if string not already seen, add it to hash
|
423
|
+
if cell.datatype == 's'
|
424
|
+
if string_hash[cell.value.to_s].nil?
|
425
|
+
string_hash[string_index]=cell.value.to_s
|
426
|
+
string_hash[cell.value.to_s]=string_index
|
427
|
+
string_index += 1
|
428
|
+
end
|
429
|
+
@num_strings += 1
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
if string_hash.empty?
|
438
|
+
@shared_strings = nil
|
439
|
+
else
|
440
|
+
@shared_strings = string_hash
|
441
|
+
@size = string_index
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def validate_before_write
|
447
|
+
## TODO CHECK IF STYLE IS OK if not raise
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|