roo 1.13.2 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.simplecov +4 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG.md +515 -0
  6. data/Gemfile +16 -10
  7. data/Guardfile +24 -0
  8. data/LICENSE +3 -1
  9. data/README.md +254 -0
  10. data/Rakefile +23 -23
  11. data/examples/roo_soap_client.rb +28 -31
  12. data/examples/roo_soap_server.rb +4 -6
  13. data/examples/write_me.rb +9 -10
  14. data/lib/roo/base.rb +334 -395
  15. data/lib/roo/csv.rb +120 -113
  16. data/lib/roo/excelx/cell.rb +77 -0
  17. data/lib/roo/excelx/comments.rb +22 -0
  18. data/lib/roo/excelx/extractor.rb +22 -0
  19. data/lib/roo/excelx/relationships.rb +25 -0
  20. data/lib/roo/excelx/shared_strings.rb +37 -0
  21. data/lib/roo/excelx/sheet.rb +107 -0
  22. data/lib/roo/excelx/sheet_doc.rb +200 -0
  23. data/lib/roo/excelx/styles.rb +64 -0
  24. data/lib/roo/excelx/workbook.rb +59 -0
  25. data/lib/roo/excelx.rb +413 -597
  26. data/lib/roo/font.rb +17 -0
  27. data/lib/roo/libre_office.rb +5 -0
  28. data/lib/roo/link.rb +15 -0
  29. data/lib/roo/{openoffice.rb → open_office.rb} +681 -496
  30. data/lib/roo/spreadsheet.rb +20 -23
  31. data/lib/roo/utils.rb +78 -0
  32. data/lib/roo/version.rb +3 -0
  33. data/lib/roo.rb +18 -24
  34. data/roo.gemspec +20 -204
  35. data/spec/lib/roo/base_spec.rb +1 -4
  36. data/spec/lib/roo/csv_spec.rb +21 -13
  37. data/spec/lib/roo/excelx/format_spec.rb +7 -6
  38. data/spec/lib/roo/excelx_spec.rb +424 -11
  39. data/spec/lib/roo/libreoffice_spec.rb +16 -6
  40. data/spec/lib/roo/openoffice_spec.rb +13 -8
  41. data/spec/lib/roo/spreadsheet_spec.rb +40 -12
  42. data/spec/lib/roo/utils_spec.rb +106 -0
  43. data/spec/spec_helper.rb +2 -1
  44. data/test/test_generic_spreadsheet.rb +117 -139
  45. data/test/test_helper.rb +9 -56
  46. data/test/test_roo.rb +274 -478
  47. metadata +65 -303
  48. data/CHANGELOG +0 -417
  49. data/Gemfile.lock +0 -78
  50. data/README.markdown +0 -126
  51. data/VERSION +0 -1
  52. data/lib/roo/excel.rb +0 -355
  53. data/lib/roo/excel2003xml.rb +0 -300
  54. data/lib/roo/google.rb +0 -292
  55. data/lib/roo/roo_rails_helper.rb +0 -83
  56. data/lib/roo/worksheet.rb +0 -18
  57. data/scripts/txt2html +0 -67
  58. data/spec/lib/roo/excel2003xml_spec.rb +0 -15
  59. data/spec/lib/roo/excel_spec.rb +0 -17
  60. data/spec/lib/roo/google_spec.rb +0 -64
  61. data/test/files/1900_base.xls +0 -0
  62. data/test/files/1900_base.xlsx +0 -0
  63. data/test/files/1904_base.xls +0 -0
  64. data/test/files/1904_base.xlsx +0 -0
  65. data/test/files/Bibelbund.csv +0 -3741
  66. data/test/files/Bibelbund.ods +0 -0
  67. data/test/files/Bibelbund.xls +0 -0
  68. data/test/files/Bibelbund.xlsx +0 -0
  69. data/test/files/Bibelbund.xml +0 -62518
  70. data/test/files/Bibelbund1.ods +0 -0
  71. data/test/files/Pfand_from_windows_phone.xlsx +0 -0
  72. data/test/files/bad_excel_date.xls +0 -0
  73. data/test/files/bbu.ods +0 -0
  74. data/test/files/bbu.xls +0 -0
  75. data/test/files/bbu.xlsx +0 -0
  76. data/test/files/bbu.xml +0 -152
  77. data/test/files/bode-v1.ods.zip +0 -0
  78. data/test/files/bode-v1.xls.zip +0 -0
  79. data/test/files/boolean.csv +0 -2
  80. data/test/files/boolean.ods +0 -0
  81. data/test/files/boolean.xls +0 -0
  82. data/test/files/boolean.xlsx +0 -0
  83. data/test/files/boolean.xml +0 -112
  84. data/test/files/borders.ods +0 -0
  85. data/test/files/borders.xls +0 -0
  86. data/test/files/borders.xlsx +0 -0
  87. data/test/files/borders.xml +0 -144
  88. data/test/files/bug-numbered-sheet-names.xlsx +0 -0
  89. data/test/files/bug-row-column-fixnum-float.xls +0 -0
  90. data/test/files/bug-row-column-fixnum-float.xml +0 -127
  91. data/test/files/comments.ods +0 -0
  92. data/test/files/comments.xls +0 -0
  93. data/test/files/comments.xlsx +0 -0
  94. data/test/files/csvtypes.csv +0 -1
  95. data/test/files/datetime.ods +0 -0
  96. data/test/files/datetime.xls +0 -0
  97. data/test/files/datetime.xlsx +0 -0
  98. data/test/files/datetime.xml +0 -142
  99. data/test/files/datetime_floatconv.xls +0 -0
  100. data/test/files/datetime_floatconv.xml +0 -148
  101. data/test/files/dreimalvier.ods +0 -0
  102. data/test/files/emptysheets.ods +0 -0
  103. data/test/files/emptysheets.xls +0 -0
  104. data/test/files/emptysheets.xlsx +0 -0
  105. data/test/files/emptysheets.xml +0 -105
  106. data/test/files/excel2003.xml +0 -21140
  107. data/test/files/false_encoding.xls +0 -0
  108. data/test/files/false_encoding.xml +0 -132
  109. data/test/files/file_item_error.xlsx +0 -0
  110. data/test/files/formula.ods +0 -0
  111. data/test/files/formula.xls +0 -0
  112. data/test/files/formula.xlsx +0 -0
  113. data/test/files/formula.xml +0 -134
  114. data/test/files/formula_parse_error.xls +0 -0
  115. data/test/files/formula_parse_error.xml +0 -1833
  116. data/test/files/formula_string_error.xlsx +0 -0
  117. data/test/files/html-escape.ods +0 -0
  118. data/test/files/link.xls +0 -0
  119. data/test/files/link.xlsx +0 -0
  120. data/test/files/matrix.ods +0 -0
  121. data/test/files/matrix.xls +0 -0
  122. data/test/files/named_cells.ods +0 -0
  123. data/test/files/named_cells.xls +0 -0
  124. data/test/files/named_cells.xlsx +0 -0
  125. data/test/files/no_spreadsheet_file.txt +0 -1
  126. data/test/files/numbers1.csv +0 -18
  127. data/test/files/numbers1.ods +0 -0
  128. data/test/files/numbers1.xls +0 -0
  129. data/test/files/numbers1.xlsx +0 -0
  130. data/test/files/numbers1.xml +0 -312
  131. data/test/files/numeric-link.xlsx +0 -0
  132. data/test/files/only_one_sheet.ods +0 -0
  133. data/test/files/only_one_sheet.xls +0 -0
  134. data/test/files/only_one_sheet.xlsx +0 -0
  135. data/test/files/only_one_sheet.xml +0 -67
  136. data/test/files/paragraph.ods +0 -0
  137. data/test/files/paragraph.xls +0 -0
  138. data/test/files/paragraph.xlsx +0 -0
  139. data/test/files/paragraph.xml +0 -127
  140. data/test/files/prova.xls +0 -0
  141. data/test/files/ric.ods +0 -0
  142. data/test/files/simple_spreadsheet.ods +0 -0
  143. data/test/files/simple_spreadsheet.xls +0 -0
  144. data/test/files/simple_spreadsheet.xlsx +0 -0
  145. data/test/files/simple_spreadsheet.xml +0 -225
  146. data/test/files/simple_spreadsheet_from_italo.ods +0 -0
  147. data/test/files/simple_spreadsheet_from_italo.xls +0 -0
  148. data/test/files/simple_spreadsheet_from_italo.xml +0 -242
  149. data/test/files/so_datetime.csv +0 -7
  150. data/test/files/style.ods +0 -0
  151. data/test/files/style.xls +0 -0
  152. data/test/files/style.xlsx +0 -0
  153. data/test/files/style.xml +0 -154
  154. data/test/files/time-test.csv +0 -2
  155. data/test/files/time-test.ods +0 -0
  156. data/test/files/time-test.xls +0 -0
  157. data/test/files/time-test.xlsx +0 -0
  158. data/test/files/time-test.xml +0 -131
  159. data/test/files/type_excel.ods +0 -0
  160. data/test/files/type_excel.xlsx +0 -0
  161. data/test/files/type_excelx.ods +0 -0
  162. data/test/files/type_excelx.xls +0 -0
  163. data/test/files/type_openoffice.xls +0 -0
  164. data/test/files/type_openoffice.xlsx +0 -0
  165. data/test/files/whitespace.ods +0 -0
  166. data/test/files/whitespace.xls +0 -0
  167. data/test/files/whitespace.xlsx +0 -0
  168. data/test/files/whitespace.xml +0 -184
  169. data/test/rm_sub_test.rb +0 -12
  170. data/test/rm_test.rb +0 -7
  171. data/website/index.html +0 -385
  172. data/website/index.txt +0 -423
  173. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  174. data/website/stylesheets/screen.css +0 -130
  175. data/website/template.rhtml +0 -48
data/lib/roo/csv.rb CHANGED
@@ -1,113 +1,120 @@
1
- require 'csv'
2
- require 'time'
3
-
4
- # The CSV class can read csv files (must be separated with commas) which then
5
- # can be handled like spreadsheets. This means you can access cells like A5
6
- # within these files.
7
- # The CSV class provides only string objects. If you want conversions to other
8
- # types you have to do it yourself.
9
- #
10
- # You can pass options to the underlying CSV parse operation, via the
11
- # :csv_options option.
12
- #
13
-
14
- class Roo::CSV < Roo::Base
15
- def initialize(filename, options = {})
16
- super
17
- end
18
-
19
- attr_reader :filename
20
-
21
- # Returns an array with the names of the sheets. In CSV class there is only
22
- # one dummy sheet, because a csv file cannot have more than one sheet.
23
- def sheets
24
- ['default']
25
- end
26
-
27
- def cell(row, col, sheet=nil)
28
- sheet ||= @default_sheet
29
- read_cells(sheet)
30
- @cell[normalize(row,col)]
31
- end
32
-
33
- def celltype(row, col, sheet=nil)
34
- sheet ||= @default_sheet
35
- read_cells(sheet)
36
- @cell_type[normalize(row,col)]
37
- end
38
-
39
- def cell_postprocessing(row,col,value)
40
- value
41
- end
42
-
43
- def csv_options
44
- @options[:csv_options] || {}
45
- end
46
-
47
- private
48
-
49
- TYPE_MAP = {
50
- String => :string,
51
- Float => :float,
52
- Date => :date,
53
- DateTime => :datetime,
54
- }
55
-
56
- def celltype_class(value)
57
- TYPE_MAP[value.class]
58
- end
59
-
60
- def each_row(options, &block)
61
- if uri?(filename)
62
- make_tmpdir do |tmpdir|
63
- tmp_filename = download_uri(filename, tmpdir)
64
- CSV.foreach(tmp_filename, options, &block)
65
- end
66
- else
67
- CSV.foreach(filename, options, &block)
68
- end
69
- end
70
-
71
- def read_cells(sheet=nil)
72
- sheet ||= @default_sheet
73
- return if @cells_read[sheet]
74
- @first_row[sheet] = 1
75
- @last_row[sheet] = 0
76
- @first_column[sheet] = 1
77
- @last_column[sheet] = 1
78
- rownum = 1
79
- each_row csv_options do |row|
80
- row.each_with_index do |elem,i|
81
- @cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
82
- @cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
83
- if i+1 > @last_column[sheet]
84
- @last_column[sheet] += 1
85
- end
86
- end
87
- rownum += 1
88
- @last_row[sheet] += 1
89
- end
90
- @cells_read[sheet] = true
91
- #-- adjust @first_row if neccessary
92
- while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
93
- @first_row[sheet] += 1
94
- end
95
- #-- adjust @last_row if neccessary
96
- while !row(@last_row[sheet]).any? and @last_row[sheet] and
97
- @last_row[sheet] > @first_row[sheet]
98
- @last_row[sheet] -= 1
99
- end
100
- #-- adjust @first_column if neccessary
101
- while !column(@first_column[sheet]).any? and
102
- @first_column[sheet] and
103
- @first_column[sheet] < @last_column[sheet]
104
- @first_column[sheet] += 1
105
- end
106
- #-- adjust @last_column if neccessary
107
- while !column(@last_column[sheet]).any? and
108
- @last_column[sheet] and
109
- @last_column[sheet] > @first_column[sheet]
110
- @last_column[sheet] -= 1
111
- end
112
- end
113
- end
1
+ require 'csv'
2
+ require 'time'
3
+
4
+ # The CSV class can read csv files (must be separated with commas) which then
5
+ # can be handled like spreadsheets. This means you can access cells like A5
6
+ # within these files.
7
+ # The CSV class provides only string objects. If you want conversions to other
8
+ # types you have to do it yourself.
9
+ #
10
+ # You can pass options to the underlying CSV parse operation, via the
11
+ # :csv_options option.
12
+ #
13
+
14
+ class Roo::CSV < Roo::Base
15
+
16
+ attr_reader :filename
17
+
18
+ # Returns an array with the names of the sheets. In CSV class there is only
19
+ # one dummy sheet, because a csv file cannot have more than one sheet.
20
+ def sheets
21
+ ['default']
22
+ end
23
+
24
+ def cell(row, col, sheet=nil)
25
+ sheet ||= default_sheet
26
+ read_cells(sheet)
27
+ @cell[normalize(row,col)]
28
+ end
29
+
30
+ def celltype(row, col, sheet=nil)
31
+ sheet ||= default_sheet
32
+ read_cells(sheet)
33
+ @cell_type[normalize(row,col)]
34
+ end
35
+
36
+ def cell_postprocessing(row,col,value)
37
+ value
38
+ end
39
+
40
+ def csv_options
41
+ @options[:csv_options] || {}
42
+ end
43
+
44
+ private
45
+
46
+ TYPE_MAP = {
47
+ String => :string,
48
+ Float => :float,
49
+ Date => :date,
50
+ DateTime => :datetime,
51
+ }
52
+
53
+ def celltype_class(value)
54
+ TYPE_MAP[value.class]
55
+ end
56
+
57
+ def each_row(options, &block)
58
+ if uri?(filename)
59
+ make_tmpdir do |tmpdir|
60
+ tmp_filename = download_uri(filename, tmpdir)
61
+ CSV.foreach(tmp_filename, options, &block)
62
+ end
63
+ else
64
+ CSV.foreach(filename, options, &block)
65
+ end
66
+ end
67
+
68
+ def read_cells(sheet = default_sheet)
69
+ sheet ||= default_sheet
70
+ return if @cells_read[sheet]
71
+ @first_row[sheet] = 1
72
+ @last_row[sheet] = 0
73
+ @first_column[sheet] = 1
74
+ @last_column[sheet] = 1
75
+ rownum = 1
76
+ each_row csv_options do |row|
77
+ row.each_with_index do |elem,i|
78
+ @cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
79
+ @cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
80
+ if i+1 > @last_column[sheet]
81
+ @last_column[sheet] += 1
82
+ end
83
+ end
84
+ rownum += 1
85
+ @last_row[sheet] += 1
86
+ end
87
+ @cells_read[sheet] = true
88
+ #-- adjust @first_row if neccessary
89
+ while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
90
+ @first_row[sheet] += 1
91
+ end
92
+ #-- adjust @last_row if neccessary
93
+ while !row(@last_row[sheet]).any? and @last_row[sheet] and
94
+ @last_row[sheet] > @first_row[sheet]
95
+ @last_row[sheet] -= 1
96
+ end
97
+ #-- adjust @first_column if neccessary
98
+ while !column(@first_column[sheet]).any? and
99
+ @first_column[sheet] and
100
+ @first_column[sheet] < @last_column[sheet]
101
+ @first_column[sheet] += 1
102
+ end
103
+ #-- adjust @last_column if neccessary
104
+ while !column(@last_column[sheet]).any? and
105
+ @last_column[sheet] and
106
+ @last_column[sheet] > @first_column[sheet]
107
+ @last_column[sheet] -= 1
108
+ end
109
+ end
110
+
111
+ def clean_sheet(sheet)
112
+ read_cells(sheet)
113
+
114
+ @cell.each_pair do |coord, value|
115
+ @cell[coord] = sanitize_value(value) if value.is_a?(::String)
116
+ end
117
+
118
+ @cleaned[sheet] = true
119
+ end
120
+ end
@@ -0,0 +1,77 @@
1
+ require 'date'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Cell
6
+ attr_reader :type, :formula, :value, :excelx_type, :excelx_value, :style, :hyperlink, :coordinate
7
+ attr_writer :value
8
+
9
+ def initialize(value, type, formula, excelx_type, excelx_value, style, hyperlink, base_date, coordinate)
10
+ @type = type
11
+ @formula = formula
12
+ @base_date = base_date if [:date, :datetime].include?(@type)
13
+ @excelx_type = excelx_type
14
+ @excelx_value = excelx_value
15
+ @style = style
16
+ @value = type_cast_value(value)
17
+ @value = Roo::Link.new(hyperlink, @value.to_s) if hyperlink
18
+ @coordinate = coordinate
19
+ end
20
+
21
+ def type
22
+ case
23
+ when @formula
24
+ :formula
25
+ when @value.is_a?(Roo::Link)
26
+ :link
27
+ else
28
+ @type
29
+ end
30
+ end
31
+
32
+ class Coordinate
33
+ attr_accessor :row, :column
34
+
35
+ def initialize(row, column)
36
+ @row, @column = row, column
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def type_cast_value(value)
43
+ case @type
44
+ when :float, :percentage
45
+ value.to_f
46
+ when :date
47
+ create_date(@base_date + value.to_i)
48
+ when :datetime
49
+ create_datetime(@base_date + value.to_f.round(6))
50
+ when :time
51
+ value.to_f * 86_400
52
+ else
53
+ value
54
+ end
55
+ end
56
+
57
+ def create_date(date)
58
+ yyyy, mm, dd = date.strftime('%Y-%m-%d').split('-')
59
+
60
+ Date.new(yyyy.to_i, mm.to_i, dd.to_i)
61
+ end
62
+
63
+ def create_datetime(date)
64
+ datetime_string = date.strftime('%Y-%m-%d %H:%M:%S.%N')
65
+ t = round_datetime(datetime_string)
66
+
67
+ DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
68
+ end
69
+
70
+ def round_datetime(datetime_string)
71
+ /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
72
+
73
+ Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,22 @@
1
+ require 'roo/excelx/extractor'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Comments < Excelx::Extractor
6
+ def comments
7
+ @comments ||= extract_comments
8
+ end
9
+
10
+ private
11
+
12
+ def extract_comments
13
+ return {} unless doc_exists?
14
+
15
+ Hash[doc.xpath('//comments/commentList/comment').map do |comment|
16
+ value = (comment.at_xpath('./text/r/t') || comment.at_xpath('./text/t')).text
17
+ [::Roo::Utils.ref_to_key(comment.attributes['ref'].to_s), value]
18
+ end]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Roo
2
+ class Excelx
3
+ class Extractor
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ private
9
+
10
+ def doc
11
+ @doc ||=
12
+ if doc_exists?
13
+ ::Roo::Utils.load_xml(@path).remove_namespaces!
14
+ end
15
+ end
16
+
17
+ def doc_exists?
18
+ @path && File.exist?(@path)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'roo/excelx/extractor'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Relationships < Excelx::Extractor
6
+ def [](index)
7
+ to_a[index]
8
+ end
9
+
10
+ def to_a
11
+ @relationships ||= extract_relationships
12
+ end
13
+
14
+ private
15
+
16
+ def extract_relationships
17
+ return [] unless doc_exists?
18
+
19
+ Hash[doc.xpath('/Relationships/Relationship').map do |rel|
20
+ [rel.attribute('Id').text, rel]
21
+ end]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ require 'roo/excelx/extractor'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class SharedStrings < Excelx::Extractor
6
+ def [](index)
7
+ to_a[index]
8
+ end
9
+
10
+ def to_a
11
+ @array ||= extract_shared_strings
12
+ end
13
+
14
+ private
15
+
16
+ def extract_shared_strings
17
+ return [] unless doc_exists?
18
+
19
+ # read the shared strings xml document
20
+ doc.xpath('/sst/si').map do |si|
21
+ shared_string = ''
22
+ si.children.each do |elem|
23
+ case elem.name
24
+ when 'r'
25
+ elem.children.each do |r_elem|
26
+ shared_string << r_elem.content if r_elem.name == 't'
27
+ end
28
+ when 't'
29
+ shared_string = elem.content
30
+ end
31
+ end
32
+ shared_string
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,107 @@
1
+ module Roo
2
+ class Excelx
3
+ class Sheet
4
+ def initialize(name, rels_path, sheet_path, comments_path, styles, shared_strings, workbook, options = {})
5
+ @name = name
6
+ @rels = Relationships.new(rels_path)
7
+ @comments = Comments.new(comments_path)
8
+ @styles = styles
9
+ @sheet = SheetDoc.new(sheet_path, @rels, @styles, shared_strings, workbook, options)
10
+ end
11
+
12
+ def cells
13
+ @cells ||= @sheet.cells(@rels)
14
+ end
15
+
16
+ def present_cells
17
+ @present_cells ||= cells.select { |_, cell| cell && cell.value }
18
+ end
19
+
20
+ # Yield each row as array of Excelx::Cell objects
21
+ # accepts options max_rows (int) (offset by 1 for header),
22
+ # pad_cells (boolean) and offset (int)
23
+ def each_row(options = {}, &block)
24
+ row_count = 0
25
+ options[:offset] ||= 0
26
+ @sheet.each_row_streaming do |row|
27
+ break if options[:max_rows] && row_count == options[:max_rows] + options[:offset] + 1
28
+ if block_given? && !(options[:offset] && row_count < options[:offset])
29
+ block.call(cells_for_row_element(row, options))
30
+ end
31
+ row_count += 1
32
+ end
33
+ end
34
+
35
+ def row(row_number)
36
+ first_column.upto(last_column).map do |col|
37
+ cells[[row_number, col]]
38
+ end.map { |cell| cell && cell.value }
39
+ end
40
+
41
+ def column(col_number)
42
+ first_row.upto(last_row).map do |row|
43
+ cells[[row, col_number]]
44
+ end.map { |cell| cell && cell.value }
45
+ end
46
+
47
+ # returns the number of the first non-empty row
48
+ def first_row
49
+ @first_row ||= present_cells.keys.map { |row, _| row }.min
50
+ end
51
+
52
+ def last_row
53
+ @last_row ||= present_cells.keys.map { |row, _| row }.max
54
+ end
55
+
56
+ # returns the number of the first non-empty column
57
+ def first_column
58
+ @first_column ||= present_cells.keys.map { |_, col| col }.min
59
+ end
60
+
61
+ # returns the number of the last non-empty column
62
+ def last_column
63
+ @last_column ||= present_cells.keys.map { |_, col| col }.max
64
+ end
65
+
66
+ def excelx_format(key)
67
+ cell = cells[key]
68
+ @styles.style_format(cell.style).to_s if cell
69
+ end
70
+
71
+ def hyperlinks
72
+ @hyperlinks ||= @sheet.hyperlinks(@rels)
73
+ end
74
+
75
+ def comments
76
+ @comments.comments
77
+ end
78
+
79
+ def dimensions
80
+ @sheet.dimensions
81
+ end
82
+
83
+ private
84
+
85
+ # Take an xml row and return an array of Excelx::Cell objects
86
+ # optionally pad array to header width(assumed 1st row).
87
+ # takes option pad_cells (boolean) defaults false
88
+ def cells_for_row_element(row_element, options = {})
89
+ return [] unless row_element
90
+ cell_col = 0
91
+ cells = []
92
+ @sheet.each_cell(row_element) do |cell|
93
+ cells.concat(pad_cells(cell, cell_col)) if options[:pad_cells]
94
+ cells << cell
95
+ cell_col = cell.coordinate.column
96
+ end
97
+ cells
98
+ end
99
+
100
+ def pad_cells(cell, last_column)
101
+ pad = []
102
+ (cell.coordinate.column - 1 - last_column).times { pad << nil }
103
+ pad
104
+ end
105
+ end
106
+ end
107
+ end