xlsx_writer 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +12 -0
  2. data/README.markdown +1 -1
  3. data/foo.rb +2 -1
  4. data/lib/xlsx_writer.rb +100 -19
  5. data/lib/xlsx_writer/autofilter.rb +1 -1
  6. data/lib/xlsx_writer/cell.rb +140 -136
  7. data/lib/xlsx_writer/header_footer.rb +1 -1
  8. data/lib/xlsx_writer/page_setup.rb +1 -1
  9. data/lib/xlsx_writer/row.rb +8 -23
  10. data/lib/xlsx_writer/shared_strings.rb +70 -0
  11. data/lib/xlsx_writer/sheet.rb +145 -0
  12. data/lib/xlsx_writer/version.rb +2 -2
  13. data/lib/xlsx_writer/xml.rb +11 -3
  14. data/lib/xlsx_writer/{generators → xml}/app.erb +0 -0
  15. data/lib/xlsx_writer/{generators → xml}/app.rb +1 -1
  16. data/lib/xlsx_writer/{generators → xml}/content_types.erb +1 -0
  17. data/lib/xlsx_writer/{generators → xml}/content_types.rb +1 -1
  18. data/lib/xlsx_writer/{generators → xml}/doc_props.erb +0 -0
  19. data/lib/xlsx_writer/{generators → xml}/doc_props.rb +1 -1
  20. data/lib/xlsx_writer/{generators → xml}/image.rb +1 -1
  21. data/lib/xlsx_writer/{generators → xml}/rels.erb +0 -0
  22. data/lib/xlsx_writer/{generators → xml}/rels.rb +1 -1
  23. data/lib/xlsx_writer/{generators → xml}/sheet_rels.erb +0 -0
  24. data/lib/xlsx_writer/{generators → xml}/sheet_rels.rb +1 -1
  25. data/lib/xlsx_writer/{generators → xml}/styles.erb +0 -0
  26. data/lib/xlsx_writer/{generators → xml}/styles.rb +1 -1
  27. data/lib/xlsx_writer/{generators → xml}/vml_drawing.erb +0 -0
  28. data/lib/xlsx_writer/{generators → xml}/vml_drawing.rb +1 -1
  29. data/lib/xlsx_writer/{generators → xml}/vml_drawing_rels.erb +0 -0
  30. data/lib/xlsx_writer/{generators → xml}/vml_drawing_rels.rb +1 -1
  31. data/lib/xlsx_writer/{generators → xml}/workbook.erb +0 -0
  32. data/lib/xlsx_writer/{generators → xml}/workbook.rb +1 -1
  33. data/lib/xlsx_writer/{generators → xml}/workbook_rels.erb +1 -0
  34. data/lib/xlsx_writer/{generators → xml}/workbook_rels.rb +1 -1
  35. data/test/helper.rb +2 -0
  36. data/test/test_xlsx_writer.rb +43 -16
  37. data/xlsx_writer.gemspec +1 -0
  38. metadata +41 -25
  39. data/lib/xlsx_writer/document.rb +0 -88
  40. data/lib/xlsx_writer/generators/sheet.rb +0 -138
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class HeaderFooter
3
3
  attr_reader :header
4
4
  attr_reader :footer
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class PageSetup
3
3
  DEFAULT = {
4
4
  :top => 1.0,
@@ -1,36 +1,21 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class Row
3
3
  attr_reader :sheet
4
4
  attr_reader :cells
5
- attr_reader :width
5
+ attr_reader :y
6
6
 
7
- def initialize(sheet, columns)
8
- @width = {}
7
+ def initialize(sheet, raw_cells, y)
9
8
  @sheet = sheet
10
- @cells = columns.map do |column|
11
- Cell.new self, column
12
- end
13
- end
14
-
15
- def ndx
16
- sheet.rows.index(self) + 1
17
- end
18
-
19
- def length
20
- cells.length
21
- end
22
-
23
- def cell_width(x)
24
- @width[x] ||= if (cell = cells[x])
25
- cell.pixel_width
26
- else
27
- 0
9
+ @y = y
10
+ @cells = []
11
+ raw_cells.each_with_index do |cell, x|
12
+ @cells << Cell.new(self, cell, x, y)
28
13
  end
29
14
  end
30
15
 
31
16
  def to_xml
32
17
  ary = []
33
- ary << %{<row r="#{ndx}">}
18
+ ary << %{<row r="#{y}">}
34
19
  cells.each do |cell|
35
20
  ary << cell.to_xml
36
21
  end
@@ -0,0 +1,70 @@
1
+ require 'digest/md5'
2
+
3
+ class XlsxWriter
4
+ class SharedStrings
5
+ BUFSIZE = 131072 #128kb
6
+
7
+ attr_reader :document
8
+ attr_reader :path
9
+ attr_reader :indexes
10
+
11
+ def initialize(document)
12
+ @mutex = Mutex.new
13
+ @document = document
14
+ @indexes = {}
15
+ @path = File.join document.staging_dir, relative_path
16
+ FileUtils.mkdir_p File.dirname(path)
17
+ @strings_tmp_file_writer = File.open(strings_tmp_file_path, 'wb')
18
+ end
19
+
20
+ def relative_path
21
+ 'xl/sharedstrings.xml'
22
+ end
23
+
24
+ def ndx(str)
25
+ @mutex.synchronize do
26
+ digest = Digest::MD5.digest str
27
+ unless ndx = indexes[digest]
28
+ ndx = indexes.length
29
+ indexes[digest] = ndx
30
+ @strings_tmp_file_writer.write %{<si><t>#{str.fast_xs}</t></si>}
31
+ end
32
+ ndx
33
+ end
34
+ end
35
+
36
+ def generated?
37
+ @generated == true
38
+ end
39
+
40
+ def generate
41
+ return if generated?
42
+ @mutex.synchronize do
43
+ return if generated?
44
+ @generated = true
45
+ @strings_tmp_file_writer.close
46
+ File.open(path, 'wb') do |f|
47
+ f.write <<-EOS
48
+ <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
49
+ <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="#{indexes.length}" uniqueCount="#{indexes.length}">
50
+ EOS
51
+ File.open(strings_tmp_file_path, 'rb') do |strings_tmp_file_reader|
52
+ buffer = ''
53
+ while strings_tmp_file_reader.read(BUFSIZE, buffer)
54
+ f.write buffer
55
+ end
56
+ end
57
+ f.write %{</sst>}
58
+ end
59
+ File.unlink strings_tmp_file_path
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def strings_tmp_file_path
66
+ path + '.strings_tmp_file'
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,145 @@
1
+ require 'fast_xs'
2
+
3
+ class XlsxWriter
4
+ class Sheet
5
+ class << self
6
+ def excel_name(value)
7
+ str = value.to_s.dup
8
+ str.gsub! '/', '' # remove forward slashes
9
+ str.gsub! /\s+/, '' # compress "inner" whitespace
10
+ str.strip! # trim whitespace from ends
11
+ str.fast_xs
12
+ end
13
+ end
14
+
15
+ BUFSIZE = 131072 #128kb
16
+
17
+ attr_reader :document
18
+ attr_reader :name
19
+ attr_reader :ndx
20
+ attr_reader :autofilters
21
+ attr_reader :path
22
+ attr_reader :row_count
23
+ attr_reader :max_row_length
24
+ attr_reader :max_cell_pixel_width
25
+
26
+ # Freeze the pane under this top left cell
27
+ attr_accessor :freeze_top_left
28
+
29
+ def initialize(document, name, ndx)
30
+ @mutex = Mutex.new
31
+ @document = document
32
+ @ndx = ndx
33
+ @name = Sheet.excel_name name
34
+ @row_count = 0
35
+ @autofilters = []
36
+ @max_row_length = 1
37
+ @max_cell_pixel_width = Hash.new(Cell.pixel_width(5))
38
+ @path = ::File.join document.staging_dir, relative_path
39
+ ::FileUtils.mkdir_p ::File.dirname(path)
40
+ @rows_tmp_file_writer = ::File.open(rows_tmp_file_path, 'wb')
41
+ end
42
+
43
+ def generated?
44
+ @generated == true
45
+ end
46
+
47
+ def generate
48
+ return if generated?
49
+ @mutex.synchronize do
50
+ return if generated?
51
+ @generated = true
52
+ @rows_tmp_file_writer.close
53
+ File.open(path, 'wb') do |f|
54
+ f.write <<-EOS
55
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
56
+ <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
57
+ EOS
58
+ if freeze_top_left
59
+ f.write <<-EOS
60
+ <sheetViews>
61
+ <sheetView workbookViewId="0">
62
+ <pane ySplit="#{y_split}" topLeftCell="#{freeze_top_left}" activePane="topLeft" state="frozen"/>
63
+ </sheetView>
64
+ </sheetViews>
65
+ EOS
66
+ end
67
+ f.write %{<cols>}
68
+ (0..max_row_length-1).each do |x|
69
+ f.write %{<col min="#{x+1}" max="#{x+1}" width="#{max_cell_pixel_width[x]}" bestFit="1" customWidth="1" />}
70
+ end
71
+ f.write %{</cols>}
72
+ f.write %{<sheetData>}
73
+ File.open(rows_tmp_file_path, 'rb') do |rows_tmp_file_reader|
74
+ buffer = ''
75
+ while rows_tmp_file_reader.read(BUFSIZE, buffer)
76
+ f.write buffer
77
+ end
78
+ end
79
+ f.write %{</sheetData>}
80
+ autofilters.each { |autofilter| f.write autofilter.to_xml }
81
+ f.write document.page_setup.to_xml
82
+ f.write document.header_footer.to_xml
83
+ f.write %{</worksheet>}
84
+ end
85
+ File.unlink rows_tmp_file_path
86
+ converted = UnixUtils.unix2dos path
87
+ FileUtils.mv converted, path
88
+ SheetRels.new(document, self).generate
89
+ end
90
+ end
91
+
92
+ def local_id
93
+ ndx - 1
94
+ end
95
+
96
+ # +1 because styles.xml occupies the first spot
97
+ def rid
98
+ "rId#{ndx + 1}"
99
+ end
100
+
101
+ def relative_path
102
+ "xl/worksheets/sheet#{ndx}.xml"
103
+ end
104
+
105
+ def absolute_path
106
+ "/#{relative_path}"
107
+ end
108
+
109
+ # specify range like "A1:C1"
110
+ def add_autofilter(range)
111
+ raise ::RuntimeError, "Can't add autofilter, already generated!" if generated?
112
+ autofilters << Autofilter.new(self, range)
113
+ end
114
+
115
+ def add_row(cells)
116
+ raise ::RuntimeError, "Can't add row, already generated!" if generated?
117
+ @row_count += 1
118
+ row = Row.new self, cells, row_count
119
+ @rows_tmp_file_writer.write row.to_xml
120
+ if (l = row.cells.length) > max_row_length
121
+ @max_row_length = l
122
+ end
123
+ row.cells.each_with_index do |cell, x|
124
+ if (w = cell.pixel_width) > max_cell_pixel_width[x]
125
+ max_cell_pixel_width[x] = w
126
+ end
127
+ end
128
+ nil
129
+ end
130
+
131
+ private
132
+
133
+ def rows_tmp_file_path
134
+ path + '.rows_tmp_file'
135
+ end
136
+
137
+ def y_split
138
+ if freeze_top_left =~ /(\d+)$/
139
+ $1.to_i - 1
140
+ else
141
+ raise "freeze_top_left must be like 'A3', was #{freeze_top_left}"
142
+ end
143
+ end
144
+ end
145
+ end
@@ -1,3 +1,3 @@
1
- module XlsxWriter
2
- VERSION = '0.3.2'
1
+ class XlsxWriter
2
+ VERSION = '0.4.0'
3
3
  end
@@ -1,13 +1,21 @@
1
1
  require 'erb'
2
2
  require 'fileutils'
3
3
 
4
- module XlsxWriter
4
+ class XlsxWriter
5
5
  class Xml
6
+ class << self
7
+ def auto
8
+ descendants.reject do |klass|
9
+ klass.const_defined?(:AUTO) and klass.const_get(:AUTO) == false
10
+ end
11
+ end
12
+ end
13
+
6
14
  attr_reader :document
7
15
 
8
16
  def initialize(document)
17
+ @mutex = Mutex.new
9
18
  @document = document
10
- @mutex = ::Mutex.new
11
19
  end
12
20
 
13
21
  def generate
@@ -36,7 +44,7 @@ module XlsxWriter
36
44
  end
37
45
 
38
46
  def template_path
39
- ::File.expand_path "../generators/#{self.class.name.demodulize.underscore}.erb", __FILE__
47
+ ::File.expand_path "../xml/#{self.class.name.demodulize.underscore}.erb", __FILE__
40
48
  end
41
49
 
42
50
  def render
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class App < Xml
3
3
  def relative_path
4
4
  "docProps/app.xml"
@@ -11,4 +11,5 @@
11
11
  <Override PartName="<%= sheet.absolute_path %>" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
12
12
  <% end %>
13
13
  <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
14
+ <Override PartName="/xl/sharedstrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
14
15
  </Types>
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class ContentTypes < Xml
3
3
  def relative_path
4
4
  "[Content_Types].xml"
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class DocProps < Xml
3
3
  def relative_path
4
4
  "docProps/core.xml"
@@ -1,6 +1,6 @@
1
1
  require 'fileutils'
2
2
 
3
- module XlsxWriter
3
+ class XlsxWriter
4
4
  class Image
5
5
  DEFAULT = {
6
6
  :croptop => 0,
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class Rels < Xml
3
3
  def relative_path
4
4
  "_rels/.rels"
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class SheetRels < Xml
3
3
 
4
4
  AUTO = false
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class Styles < Xml
3
3
  def relative_path
4
4
  "xl/styles.xml"
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class VmlDrawing < Xml
3
3
  def relative_path
4
4
  "xl/drawings/vmlDrawing1.vml"
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class VmlDrawingRels < Xml
3
3
  def relative_path
4
4
  "xl/drawings/_rels/vmlDrawing1.vml.rels"
File without changes
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class Workbook < Xml
3
3
  def relative_path
4
4
  "xl/workbook.xml"
@@ -1,5 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
2
  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3
+ <Relationship Id="rId0" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedstrings.xml"/>
3
4
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="/xl/styles.xml"/>
4
5
  <% document.sheets.each do |sheet| %>
5
6
  <Relationship Id="<%= sheet.rid %>" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="<%= sheet.absolute_path %>"/>
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class WorkbookRels < Xml
3
3
  def relative_path
4
4
  "xl/_rels/workbook.xml.rels"
data/test/helper.rb CHANGED
@@ -8,5 +8,7 @@ MiniTest::Unit.runner = MiniTest::SuiteRunner.new
8
8
  MiniTest::Unit.runner.reporters << MiniTest::Reporters::SpecReporter.new
9
9
 
10
10
  require 'remote_table'
11
+ require 'bigdecimal'
12
+ require 'decimal'
11
13
 
12
14
  require 'xlsx_writer'
@@ -12,7 +12,7 @@ describe XlsxWriter do
12
12
  @sheet1.add_row(['negative', false])
13
13
  end
14
14
  after do
15
- @doc.cleanup
15
+ @doc.try :cleanup
16
16
  end
17
17
  it "returns a path to an xlsx" do
18
18
  File.exist?(@doc.path).must_equal true
@@ -51,25 +51,26 @@ describe XlsxWriter do
51
51
  end
52
52
 
53
53
  describe "quiet booleans" do
54
- before do
55
- @doc = XlsxWriter::Document.new
56
- @sheet1 = @doc.add_sheet("QuietBooleans")
57
- @sheet1.add_row(['affirmative', 'negative'])
58
- @sheet1.add_row([true, false])
59
- end
60
- after do
61
- @doc.cleanup
62
- end
63
54
  it "shows TRUE or FALSE for booleans by default" do
64
- t = RemoteTable.new(@doc.path, :format => :xlsx)
55
+ doc = XlsxWriter::Document.new
56
+ sheet1 = doc.add_sheet("QuietBooleans")
57
+ sheet1.add_row(['affirmative', 'negative'])
58
+ sheet1.add_row([true, false])
59
+ t = RemoteTable.new(doc.path, :format => :xlsx)
65
60
  t[0]['affirmative'].must_equal 'TRUE'
66
61
  t[0]['negative'].must_equal 'FALSE'
62
+ doc.cleanup
67
63
  end
68
64
  it "shows TRUE or blank for false if quiet booleans is enabled" do
69
- @doc.quiet_booleans!
70
- t = RemoteTable.new(@doc.path, :format => :xlsx)
65
+ doc = XlsxWriter::Document.new
66
+ doc.quiet_booleans!
67
+ sheet1 = doc.add_sheet("QuietBooleans")
68
+ sheet1.add_row(['affirmative', 'negative'])
69
+ sheet1.add_row([true, false])
70
+ t = RemoteTable.new(doc.path, :format => :xlsx)
71
71
  t[0]['affirmative'].must_equal 'TRUE'
72
72
  t[0]['negative'].must_equal ''
73
+ doc.cleanup
73
74
  end
74
75
  end
75
76
 
@@ -81,7 +82,7 @@ describe XlsxWriter do
81
82
  @sheet1.add_row([3, 4])
82
83
  end
83
84
  after do
84
- @doc.cleanup
85
+ @doc.try :cleanup
85
86
  end
86
87
  it "doesn't freeze by default" do
87
88
  dir = UnixUtils.unzip @doc.path
@@ -98,6 +99,32 @@ describe XlsxWriter do
98
99
  end
99
100
  end
100
101
 
102
+ describe "numeric types" do
103
+ before do
104
+ @reference = {
105
+ # 'Integer' => [1, '1'], - remote_table always converts to 1.0
106
+ 'Float' => [1.2345, '1.2345'],
107
+ 'Decimal' => [Decimal('6.789'), '6.789'],
108
+ 'BigDecimal' => [::BigDecimal.new(9.876, 4), '9.876'],
109
+ 'Rational' => [Rational(2, 3), '0.6666'],
110
+ }
111
+ @doc = XlsxWriter::Document.new
112
+ @sheet1 = @doc.add_sheet("Numbers")
113
+ @reference.each do |k, v|
114
+ @sheet1.add_row([k, v.first])
115
+ end
116
+ end
117
+ after do
118
+ @doc.try :cleanup
119
+ end
120
+ it "renders different numeric types into plain decimals" do
121
+ t = RemoteTable.new(@doc.path, :format => :xlsx, :headers => %w{ type number })
122
+ t.each do |row|
123
+ row['number'].to_s.first(6).must_equal @reference[row['type']].last, "for #{row['type']}"
124
+ end
125
+ end
126
+ end
127
+
101
128
  describe "example with autofilter, header image, and footer text" do
102
129
  before do
103
130
  @doc = XlsxWriter::Document.new
@@ -129,8 +156,8 @@ describe XlsxWriter do
129
156
  @unzipped = UnixUtils.unzip @doc.path
130
157
  end
131
158
  after do
132
- @doc.cleanup
133
- FileUtils.rm_rf @unzipped
159
+ @doc.try :cleanup
160
+ FileUtils.rm_rf @unzipped if @unzipped
134
161
  end
135
162
  it "has an autofilter" do
136
163
  File.read("#{@unzipped}/xl/worksheets/sheet1.xml").must_include %{<autoFilter ref="A1:F1" />}