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/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
|