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