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.
- data/CHANGELOG +12 -0
- data/README.markdown +1 -1
- data/foo.rb +2 -1
- data/lib/xlsx_writer.rb +100 -19
- data/lib/xlsx_writer/autofilter.rb +1 -1
- data/lib/xlsx_writer/cell.rb +140 -136
- data/lib/xlsx_writer/header_footer.rb +1 -1
- data/lib/xlsx_writer/page_setup.rb +1 -1
- data/lib/xlsx_writer/row.rb +8 -23
- data/lib/xlsx_writer/shared_strings.rb +70 -0
- data/lib/xlsx_writer/sheet.rb +145 -0
- data/lib/xlsx_writer/version.rb +2 -2
- data/lib/xlsx_writer/xml.rb +11 -3
- data/lib/xlsx_writer/{generators → xml}/app.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/app.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/content_types.erb +1 -0
- data/lib/xlsx_writer/{generators → xml}/content_types.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/doc_props.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/doc_props.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/image.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/rels.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/rels.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/sheet_rels.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/sheet_rels.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/styles.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/styles.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/vml_drawing.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/vml_drawing.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/vml_drawing_rels.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/vml_drawing_rels.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/workbook.erb +0 -0
- data/lib/xlsx_writer/{generators → xml}/workbook.rb +1 -1
- data/lib/xlsx_writer/{generators → xml}/workbook_rels.erb +1 -0
- data/lib/xlsx_writer/{generators → xml}/workbook_rels.rb +1 -1
- data/test/helper.rb +2 -0
- data/test/test_xlsx_writer.rb +43 -16
- data/xlsx_writer.gemspec +1 -0
- metadata +41 -25
- data/lib/xlsx_writer/document.rb +0 -88
- data/lib/xlsx_writer/generators/sheet.rb +0 -138
data/lib/xlsx_writer/row.rb
CHANGED
@@ -1,36 +1,21 @@
|
|
1
|
-
|
1
|
+
class XlsxWriter
|
2
2
|
class Row
|
3
3
|
attr_reader :sheet
|
4
4
|
attr_reader :cells
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :y
|
6
6
|
|
7
|
-
def initialize(sheet,
|
8
|
-
@width = {}
|
7
|
+
def initialize(sheet, raw_cells, y)
|
9
8
|
@sheet = sheet
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
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="#{
|
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
|
data/lib/xlsx_writer/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.
|
1
|
+
class XlsxWriter
|
2
|
+
VERSION = '0.4.0'
|
3
3
|
end
|
data/lib/xlsx_writer/xml.rb
CHANGED
@@ -1,13 +1,21 @@
|
|
1
1
|
require 'erb'
|
2
2
|
require 'fileutils'
|
3
3
|
|
4
|
-
|
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 "../
|
47
|
+
::File.expand_path "../xml/#{self.class.name.demodulize.underscore}.erb", __FILE__
|
40
48
|
end
|
41
49
|
|
42
50
|
def render
|
File without changes
|
@@ -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>
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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 %>"/>
|
data/test/helper.rb
CHANGED
data/test/test_xlsx_writer.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
70
|
-
|
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" />}
|