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.
@@ -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