diff --git a/lib/roo/excelx.rb b/lib/roo/excelx.rb index 64637a02..6353caf5 100644 --- a/lib/roo/excelx.rb +++ b/lib/roo/excelx.rb @@ -3,6 +3,10 @@ require 'roo/link' class Roo::Excelx < Roo::Base + autoload :Workbook, 'roo/excelx/workbook' + autoload :SharedStrings, 'roo/excelx/shared_strings' + autoload :Styles, 'roo/excelx/styles' + module Format EXCEPTIONAL_FORMATS = { 'h:mm am/pm' => :date, @@ -64,44 +68,256 @@ def to_type(format) module_function :to_type end + class Cell + attr_reader :type, :formula, :value, :excelx_type, :excelx_value, :style, :hyperlink + + def initialize(value, type, formula, excelx_type, excelx_value, style, hyperlink, base_date) + @type = type + @formula = formula + @base_date = base_date if [:date, :datetime].include?(@type) + @excelx_type = excelx_type + @excelx_value = excelx_value + @style = style + @value = type_cast_value(value) + if hyperlink + @value = Roo::Link.new(hyperlink, @value.to_s) + end + end + + def type + if @formula + :formula + elsif @value.is_a?(Roo::Link) + :link + else + @type + end + end + + private + + def type_cast_value(value) + case @type + when :float, :percentage + value.to_f + when :date + yyyy,mm,dd = (@base_date+value.to_i).strftime("%Y-%m-%d").split('-') + Date.new(yyyy.to_i,mm.to_i,dd.to_i) + when :datetime + create_datetime_from((@base_date+value.to_f.round(6)).strftime("%Y-%m-%d %H:%M:%S.%N")) + when :time + value.to_f*(24*60*60) + when :string + value + else + value + end + end + + def create_datetime_from(datetime_string) + date_part,time_part = round_time_from(datetime_string).split(' ') + yyyy,mm,dd = date_part.split('-') + hh,mi,ss = time_part.split(':') + DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i) + end + + def round_time_from(datetime_string) + date_part,time_part = datetime_string.split(' ') + yyyy,mm,dd = date_part.split('-') + hh,mi,ss = time_part.split(':') + Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0).strftime("%Y-%m-%d %H:%M:%S") + end + end + class Sheet - def initialize(name, rels_doc, sheet_doc, comments_doc) + def initialize(name, rels_doc, sheet_doc, comments_doc, styles, shared_strings, workbook) @name = name @rels_doc = rels_doc @sheet_doc = sheet_doc @comments_doc = comments_doc + @styles = styles + @shared_strings = shared_strings + @workbook = workbook + end + + def cells + @cells ||= Hash[@sheet_doc.xpath("/worksheet/sheetData/row/c").map do |cell_xml| + key = ref_to_key(cell_xml['r']) + [key, cell_from_xml(cell_xml, hyperlinks[key])] + end] + end + + def present_cells + @present_cells ||= cells.select {|key, cell| cell && cell.value } + end + + def row(row_number) + first_column.upto(last_column).map do |col| + cells[[row_number,col]] + end.map {|cell| cell && cell.value } + end + + def column(col_number) + first_row.upto(last_row).map do |row| + cells[[row,col_number]] + end.map {|cell| cell && cell.value } + end + + # returns the number of the first non-empty row + def first_row + @first_row ||= present_cells.keys.map {|row, col| row }.min + end + + def last_row + @last_row ||= present_cells.keys.map {|row, col| row }.max + end + + # returns the number of the first non-empty column + def first_column(sheet=nil) + @first_column ||= present_cells.keys.map {|row, col| col }.min end - def comment(key) - comments[key] + # returns the number of the last non-empty column + def last_column(sheet=nil) + @last_column ||= present_cells.keys.map {|row, col| col }.max + end + + def excelx_format(key) + @styles.style_format(cells[key].style).to_s end def comments @comments ||= if @comments_doc Hash[@comments_doc.xpath("//comments/commentList/comment").map do |comment| - [ref_to_key(comment), comment.at_xpath('./text/r/t').text ] + [ref_to_key(comment.attributes['ref'].to_s), comment.at_xpath('./text/r/t').text] end] else {} end end - def hyperlink(key) - hyperlinks[key] + private + +=begin +Datei xl/comments1.xml + + + + + + + + + + + + + + + Kommentar fuer B4 + + + + + + + + + + + + Kommentar fuer B5 + + + + + +=end +=begin + if @comments_doc[self.sheets.index(sheet)] + read_comments(sheet) end +=end - private + def cell_from_xml(cell_xml, hyperlink) + style = cell_xml['s'].to_i # should be here + # c: + # 22606 + # , format: , tmp_type: float + value_type = + case cell_xml['t'] + when 's' + :shared + when 'b' + :boolean + # 2011-02-25 BEGIN + when 'str' + :string + # 2011-02-25 END + # 2011-09-15 BEGIN + when 'inlineStr' + :inlinestr + # 2011-09-15 END + else + format = @styles.style_format(style) + Format.to_type(format) + end + formula = nil + cell_xml.children.each do |cell| + case cell.name + when 'is' + cell.children.each do |inline_str| + if inline_str.name == 't' + return Cell.new(inline_str.content,:string,formula,:string,inline_str.content,style, hyperlink, @workbook.base_date) + end + end + when 'f' + formula = cell.content + when 'v' + if [:time, :datetime].include?(value_type) && cell.content.to_f >= 1.0 + value_type = + if (cell.content.to_f - cell.content.to_f.floor).abs > 0.000001 + :datetime + else + :date + end + end + excelx_type = [:numeric_or_formula,format.to_s] + value = + case value_type + when :shared + value_type = :string + excelx_type = :string + @shared_strings[cell.content.to_i] + when :boolean + (cell.content.to_i == 1 ? 'TRUE' : 'FALSE') + when :date, :time, :datetime + cell.content + when :formula + cell.content.to_f + when :string + excelx_type = :string + cell.content + else + value_type = :float + cell.content + end + return Cell.new(value,value_type,formula,excelx_type,cell.content,style, hyperlink, @workbook.base_date) + end + end + nil + end - def ref_to_key(element) - Roo::Base.split_coordinate(element.attributes['ref'].to_s) + def ref_to_key(ref) + Roo::Base.split_coordinate(ref) end def hyperlinks @hyperlinks ||= Hash[@sheet_doc.xpath("/worksheet/hyperlinks/hyperlink").map do |hyperlink| if hyperlink.attribute('id') && relationship = relationships[hyperlink.attribute('id').text] - [ref_to_key(hyperlink), relationship.attribute('Target').text] + [ref_to_key(hyperlink.attributes['ref'].to_s), relationship.attribute('Target').text] end end.compact] end @@ -116,6 +332,13 @@ def relationships end end + + class << self + def load_xml(path) + super.remove_namespaces! + end + end + # initialization and opening of a spreadsheet file # values for packed: :zip def initialize(filename, options = {}) @@ -129,25 +352,18 @@ def initialize(filename, options = {}) @comments_files = [] @rels_files = [] process_zipfile(@tmpdir, @filename) + @sheet_doc = load_xmls(@sheet_files) @comments_doc = load_xmls(@comments_files) @rels_doc = load_xmls(@rels_files) + @sheets = [] super(filename, options) - @formula = {} - @excelx_type = {} - @excelx_value = {} - @style = {} - end - - def method_missing(m,*args) - # is method name a label name - read_labels - if @label.has_key?(m.to_s) - sheet ||= default_sheet - read_cells(sheet) - row,col = label(m.to_s) - cell(row,col) + end + + def method_missing(method,*args) + if label = workbook.defined_names[method.to_s] + sheet_for(label.sheet).cells[label.key].value else # call super for methods like #a1 super @@ -159,7 +375,8 @@ def sheet_for(sheet) validate_sheet!(sheet) n = self.sheets.index(sheet) - Sheet.new(sheet, @rels_doc[n], @sheet_doc[n], @comments_doc[n]) + @sheets[n] ||= + Sheet.new(sheet, @rels_doc[n], @sheet_doc[n], @comments_doc[n], styles, shared_strings, workbook) end # Returns the content of a spreadsheet-cell. @@ -167,70 +384,80 @@ def sheet_for(sheet) # (1,1), (1,'A'), ('A',1), ('a',1) all refers to the # cell at the first line and first row. def cell(row, col, sheet=nil) - sheet ||= default_sheet - sheet_object = sheet_for(sheet) - read_cells(sheet) - row,col = key = normalize(row,col) - case celltype(row,col,sheet) - when :date - yyyy,mm,dd = @cell[sheet][key].split('-') - Date.new(yyyy.to_i,mm.to_i,dd.to_i) - when :datetime - create_datetime_from(@cell[sheet][key]) - when :link - Roo::Link.new(sheet_object.hyperlink(key), @cell[sheet][key].to_s) - else - @cell[sheet][key] + key = normalize(row,col) + cell = sheet_for(sheet).cells[key] + cell.value if cell + end + + def row(rownumber,sheet=nil) + sheet_for(sheet).row(rownumber) + end + + # returns all values in this column as an array + # column numbers are 1,2,3,... like in the spreadsheet + def column(column_number,sheet=nil) + if column_number.class == String + column_number = self.class.letter_to_number(column_number) end + sheet_for(sheet).column(column_number) + end + + # returns the number of the first non-empty row + def first_row(sheet=nil) + sheet_for(sheet).first_row + end + + # returns the number of the last non-empty row + def last_row(sheet=nil) + sheet_for(sheet).last_row end + # returns the number of the first non-empty column + def first_column(sheet=nil) + sheet_for(sheet).first_column + end + + # returns the number of the last non-empty column + def last_column(sheet=nil) + sheet_for(sheet).last_column + end + + # set a cell to a certain value + # (this will not be saved back to the spreadsheet file!) + def set(row,col,value,sheet=nil) #:nodoc: + key = normalize(row,col) + cell_type = + case value + when Fixnum then :float + when String, Float then :string + else + raise ArgumentError, "Type for #{value} not set" + end + sheet_for(sheet).cells[key] = Cell.new(value, cell_type, nil, cell_type, value, nil, nil, nil) + end + + # Returns the formula at (row,col). # Returns nil if there is no formula. # The method #formula? checks if there is a formula. def formula(row,col,sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - row,col = normalize(row,col) - @formula[sheet][[row,col]] && @formula[sheet][[row,col]] + key = normalize(row,col) + sheet_for(sheet).cells[key].formula end alias_method :formula?, :formula # returns each formula in the selected sheet as an array of elements # [row, col, formula] def formulas(sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - if @formula[sheet] - @formula[sheet].map do |coord, formula| - [coord[0], coord[1], formula] - end - else - [] - end - end - - class Font - attr_accessor :bold, :italic, :underline - - def bold? - @bold == true - end - - def italic? - @italic == true - end - - def underline? - @underline == true + sheet_for(sheet).cells.select {|key, cell| cell.formula }.map do |(x, y), cell| + [x, y, cell.formula] end end # Given a cell, return the cell's style def font(row, col, sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - row,col = normalize(row,col) - style_definitions[@style[sheet][[row,col]].to_i] + key = normalize(row,col) + styles.definitions[sheet_for(sheet).cells[key].style] end # returns the type of a cell: @@ -242,18 +469,8 @@ def font(row, col, sheet=nil) # * :time # * :datetime def celltype(row,col,sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - sheet_object = sheet_for(sheet) - - key = normalize(row,col) - if @formula[sheet][key] - :formula - elsif sheet_object.hyperlink(key) - :link - else - @cell_type[sheet][key] - end + key = normalize(row, col) + sheet_for(sheet).cells[key].type end # returns the internal type of an excel cell @@ -261,68 +478,66 @@ def celltype(row,col,sheet=nil) # * :string # Note: this is only available within the Excelx class def excelx_type(row,col,sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - row,col = normalize(row,col) - @excelx_type[sheet][[row,col]] + key = normalize(row,col) + sheet_for(sheet).cells[key].excelx_type end # returns the internal value of an excelx cell # Note: this is only available within the Excelx class def excelx_value(row,col,sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - row,col = normalize(row,col) - @excelx_value[sheet][[row,col]] + key = normalize(row,col) + sheet_for(sheet).cells[key].excelx_value end # returns the internal format of an excel cell def excelx_format(row,col,sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - row,col = normalize(row,col) - style_format(@style[sheet][[row,col]]).to_s + key = normalize(row,col) + sheet_for(sheet).excelx_format(key) end # returns an array of sheet names in the spreadsheet def sheets - workbook_doc.xpath("//sheet").map do |sheet| + workbook.sheets.map do |sheet| sheet['name'] end end + def empty?(row,col,sheet=nil) + sheet = sheet_for(sheet) + key = normalize(row,col) + cell = sheet.cells[key] + !cell || !cell.value || (cell.type == :string && cell.value.empty?) \ + || (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column) + + end + # shows the internal representation of all cells # for debugging purposes def to_s(sheet=nil) - sheet ||= default_sheet - read_cells(sheet) - @cell[sheet].inspect + sheet_for(sheet).cells.inspect end # returns the row,col values of the labelled cell # (nil,nil) if label is not defined - def label(labelname) - read_labels - if @label.empty? || !@label.has_key?(labelname) + def label(name) + labels = workbook.defined_names + if labels.empty? || !labels.key?(name) [nil,nil,nil] else - [@label[labelname][1].to_i, - self.class.letter_to_number(@label[labelname][2]), - @label[labelname][0]] + [labels[name].row, + labels[name].col, + labels[name].sheet] end end # Returns an array which all labels. Each element is an array with # [labelname, [row,col,sheetname]] def labels - # sheet ||= default_sheet - # read_cells(sheet) - read_labels - @label.map do |label| - [ label[0], # name - [ label[1][1].to_i, # row - self.class.letter_to_number(label[1][2]), # column - label[1][0], # sheet + @labels ||= workbook.defined_names.map do |name, label| + [ name, + [ label.row, + label.col, + label.sheet, ] ] end end @@ -335,14 +550,14 @@ def hyperlink?(row,col,sheet=nil) # nil if there is no hyperlink def hyperlink(row,col,sheet=nil) key = normalize(row,col) - sheet_for(sheet).hyperlink(key) + sheet_for(sheet).hyperlinks[key] end # returns the comment at (row/col) # nil if there is no comment def comment(row,col,sheet=nil) key = normalize(row,col) - sheet_for(sheet).comment(key) + sheet_for(sheet).comments[key] end # true, if there is a comment @@ -358,198 +573,10 @@ def comments(sheet=nil) private - def workbook_doc - @workbook_doc ||= load_xml(File.join(@tmpdir, "roo_workbook.xml")) - end - - def load_xml(path) - super.remove_namespaces! - end - def load_xmls(paths) paths.compact.map do |item| - load_xml(item).remove_namespaces! - end - end - - # helper function to set the internal representation of cells - def set_cell_values(sheet,x,y,i,v,value_type,formula, - excelx_type=nil, - excelx_value=nil, - style=nil) - key = [y,x+i] - @cell_type[sheet] ||= {} - @cell_type[sheet][key] = value_type - @formula[sheet] ||= {} - @formula[sheet][key] = formula if formula - @cell[sheet] ||= {} - @cell[sheet][key] = - case @cell_type[sheet][key] - when :float - v.to_f - when :string - v - when :date - (base_date+v.to_i).strftime("%Y-%m-%d") - when :datetime - (base_date+v.to_f.round(6)).strftime("%Y-%m-%d %H:%M:%S.%N") - when :percentage - v.to_f - when :time - v.to_f*(24*60*60) - else - v - end - - @excelx_type[sheet] ||= {} - @excelx_type[sheet][key] = excelx_type - @excelx_value[sheet] ||= {} - @excelx_value[sheet][key] = excelx_value - @style[sheet] ||= {} - @style[sheet][key] = style - end - - def read_cell_from_xml(sheet, cell_xml) - style = cell_xml['s'].to_i # should be here - # c: - # 22606 - # , format: , tmp_type: float - value_type = - case cell_xml['t'] - when 's' - :shared - when 'b' - :boolean - # 2011-02-25 BEGIN - when 'str' - :string - # 2011-02-25 END - # 2011-09-15 BEGIN - when 'inlineStr' - :inlinestr - # 2011-09-15 END - else - format = style_format(style) - Format.to_type(format) - end - formula = nil - cell_xml.children.each do |cell| - case cell.name - when 'is' - cell.children.each do |is| - if is.name == 't' - inlinestr_content = is.content - value_type = :string - v = inlinestr_content - excelx_type = :string - y, x = self.class.split_coordinate(cell_xml['r']) - excelx_value = inlinestr_content #cell.content - set_cell_values(sheet,x,y,0,v,value_type,formula,excelx_type,excelx_value,style) - end - end - when 'f' - formula = cell.content - when 'v' - if [:time, :datetime].include?(value_type) && cell.content.to_f >= 1.0 - value_type = - if (cell.content.to_f - cell.content.to_f.floor).abs > 0.000001 - :datetime - else - :date - end - end - excelx_type = [:numeric_or_formula,format.to_s] - excelx_value = cell.content - v = - case value_type - when :shared - value_type = :string - excelx_type = :string - shared_strings[cell.content.to_i] - when :boolean - (cell.content.to_i == 1 ? 'TRUE' : 'FALSE') - when :date - cell.content - when :time - cell.content - when :datetime - cell.content - when :formula - cell.content.to_f #TODO: !!!! - when :string - excelx_type = :string - cell.content - else - value_type = :float - cell.content - end - y, x = self.class.split_coordinate(cell_xml['r']) - set_cell_values(sheet,x,y,0,v,value_type,formula,excelx_type,excelx_value,style) - end - end - end - - # read all cells in the selected sheet - def read_cells(sheet=nil) - sheet ||= default_sheet - validate_sheet!(sheet) - return if @cells_read[sheet] - - @sheet_doc[sheets.index(sheet)].xpath("/worksheet/sheetData/row/c").each do |c| - read_cell_from_xml(sheet, c) - end - @cells_read[sheet] = true - # begin comments -=begin -Datei xl/comments1.xml - - - - - - - - - - - - - - - Kommentar fuer B4 - - - - - - - - - - - - Kommentar fuer B5 - - - - - -=end -=begin - if @comments_doc[self.sheets.index(sheet)] - read_comments(sheet) + load_xml(item) end -=end - #end comments - end - - def read_labels - @label ||= Hash[workbook_doc.xpath("//definedName").map do |defined_name| - # "Sheet1!$C$5" - sheet, coordinates = defined_name.text.split('!$', 2) - col,row = coordinates.split('$') - [defined_name['name'], [sheet,row,col]] - end] end # Extracts all needed files from the zip file @@ -595,109 +622,15 @@ def process_zipfile(tmpdir, zipfilename) end end - def shared_strings - @shared_strings ||= - if File.exist?(shared_strings_path) - # read the shared strings xml document - xml = load_xml(shared_strings_path) - xml.xpath("/sst/si").map do |si| - shared_string = '' - si.children.each do |elem| - case elem.name - when 'r' - elem.children.each do |r_elem| - if r_elem.name == 't' - shared_string << r_elem.content - end - end - when 't' - shared_string = elem.content - end - end - shared_string - end - else - [] - end + def styles + @styles ||= Styles.new(File.join(@tmpdir, 'roo_styles.xml')) end - def shared_strings_path - @shared_strings_path ||= File.join(@tmpdir, 'roo_sharedStrings.xml') - end - - ##### STYLES - def style_definitions - @style_definitions ||= styles_doc.xpath("//cellXfs").flat_map do |xfs| - xfs.children.map do |xf| - fonts[xf['fontId'].to_i] - end - end - end - - def num_fmt_ids - @num_fmt_ids ||= styles_doc.xpath("//cellXfs").flat_map do |xfs| - xfs.children.map do |xf| - xf['numFmtId'] - end - end - end - - def num_fmts - @num_fmts ||= Hash[styles_doc.xpath("//numFmt").map do |num_fmt| - [num_fmt['numFmtId'], num_fmt['formatCode']] - end] - end - - def fonts - @fonts ||= styles_doc.xpath("//fonts/font").map do |font_el| - Font.new.tap do |font| - font.bold = !font_el.xpath('./b').empty? - font.italic = !font_el.xpath('./i').empty? - font.underline = !font_el.xpath('./u').empty? - end - end - end - - def styles_doc - @styles_doc ||= - if File.exist?(File.join(@tmpdir, 'roo_styles.xml')) - load_xml(File.join(@tmpdir, 'roo_styles.xml')) - end - end - - # convert internal excelx attribute to a format - def style_format(style) - id = num_fmt_ids[style.to_i] - num_fmts[id] || Format::STANDARD_FORMATS[id.to_i] - end - ###### END STYLES - - def base_date - @base_date ||= - begin - # Default to 1900 (minus one day due to excel quirk) but use 1904 if - # it's set in the Workbook's workbookPr - # http://msdn.microsoft.com/en-us/library/ff530155(v=office.12).aspx - workbook_doc.css("workbookPr[date1904]").each do |workbookPr| - if workbookPr["date1904"] =~ /true|1/i - return Date.new(1904,01,01) - end - end - Date.new(1899,12,30) - end - end - - def create_datetime_from(datetime_string) - date_part,time_part = round_time_from(datetime_string).split(' ') - yyyy,mm,dd = date_part.split('-') - hh,mi,ss = time_part.split(':') - DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i) + def shared_strings + @shared_strings ||= SharedStrings.new(File.join(@tmpdir, 'roo_sharedStrings.xml')) end - def round_time_from(datetime_string) - date_part,time_part = datetime_string.split(' ') - yyyy,mm,dd = date_part.split('-') - hh,mi,ss = time_part.split(':') - Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0).strftime("%Y-%m-%d %H:%M:%S") + def workbook + @workbook ||= Workbook.new(File.join(@tmpdir, "roo_workbook.xml")) end end diff --git a/lib/roo/excelx/extractor.rb b/lib/roo/excelx/extractor.rb new file mode 100644 index 00000000..565e3cd5 --- /dev/null +++ b/lib/roo/excelx/extractor.rb @@ -0,0 +1,20 @@ +module Roo + class Excelx::Extractor + def initialize(path) + @path = path + end + + private + + def doc + @doc ||= + if doc_exists? + Roo::Excelx.load_xml(@path) + end + end + + def doc_exists? + File.exist?(@path) + end + end +end diff --git a/lib/roo/excelx/shared_strings.rb b/lib/roo/excelx/shared_strings.rb new file mode 100644 index 00000000..feed958c --- /dev/null +++ b/lib/roo/excelx/shared_strings.rb @@ -0,0 +1,34 @@ +require 'roo/excelx/extractor' + +module Roo + class Excelx::SharedStrings < Excelx::Extractor + def [](index) + to_a[index] + end + + def to_a + @array ||= + if doc_exists? + # read the shared strings xml document + doc.xpath("/sst/si").map do |si| + shared_string = '' + si.children.each do |elem| + case elem.name + when 'r' + elem.children.each do |r_elem| + if r_elem.name == 't' + shared_string << r_elem.content + end + end + when 't' + shared_string = elem.content + end + end + shared_string + end + else + [] + end + end + end +end diff --git a/lib/roo/excelx/styles.rb b/lib/roo/excelx/styles.rb new file mode 100644 index 00000000..98fbdd5c --- /dev/null +++ b/lib/roo/excelx/styles.rb @@ -0,0 +1,61 @@ +require 'roo/excelx/extractor' + +module Roo + class Excelx::Styles < Excelx::Extractor + class Font + attr_accessor :bold, :italic, :underline + + def bold? + @bold == true + end + + def italic? + @italic == true + end + + def underline? + @underline == true + end + end + + # convert internal excelx attribute to a format + def style_format(style) + id = num_fmt_ids[style.to_i] + num_fmts[id] || Excelx::Format::STANDARD_FORMATS[id.to_i] + end + + def definitions + @definitions ||= doc.xpath("//cellXfs").flat_map do |xfs| + xfs.children.map do |xf| + fonts[xf['fontId'].to_i] + end + end + end + + private + + def num_fmt_ids + @num_fmt_ids ||= doc.xpath("//cellXfs").flat_map do |xfs| + xfs.children.map do |xf| + xf['numFmtId'] + end + end + end + + def num_fmts + @num_fmts ||= Hash[doc.xpath("//numFmt").map do |num_fmt| + [num_fmt['numFmtId'], num_fmt['formatCode']] + end] + end + + def fonts + @fonts ||= doc.xpath("//fonts/font").map do |font_el| + Font.new.tap do |font| + font.bold = !font_el.xpath('./b').empty? + font.italic = !font_el.xpath('./i').empty? + font.underline = !font_el.xpath('./u').empty? + end + end + end + end +end diff --git a/lib/roo/excelx/workbook.rb b/lib/roo/excelx/workbook.rb new file mode 100644 index 00000000..537aaae2 --- /dev/null +++ b/lib/roo/excelx/workbook.rb @@ -0,0 +1,52 @@ +require 'roo/excelx/extractor' + +module Roo + class Excelx::Workbook < Excelx::Extractor + class Label + attr_reader :sheet, :row, :col, :name + + def initialize(name, sheet, row, col) + @name = name + @sheet = sheet + @row = row.to_i + @col = Roo::Base.letter_to_number(col) + end + + def key + [@row, @col] + end + end + + def sheets + doc.xpath("//sheet") + end + + # aka labels + def defined_names + Hash[doc.xpath("//definedName").map do |defined_name| + # "Sheet1!$C$5" + sheet, coordinates = defined_name.text.split('!$', 2) + col,row = coordinates.split('$') + name = defined_name['name'] + [name, Label.new(name, sheet,row,col)] + end] + end + + def base_date + @base_date ||= + begin + # Default to 1900 (minus one day due to excel quirk) but use 1904 if + # it's set in the Workbook's workbookPr + # http://msdn.microsoft.com/en-us/library/ff530155(v=office.12).aspx + result = Date.new(1899,12,30) # default + doc.xpath("//workbookPr[date1904]").each do |workbookPr| + if workbookPr["date1904"] =~ /true|1/i + result = Date.new(1904,01,01) + break + end + end + result + end + end + end +end diff --git a/test/test_roo.rb b/test/test_roo.rb index d2bac112..7b269a83 100644 --- a/test/test_roo.rb +++ b/test/test_roo.rb @@ -1104,12 +1104,12 @@ def test_simple2_excelx def test_datetime with_each_spreadsheet(:name=>'datetime') do |oo| val = oo.cell('c',3) - assert_kind_of DateTime, val assert_equal :datetime, oo.celltype('c',3) assert_equal DateTime.new(1961,11,21,12,17,18), val + assert_kind_of DateTime, val val = oo.cell('a',1) - assert_kind_of Date, val assert_equal :date, oo.celltype('a',1) + assert_kind_of Date, val assert_equal Date.new(1961,11,21), val assert_equal Date.new(1961,11,21), oo.cell('a',1) assert_equal DateTime.new(1961,11,21,12,17,18), oo.cell('a',3)