ooxl 0.0.1.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c939544ca8395b5cad397848b69e789073e4e036
4
+ data.tar.gz: 85910bb29f92f0bbfab570ca7125694efaa81a90
5
+ SHA512:
6
+ metadata.gz: 42f4c2e796683bbafddb15312be2a9e175205c2d1b8cf51a19cabdb944ba0d18a92e1462fc80588c565bb937384e2cb92666ae6e2db3162fc1d88614bed4cd02
7
+ data.tar.gz: 0db40f7d63f5647c901fc94d85a63835f246794a11ab26dfb2523b1a908046b6b74a26943246b36cdffb39087cab4d4b39b3b22e2f00769c627f6e37a3c435e9
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /test
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.3
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in moon_xl.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # OOXML Excel
2
+
3
+ TODO: Description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ooxml_excel'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ooxml_excel
20
+
21
+ ## Usage
22
+
23
+ ### Using `OOXML::Excel` to read spreadsheet:
24
+ ```
25
+ ooxml_excel = OOXML::Excel.new('example.xlsx')
26
+ ```
27
+
28
+ ### Fetching all sheets:
29
+ ```
30
+ ooxml_excel.sheets # ['Test Sheet 1', 'Test Sheet 2']
31
+ ```
32
+
33
+ ### Accessing Rows and Cells
34
+ ```
35
+ sheet = ooxml_excel.sheet('Test Sheet 1')
36
+
37
+ # Rows
38
+ sheet[0] # Access the first row
39
+ sheet.rows[0] # Same as above
40
+
41
+ # Cells
42
+ sheet[0].cells # Fetch all cells
43
+ sheet.rows[0].cells # longer way to do it
44
+
45
+ sheet[0][0] # Access the first cell of the row
46
+ sheet.rows[0].cells[0] # longer way to do it
47
+
48
+ sheet[0][0].value # Access cell value
49
+ sheet.rows[0].cells[0].value # longer way to do it
50
+
51
+ ooxml_excel['Test Sheet 1'][0][0].value
52
+ ```
53
+
54
+ ### Iterating through each row:
55
+ ```
56
+ # as an array of strings
57
+ ooxml_excel.sheet('Test Sheet 1').each_row do |row|
58
+ # Do something here...
59
+ p row # ['text', 'text']
60
+ end
61
+
62
+ # as an array of objects
63
+ ooxml_excel.sheet('Test Sheet 1').each_row_as_object do |row|
64
+ # Do something here...
65
+ p row.cells # [OOXML::Excel::Row::Cell, ...]
66
+ end
67
+
68
+ ```
69
+
70
+ ### Fetching Columns
71
+ ```
72
+ # Fetch all columns
73
+ ooxml_excel.sheet('Test Sheet 1').columns
74
+
75
+ # Checking if the column is hidden
76
+ ooxml_excel.sheet('Test Sheet 1').column(1).hidden? # column index
77
+ ooxml_excel.sheet('Test Sheet 1').column('A').hidden? # column letter
78
+ ```
79
+
80
+ ### Fetching Styles
81
+ ```
82
+ # Font
83
+ font_object = ooxml_excel.sheet('Test Sheet 1').font('A1')
84
+ font_object.bold? # false
85
+ font_object.name # Arial
86
+ font_object.rgb_color # FFE10000
87
+ font_object.size # 8
88
+
89
+ # Cell Fill
90
+ fill_object = ooxml_excel.sheet('Test Sheet 1').fill('A1')
91
+ fill_object.bg_color # FFE10000
92
+ fill_object.fg_color # FFE10000
93
+ ```
94
+ ### Fetching Data from named/cell range
95
+ ```
96
+ # named range
97
+ ooxml_excel.named_range('my_named_range') # ['value' 'from', 'range']
98
+
99
+ # cell range
100
+ ooxml['Lists'!$A$1:$A$6] # ['1','2','3','4','5','6']
101
+
102
+ # or
103
+ ooxml['Lists'!A1:A6] # ['1','2','3','4','5','6']
104
+
105
+ # or loading a single value
106
+ ooxml['Lists'!A1] # ['1']
107
+
108
+ # or loading a box type values
109
+ ooxml['Lists!A1:B2'] # [['1', '2'], ['2','3']]
110
+
111
+ ```
112
+ ### Fetching Data Validation
113
+ ```
114
+ # All Validations
115
+ data_validations = ooxml_excel.sheet('Test Sheet 1').data_validations
116
+
117
+ # Specific validation for cell
118
+ data_validation = ooxml.sheet('Input Sheet').data_validation('D4')
119
+
120
+ data_validation.prompt # "Sample Validation Message"
121
+ data_validation.formula # 20
122
+ data_validation.type #textLength
123
+
124
+ ```
125
+
126
+ ## Development
127
+
128
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
129
+
130
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
131
+
132
+ ## Contributing
133
+
134
+ Bug reports and pull requests are welcome on GitHub at https://github.com/halcjames/ooxml_excel.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ooxml_excel"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/ooxl.rb ADDED
@@ -0,0 +1,18 @@
1
+ # dependencies
2
+ require 'nokogiri'
3
+ require 'active_support/all'
4
+ require 'zip'
5
+
6
+ # gem
7
+ require "ooxl/version"
8
+
9
+ # library
10
+ require "ooxl/ooxl"
11
+ require "ooxl/util"
12
+ require "ooxl/xl_objects/workbook"
13
+ require "ooxl/xl_objects/comments"
14
+ require "ooxl/xl_objects/styles"
15
+ require "ooxl/xl_objects/sheet"
16
+ require "ooxl/xl_objects/row"
17
+ require "ooxl/xl_objects/column"
18
+ require "ooxl/xl_objects/cell"
data/lib/ooxl/ooxl.rb ADDED
@@ -0,0 +1,90 @@
1
+ class OOXL
2
+ include Enumerable
3
+ def initialize(spreadsheet_filepath)
4
+ @workbook = nil
5
+ @sheets = {}
6
+ @styles = []
7
+ @comments = {}
8
+ parse_spreadsheet_contents(spreadsheet_filepath)
9
+ end
10
+
11
+ def self.open(spreadsheet_filepath)
12
+ new(spreadsheet_filepath)
13
+ end
14
+
15
+ def sheets
16
+ @workbook.sheets.map { |sheet| sheet[:name]}
17
+ end
18
+
19
+ def each
20
+ sheets.each do |sheet_name|
21
+ yield sheet(sheet_name)
22
+ end
23
+ end
24
+
25
+ def sheet(sheet_name)
26
+ sheet_index = @workbook.sheets.index { |sheet| sheet[:name] == sheet_name}
27
+ raise "No #{sheet_name} in workbook." if sheet_index.nil?
28
+ sheet = @sheets.fetch((sheet_index+1).to_s)
29
+
30
+ # shared variables
31
+ sheet.name = sheet_name
32
+ sheet.comments = @comments[(sheet_index+1)]
33
+ sheet.styles = @styles
34
+ sheet.defined_names = @workbook.defined_names
35
+ sheet
36
+ end
37
+
38
+ def [](text)
39
+ # immediately treat as cell range if an exclamation point is detected
40
+ # otherwise, normally load a sheet
41
+
42
+ text.include?('!') ? load_list_values(text) : sheet(text)
43
+ end
44
+
45
+ def named_range(name)
46
+ # "Hidden11390550_39"=>"Hidden!$B$734:$B$735"
47
+ # ooxml.named_range('Hidden11390550_107')
48
+ # a typical named range would be be
49
+ # yes_no => 'Lists'!$A$1:$A$6
50
+ defined_name = @workbook.defined_names[name]
51
+ load_list_values(defined_name) if defined_name.present?
52
+ end
53
+
54
+ private
55
+
56
+ def load_list_values(range_text)
57
+ # get the sheet name => 'Lists'
58
+ sheet_name = range_text.gsub(/[\$\']/, '').scan(/^[^!]*/).first
59
+ # fetch the cell range => '$A$1:$A$6'
60
+ cell_range = range_text.gsub(/\$/, '').scan(/(?<=!).+/).first
61
+ # get the sheet object and fetch the cells in range
62
+ sheet(sheet_name).list_values_from_cell_range(cell_range)
63
+ end
64
+
65
+ def parse_spreadsheet_contents(spreadsheet)
66
+ shared_strings = []
67
+ Zip::File.open(spreadsheet) do |spreadsheet_zip|
68
+ spreadsheet_zip.each do |entry|
69
+ case entry.name
70
+ when /xl\/worksheets\/sheet(\d+)?\.xml/
71
+ sheet_id = entry.name.scan(/xl\/worksheets\/sheet(\d+)?\.xml/).flatten.first
72
+ @sheets[sheet_id] = OOXL::Sheet.new(entry.get_input_stream.read, shared_strings)
73
+ when /xl\/styles\.xml/
74
+ @styles = OOXL::Styles.load_from_stream(entry.get_input_stream.read)
75
+ when /xl\/comments(\d+)?\.xml/
76
+ comment_id = entry.name.scan(/xl\/comments(\d+)\.xml/).flatten.first
77
+ @comments[comment_id] = OOXL::Comments.load_from_stream(entry.get_input_stream.read)
78
+ when "xl/sharedStrings.xml"
79
+ Nokogiri.XML(entry.get_input_stream.read).remove_namespaces!.xpath('sst/si').each do |shared_string_node|
80
+ shared_strings << shared_string_node.at('t').text
81
+ end
82
+ when "xl/workbook.xml"
83
+ @workbook = OOXL::Workbook.load_from_stream(entry.get_input_stream.read)
84
+ else
85
+ # unsupported for now..
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
data/lib/ooxl/util.rb ADDED
@@ -0,0 +1,16 @@
1
+ class OOXL
2
+ module Util
3
+ COLUMN_LETTERS = ('A'..'ZZZZ').to_a
4
+ def letter_equivalent(index)
5
+ COLUMN_LETTERS.fetch(index)
6
+ end
7
+
8
+ def letter_index(letter)
9
+ COLUMN_LETTERS.index { |c_letter| c_letter == letter}
10
+ end
11
+
12
+ def uniform_reference(ref)
13
+ ref.to_s[/[A-Z]/] ? letter_index(ref) + 1 : ref
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class OOXL
2
+ VERSION = "0.0.1.2"
3
+ end
@@ -0,0 +1,106 @@
1
+ class OOXL
2
+ class BlankCell
3
+ attr_reader :id
4
+
5
+ def initialize(id)
6
+ @id = id
7
+ end
8
+ def value
9
+ nil
10
+ end
11
+ end
12
+
13
+ class Cell
14
+ attr_accessor :id, :type_id, :style_id, :value_id, :shared_strings, :styles
15
+
16
+ def initialize(**attrs)
17
+ attrs.each { |property, value| send("#{property}=", value)}
18
+ end
19
+
20
+ def next_id(offset: 1, location: "bottom")
21
+ _, column_letter, column_index = id.partition(/[A-Z]/)
22
+
23
+ # ensure that all are numbers
24
+ column_index = column_index.to_i
25
+ offset = offset.to_i if offset.is_a?(String)
26
+
27
+ # increment based on specified location
28
+ case location
29
+ when "top"
30
+ if column_index == 1 || column_index < offset
31
+ column_index = 1
32
+ else
33
+ column_index -= offset
34
+ end
35
+ when "bottom"
36
+ column_index += offset
37
+ when "left"
38
+ return id if column_letter == 'A'
39
+ 1.upto(offset) { |count| column_letter = (column_letter.ord-1).chr unless column_letter == 'A' }
40
+ when "right"
41
+ 1.upto(offset) { |count| column_letter.next! }
42
+ else
43
+ id
44
+ end
45
+
46
+ "#{column_letter}#{column_index}"
47
+ end
48
+
49
+ def type
50
+ @type ||= begin
51
+ case type_id
52
+ when 's' then :string
53
+ when 'n' then :number
54
+ when 'b' then :boolean
55
+ when 'd' then :date
56
+ when 'str' then :formula
57
+ when 'inlineStr' then :inline_str
58
+ else
59
+ :error
60
+ end
61
+ end
62
+ end
63
+
64
+ def style
65
+ @style ||= begin
66
+ if style_id.present?
67
+ style = styles.by_id(style_id.to_i)
68
+ end
69
+ end
70
+ end
71
+
72
+ def number_format
73
+ if (style.present?)
74
+ nf = style[:number_format]
75
+ (nf.present?) ? nf.gsub("\\", "") : nil
76
+ end
77
+ end
78
+
79
+ def font
80
+ (style.present?) ? style[:font] : nil
81
+ end
82
+
83
+ def fill
84
+ (style.present?) ? style[:fill]: nil
85
+ end
86
+
87
+ def value
88
+ case type
89
+ when :string
90
+ (value_id.present?) ? shared_strings[value_id.to_i] : nil
91
+ else
92
+ # TODO: to support other types soon
93
+ value_id
94
+ end
95
+ end
96
+
97
+ def self.load_from_node(cell_node, shared_strings, styles)
98
+ new(id: cell_node.attributes["r"].try(:value),
99
+ type_id: cell_node.attributes["t"].try(:value),
100
+ style_id: cell_node.attributes["s"].try(:value),
101
+ value_id: cell_node.at('v').try(:text),
102
+ shared_strings: shared_strings,
103
+ styles: styles )
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,19 @@
1
+ class OOXL
2
+ class Column
3
+ attr_accessor :id, :width, :custom_width, :id_range, :hidden
4
+ alias_method :hidden?, :hidden
5
+
6
+ def initialize(**attrs)
7
+ attrs.each { |property, value| send("#{property}=", value)}
8
+ end
9
+
10
+ def self.load_from_node(column_node)
11
+ hidden_attr = column_node.attributes["hidden"]
12
+ new(id: column_node.attributes["min"].try(:value),
13
+ width: column_node.attributes["width"].try(:value),
14
+ custom_width: column_node.attributes["custom_width"].try(:value),
15
+ id_range: (column_node.attributes["min"].value.to_i..column_node.attributes["max"].value.to_i).to_a,
16
+ hidden: (hidden_attr.present?) ? hidden_attr.value == "1" : false)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ class OOXL
2
+ class Comments
3
+ attr_reader :comments
4
+
5
+ def initialize(comments)
6
+ @comments = comments
7
+ end
8
+
9
+ def [](id)
10
+ @comments[id]
11
+ end
12
+
13
+ def self.load_from_stream(comment_xml)
14
+ comment_xml =Nokogiri.XML(comment_xml).remove_namespaces!
15
+
16
+ comments = comment_xml.xpath("//comments/commentList/comment").map do |comment_node|
17
+ comment_text_node = comment_node.xpath('./text/r/t')
18
+
19
+ value = if comment_text_node.is_a?(Array)
20
+ comment_text_node.map { |comment_text_node| comment_text_node.text }.join('')
21
+ else
22
+ comment_text_node.text
23
+ end
24
+
25
+ # value = (comment_node.xpath('./text/r/t').last || comment_node.at_xpath('./text/r/t') || comment_node.at_xpath('./text/t')).text
26
+ id = comment_node.attributes["ref"].to_s
27
+ [id, value]
28
+ end.to_h
29
+ new(comments)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ class OOXL
2
+ class Row
3
+ include Enumerable
4
+ attr_accessor :id, :spans, :cells
5
+
6
+ def initialize(**attrs)
7
+ attrs.each { |property, value| send("#{property}=", value)}
8
+ end
9
+
10
+ def [](id)
11
+ cell = if id.is_a?(String)
12
+ cells.find { |row| row.id == id}
13
+ else
14
+ cells[id]
15
+ end
16
+ (cell.present?) ? cell : BlankCell.new(id)
17
+ end
18
+
19
+ def each
20
+ cells.each { |cell| yield cell }
21
+ end
22
+
23
+ def self.load_from_node(row_node, shared_strings, styles)
24
+ new(id: row_node.attributes["r"].try(:value),
25
+ spans: row_node.attributes["spans"].try(:value),
26
+ cells: row_node.xpath('c').map { |cell_node| OOXL::Cell.load_from_node(cell_node, shared_strings, styles) } )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,156 @@
1
+ require_relative 'sheet/data_validation'
2
+ class OOXL
3
+ class Sheet
4
+ include OOXL::Util
5
+ include Enumerable
6
+ attr_reader :columns, :data_validations, :shared_strings
7
+ attr_accessor :comments, :styles, :defined_names, :name
8
+
9
+ def initialize(xml, shared_strings)
10
+ @xml = Nokogiri.XML(xml).remove_namespaces!
11
+ @shared_strings = shared_strings
12
+ @comments = {}
13
+ @defined_names = {}
14
+ @styles = []
15
+ end
16
+
17
+ def code_name
18
+ @code_name ||= @xml.xpath('//sheetPr').attribute('codeName').try(:value)
19
+ end
20
+
21
+ def comment(cell_ref)
22
+ @comments[cell_ref]
23
+ end
24
+
25
+ def data_validation(cell_ref)
26
+ data_validations.find { |data_validation| data_validation.in_sqref_range?(cell_ref)}
27
+ end
28
+
29
+ def column(id)
30
+ uniformed_reference = uniform_reference(id)
31
+ columns.find { |column| column.id_range.include?(uniformed_reference)}
32
+ end
33
+
34
+ def columns
35
+ @columns ||= begin
36
+ @xml.xpath('//cols/col').map do |column_node|
37
+ Column.load_from_node(column_node)
38
+ end
39
+ end
40
+ end
41
+
42
+ def [](id)
43
+ if id.is_a?(String)
44
+ rows.find { |row| row.id == id}
45
+ else
46
+ rows[id]
47
+ end
48
+ end
49
+
50
+ def row(index)
51
+ rows.find { |row| row.id == index.to_s}
52
+ end
53
+
54
+ def rows
55
+ @rows ||= begin
56
+ # TODO: get the value of merged cells
57
+ # merged_cells = @xml.xpath('//mergeCells/mergeCell').map { |merged_cell| merged_cell.attributes["ref"].try(:value) }
58
+ @xml.xpath('//sheetData/row').map do |row_node|
59
+ Row.load_from_node(row_node, @shared_strings, styles)
60
+ end
61
+ end
62
+ end
63
+
64
+ def each
65
+ rows.each { |row| yield row }
66
+ end
67
+
68
+ def font(cell_reference)
69
+ style_id = fetch_style_style_id(cell_reference)
70
+ if style_id.present?
71
+ style = @styles.by_id(style_id.to_i)
72
+
73
+ (style.present?) ? style[:font] : nil
74
+ end
75
+ end
76
+
77
+ def fill(cell_reference)
78
+ style_id = fetch_style_style_id(cell_reference)
79
+ if style_id.present?
80
+ style = @styles.by_id(style_id.to_i)
81
+ (style.present?) ? style[:fill] : nil
82
+ end
83
+ end
84
+
85
+
86
+ def data_validations
87
+ @data_validations ||= begin
88
+ @xml.xpath('//dataValidations/dataValidation').map do |data_validation_node|
89
+ Sheet::DataValidation.load_from_node(data_validation_node)
90
+ end
91
+ end
92
+ end
93
+
94
+ # a shortcut for:
95
+ # formula = data_validation('A1').formula
96
+ # ooxl.named_range(formula)
97
+ def cell_range(cell_ref)
98
+ data_validation = data_validations.find { |data_validation| data_validation.in_sqref_range?(cell_ref)}
99
+ if data_validation.respond_to?(:type) && data_validation.type == "list"
100
+ if data_validation.formula[/[\s\$\,\:]/]
101
+ (data_validation.formula[/\$/].present?) ? "#{name}!#{data_validation.formula}" : data_validation.formula
102
+ else
103
+ @defined_names.fetch(data_validation.formula)
104
+ end
105
+ end
106
+ end
107
+
108
+ def list_values_from_cell_range(cell_range)
109
+ return [] if cell_range.blank?
110
+
111
+ # cell_range values separated by comma
112
+ if cell_range.include?(":")
113
+ cell_letters = cell_range.gsub(/[\d]/, '').split(':')
114
+ start_index, end_index = cell_range.gsub(/[^\d:]/, '').split(':').map(&:to_i)
115
+
116
+ # This will allow values from this pattern
117
+ # 'SheetName!A1:C3'
118
+ # The number after the cell letter will be the index
119
+ # 1 => start_index
120
+ # 3 => end_index
121
+ # Expected output would be: [['value', 'value', 'value'], ['value', 'value', 'value'], ['value', 'value', 'value']]
122
+ if cell_letters.uniq.size > 1
123
+ start_index.upto(end_index).map do |row_index|
124
+ (cell_letters.first..cell_letters.last).map do |cell_letter|
125
+ row = rows[row_index-1]
126
+ next if row.blank?
127
+ row["#{cell_letter}#{row_index}"].value
128
+ end
129
+ end
130
+ else
131
+ cell_letter = cell_letters.uniq.first
132
+ (start_index..end_index).to_a.map do |row_index|
133
+ row = rows[row_index-1]
134
+ next if row.blank?
135
+ row["#{cell_letter}#{row_index}"].value
136
+ end
137
+ end
138
+ else
139
+ # when only one value: B2
140
+ row_index = cell_range.gsub(/[^\d:]/, '').split(':').map(&:to_i).first
141
+ row = rows[row_index-1]
142
+ return if row.blank?
143
+ [row[cell_range].value]
144
+ end
145
+ end
146
+
147
+ private
148
+ def fetch_style_style_id(cell_reference)
149
+ raise 'Invalid Cell Reference!' if cell_reference[/[A-Z]{1,}\d+/].blank?
150
+ row_index = cell_reference.scan(/[A-Z{1,}](\d+)/).flatten.first.to_i - 1
151
+ return if rows[row_index].blank? || rows[row_index][cell_reference].blank?
152
+ rows[row_index][cell_reference].style_id
153
+ end
154
+
155
+ end
156
+ end
@@ -0,0 +1,69 @@
1
+ class OOXL
2
+ class Sheet
3
+ class DataValidation
4
+ attr_accessor :allow_blank, :prompt, :type, :sqref, :formula
5
+
6
+ def in_sqref_range?(cell_id)
7
+ return if cell_id.blank?
8
+ cell_letter = cell_id.gsub(/[\d]/, '')
9
+ index = cell_id.gsub(/[^\d]/, '').to_i
10
+ range = sqref_range.find { |single_cell_letter_or_range, row_range| single_cell_letter_or_range.is_a?(Range) ? single_cell_letter_or_range.cover?(cell_letter) : single_cell_letter_or_range == cell_letter}
11
+ range.last.include?(index) if range.present?
12
+ end
13
+
14
+ def self.load_from_node(data_validation_node)
15
+ allow_blank = data_validation_node.attribute('allowBlank').try(:value)
16
+ prompt = data_validation_node.attribute('prompt').try(:value)
17
+ type = data_validation_node.attribute('type').try(:value)
18
+ sqref = data_validation_node.attribute('sqref').try(:value)
19
+ formula = data_validation_node.at('formula1').try(:content)
20
+
21
+ self.new(allow_blank: allow_blank,
22
+ prompt: prompt,
23
+ type: type,
24
+ sqref: sqref,
25
+ formula: formula)
26
+ end
27
+
28
+ private
29
+ def initialize(**attrs)
30
+ attrs.each { |property, value| send("#{property}=", value)}
31
+ end
32
+
33
+ def sqref_range
34
+ @sqref_range ||= begin
35
+ # "BH5:BH271 BI5:BI271"
36
+ if !sqref.include?(':')
37
+ cell_letter = sqref.gsub(/[\d]/, '')
38
+ index = sqref.gsub(/[^\d]/, '').to_i
39
+ { cell_letter => (index..index)}
40
+ else
41
+ sqref.split( ' ').map do |splitted_by_space_sqref|
42
+ # ["BH5:BH271, "BI5:BI271"]
43
+ if splitted_by_space_sqref.is_a?(Array)
44
+ splitted_by_space_sqref.map do |sqref|
45
+ build_range(splitted_by_space_sqref)
46
+ end
47
+ else
48
+ # "BH5:BH271"
49
+ build_range(splitted_by_space_sqref)
50
+ end
51
+ end.to_h
52
+ end
53
+ end
54
+ end
55
+
56
+ def build_range(sqref)
57
+ splitted_sqref = sqref.gsub(/[\d]/, '')
58
+ sqref_without_letters = sqref.gsub(/[^\d:]/, '')
59
+ if sqref.include?(':')
60
+ start_letter, end_letter = splitted_sqref.split(':')
61
+ start_index, end_index = sqref_without_letters.split(':').map(&:to_i)
62
+ [(start_letter..end_letter),(start_index..end_index)]
63
+ else
64
+ [(splitted_sqref..splitted_sqref),(sqref_without_letters..sqref_without_letters)]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'styles/cell_style_reference'
2
+ require_relative 'styles/fill'
3
+ require_relative 'styles/font'
4
+ require_relative 'styles/number_formatting'
5
+ class OOXL
6
+ class Styles
7
+ attr_accessor :fonts, :fills, :number_formats, :cell_style_xfs
8
+ def initialize(**attrs)
9
+ attrs.each { |property, value| send("#{property}=", value)}
10
+ end
11
+
12
+ def by_id(id)
13
+ cell_style = cell_style_xfs.fetch(id)
14
+ {
15
+ font: fonts_by_index(cell_style.font_id),
16
+ fill: fills_by_index(cell_style.fill_id),
17
+ number_format: number_formats_by_index(cell_style.number_formatting_id),
18
+ }
19
+ end
20
+
21
+ def fonts_by_index(font_index)
22
+ @fonts[font_index]
23
+ end
24
+
25
+ def fills_by_index(fill_index)
26
+ @fills[fill_index]
27
+ end
28
+
29
+ def number_formats_by_index(number_format_index)
30
+ @number_formats.find { |number_format| number_format.id == number_format_index.to_s}.try(:code)
31
+ end
32
+
33
+ def self.load_from_stream(xml_stream)
34
+ style_doc = Nokogiri.XML(xml_stream).remove_namespaces!
35
+ fonts = style_doc.xpath('//fonts/font')
36
+ fills = style_doc.xpath('//fills/fill')
37
+ number_formats = style_doc.xpath('//numFmts/numFmt')
38
+ # This element contains the master formatting records (xf) which
39
+ # define the formatting applied to cells in this workbook.
40
+ # link: https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.cellformats(v=office.14).aspx
41
+ cell_style_xfs = style_doc.xpath('//cellXfs/xf')
42
+
43
+ self.new(
44
+ fonts: fonts.map { |font_node| Styles::Font.load_from_node(font_node)},
45
+ fills: fills.map { |fill_node| Styles::Fill.load_from_node(fill_node) if fill_node.to_s.include?('patternFill')},
46
+ number_formats: number_formats.map { |num_fmt_node| Styles::NumberFormatting.load_from_node(num_fmt_node) },
47
+ cell_style_xfs: cell_style_xfs.map { |cell_style_xfs_node| Styles::CellStyleReference.load_from_node(cell_style_xfs_node)}
48
+ )
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ class OOXL
2
+ class Styles
3
+ class CellStyleReference
4
+ attr_accessor :id, :number_formatting_id, :fill_id, :font_id
5
+ def initialize(**attrs)
6
+ attrs.each { |property, value| send("#{property}=", value)}
7
+ end
8
+ def self.load_from_node(cell_style_xfs_node)
9
+ attributes = cell_style_xfs_node.attributes
10
+ self.new(
11
+ id: attributes["xfId"].value.to_i,
12
+ number_formatting_id: attributes["numFmtId"].value.to_i,
13
+ fill_id: attributes["fillId"].value.to_i,
14
+ font_id: attributes["fontId"].value.to_i
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ class OOXL
2
+ class Styles
3
+ class Fill
4
+ attr_accessor :pattern_type, :fg_color, :fg_color_theme, :fg_color_tint, :bg_color_index, :bg_color, :fg_color_index
5
+
6
+ def initialize(**attrs)
7
+ attrs.each { |property, value| send("#{property}=", value)}
8
+ end
9
+ def self.load_from_node(fill_node)
10
+ pattern_fill = fill_node.at('patternFill')
11
+
12
+ pattern_type = pattern_fill.attributes["patternType"].value
13
+ if pattern_type == "solid"
14
+ fg_color = pattern_fill.at('fgColor')
15
+ bg_color = pattern_fill.at('bgColor')
16
+ fg_color_index = fg_color.class == Nokogiri::XML::Element ? fg_color.attributes["indexed"].try(:value) : nil
17
+
18
+ self.new(pattern_type: pattern_type,
19
+ fg_color: (fg_color.present?) ? fg_color.attributes["rgb"].try(:value) : nil,
20
+ fg_color_theme: (fg_color.present?) ? fg_color.attributes["theme"].try(:value) : nil,
21
+ fg_color_tint: (fg_color.present?) ? fg_color.attributes["tint"].try(:value) : nil,
22
+ bg_color: (bg_color.present?) ? bg_color.attributes["rgb"].try(:value) : nil,
23
+ bg_color_index: (bg_color.present?) ? bg_color.attributes["index"].try(:value) : nil,
24
+ fg_color_index: fg_color_index)
25
+ else
26
+ self.new(pattern_type: pattern_type)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ class OOXL
2
+ class Styles
3
+ class Font
4
+ attr_accessor :size, :name, :rgb_color, :theme, :bold
5
+ alias_method :bold?, :bold
6
+ def initialize(**attrs)
7
+ attrs.each { |property, value| send("#{property}=", value)}
8
+ end
9
+ def self.load_from_node(font_node)
10
+ font_size_node = font_node.at('sz')
11
+ font_color_node = font_node.at('color')
12
+ font_name_node = font_node.at('name')
13
+ font_bold_node = font_node.at('b')
14
+ self.new(
15
+ size: font_size_node && font_size_node.attributes["val"].value,
16
+ name: font_name_node && font_name_node.attributes["val"].value,
17
+ rgb_color: font_color_node && font_color_node.attributes["rgb"].try(:value) ,
18
+ theme: font_color_node && font_color_node.attributes["theme"].try(:value),
19
+ bold: font_bold_node.present?
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ class OOXL
2
+ class Styles
3
+ class NumberFormatting
4
+ attr_accessor :id, :code
5
+ def self.load_from_node(num_fmt_node)
6
+ new_format = self.new.tap do |number_format|
7
+ number_format.id = num_fmt_node.attributes["numFmtId"].try(:value)
8
+ number_format.code = num_fmt_node.attributes["formatCode"].try(:value)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ class OOXL
2
+ class Workbook
3
+ def initialize(xml)
4
+ @xml = xml
5
+ end
6
+
7
+ def sheets
8
+ @sheets ||= begin
9
+ @xml.xpath('//sheets/sheet').map do |sheet_node|
10
+ name = sheet_node.attribute('name').value
11
+ rel_id = sheet_node.attribute('id').value.gsub(/[^\d+]/, '')
12
+ sheet_id = sheet_node.attribute('sheetId').value
13
+ { name: name, sheet_id: sheet_id, relationship_id: rel_id}
14
+ end
15
+ end
16
+ end
17
+
18
+ def defined_names
19
+ @defined_names ||= begin
20
+ @xml.xpath('//definedNames/definedName').map do |defined_names_node|
21
+ name = defined_names_node.attribute('name').value
22
+ reference = defined_names_node.text
23
+ [name, reference]
24
+ end.to_h
25
+ end
26
+ end
27
+
28
+ def self.load_from_stream(xml_stream)
29
+ self.new (Nokogiri.XML(xml_stream).remove_namespaces!)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ooxl/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ooxl"
8
+ spec.version = OOXL::VERSION
9
+ spec.authors = ["James Mones"]
10
+ spec.email = ["bajong009@gmail.com"]
11
+ spec.summary = %q{OOXL Excel - Parse Excel Spreadsheets (xlsx, xlsm).}
12
+ spec.description = %q{A Ruby spreadsheet parser for Excel (xlsx, xlsm).}
13
+ spec.homepage = "https://github.com/halcjames/ooxml_excel"
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ # if spec.respond_to?(:metadata)
18
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
19
+ # else
20
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
21
+ # end
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+ spec.add_dependency 'activesupport'
28
+ spec.add_dependency 'nokogiri', '~> 1'
29
+ spec.add_dependency 'rubyzip', '~> 1.1', '< 2.0.0'
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.12"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ooxl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - James Mones
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubyzip
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 2.0.0
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.1'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: 2.0.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.12'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.12'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '10.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ description: A Ruby spreadsheet parser for Excel (xlsx, xlsm).
104
+ email:
105
+ - bajong009@gmail.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - ".gitignore"
111
+ - ".rspec"
112
+ - ".travis.yml"
113
+ - Gemfile
114
+ - README.md
115
+ - Rakefile
116
+ - bin/console
117
+ - bin/setup
118
+ - lib/ooxl.rb
119
+ - lib/ooxl/ooxl.rb
120
+ - lib/ooxl/util.rb
121
+ - lib/ooxl/version.rb
122
+ - lib/ooxl/xl_objects/cell.rb
123
+ - lib/ooxl/xl_objects/column.rb
124
+ - lib/ooxl/xl_objects/comments.rb
125
+ - lib/ooxl/xl_objects/row.rb
126
+ - lib/ooxl/xl_objects/sheet.rb
127
+ - lib/ooxl/xl_objects/sheet/data_validation.rb
128
+ - lib/ooxl/xl_objects/styles.rb
129
+ - lib/ooxl/xl_objects/styles/cell_style_reference.rb
130
+ - lib/ooxl/xl_objects/styles/fill.rb
131
+ - lib/ooxl/xl_objects/styles/font.rb
132
+ - lib/ooxl/xl_objects/styles/number_formatting.rb
133
+ - lib/ooxl/xl_objects/workbook.rb
134
+ - ooxml_excel.gemspec
135
+ homepage: https://github.com/halcjames/ooxml_excel
136
+ licenses: []
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.4.5.1
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: OOXL Excel - Parse Excel Spreadsheets (xlsx, xlsm).
158
+ test_files: []