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