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
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
@@ -33,7 +33,7 @@ Features not present in simple_xlsx_writer:
33
33
 
34
34
  require 'xlsx_writer'
35
35
 
36
- doc = XlsxWriter::Document.new
36
+ doc = XlsxWriter.new
37
37
 
38
38
  # show TRUE for true but a blank cell instead of FALSE
39
39
  doc.quiet_booleans!
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(['a', 'a'])
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/generators/sheet'
18
- require 'xlsx_writer/generators/sheet_rels'
19
- require 'xlsx_writer/generators/image'
20
-
21
- # generators
22
- require 'xlsx_writer/generators/app'
23
- require 'xlsx_writer/generators/content_types'
24
- require 'xlsx_writer/generators/doc_props'
25
- require 'xlsx_writer/generators/rels'
26
- require 'xlsx_writer/generators/styles'
27
- require 'xlsx_writer/generators/workbook'
28
- require 'xlsx_writer/generators/workbook_rels'
29
- require 'xlsx_writer/generators/vml_drawing'
30
- require 'xlsx_writer/generators/vml_drawing_rels'
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
@@ -1,4 +1,4 @@
1
- module XlsxWriter
1
+ class XlsxWriter
2
2
  class Autofilter < ::Struct.new(:sheet, :range)
3
3
  def to_xml
4
4
  %{<autoFilter ref="#{range}" />}
@@ -1,48 +1,10 @@
1
1
  require 'fast_xs'
2
2
 
3
- module XlsxWriter
3
+ class XlsxWriter
4
4
  class Cell
5
5
  class << self
6
- # TODO make a class for this
7
- def excel_type(calculated_type)
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
- def excel_string(value)
56
- value.to_s.fast_xs
57
- end
58
-
59
- def excel_number(value)
60
- str = value.to_s.dup
61
- unless str =~ /\A[0-9\.\-]*\z/
62
- raise ::ArgumentError, %{Bad value "#{value}" Only numbers and dots (.) allowed in number fields}
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
- str.fast_xs
65
- end
66
-
67
- alias :excel_currency :excel_number
68
- alias :excel_integer :excel_number
69
- alias :excel_decimal :excel_number
70
-
71
- # doesn't necessarily work for times yet
72
- def excel_date(value)
73
- if value.is_a?(::String)
74
- ((::Time.parse(str) - JAN_1_1900) / 86_400).round
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 excel_boolean(value)
81
- value ? 1 : 0
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(character_width)
87
- [
88
- ((character_width.to_f*MAX_DIGIT_WIDTH+5)/MAX_DIGIT_WIDTH*256)/256,
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
- if (defined?(Decimal) and value.is_a?(Decimal)) or (defined?(BigDecimal) and value.is_a?(BigDecimal))
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, calculated_type = nil)
115
- calculated_type ||= calculate_type(value)
116
- case calculated_type
117
- when :String
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 :Number, :Integer, :Decimal
72
+ when :Decimal
120
73
  # -1000000.5
121
- len = round(value, 2).to_s.length
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
- if ::RUBY_VERSION >= '1.9'
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
- ::Math.log number, base
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
- ::Math.log(number) / ::Math.log(base)
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 = ::Time.parse('1899-12-30 00:00:00 UTC')
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 :pixel_width
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, data)
170
+ def initialize(row, raw_value, x, y)
171
171
  @row = row
172
- if data.is_a?(::Hash)
173
- data = data.symbolize_keys
174
- @value = data[:value]
175
- faded = data[:faded]
176
- calculated_type = data[:type] || Cell.calculate_type(@value)
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 = data
179
- faded = false
180
- calculated_type = Cell.calculate_type @value
179
+ @value = raw_value
180
+ @type = Cell.type value
181
181
  end
182
- character_width = Cell.character_width @value, calculated_type
183
- @pixel_width = Cell.pixel_width character_width
184
- @excel_type = Cell.excel_type calculated_type
185
- @excel_style_number = Cell.excel_style_number calculated_type, faded
186
- @excel_value = Cell.send "excel_#{calculated_type.to_s.underscore}", @value
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 value.nil? or (value.is_a?(String) and value.empty?) or (value == false and quiet_booleans?)
191
- %{<c r="#{excel_column_letter}#{row.ndx}" s="0" t="inlineStr" />}
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="#{excel_column_letter}#{row.ndx}" s="#{excel_style_number}" t="#{excel_type}"><v>#{excel_value}</v></c>}
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
- # 0 -> A (zero based!)
200
- def excel_column_letter
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
- private
205
-
206
- def quiet_booleans?
207
- return @quiet_booleans if defined?(@quiet_booleans)
208
- @quiet_booleans = row.sheet.document.quiet_booleans?
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