xlsx_writer 0.1.2 → 0.2.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/.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