xlsx_writer 0.3.2 → 0.4.0

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 (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" />}