xlsx_writer 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -10,4 +10,6 @@ tmp
10
10
  notes.txt
11
11
  *.\#*
12
12
  *.rbc
13
- pkg
13
+ pkg
14
+ .DS_Store
15
+ Gemfile.lock
data/CHANGELOG ADDED
@@ -0,0 +1,13 @@
1
+ 0.2.0 / 2012-04-23
2
+
3
+ * Enhancements
4
+
5
+ * Add tests
6
+ * Test on MRI 1.8, MRI 1.9, and JRuby 1.6.7+
7
+ * Simply, DRY, and make thread-safe
8
+ * Use unix_utils instead of our own spawning code
9
+ * Stop using autoload
10
+
11
+ * Bug fixes
12
+
13
+ * Wrap croptop and cropleft in quotes
data/Gemfile CHANGED
@@ -1,3 +1,9 @@
1
1
  source :rubygems
2
2
 
3
- gemspec :path => '.'
3
+ gemspec
4
+
5
+ # development dependencies
6
+ gem 'minitest'
7
+ gem 'minitest-reporters'
8
+ gem 'yard'
9
+ gem 'remote_table'
data/Rakefile CHANGED
@@ -1,13 +1,15 @@
1
- require 'rubygems'
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
2
4
  require 'rake'
3
5
  require 'rake/testtask'
4
-
5
- task :default => [:test]
6
-
7
- Rake::TestTask.new do |test|
8
- test.libs << "test"
9
- test.test_files = Dir['test/**/*_test.rb'].sort
10
- test.verbose = true
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/**/test_*.rb'
9
+ test.verbose = true
11
10
  end
12
11
 
12
+ task :default => :test
13
13
 
14
+ require 'yard'
15
+ YARD::Rake::YardocTask.new
@@ -64,7 +64,6 @@ module XlsxWriter
64
64
  alias :excel_decimal :excel_number
65
65
 
66
66
  # doesn't necessarily work for times yet
67
- JAN_1_1900 = ::Time.parse('1900-01-01')
68
67
  def excel_date(value)
69
68
  if value.is_a?(::String)
70
69
  ((::Time.parse(str) - JAN_1_1900) / 86_400).round
@@ -76,61 +75,102 @@ module XlsxWriter
76
75
  def excel_boolean(value)
77
76
  value ? 1 : 0
78
77
  end
78
+
79
+ # width = Truncate([{Number of Characters} * {Maximum Digit Width} + {5 pixel padding}]/{Maximum Digit Width}*256)/256
80
+ # 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.
81
+ def pixel_width(character_width)
82
+ [
83
+ ((character_width.to_f*MAX_DIGIT_WIDTH+5)/MAX_DIGIT_WIDTH*256)/256,
84
+ MAX_REASONABLE_WIDTH
85
+ ].min
86
+ end
87
+
88
+ def calculate_type(value)
89
+ if value.is_a?(::Date)
90
+ :Date
91
+ elsif value.is_a?(::Integer)
92
+ :Integer
93
+ elsif value.is_a?(::Float) or (defined?(::BigDecimal) and value.is_a?(::BigDecimal)) or (defined?(::Decimal) and value.is_a?(::Decimal))
94
+ :Decimal
95
+ elsif value.is_a?(::Numeric)
96
+ :Number
97
+ else
98
+ :String
99
+ end
100
+ end
101
+
102
+ def character_width(value, calculated_type = nil)
103
+ calculated_type ||= calculate_type(value)
104
+ case calculated_type
105
+ when :String
106
+ value.to_s.length
107
+ when :Number, :Integer, :Decimal
108
+ # -1000000.5
109
+ len = round(value, 2).to_s.length
110
+ len += 2 if calculated_type == :Decimal
111
+ len += 1 if value < 0
112
+ len
113
+ when :Currency
114
+ # (1,000,000.50)
115
+ len = round(value, 2).to_s.length + log_base(value.abs, 1e3).floor
116
+ len += 2 if value < 0
117
+ len
118
+ when :Date
119
+ DATE_LENGTH
120
+ when :Boolean
121
+ BOOLEAN_LENGTH
122
+ end
123
+ end
124
+
125
+ if ::RUBY_VERSION >= '1.9'
126
+ def round(number, precision)
127
+ number.round precision
128
+ end
129
+ def log_base(number, base)
130
+ ::Math.log number, base
131
+ end
132
+ else
133
+ def round(number, precision)
134
+ (number * (10 ** precision).to_i).round / (10 ** precision).to_f
135
+ end
136
+ # http://blog.vagmim.com/2010/01/logarithm-to-any-base-in-ruby.html
137
+ def log_base(number, base)
138
+ ::Math.log(number) / ::Math.log(base)
139
+ end
140
+ end
79
141
  end
80
142
 
81
143
  ABC = ('A'..'Z').to_a
82
-
83
- attr_reader :row
84
- attr_reader :data
85
-
86
- def initialize(row, data)
87
- @row = row
88
- @data = data.is_a?(::Hash) ? data.symbolize_keys : data
89
- end
90
-
91
- # width = Truncate([{Number of Characters} * {Maximum Digit Width} + {5 pixel padding}]/{Maximum Digit Width}*256)/256
92
- # 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.
93
144
  MAX_DIGIT_WIDTH = 5
94
145
  MAX_REASONABLE_WIDTH = 75
95
- def pixel_width
96
- @pixel_width ||= [
97
- ((character_width.to_f*MAX_DIGIT_WIDTH+5)/MAX_DIGIT_WIDTH*256)/256,
98
- MAX_REASONABLE_WIDTH
99
- ].min
100
- end
101
-
102
146
  DATE_LENGTH = 'YYYY-MM-DD'.length
103
147
  BOOLEAN_LENGTH = 'FALSE'.length
104
- def character_width
105
- @character_width ||= case calculated_type
106
- when :String
107
- value.to_s.length
108
- when :Number, :Integer, :Decimal
109
- # -1000000.5
110
- len = value.round(2).to_s.length
111
- len += 2 if calculated_type == :Decimal
112
- len += 1 if value < 0
113
- len
114
- when :Currency
115
- # (1,000,000.50)
116
- len = value.round(2).to_s.length + ::Math.log(value.abs, 1_000).floor
117
- len += 2 if value < 0
118
- len
119
- when :Date
120
- DATE_LENGTH
121
- when :Boolean
122
- BOOLEAN_LENGTH
123
- end
124
- end
125
-
126
- def unstyled?
127
- !styled?
128
- end
148
+ JAN_1_1900 = ::Time.parse '1900-01-01'
129
149
 
130
- def styled?
131
- data.is_a?(::Hash)
150
+ attr_reader :row
151
+ attr_reader :value
152
+ attr_reader :pixel_width
153
+ attr_reader :excel_type
154
+ attr_reader :excel_style_number
155
+ attr_reader :excel_value
156
+
157
+ def initialize(row, data)
158
+ @row = row
159
+ if data.is_a?(::Hash)
160
+ data = data.symbolize_keys
161
+ calculated_type = data[:type]
162
+ @value = data[:value]
163
+ else
164
+ @value = data
165
+ calculated_type = Cell.calculate_type @value
166
+ end
167
+ character_width = Cell.character_width @value, calculated_type
168
+ @pixel_width = Cell.pixel_width character_width
169
+ @excel_type = Cell.excel_type calculated_type
170
+ @excel_style_number = Cell.excel_style_number calculated_type
171
+ @excel_value = Cell.send "excel_#{calculated_type.to_s.underscore}", @value
132
172
  end
133
-
173
+
134
174
  def to_xml
135
175
  if value.blank?
136
176
  %{<c r="#{excel_column_letter}#{row.ndx}" s="0" t="inlineStr" />}
@@ -145,38 +185,5 @@ module XlsxWriter
145
185
  def excel_column_letter
146
186
  Cell.excel_column_letter row.cells.index(self)
147
187
  end
148
-
149
- # detect dates here, even if we're not styled
150
- def excel_type
151
- Cell.excel_type calculated_type
152
- end
153
-
154
- def excel_style_number
155
- Cell.excel_style_number calculated_type
156
- end
157
-
158
- def calculated_type
159
- @calculated_type ||= if styled?
160
- data[:type]
161
- elsif value.is_a?(::Date)
162
- :Date
163
- elsif value.is_a?(::Integer)
164
- :Integer
165
- elsif value.is_a?(::Float) or (defined?(::BigDecimal) and value.is_a?(::BigDecimal)) or (defined?(::Decimal) and value.is_a?(::Decimal))
166
- :Decimal
167
- elsif value.is_a?(::Numeric)
168
- :Number
169
- else
170
- :String
171
- end
172
- end
173
-
174
- def value
175
- styled? ? data[:value] : data
176
- end
177
-
178
- def excel_value
179
- Cell.send "excel_#{calculated_type.to_s.underscore}", value
180
- end
181
188
  end
182
189
  end
@@ -11,6 +11,23 @@ module XlsxWriter
11
11
  end
12
12
  end
13
13
  end
14
+
15
+ attr_reader :staging_dir
16
+ attr_reader :sheets
17
+ attr_reader :images
18
+ attr_reader :page_setup
19
+ attr_reader :header_footer
20
+
21
+ def initialize
22
+ staging_dir = ::UnixUtils.tmp_path 'xlsx_writer'
23
+ ::FileUtils.mkdir_p staging_dir
24
+ @staging_dir = staging_dir
25
+ @sheets = []
26
+ @images = []
27
+ @page_setup = PageSetup.new
28
+ @header_footer = HeaderFooter.new
29
+ @mutex = ::Mutex.new
30
+ end
14
31
 
15
32
  def add_sheet(name)
16
33
  raise ::RuntimeError, "Can't add sheet, already generated!" if generated?
@@ -19,14 +36,6 @@ module XlsxWriter
19
36
  sheet
20
37
  end
21
38
 
22
- def page_setup
23
- @page_setup ||= PageSetup.new
24
- end
25
-
26
- def header_footer
27
- @header_footer ||= HeaderFooter.new self
28
- end
29
-
30
39
  delegate :header, :footer, :to => :header_footer
31
40
 
32
41
  def add_image(path, width, height)
@@ -37,44 +46,34 @@ module XlsxWriter
37
46
  end
38
47
 
39
48
  def path
40
- generate unless generated?
41
- @path
49
+ @path || @mutex.synchronize do
50
+ @path ||= begin
51
+ sheets.each(&:generate)
52
+ images.each(&:generate)
53
+ Document.auto.each do |part|
54
+ part.new(self).generate
55
+ end
56
+ with_zip_extname = ::UnixUtils.zip staging_dir
57
+ with_xlsx_extname = with_zip_extname.sub(/.zip$/, '.xlsx')
58
+ ::FileUtils.mv with_zip_extname, with_xlsx_extname
59
+ @generated = true
60
+ with_xlsx_extname
61
+ end
62
+ end
42
63
  end
43
64
 
44
65
  def cleanup
45
- ::File.unlink(@path) if ::File.exist?(@path)
46
- ::FileUtils.rm_rf(@staging_dir) if ::File.exist?(@staging_dir)
47
- @path = nil
48
- @staging_dir = nil
49
- @generated = false
50
- end
51
-
52
- def sheets #:nodoc:
53
- @sheets ||= []
54
- end
55
-
56
- def images
57
- @images ||= []
58
- end
59
-
60
- def staging_dir
61
- @staging_dir ||= Utils.tmp_path
62
- ::FileUtils.mkdir_p @staging_dir
63
- @staging_dir
66
+ ::FileUtils.rm_rf(@staging_dir) if ::File.exist?(@staging_dir.to_s)
67
+ if generated?
68
+ ::File.unlink(@path) if ::File.exist?(@path.to_s)
69
+ end
64
70
  end
65
-
66
- private
67
71
 
68
72
  def generate
69
- sheets.each(&:generate)
70
- images.each(&:generate)
71
- Document.auto.each do |part|
72
- part.new(self).generate
73
- end
74
- @path = Utils.zip staging_dir
75
- @generated = true
73
+ path
74
+ true
76
75
  end
77
-
76
+
78
77
  def generated?
79
78
  @generated == true
80
79
  end
@@ -1,24 +1,44 @@
1
1
  require 'fileutils'
2
+
2
3
  module XlsxWriter
3
- class Image < ::Struct.new(:document, :original_path, :width, :height, :lcr, :croptop, :cropleft)
4
-
4
+ class Image
5
+ DEFAULT = {
6
+ :croptop => 0,
7
+ :cropleft => 0
8
+ }
5
9
  AUTO = false
6
-
10
+
11
+ attr_reader :document
12
+ attr_reader :original_path
13
+ attr_reader :width
14
+ attr_reader :height
15
+ attr_accessor :lcr
16
+ attr_writer :croptop
17
+ attr_writer :cropleft
18
+
19
+ def initialize(document, original_path, width, height)
20
+ @document = document
21
+ @original_path = original_path
22
+ @width = width
23
+ @height = height
24
+ @mutex = ::Mutex.new
25
+ end
26
+
7
27
  def to_xml
8
28
  <<-EOS
9
29
  <v:shape id="#{id}" o:spid="#{o_spid}" type="#_x0000_t75" style="position:absolute;margin-left:0;margin-top:0;width:#{width}pt;height:#{height}pt;z-index:1">
10
- <v:imagedata o:relid="#{rid}" o:title="#{o_title}" croptop=#{croptop} cropleft=#{cropleft}/>
30
+ <v:imagedata o:relid="#{rid}" o:title="#{o_title}" croptop="#{croptop}" cropleft="#{cropleft}"/>
11
31
  <o:lock v:ext="edit" rotation="t"/>
12
32
  </v:shape>
13
33
  EOS
14
34
  end
15
35
 
16
36
  def croptop
17
- self[:croptop] || 0
37
+ @croptop || DEFAULT[:croptop]
18
38
  end
19
39
 
20
40
  def cropleft
21
- self[:cropleft] || 0
41
+ @cropleft || DEFAULT[:cropleft]
22
42
  end
23
43
 
24
44
  def id
@@ -28,9 +48,26 @@ EOS
28
48
  o_spid #?
29
49
  end
30
50
  end
31
-
51
+
32
52
  def generate
33
- ::FileUtils.cp original_path, staging_path
53
+ path
54
+ true
55
+ end
56
+
57
+ def generated?
58
+ @generated == true
59
+ end
60
+
61
+ def path
62
+ @path || @mutex.synchronize do
63
+ @path ||= begin
64
+ memo = ::File.join document.staging_dir, relative_path
65
+ ::FileUtils.mkdir_p ::File.dirname(memo)
66
+ ::FileUtils.cp original_path, memo
67
+ @generated = true
68
+ memo
69
+ end
70
+ end
34
71
  end
35
72
 
36
73
  def ndx
@@ -53,16 +90,8 @@ EOS
53
90
  "/#{relative_path}"
54
91
  end
55
92
 
56
- private
57
-
58
93
  def relative_path
59
94
  "xl/media/image#{ndx}.emf"
60
95
  end
61
-
62
- def staging_path
63
- p = ::File.join document.staging_dir, relative_path
64
- ::FileUtils.mkdir_p ::File.dirname(p)
65
- p
66
- end
67
96
  end
68
97
  end
@@ -15,10 +15,14 @@ module XlsxWriter
15
15
  AUTO = false
16
16
 
17
17
  attr_reader :name
18
+ attr_reader :rows
19
+ attr_reader :autofilters
18
20
 
19
21
  def initialize(document, name)
20
- @document = document
21
22
  @name = Sheet.excel_name name
23
+ @rows = []
24
+ @autofilters = []
25
+ super document
22
26
  end
23
27
 
24
28
  def ndx
@@ -38,20 +42,12 @@ module XlsxWriter
38
42
  "/#{relative_path}"
39
43
  end
40
44
 
41
- def autofilters
42
- @autofilters ||= []
43
- end
44
-
45
45
  # specify range like "A1:C1"
46
46
  def add_autofilter(range)
47
47
  raise ::RuntimeError, "Can't add autofilter, already generated!" if generated?
48
48
  autofilters << Autofilter.new(range)
49
49
  end
50
50
 
51
- def rows
52
- @rows ||= []
53
- end
54
-
55
51
  def add_row(data)
56
52
  raise ::RuntimeError, "Can't add row, already generated!" if generated?
57
53
  row = Row.new self, data
@@ -60,18 +56,23 @@ module XlsxWriter
60
56
  end
61
57
 
62
58
  # override Xml method to save memory
63
- def generate
64
- @path = staging_path
65
- ::File.open(@path, 'wb') do |out|
66
- to_file out
59
+ def path
60
+ @path || @mutex.synchronize do
61
+ @path ||= begin
62
+ memo = ::File.join document.staging_dir, relative_path
63
+ ::FileUtils.mkdir_p ::File.dirname(memo)
64
+ ::File.open(memo, 'wb') do |f|
65
+ to_file f
66
+ end
67
+ converted = UnixUtils.unix2dos memo
68
+ ::FileUtils.mv converted, memo
69
+ SheetRels.new(document, self).path
70
+ @generated = true
71
+ memo
72
+ end
67
73
  end
68
- Utils.unix2dos @path
69
- SheetRels.new(document, self).generate
70
- @generated = true
71
74
  end
72
-
73
- delegate :header_footer, :page_setup, :to => :document
74
-
75
+
75
76
  private
76
77
 
77
78
  # not using ERB to save memory
@@ -89,17 +90,25 @@ EOS
89
90
  rows.each { |row| f.puts row.to_xml }
90
91
  f.puts %{</sheetData>}
91
92
  autofilters.each { |autofilter| f.puts autofilter.to_xml }
92
- f.puts page_setup.to_xml
93
- f.puts header_footer.to_xml
93
+ f.puts document.page_setup.to_xml
94
+ f.puts document.header_footer.to_xml
94
95
  f.puts %{</worksheet>}
95
96
  end
96
97
 
97
98
  def max_length
98
- rows.max_by { |row| row.length }.length
99
+ if max = rows.max_by { |row| row.length }
100
+ max.length
101
+ else
102
+ 1
103
+ end
99
104
  end
100
105
 
101
106
  def max_cell_width(x)
102
- rows.max_by { |row| row.cell_width(x) }.cell_width(x)
107
+ if max = rows.max_by { |row| row.cell_width(x) }
108
+ max.cell_width x
109
+ else
110
+ Cell.pixel_width 5
111
+ end
103
112
  end
104
113
  end
105
114
  end
@@ -6,8 +6,8 @@ module XlsxWriter
6
6
  attr_reader :sheet
7
7
 
8
8
  def initialize(document, sheet)
9
- @document = document
10
9
  @sheet = sheet
10
+ super document
11
11
  end
12
12
 
13
13
  def relative_path
@@ -1,13 +1,13 @@
1
1
  module XlsxWriter
2
- class HeaderFooter < ::Struct.new(:document, :header, :footer)
3
- def header
4
- self[:header] ||= H.new self
5
- end
6
-
7
- def footer
8
- self[:footer] ||= F.new self
2
+ class HeaderFooter
3
+ attr_reader :header
4
+ attr_reader :footer
5
+
6
+ def initialize
7
+ @header = HF.new 'H', 'oddHeader'
8
+ @footer = HF.new 'F', 'oddFooter'
9
9
  end
10
-
10
+
11
11
  def to_xml
12
12
  lines = []
13
13
  lines << %{<headerFooter>}
@@ -20,25 +20,23 @@ module XlsxWriter
20
20
  lines.join("\n")
21
21
  end
22
22
 
23
- class HF < ::Struct.new(:header_footer, :left, :center, :right)
24
- def left
25
- self[:left] ||= L.new self
26
- end
23
+ class HF
24
+ attr_reader :id
25
+ attr_reader :tag
26
+ attr_reader :left
27
+ attr_reader :center
28
+ attr_reader :right
27
29
 
28
- def center
29
- self[:center] ||= C.new self
30
- end
31
-
32
- def right
33
- self[:right] ||= R.new self
34
- end
35
-
36
- def hf
37
- self.class.name.demodulize
30
+ def initialize(id, tag)
31
+ @id = id
32
+ @tag = tag
33
+ @left = LCR.new self, 'L'
34
+ @center = LCR.new self, 'C'
35
+ @right = LCR.new self, 'R'
38
36
  end
39
37
 
40
38
  def to_xml
41
- %{<#{tag}>#{parts.map(&:to_s).join}</#{tag}>}
39
+ %{<#{tag}>#{parts.map(&:code).join}</#{tag}>}
42
40
  end
43
41
 
44
42
  def parts
@@ -49,10 +47,19 @@ module XlsxWriter
49
47
  parts.any?(&:has_image?)
50
48
  end
51
49
 
52
- class LCR < ::Struct.new(:hf, :contents)
50
+ class LCR
53
51
  FONT = %{"Arial,Regular"}
54
52
  SIZE = 10
55
53
 
54
+ attr_accessor :contents
55
+ attr_reader :hf
56
+ attr_reader :id
57
+
58
+ def initialize(hf, id)
59
+ @hf = hf
60
+ @id = id
61
+ end
62
+
56
63
  def present?
57
64
  contents.present?
58
65
  end
@@ -61,12 +68,8 @@ module XlsxWriter
61
68
  ::Array.wrap(contents).any? { |v| v.is_a?(XlsxWriter::Image) }
62
69
  end
63
70
 
64
- def lcr
65
- self.class.name.demodulize
66
- end
67
-
68
71
  def image_id
69
- [ lcr, hf.hf ].join
72
+ [ id, hf.id ].join
70
73
  end
71
74
 
72
75
  def render
@@ -92,25 +95,10 @@ module XlsxWriter
92
95
  "K000000#{out}"
93
96
  end
94
97
 
95
- def to_s
96
- [ '', lcr, FONT, SIZE, render ].join('&amp;')
98
+ def code
99
+ [ '', id, FONT, SIZE, render ].join('&amp;')
97
100
  end
98
101
  end
99
-
100
- class L < LCR; end
101
- class C < LCR; end
102
- class R < LCR; end
103
- end
104
-
105
- class H < HF
106
- def tag
107
- 'oddHeader'
108
- end
109
- end
110
- class F < HF
111
- def tag
112
- 'oddFooter'
113
- end
114
102
  end
115
103
  end
116
104
  end
@@ -1,39 +1,22 @@
1
1
  module XlsxWriter
2
- class PageSetup < ::Struct.new(:top, :right, :bottom, :left, :header, :footer, :orientation, :vertical_dpi, :horizontal_dpi)
3
- def top
4
- self[:top] || 1.0
5
- end
6
-
7
- def right
8
- self[:right] || 0.75
9
- end
10
-
11
- def bottom
12
- self[:bottom] || 1.0
13
- end
14
-
15
- def left
16
- self[:left] || 0.75
17
- end
18
-
19
- def header
20
- self[:header] || 0.5
21
- end
22
-
23
- def footer
24
- self[:footer] || 0.5
25
- end
26
-
27
- def orientation
28
- self[:orientation] || 'landscape'
29
- end
30
-
31
- def vertical_dpi
32
- self[:vertical_dpi] || 4294967292
33
- end
34
-
35
- def horizontal_dpi
36
- self[:horizontal_dpi] || 4294967292
2
+ class PageSetup
3
+ DEFAULT = {
4
+ :top => 1.0,
5
+ :right => 0.75,
6
+ :bottom => 1.0,
7
+ :left => 0.75,
8
+ :header => 0.5,
9
+ :footer => 0.5,
10
+ :orientation => 'landscape',
11
+ :vertical_dpi => 4294967292,
12
+ :horizontal_dpi => 4294967292
13
+ }
14
+
15
+ DEFAULT.keys.each do |attr|
16
+ attr_writer attr
17
+ define_method attr do
18
+ instance_variable_get(:"@#{attr}") || DEFAULT[attr]
19
+ end
37
20
  end
38
21
 
39
22
  def to_xml
@@ -2,8 +2,10 @@ module XlsxWriter
2
2
  class Row
3
3
  attr_reader :sheet
4
4
  attr_reader :cells
5
+ attr_reader :width
5
6
 
6
7
  def initialize(sheet, columns)
8
+ @width = {}
7
9
  @sheet = sheet
8
10
  @cells = columns.map do |column|
9
11
  Cell.new self, column
@@ -19,7 +21,7 @@ module XlsxWriter
19
21
  end
20
22
 
21
23
  def cell_width(x)
22
- if cell = cells[x]
24
+ @width[x] ||= if (cell = cells[x])
23
25
  cell.pixel_width
24
26
  else
25
27
  0
@@ -35,8 +37,5 @@ module XlsxWriter
35
37
  ary << %{</row>}
36
38
  ary.join
37
39
  end
38
-
39
- extend ::ActiveSupport::Memoizable
40
- memoize :cell_width
41
40
  end
42
41
  end
@@ -1,3 +1,3 @@
1
1
  module XlsxWriter
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -4,26 +4,37 @@ require 'fileutils'
4
4
  module XlsxWriter
5
5
  class Xml
6
6
  attr_reader :document
7
-
7
+
8
8
  def initialize(document)
9
9
  @document = document
10
+ @mutex = ::Mutex.new
10
11
  end
11
-
12
- def path
13
- generate unless generated?
14
- @path
12
+
13
+ def generate
14
+ path
15
+ true
15
16
  end
16
-
17
+
17
18
  def generated?
18
19
  @generated == true
19
20
  end
20
21
 
21
- def staging_path
22
- p = ::File.join document.staging_dir, relative_path
23
- ::FileUtils.mkdir_p ::File.dirname(p)
24
- p
22
+ def path
23
+ @path || @mutex.synchronize do
24
+ @path ||= begin
25
+ memo = ::File.join document.staging_dir, relative_path
26
+ ::FileUtils.mkdir_p ::File.dirname(memo)
27
+ ::File.open(memo, 'wb') do |f|
28
+ f.write render
29
+ end
30
+ converted = ::UnixUtils.unix2dos memo
31
+ ::FileUtils.mv converted, memo
32
+ @generated = true
33
+ memo
34
+ end
35
+ end
25
36
  end
26
-
37
+
27
38
  def template_path
28
39
  ::File.expand_path "../generators/#{self.class.name.demodulize.underscore}.erb", __FILE__
29
40
  end
@@ -31,14 +42,5 @@ module XlsxWriter
31
42
  def render
32
43
  ::ERB.new(::File.read(template_path), nil, '<>').result(binding)
33
44
  end
34
-
35
- def generate
36
- @path = staging_path
37
- ::File.open(@path, 'wb') do |out|
38
- out.write render
39
- end
40
- Utils.unix2dos @path
41
- @generated = true
42
- end
43
45
  end
44
46
  end
data/lib/xlsx_writer.rb CHANGED
@@ -1,34 +1,30 @@
1
+ require 'thread'
1
2
  require 'active_support/core_ext'
2
-
3
- require 'xlsx_writer/version'
3
+ require 'unix_utils'
4
4
 
5
5
  module XlsxWriter
6
- def self.gem_dir
7
- ::File.join ::File.dirname(__FILE__), 'xlsx_writer'
8
- end
9
-
10
- autoload :Cell, "#{gem_dir}/cell"
11
- autoload :Document, "#{gem_dir}/document"
12
- autoload :Row, "#{gem_dir}/row"
13
- autoload :Utils, "#{gem_dir}/utils"
14
- autoload :Xml, "#{gem_dir}/xml"
15
- autoload :HeaderFooter, "#{gem_dir}/header_footer"
16
- autoload :Autofilter, "#{gem_dir}/autofilter"
17
- autoload :PageSetup, "#{gem_dir}/page_setup"
18
-
19
- # manual
20
- autoload :Sheet, "#{gem_dir}/generators/sheet"
21
- autoload :SheetRels, "#{gem_dir}/generators/sheet_rels"
22
- autoload :Image, "#{gem_dir}/generators/image"
23
-
24
- # generators
25
- autoload :App, "#{gem_dir}/generators/app"
26
- autoload :ContentTypes, "#{gem_dir}/generators/content_types"
27
- autoload :DocProps, "#{gem_dir}/generators/doc_props"
28
- autoload :Rels, "#{gem_dir}/generators/rels"
29
- autoload :Styles, "#{gem_dir}/generators/styles"
30
- autoload :Workbook, "#{gem_dir}/generators/workbook"
31
- autoload :WorkbookRels, "#{gem_dir}/generators/workbook_rels"
32
- autoload :VmlDrawing, "#{gem_dir}/generators/vml_drawing"
33
- autoload :VmlDrawingRels, "#{gem_dir}/generators/vml_drawing_rels"
34
6
  end
7
+
8
+ require 'xlsx_writer/cell'
9
+ require 'xlsx_writer/document'
10
+ require 'xlsx_writer/row'
11
+ require 'xlsx_writer/xml'
12
+ require 'xlsx_writer/header_footer'
13
+ require 'xlsx_writer/autofilter'
14
+ require 'xlsx_writer/page_setup'
15
+
16
+ # 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'
data/test/helper.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'minitest/spec'
5
+ require 'minitest/autorun'
6
+ require 'minitest/reporters'
7
+ MiniTest::Unit.runner = MiniTest::SuiteRunner.new
8
+ MiniTest::Unit.runner.reporters << MiniTest::Reporters::SpecReporter.new
9
+
10
+ require 'remote_table'
11
+
12
+ require 'xlsx_writer'
Binary file
Binary file
@@ -0,0 +1,110 @@
1
+ # encoding: UTF-8
2
+ require 'helper'
3
+
4
+ describe XlsxWriter do
5
+ describe "hello world example" do
6
+ before do
7
+ @doc = XlsxWriter::Document.new
8
+ @sheet1 = @doc.add_sheet("People")
9
+ @sheet1.add_row(['header1', 'header2'])
10
+ @sheet1.add_row(['hello', 'world'])
11
+ end
12
+ after do
13
+ @doc.cleanup
14
+ end
15
+ it "returns a path to an xlsx" do
16
+ File.exist?(@doc.path).must_equal true
17
+ File.extname(@doc.path).must_equal '.xlsx'
18
+ end
19
+ it "is a readable xlsx" do
20
+ RemoteTable.new("file://#{@doc.path}", :format => :xlsx).rows.first.must_equal('header1' => 'hello', 'header2' => 'world')
21
+ end
22
+ it "only generates once" do
23
+ @doc.generate
24
+ mtime = File.mtime(@doc.path)
25
+ md5 = UnixUtils.md5sum(@doc.path)
26
+ @doc.generate
27
+ File.mtime(@doc.path).must_equal mtime
28
+ UnixUtils.md5sum(@doc.path).must_equal md5
29
+ end
30
+ it "won't accept new sheets once it's been generated" do
31
+ @doc.add_sheet 'okfine'
32
+ @doc.generate
33
+ lambda {
34
+ @doc.add_sheet 'toolate'
35
+ }.must_raise(RuntimeError, /already generated/)
36
+ end
37
+ it "won't accept new rows once it's been generated" do
38
+ @sheet1.add_row(['phew'])
39
+ @doc.generate
40
+ lambda {
41
+ @sheet1.add_row(['too', 'late', 'to', 'apologize'])
42
+ }.must_raise(RuntimeError, /already generated/)
43
+ end
44
+ it "automatically generates if you call #path" do
45
+ @doc.generated?.must_equal false
46
+ @doc.path
47
+ @doc.generated?.must_equal true
48
+ end
49
+ end
50
+
51
+ describe "example with autofilter, header image, and footer text" do
52
+ before do
53
+ @doc = XlsxWriter::Document.new
54
+ sheet1 = @doc.add_sheet("People")
55
+ # DATA
56
+ sheet1.add_row(%w{DoB Name Occupation Number Integer Float})
57
+ sheet1.add_row([
58
+ Date.parse("July 31, 1912"),
59
+ "Milton Friedman",
60
+ "Economist / Statistician",
61
+ {:type => :Currency, :value => 99_000_000},
62
+ 500_000,
63
+ 500_000.00,
64
+ ])
65
+ sheet1.add_autofilter 'A1:F1'
66
+ # FORMATTING
67
+ @doc.page_setup.top = 1.5
68
+ # hint: set up your header/footer in Excel, save, unzip the xlsx, get the .emf files, croptop, etc. from there
69
+ left_header_image = @doc.add_image(File.expand_path('../support/image1.emf', __FILE__), 118, 107)
70
+ left_header_image.croptop = '11025f'
71
+ left_header_image.cropleft = '9997f'
72
+ center_footer_image = @doc.add_image(File.expand_path('../support/image2.emf', __FILE__), 116, 36)
73
+ @doc.page_setup.header = 0
74
+ @doc.page_setup.footer = 0
75
+ @doc.header.left.contents = left_header_image
76
+ @doc.header.right.contents = 'Reporting Program'
77
+ @doc.footer.center.contents = [ 'Powered by ', center_footer_image ]
78
+ @doc.footer.right.contents = :page_x_of_y
79
+ end
80
+ after do
81
+ @doc.cleanup
82
+ end
83
+ it "has an autofilter" do
84
+ contents = UnixUtils.unzip @doc.path
85
+ File.read("#{contents}/xl/worksheets/sheet1.xml").must_include %{<autoFilter ref="A1:F1" />}
86
+ end
87
+ it "has a header image" do
88
+ contents = UnixUtils.unzip @doc.path
89
+ File.read("#{contents}/xl/drawings/_rels/vmlDrawing1.vml.rels").must_include %{<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="/xl/media/image1.emf"/>}
90
+ File.read("#{contents}/xl/drawings/vmlDrawing1.vml").must_include %{<v:imagedata o:relid="rId1" o:title="image1.emf" croptop="11025f" cropleft="9997f"/>}
91
+ original = UnixUtils.md5sum File.expand_path("../support/image1.emf", __FILE__)
92
+ UnixUtils.md5sum("#{contents}/xl/media/image1.emf").must_equal original
93
+ end
94
+ end
95
+
96
+ describe 'Cell' do
97
+ describe :round do
98
+ it "works same in 1.8 and 1.9" do
99
+ XlsxWriter::Cell.round(12.3456, 1).must_equal 12.3
100
+ XlsxWriter::Cell.round(12.3456, 2).must_equal 12.35
101
+ end
102
+ end
103
+ describe :log_base do
104
+ it "works same in 1.8 and 1.9" do
105
+ XlsxWriter::Cell.log_base(4, 1e3).must_be_close_to 0.2
106
+ XlsxWriter::Cell.log_base(4, 12.345).must_be_close_to 0.552
107
+ end
108
+ end
109
+ end
110
+ end
data/xlsx_writer.gemspec CHANGED
@@ -1,6 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
3
- require 'xlsx_writer/version'
2
+ require File.expand_path('../lib/xlsx_writer/version', __FILE__)
4
3
 
5
4
  Gem::Specification.new do |s|
6
5
  s.name = "xlsx_writer"
@@ -13,7 +12,7 @@ Gem::Specification.new do |s|
13
12
 
14
13
  s.add_runtime_dependency 'activesupport'
15
14
  s.add_runtime_dependency 'fast_xs'
16
- s.add_runtime_dependency 'posix-spawn'
15
+ s.add_runtime_dependency 'unix_utils'
17
16
 
18
17
  s.files = `git ls-files`.split("\n")
19
18
  s.test_files = `git ls-files -- {test,features}/*`.split("\n")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xlsx_writer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,11 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2011-12-15 00:00:00.000000000Z
14
+ date: 2012-04-24 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
18
- requirement: &2182482680 !ruby/object:Gem::Requirement
18
+ requirement: !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
21
  - - ! '>='
@@ -23,10 +23,15 @@ dependencies:
23
23
  version: '0'
24
24
  type: :runtime
25
25
  prerelease: false
26
- version_requirements: *2182482680
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: fast_xs
29
- requirement: &2182482200 !ruby/object:Gem::Requirement
34
+ requirement: !ruby/object:Gem::Requirement
30
35
  none: false
31
36
  requirements:
32
37
  - - ! '>='
@@ -34,10 +39,15 @@ dependencies:
34
39
  version: '0'
35
40
  type: :runtime
36
41
  prerelease: false
37
- version_requirements: *2182482200
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
38
48
  - !ruby/object:Gem::Dependency
39
- name: posix-spawn
40
- requirement: &2182481720 !ruby/object:Gem::Requirement
49
+ name: unix_utils
50
+ requirement: !ruby/object:Gem::Requirement
41
51
  none: false
42
52
  requirements:
43
53
  - - ! '>='
@@ -45,7 +55,12 @@ dependencies:
45
55
  version: '0'
46
56
  type: :runtime
47
57
  prerelease: false
48
- version_requirements: *2182481720
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
49
64
  description: Writes XLSX files. Minimal XML and style. Supports autofilters and headers/footers
50
65
  with images and page numbers.
51
66
  email:
@@ -56,12 +71,12 @@ extra_rdoc_files: []
56
71
  files:
57
72
  - .bundle/config
58
73
  - .gitignore
74
+ - CHANGELOG
59
75
  - Gemfile
60
76
  - LICENSE
61
77
  - README.markdown
62
78
  - Rakefile
63
79
  - lib/xlsx_writer.rb
64
- - lib/xlsx_writer/.DS_Store
65
80
  - lib/xlsx_writer/autofilter.rb
66
81
  - lib/xlsx_writer/cell.rb
67
82
  - lib/xlsx_writer/document.rb
@@ -90,9 +105,12 @@ files:
90
105
  - lib/xlsx_writer/header_footer.rb
91
106
  - lib/xlsx_writer/page_setup.rb
92
107
  - lib/xlsx_writer/row.rb
93
- - lib/xlsx_writer/utils.rb
94
108
  - lib/xlsx_writer/version.rb
95
109
  - lib/xlsx_writer/xml.rb
110
+ - test/helper.rb
111
+ - test/support/image1.emf
112
+ - test/support/image2.emf
113
+ - test/test_xlsx_writer.rb
96
114
  - xlsx_writer.gemspec
97
115
  homepage: https://github.com/seamusabshere/xlsx_writer
98
116
  licenses: []
@@ -114,9 +132,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
132
  version: '0'
115
133
  requirements: []
116
134
  rubyforge_project:
117
- rubygems_version: 1.8.10
135
+ rubygems_version: 1.8.21
118
136
  signing_key:
119
137
  specification_version: 3
120
138
  summary: Writes XLSX files. Minimal XML and style. Supports autofilters and headers/footers
121
139
  with images and page numbers.
122
- test_files: []
140
+ test_files:
141
+ - test/helper.rb
142
+ - test/support/image1.emf
143
+ - test/support/image2.emf
144
+ - test/test_xlsx_writer.rb
145
+ has_rdoc:
Binary file
@@ -1,34 +0,0 @@
1
- require 'fileutils'
2
- require 'tmpdir'
3
- require 'posix/spawn'
4
-
5
- module XlsxWriter
6
- module Utils
7
- def self.tmp_path(basename = nil, extname = nil)
8
- ::Kernel.srand
9
- ::File.join ::Dir.tmpdir, "XlsxWriter-#{basename}#{::Kernel.rand(99999999)}#{extname ? ".#{extname}" : ''}"
10
- end
11
-
12
- # zip -r -q #{filename} .
13
- def self.zip(src_dir)
14
- out_path = tmp_path('zip', 'zip')
15
- child = ::POSIX::Spawn::Child.new 'zip', '-rq', out_path, '.', :chdir => src_dir
16
- if child.success?
17
- out_path
18
- else
19
- raise ::RuntimeError, child.err
20
- end
21
- end
22
-
23
- # use awk to convert [CR]LF to CRLF
24
- def self.unix2dos(path)
25
- out_path = tmp_path
26
- ::File.open(out_path, 'wb') do |out|
27
- pid = ::POSIX::Spawn.spawn 'awk', '{ sub(/\r?$/,"\r"); print }', path, :out => out
28
- ::Process.waitpid pid
29
- end
30
- ::FileUtils.mv out_path, path
31
- path
32
- end
33
- end
34
- end