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/CHANGELOG
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
0.4.0 / 2012-07-19
|
2
|
+
|
3
|
+
* Enhancements
|
4
|
+
|
5
|
+
* Use a shared string file! thanks to @ledbettj in https://github.com/seamusabshere/xlsx_writer/pull/3
|
6
|
+
* Write rows and shared strings directly to disk to minimize memory usage
|
7
|
+
* Simplify syntax: XlsxWriter.new instead of XlsxWriter::Document.new
|
8
|
+
|
9
|
+
* Bug fixes
|
10
|
+
|
11
|
+
* Render BigDecimal and Rational as decimal
|
12
|
+
|
1
13
|
0.3.2 / 2012-07-12
|
2
14
|
|
3
15
|
* Bug fixes
|
data/README.markdown
CHANGED
data/foo.rb
CHANGED
@@ -19,7 +19,8 @@ require 'xlsx_writer'
|
|
19
19
|
# @sheet1.add_autofilter 'A1:B1'
|
20
20
|
|
21
21
|
@sheet2 = @doc.add_sheet("Sheet2")
|
22
|
-
@sheet2.add_row(['
|
22
|
+
@sheet2.add_row(['one', 'two'])
|
23
|
+
@sheet2.add_row(['a', 1])
|
23
24
|
@sheet2.add_row(['false1', false])
|
24
25
|
@sheet2.add_row(['false2', {:value => false, :type => :Boolean}])
|
25
26
|
@sheet2.add_row(['false3', 'faLse'])
|
data/lib/xlsx_writer.rb
CHANGED
@@ -1,30 +1,111 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'fileutils'
|
2
3
|
require 'active_support/core_ext'
|
3
4
|
require 'unix_utils'
|
4
5
|
|
5
|
-
module XlsxWriter
|
6
|
-
end
|
7
|
-
|
8
6
|
require 'xlsx_writer/cell'
|
9
|
-
require 'xlsx_writer/document'
|
10
7
|
require 'xlsx_writer/row'
|
11
|
-
require 'xlsx_writer/xml'
|
12
8
|
require 'xlsx_writer/header_footer'
|
13
9
|
require 'xlsx_writer/autofilter'
|
14
10
|
require 'xlsx_writer/page_setup'
|
11
|
+
require 'xlsx_writer/sheet'
|
12
|
+
require 'xlsx_writer/shared_strings'
|
15
13
|
|
14
|
+
require 'xlsx_writer/xml'
|
16
15
|
# manual
|
17
|
-
require 'xlsx_writer/
|
18
|
-
require 'xlsx_writer/
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
require 'xlsx_writer/
|
23
|
-
require 'xlsx_writer/
|
24
|
-
require 'xlsx_writer/
|
25
|
-
require 'xlsx_writer/
|
26
|
-
require 'xlsx_writer/
|
27
|
-
require 'xlsx_writer/
|
28
|
-
require 'xlsx_writer/
|
29
|
-
|
30
|
-
|
16
|
+
require 'xlsx_writer/xml/sheet_rels'
|
17
|
+
require 'xlsx_writer/xml/image'
|
18
|
+
# automatic
|
19
|
+
require 'xlsx_writer/xml/app'
|
20
|
+
require 'xlsx_writer/xml/content_types'
|
21
|
+
require 'xlsx_writer/xml/doc_props'
|
22
|
+
require 'xlsx_writer/xml/rels'
|
23
|
+
require 'xlsx_writer/xml/styles'
|
24
|
+
require 'xlsx_writer/xml/workbook'
|
25
|
+
require 'xlsx_writer/xml/workbook_rels'
|
26
|
+
require 'xlsx_writer/xml/vml_drawing'
|
27
|
+
require 'xlsx_writer/xml/vml_drawing_rels'
|
28
|
+
|
29
|
+
class XlsxWriter
|
30
|
+
attr_reader :staging_dir
|
31
|
+
attr_reader :sheets
|
32
|
+
attr_reader :images
|
33
|
+
attr_reader :page_setup
|
34
|
+
attr_reader :header_footer
|
35
|
+
attr_reader :shared_strings
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@mutex = Mutex.new
|
39
|
+
staging_dir = UnixUtils.tmp_path 'xlsx_writer'
|
40
|
+
FileUtils.mkdir_p staging_dir
|
41
|
+
@staging_dir = staging_dir
|
42
|
+
@sheets = []
|
43
|
+
@images = []
|
44
|
+
@page_setup = PageSetup.new
|
45
|
+
@header_footer = HeaderFooter.new
|
46
|
+
@shared_strings = SharedStrings.new self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Instead of TRUE or FALSE, show TRUE and blank if false
|
50
|
+
def quiet_booleans!
|
51
|
+
@quiet_booleans = true
|
52
|
+
end
|
53
|
+
|
54
|
+
def quiet_booleans?
|
55
|
+
@quiet_booleans == true
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_sheet(name)
|
59
|
+
raise RuntimeError, "Can't add sheet, already generated!" if generated?
|
60
|
+
ndx = sheets.length + 1
|
61
|
+
sheet = Sheet.new self, name, ndx
|
62
|
+
sheets << sheet
|
63
|
+
sheet
|
64
|
+
end
|
65
|
+
|
66
|
+
delegate :header, :footer, :to => :header_footer
|
67
|
+
|
68
|
+
def add_image(path, width, height)
|
69
|
+
raise RuntimeError, "Can't add image, already generated!" if generated?
|
70
|
+
image = Image.new self, path, width, height
|
71
|
+
images << image
|
72
|
+
image
|
73
|
+
end
|
74
|
+
|
75
|
+
def path
|
76
|
+
@path || @mutex.synchronize do
|
77
|
+
@path ||= begin
|
78
|
+
sheets.each { |sheet| sheet.generate }
|
79
|
+
images.each { |image| image.generate }
|
80
|
+
shared_strings.generate
|
81
|
+
Xml.auto.each { |part| part.new(self).generate }
|
82
|
+
with_zip_extname = UnixUtils.zip staging_dir
|
83
|
+
with_xlsx_extname = with_zip_extname.sub(/.zip$/, '.xlsx')
|
84
|
+
FileUtils.mv with_zip_extname, with_xlsx_extname
|
85
|
+
@generated = true
|
86
|
+
with_xlsx_extname
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def cleanup
|
92
|
+
@mutex.synchronize do
|
93
|
+
FileUtils.rm_rf @staging_dir
|
94
|
+
FileUtils.rm_f @path
|
95
|
+
@path = nil
|
96
|
+
@generated = false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def generate
|
101
|
+
path
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def generated?
|
106
|
+
@generated == true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# backwards compatibility
|
111
|
+
XlsxWriter::Document = XlsxWriter
|
data/lib/xlsx_writer/cell.rb
CHANGED
@@ -1,48 +1,10 @@
|
|
1
1
|
require 'fast_xs'
|
2
2
|
|
3
|
-
|
3
|
+
class XlsxWriter
|
4
4
|
class Cell
|
5
5
|
class << self
|
6
|
-
#
|
7
|
-
def
|
8
|
-
case calculated_type
|
9
|
-
when :String
|
10
|
-
:inlineStr
|
11
|
-
when :Number, :Integer, :Decimal, :Date, :Currency
|
12
|
-
:n
|
13
|
-
when :Boolean
|
14
|
-
:b
|
15
|
-
else
|
16
|
-
raise ::ArgumentError, "Unknown cell type #{calculated_type}"
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# TODO make a class for this
|
21
|
-
def excel_style_number(calculated_type, faded = false)
|
22
|
-
i = case calculated_type
|
23
|
-
when :String
|
24
|
-
0
|
25
|
-
when :Boolean
|
26
|
-
0 # todo
|
27
|
-
when :Currency
|
28
|
-
1
|
29
|
-
when :Date
|
30
|
-
2
|
31
|
-
when :Number, :Integer
|
32
|
-
3
|
33
|
-
when :Decimal
|
34
|
-
4
|
35
|
-
else
|
36
|
-
raise ::ArgumentError, "Unknown cell type #{k}"
|
37
|
-
end
|
38
|
-
if faded
|
39
|
-
i * 2 + 1
|
40
|
-
else
|
41
|
-
i * 2
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def excel_column_letter(i)
|
6
|
+
# 0 -> A (zero based!)
|
7
|
+
def column_letter(i)
|
46
8
|
result = []
|
47
9
|
while i >= 26 do
|
48
10
|
result << ABC[i % 26]
|
@@ -51,77 +13,65 @@ module XlsxWriter
|
|
51
13
|
result << ABC[result.empty? ? i : i - 1]
|
52
14
|
result.reverse.join
|
53
15
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
16
|
+
|
17
|
+
# backwards compatibility
|
18
|
+
alias :excel_column_letter :column_letter
|
19
|
+
|
20
|
+
def type(value, proposed = nil)
|
21
|
+
hint = if proposed
|
22
|
+
proposed
|
23
|
+
elsif value.is_a?(String) and value =~ TRUE_FALSE_PATTERN
|
24
|
+
:Boolean
|
25
|
+
else
|
26
|
+
value.class.name.to_sym
|
63
27
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
elsif value.respond_to?(:to_date)
|
76
|
-
(value.to_date - JAN_1_1900.to_date).to_i
|
28
|
+
case hint
|
29
|
+
when :NilClass
|
30
|
+
:String
|
31
|
+
when :Fixnum
|
32
|
+
:Integer
|
33
|
+
when :Float, :Rational, :BigDecimal
|
34
|
+
:Decimal
|
35
|
+
when :TrueClass, :FalseClass
|
36
|
+
:Boolean
|
37
|
+
else
|
38
|
+
hint
|
77
39
|
end
|
78
40
|
end
|
79
|
-
|
80
|
-
def
|
81
|
-
|
41
|
+
|
42
|
+
def style_number(type, faded = false)
|
43
|
+
style_number = STYLE_NUMBER[type] or raise("Don't know style number for #{type.inspect}. Must be #{STYLE_NUMBER.keys.map(&:inspect).join(', ')}.")
|
44
|
+
if faded
|
45
|
+
style_number * 2 + 1
|
46
|
+
else
|
47
|
+
style_number * 2
|
48
|
+
end
|
82
49
|
end
|
83
50
|
|
51
|
+
def type_name(type)
|
52
|
+
TYPE_NAME[type] or raise "Don't know type name for #{type.inspect}. Must be #{TYPE_NAME.keys.map(&:inspect).join(', ')}."
|
53
|
+
end
|
54
|
+
|
84
55
|
# width = Truncate([{Number of Characters} * {Maximum Digit Width} + {5 pixel padding}]/{Maximum Digit Width}*256)/256
|
85
56
|
# Using the Calibri font as an example, the maximum digit width of 11 point font size is 7 pixels (at 96 dpi). In fact, each digit is the same width for this font. Therefore if the cell width is 8 characters wide, the value of this attribute shall be Truncate([8*7+5]/7*256)/256 = 8.7109375.
|
86
|
-
def pixel_width(
|
87
|
-
|
88
|
-
|
89
|
-
MAX_REASONABLE_WIDTH
|
90
|
-
].min
|
91
|
-
end
|
92
|
-
|
93
|
-
def calculate_type(value)
|
94
|
-
case value
|
95
|
-
when Date
|
96
|
-
:Date
|
97
|
-
when Integer
|
98
|
-
:Integer
|
99
|
-
when Float
|
100
|
-
:Decimal
|
101
|
-
when Numeric
|
102
|
-
:Number
|
103
|
-
when TrueClass, FalseClass, TRUE_FALSE_PATTERN
|
104
|
-
:Boolean
|
57
|
+
def pixel_width(value, type = nil)
|
58
|
+
if (w = ((character_width(value, type).to_f*MAX_DIGIT_WIDTH+5)/MAX_DIGIT_WIDTH*256)/256) < MAX_REASONABLE_WIDTH
|
59
|
+
w
|
105
60
|
else
|
106
|
-
|
107
|
-
:Decimal
|
108
|
-
else
|
109
|
-
:String
|
110
|
-
end
|
61
|
+
MAX_REASONABLE_WIDTH
|
111
62
|
end
|
112
63
|
end
|
113
64
|
|
114
|
-
def character_width(value,
|
115
|
-
|
116
|
-
|
117
|
-
|
65
|
+
def character_width(value, type = nil)
|
66
|
+
if type.nil?
|
67
|
+
type = Cell.type(value)
|
68
|
+
end
|
69
|
+
case type
|
70
|
+
when :String, :Integer
|
118
71
|
value.to_s.length
|
119
|
-
when :
|
72
|
+
when :Decimal
|
120
73
|
# -1000000.5
|
121
|
-
|
122
|
-
len += 2 if calculated_type == :Decimal
|
123
|
-
len += 1 if value < 0
|
124
|
-
len
|
74
|
+
round(value, 2).to_s.length + 2
|
125
75
|
when :Currency
|
126
76
|
# (1,000,000.50)
|
127
77
|
len = round(value, 2).to_s.length + log_base(value.abs, 1e3).floor
|
@@ -131,15 +81,47 @@ module XlsxWriter
|
|
131
81
|
DATE_LENGTH
|
132
82
|
when :Boolean
|
133
83
|
BOOLEAN_LENGTH
|
84
|
+
else
|
85
|
+
raise "Don't know character width for #{type.inspect}."
|
134
86
|
end
|
135
87
|
end
|
136
88
|
|
137
|
-
|
89
|
+
def escape(value, type = nil)
|
90
|
+
if type.nil?
|
91
|
+
type = Cell.type(value)
|
92
|
+
end
|
93
|
+
case type
|
94
|
+
when :Integer
|
95
|
+
value.to_s
|
96
|
+
when :Decimal, :Currency
|
97
|
+
case value
|
98
|
+
when BIG_DECIMAL
|
99
|
+
value.to_s('F')
|
100
|
+
when Rational
|
101
|
+
value.to_f.to_s
|
102
|
+
else
|
103
|
+
value.to_s
|
104
|
+
end
|
105
|
+
when :Date
|
106
|
+
# doesn't work for DateTimes or Times yet
|
107
|
+
if value.is_a?(String)
|
108
|
+
((Time.parse(str) - JAN_1_1900) / 86_400).round
|
109
|
+
elsif value.respond_to?(:to_date)
|
110
|
+
(value.to_date - JAN_1_1900.to_date).to_i
|
111
|
+
end
|
112
|
+
when :Boolean
|
113
|
+
value ? 1 : 0
|
114
|
+
else
|
115
|
+
value.fast_xs
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if RUBY_VERSION >= '1.9'
|
138
120
|
def round(number, precision)
|
139
121
|
number.round precision
|
140
122
|
end
|
141
123
|
def log_base(number, base)
|
142
|
-
|
124
|
+
Math.log number, base
|
143
125
|
end
|
144
126
|
else
|
145
127
|
def round(number, precision)
|
@@ -147,65 +129,87 @@ module XlsxWriter
|
|
147
129
|
end
|
148
130
|
# http://blog.vagmim.com/2010/01/logarithm-to-any-base-in-ruby.html
|
149
131
|
def log_base(number, base)
|
150
|
-
|
132
|
+
Math.log(number) / Math.log(base)
|
151
133
|
end
|
152
134
|
end
|
153
135
|
end
|
154
|
-
|
136
|
+
|
155
137
|
ABC = ('A'..'Z').to_a
|
156
138
|
MAX_DIGIT_WIDTH = 5
|
157
139
|
MAX_REASONABLE_WIDTH = 75
|
158
140
|
DATE_LENGTH = 'YYYY-MM-DD'.length
|
159
141
|
BOOLEAN_LENGTH = 'FALSE'.length + 1
|
160
|
-
JAN_1_1900 =
|
142
|
+
JAN_1_1900 = Time.parse('1899-12-30 00:00:00 UTC')
|
161
143
|
TRUE_FALSE_PATTERN = %r{^true|false$}i
|
162
|
-
|
144
|
+
BIG_DECIMAL = defined?(BigDecimal) ? BigDecimal : Struct.new
|
145
|
+
|
146
|
+
STYLE_NUMBER = {
|
147
|
+
:String => 0,
|
148
|
+
:Boolean => 0,
|
149
|
+
:Currency => 1,
|
150
|
+
:Date => 2,
|
151
|
+
:Integer => 3,
|
152
|
+
:Decimal => 4,
|
153
|
+
}
|
154
|
+
|
155
|
+
TYPE_NAME = {
|
156
|
+
:String => :s,
|
157
|
+
:Boolean => :b,
|
158
|
+
:Currency => :n,
|
159
|
+
:Date => :n,
|
160
|
+
:Integer => :n,
|
161
|
+
:Decimal => :n,
|
162
|
+
}
|
163
|
+
|
163
164
|
attr_reader :row
|
165
|
+
attr_reader :x
|
166
|
+
attr_reader :y
|
164
167
|
attr_reader :value
|
165
|
-
attr_reader :
|
166
|
-
attr_reader :excel_type
|
167
|
-
attr_reader :excel_style_number
|
168
|
-
attr_reader :excel_value
|
168
|
+
attr_reader :type
|
169
169
|
|
170
|
-
def initialize(row,
|
170
|
+
def initialize(row, raw_value, x, y)
|
171
171
|
@row = row
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
172
|
+
@x = x
|
173
|
+
@y = y
|
174
|
+
if raw_value.is_a?(Hash)
|
175
|
+
@value = raw_value[:value]
|
176
|
+
@type = Cell.type @value, raw_value[:type]
|
177
|
+
@faded_query = raw_value[:faded]
|
177
178
|
else
|
178
|
-
@value =
|
179
|
-
|
180
|
-
calculated_type = Cell.calculate_type @value
|
179
|
+
@value = raw_value
|
180
|
+
@type = Cell.type value
|
181
181
|
end
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
@
|
186
|
-
|
182
|
+
end
|
183
|
+
|
184
|
+
def faded?
|
185
|
+
@faded_query == true
|
186
|
+
end
|
187
|
+
|
188
|
+
def empty?
|
189
|
+
return @empty_query if defined?(@empty_query)
|
190
|
+
@empty_query = (value.nil? or (value.is_a?(String) and value.empty?) or (value == false and row.sheet.document.quiet_booleans?))
|
187
191
|
end
|
188
192
|
|
189
193
|
def to_xml
|
190
|
-
if
|
191
|
-
%{<c r="#{
|
192
|
-
elsif excel_type == :inlineStr
|
193
|
-
%{<c r="#{excel_column_letter}#{row.ndx}" s="#{excel_style_number}" t="#{excel_type}"><is><t>#{excel_value}</t></is></c>}
|
194
|
+
if empty?
|
195
|
+
%{<c r="#{Cell.column_letter(x)}#{y}" s="0" t="s" />}
|
194
196
|
else
|
195
|
-
%{<c r="#{
|
197
|
+
%{<c r="#{Cell.column_letter(x)}#{y}" s="#{Cell.style_number(type, faded?)}" t="#{Cell.type_name(type)}"><v>#{escaped_value}</v></c>}
|
196
198
|
end
|
197
199
|
end
|
198
200
|
|
199
|
-
|
200
|
-
|
201
|
-
Cell.excel_column_letter row.cells.index(self)
|
201
|
+
def pixel_width
|
202
|
+
@pixel_width ||= Cell.pixel_width value, type
|
202
203
|
end
|
203
204
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
205
|
+
def escaped_value
|
206
|
+
@escaped_value ||= begin
|
207
|
+
if type == :String
|
208
|
+
row.sheet.document.shared_strings.ndx value
|
209
|
+
else
|
210
|
+
Cell.escape value
|
211
|
+
end
|
212
|
+
end
|
209
213
|
end
|
210
214
|
end
|
211
215
|
end
|