diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a9484a..cef37fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [2.1.1] - 2015-08-02 +### Fixed invalid new lines with _x000D_ character[#231](https://github.com/roo-rb/roo/pull/231) +### Fixed missing URI issue. [#245](https://github.com/roo-rb/roo/pull/245) + ## [2.1.0] - 2015-07-18 ### Added - Added support for Excel 2007 `xlsm` files. [#232](https://github.com/roo-rb/roo/pull/232) diff --git a/README.md b/README.md index 2f091709..5f51a277 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,11 @@ [![Build Status](https://img.shields.io/travis/roo-rb/roo.svg?style=flat-square)](https://travis-ci.org/roo-rb/roo) [![Code Climate](https://img.shields.io/codeclimate/github/roo-rb/roo.svg?style=flat-square)](https://codeclimate.com/github/roo-rb/roo) [![Coverage Status](https://img.shields.io/coveralls/roo-rb/roo.svg?style=flat-square)](https://coveralls.io/r/roo-rb/roo) [![Gem Version](https://img.shields.io/gem/v/roo.svg?style=flat-square)](https://rubygems.org/gems/roo) Roo implements read access for all common spreadsheet types. It can handle: - -* Excelx -* OpenOffice / LibreOffice +* Excel 2007 - 2013 formats (xlsx, xlsm) +* LibreOffice / OpenOffice.org formats (ods) * CSV - -## Additional Libraries - -In addition, the [roo-xls](https://github.com/roo-rb/roo-xls) and [roo-google](https://github.com/roo-rb/roo-google) gems exist to extend Roo to support reading classic Excel formats (i.e. `.xls` and ``Excel2003XML``) and read/write access for Google spreadsheets. +* Excel 97, Excel 2002 XML, and Excel 2003 XML formats when using the [roo-xls](https://github.com/roo-rb/roo-xls) gem (xls, xml) +* Google spreadsheets with read/write access when using [roo-google](https://github.com/roo-rb/roo-google) ## Installation diff --git a/lib/roo.rb b/lib/roo.rb index bedcd5be..b93b3239 100644 --- a/lib/roo.rb +++ b/lib/roo.rb @@ -1,4 +1,5 @@ require 'roo/constants' +require 'roo/errors' require 'roo/spreadsheet' require 'roo/base' diff --git a/lib/roo/base.rb b/lib/roo/base.rb index f6b2e0da..6d313ba3 100644 --- a/lib/roo/base.rb +++ b/lib/roo/base.rb @@ -91,7 +91,7 @@ def collect_last_row_col_for_sheet(sheet) first_column = [first_column, key.last.to_i].min last_column = [last_column, key.last.to_i].max end if @cell[sheet] - {first_row: first_row, first_column: first_column, last_row: last_row, last_column: last_column} + { first_row: first_row, first_column: first_column, last_row: last_row, last_column: last_column } end %w(first_row last_row first_column last_column).each do |key| @@ -117,22 +117,23 @@ def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_col result = "--- \n" from_row.upto(to_row) do |row| from_column.upto(to_column) do |col| - unless empty?(row, col, sheet) - result << "cell_#{row}_#{col}: \n" - prefix.each do|k, v| - result << " #{k}: #{v} \n" - end - result << " row: #{row} \n" - result << " col: #{col} \n" - result << " celltype: #{celltype(row, col, sheet)} \n" - value = cell(row, col, sheet) - if celltype(row, col, sheet) == :time - value = integer_to_timestring(value) - end - result << " value: #{value} \n" + next if empty?(row, col, sheet) + + result << "cell_#{row}_#{col}: \n" + prefix.each do|k, v| + result << " #{k}: #{v} \n" + end + result << " row: #{row} \n" + result << " col: #{col} \n" + result << " celltype: #{celltype(row, col, sheet)} \n" + value = cell(row, col, sheet) + if celltype(row, col, sheet) == :time + value = integer_to_timestring(value) end + result << " value: #{value} \n" end end + result end @@ -170,7 +171,7 @@ def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, end def inspect - "<##{ self.class }:#{ self.object_id.to_s(8) } #{ self.instance_variables.join(' ') }>" + "<##{self.class}:#{object_id.to_s(8)} #{instance_variables.join(' ')}>" end # find a row either by row number or a condition @@ -217,7 +218,7 @@ def set(row, col, value, sheet = default_sheet) #:nodoc: row, col = normalize(row, col) cell_type = cell_type_by_value(value) set_value(row, col, value, sheet) - set_type(row, col, cell_type , sheet) + set_type(row, col, cell_type, sheet) end def cell_type_by_value(value) @@ -225,7 +226,7 @@ def cell_type_by_value(value) when Fixnum then :float when String, Float then :string else - raise ArgumentError, "Type for #{value} not set" + fail ArgumentError, "Type for #{value} not set" end end @@ -256,13 +257,13 @@ def info sheets.each do|sheet| self.default_sheet = sheet result << 'Sheet ' + n.to_s + ":\n" - unless first_row - result << ' - empty -' - else + if first_row result << " First row: #{first_row}\n" result << " Last row: #{last_row}\n" result << " First column: #{::Roo::Utils.number_to_letter(first_column)}\n" result << " Last column: #{::Roo::Utils.number_to_letter(last_column)}" + else + result << ' - empty -' end result << "\n" if sheet != sheets.last n += 1 @@ -282,12 +283,12 @@ def to_xml # sonst gibt es Fehler bei leeren Blaettern first_row.upto(last_row) do |row| first_column.upto(last_column) do |col| - unless empty?(row, col) - x.cell(cell(row, col), + next if empty?(row, col) + + x.cell(cell(row, col), row: row, column: col, type: celltype(row, col)) - end end end end @@ -318,7 +319,7 @@ def method_missing(m, *args) # access different worksheets by calling spreadsheet.sheet(1) # or spreadsheet.sheet('SHEETNAME') def sheet(index, name = false) - self.default_sheet = String === index ? index : sheets[index] + self.default_sheet = index.is_a?(::String) ? index : sheets[index] name ? [default_sheet, self] : self end @@ -352,25 +353,23 @@ def each_with_pagename # control characters and white spaces around columns def each(options = {}) - if block_given? - if options.empty? - 1.upto(last_row) do |line| - yield row(line) - end - else - clean_sheet_if_need(options) - search_or_set_header(options) - headers = @headers || - Hash[(first_column..last_column).map do |col| - [cell(@header_line, col), col] - end] - - @header_line.upto(last_row) do |line| - yield(Hash[headers.map { |k, v| [k, cell(line, v)] }]) - end + return to_enum(:each, options) unless block_given? + + if options.empty? + 1.upto(last_row) do |line| + yield row(line) end else - to_enum(:each, options) + clean_sheet_if_need(options) + search_or_set_header(options) + headers = @headers || + Hash[(first_column..last_column).map do |col| + [cell(@header_line, col), col] + end] + + @header_line.upto(last_row) do |line| + yield(Hash[headers.map { |k, v| [k, cell(line, v)] }]) + end end end @@ -393,10 +392,10 @@ def row_with(query, return_headers = false) @header_line = line_no return return_headers ? headers : line_no elsif line_no > 100 - fail "Couldn't find header row." + raise Roo::HeaderRowNotFoundError end end - fail "Couldn't find header row." + raise Roo::HeaderRowNotFoundError end protected @@ -409,23 +408,24 @@ def file_type_check(filename, exts, name, warning_level, packed = nil) filename = File.basename(filename, File.extname(filename)) end - if uri?(filename) && qs_begin = filename.rindex('?') + if uri?(filename) && (qs_begin = filename.rindex('?')) filename = filename[0..qs_begin - 1] end exts = Array(exts) - if !exts.include?(File.extname(filename).downcase) - case warning_level - when :error - warn file_type_warning_message(filename, exts) - fail TypeError, "#{filename} is not #{name} file" - when :warning - warn "are you sure, this is #{name} spreadsheet file?" - warn file_type_warning_message(filename, exts) - when :ignore - # ignore - else - fail "#{warning_level} illegal state of file_warning" - end + + return if exts.include?(File.extname(filename).downcase) + + case warning_level + when :error + warn file_type_warning_message(filename, exts) + fail TypeError, "#{filename} is not #{name} file" + when :warning + warn "are you sure, this is #{name} spreadsheet file?" + warn file_type_warning_message(filename, exts) + when :ignore + # ignore + else + fail "#{warning_level} illegal state of file_warning" end end @@ -476,9 +476,9 @@ def local_filename(filename, tmpdir, packed) return if is_stream?(filename) filename = download_uri(filename, tmpdir) if uri?(filename) filename = unzip(filename, tmpdir) if packed == :zip - unless File.file?(filename) - fail IOError, "file #{filename} does not exist" - end + + fail IOError, "file #{filename} does not exist" unless File.file?(filename) + filename end @@ -536,11 +536,8 @@ def reinitialize end def make_tmpdir(prefix = nil, root = nil, &block) - prefix = if prefix - TEMP_PREFIX + prefix - else - TEMP_PREFIX - end + prefix = "#{TEMP_PREFIX}#{prefix}" + ::Dir.mktmpdir(prefix, root || ENV['ROO_TMP'], &block).tap do |result| block_given? || track_tmpdir!(result) end @@ -588,9 +585,9 @@ def normalize(row, col) fail ArgumentError end end - if col.is_a?(::String) - col = ::Roo::Utils.letter_to_number(col) - end + + col = ::Roo::Utils.letter_to_number(col) if col.is_a?(::String) + [row, col] end @@ -641,7 +638,7 @@ def validate_sheet!(sheet) fail RangeError, "sheet index #{sheet} not found" end when String - unless sheets.include? sheet + unless sheets.include?(sheet) fail RangeError, "sheet '#{sheet}' not found" end else @@ -670,14 +667,14 @@ def process_zipfile_packed(zip, tmpdir, path = '') # parameter is nil the output goes to STDOUT def write_csv_content(file = nil, sheet = nil, separator = ',') file ||= STDOUT - if first_row(sheet) # sheet is not empty - 1.upto(last_row(sheet)) do |row| - 1.upto(last_column(sheet)) do |col| - file.print(separator) if col > 1 - file.print cell_to_csv(row, col, sheet) - end - file.print("\n") - end # sheet not empty + return unless first_row(sheet) # The sheet is empty + + 1.upto(last_row(sheet)) do |row| + 1.upto(last_column(sheet)) do |col| + file.print(separator) if col > 1 + file.print cell_to_csv(row, col, sheet) + end + file.print("\n") end end @@ -729,9 +726,9 @@ def cell_to_csv(row, col, sheet) # converts an integer value to a time string like '02:05:06' def integer_to_timestring(content) h = (content / 3600.0).floor - content = content - h * 3600 + content -= h * 3600 m = (content / 60.0).floor - content = content - m * 60 + content -= m * 60 s = content sprintf('%02d:%02d:%02d', h, m, s) end diff --git a/lib/roo/errors.rb b/lib/roo/errors.rb new file mode 100644 index 00000000..fd3fc125 --- /dev/null +++ b/lib/roo/errors.rb @@ -0,0 +1,9 @@ +module Roo + # A base error class for Roo. Most errors thrown by Roo should inherit from + # this class. + class Error < StandardError; end + + # Raised when Roo cannot find a header row that matches the given column + # name(s). + class HeaderRowNotFoundError < Error; end +end diff --git a/lib/roo/excelx.rb b/lib/roo/excelx.rb index 7df01e16..4e9c4545 100644 --- a/lib/roo/excelx.rb +++ b/lib/roo/excelx.rb @@ -2,9 +2,13 @@ require 'zip/filesystem' require 'roo/link' require 'roo/utils' +require 'forwardable' module Roo class Excelx < Roo::Base + extend Forwardable + + require 'roo/excelx/shared' require 'roo/excelx/workbook' require 'roo/excelx/shared_strings' require 'roo/excelx/styles' @@ -15,6 +19,8 @@ class Excelx < Roo::Base require 'roo/excelx/sheet_doc' require 'roo/excelx/coordinate' + delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files] => :@shared + module Format EXCEPTIONAL_FORMATS = { 'h:mm am/pm' => :date, @@ -95,9 +101,8 @@ def initialize(filename_or_stream, options = {}) end @tmpdir = make_tmpdir(basename, options[:tmpdir_root]) + @shared = Shared.new(@tmpdir) @filename = local_filename(filename_or_stream, @tmpdir, packed) - @comments_files = [] - @rels_files = [] process_zipfile(@filename || filename_or_stream) @sheet_names = workbook.sheets.map do |sheet| @@ -107,7 +112,7 @@ def initialize(filename_or_stream, options = {}) end.compact @sheets = [] @sheets_by_name = Hash[@sheet_names.map.with_index do |sheet_name, n| - @sheets[n] = Sheet.new(sheet_name, @rels_files[n], @sheet_files[n], @comments_files[n], styles, shared_strings, workbook, sheet_options) + @sheets[n] = Sheet.new(sheet_name, @shared, n, sheet_options) [sheet_name, @sheets[n]] end] @@ -405,6 +410,7 @@ def extract_sheets_in_order(entries, sheet_ids, sheets, tmpdir) name = sheets[id] entry = entries.find { |e| e.name =~ /#{name}$/ } path = "#{tmpdir}/roo_sheet#{i + 1}" + sheet_files << path @sheet_files << path entry.extract(path) end @@ -458,13 +464,13 @@ def process_zipfile_entries(entries) # sheet's comment file is in the sheet1.xml.rels file. SEE # ECMA-376 12.3.3 in "Ecma Office Open XML Part 1". nr = Regexp.last_match[1].to_i - @comments_files[nr - 1] = "#{@tmpdir}/roo_comments#{nr}" + comments_files[nr - 1] = "#{@tmpdir}/roo_comments#{nr}" when /sheet([0-9]+).xml.rels$/ # FIXME: Roo seems to use sheet[\d].xml.rels for hyperlinks only, but # it also stores the location for sharedStrings, comments, # drawings, etc. nr = Regexp.last_match[1].to_i - @rels_files[nr - 1] = "#{@tmpdir}/roo_rels#{nr}" + rels_files[nr - 1] = "#{@tmpdir}/roo_rels#{nr}" end entry.extract(path) if path diff --git a/lib/roo/excelx/shared.rb b/lib/roo/excelx/shared.rb new file mode 100644 index 00000000..3677fa25 --- /dev/null +++ b/lib/roo/excelx/shared.rb @@ -0,0 +1,32 @@ +module Roo + class Excelx + # Public: Shared class for allowing sheets to share data. This should + # reduce memory usage and reduce the number of objects being passed + # to various inititializers. + class Shared + attr_accessor :comments_files, :sheet_files, :rels_files + def initialize(dir) + @dir = dir + @comments_files = [] + @sheet_files = [] + @rels_files = [] + end + + def styles + @styles ||= Styles.new(File.join(@dir, 'roo_styles.xml')) + end + + def shared_strings + @shared_strings ||= SharedStrings.new(File.join(@dir, 'roo_sharedStrings.xml')) + end + + def workbook + @workbook ||= Workbook.new(File.join(@dir, 'roo_workbook.xml')) + end + + def base_date + workbook.base_date + end + end + end +end diff --git a/lib/roo/excelx/shared_strings.rb b/lib/roo/excelx/shared_strings.rb index c2fd5ebe..4abf3804 100644 --- a/lib/roo/excelx/shared_strings.rb +++ b/lib/roo/excelx/shared_strings.rb @@ -13,9 +13,19 @@ def to_a private + def fix_invalid_shared_strings(doc) + invalid = { '_x000D_' => "\n" } + xml = doc.to_s + + if xml[/#{invalid.keys.join('|')}/] + @doc = ::Nokogiri::XML(xml.gsub(/#{invalid.keys.join('|')}/, invalid)) + end + end + def extract_shared_strings return [] unless doc_exists? + fix_invalid_shared_strings(doc) # read the shared strings xml document doc.xpath('/sst/si').map do |si| shared_string = '' diff --git a/lib/roo/excelx/sheet.rb b/lib/roo/excelx/sheet.rb index 27aa8e82..add92f0a 100644 --- a/lib/roo/excelx/sheet.rb +++ b/lib/roo/excelx/sheet.rb @@ -1,12 +1,17 @@ +require 'forwardable' module Roo class Excelx class Sheet - def initialize(name, rels_path, sheet_path, comments_path, styles, shared_strings, workbook, options = {}) + extend Forwardable + + delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files] => :@shared + + def initialize(name, shared, sheet_index, options = {}) @name = name - @rels = Relationships.new(rels_path) - @comments = Comments.new(comments_path) - @styles = styles - @sheet = SheetDoc.new(sheet_path, @rels, @styles, shared_strings, workbook, options) + @shared = shared + @rels = Relationships.new(rels_files[sheet_index]) + @comments = Comments.new(comments_files[sheet_index]) + @sheet = SheetDoc.new(sheet_files[sheet_index], @rels, shared, options) end def cells @@ -65,7 +70,7 @@ def last_column def excelx_format(key) cell = cells[key] - @styles.style_format(cell.style).to_s if cell + styles.style_format(cell.style).to_s if cell end def hyperlinks diff --git a/lib/roo/excelx/sheet_doc.rb b/lib/roo/excelx/sheet_doc.rb index f6458d2b..3e88b44f 100644 --- a/lib/roo/excelx/sheet_doc.rb +++ b/lib/roo/excelx/sheet_doc.rb @@ -1,15 +1,17 @@ +require 'forwardable' require 'roo/excelx/extractor' module Roo class Excelx class SheetDoc < Excelx::Extractor - def initialize(path, relationships, styles, shared_strings, workbook, options = {}) + extend Forwardable + delegate [:styles, :workbook, :shared_strings, :base_date] => :@shared + + def initialize(path, relationships, shared, options = {}) super(path) + @shared = shared @options = options @relationships = relationships - @styles = styles - @shared_strings = shared_strings - @workbook = workbook end def cells(relationships) @@ -81,7 +83,7 @@ def cell_from_xml(cell_xml, hyperlink) # NOTE: This is error prone, to_i will silently turn a nil into a 0. # This works by coincidence because Format[0] is General. style = cell_xml['s'].to_i - format = @styles.style_format(style) + format = styles.style_format(style) value_type = cell_value_type(cell_xml['t'], format) formula = nil @@ -96,7 +98,7 @@ def cell_from_xml(cell_xml, hyperlink) when 'f' formula = cell.content when 'v' - return create_cell_from_value(value_type, cell, formula, format, style, hyperlink, @workbook.base_date, coordinate) + return create_cell_from_value(value_type, cell, formula, format, style, hyperlink, base_date, coordinate) end end end @@ -117,7 +119,7 @@ def create_cell_from_value(value_type, cell, formula, format, style, hyperlink, # 3. formula case value_type when :shared - value = @shared_strings[cell.content.to_i] + value = shared_strings[cell.content.to_i] Excelx::Cell.create_cell(:string, value, formula, style, hyperlink, coordinate) when :boolean, :string value = cell.content diff --git a/lib/roo/spreadsheet.rb b/lib/roo/spreadsheet.rb index aae511a2..1eef58d5 100644 --- a/lib/roo/spreadsheet.rb +++ b/lib/roo/spreadsheet.rb @@ -1,3 +1,5 @@ +require 'uri' + module Roo class Spreadsheet class << self diff --git a/lib/roo/version.rb b/lib/roo/version.rb index 8a38ad23..101b3f92 100644 --- a/lib/roo/version.rb +++ b/lib/roo/version.rb @@ -1,3 +1,3 @@ module Roo - VERSION = "2.1.0" + VERSION = "2.1.1" end diff --git a/spec/helpers.rb b/spec/helpers.rb new file mode 100644 index 00000000..f1406595 --- /dev/null +++ b/spec/helpers.rb @@ -0,0 +1,5 @@ +module Helpers + def yaml_entry(row,col,type,value) + "cell_#{row}_#{col}: \n row: #{row} \n col: #{col} \n celltype: #{type} \n value: #{value} \n" + end +end diff --git a/spec/lib/roo/base_spec.rb b/spec/lib/roo/base_spec.rb index 4e51aaf7..d00025d8 100644 --- a/spec/lib/roo/base_spec.rb +++ b/spec/lib/roo/base_spec.rb @@ -1,4 +1,233 @@ require 'spec_helper' describe Roo::Base do + let(:klass) do + Class.new(Roo::Base) do + def initialize(filename, data = {}) + super(filename) + @data ||= data + end + + def read_cells(sheet = default_sheet) + return if @cells_read[sheet] + type_map = { String => :string, Date => :date, Numeric => :float } + + @cell[sheet] = @data + @cell_type[sheet] = Hash[@data.map { |k, v| [k, type_map.find {|type,_| v.is_a?(type) }.last ] }] + @first_row[sheet] = @data.map { |k, _| k[0] }.min + @last_row[sheet] = @data.map { |k, _| k[0] }.max + @first_column[sheet] = @data.map { |k, _| k[1] }.min + @last_column[sheet] = @data.map { |k, _| k[1] }.max + @cells_read[sheet] = true + end + + def cell(row, col, sheet = nil) + sheet ||= default_sheet + read_cells(sheet) + @cell[sheet][[row, col]] + end + + def celltype(row, col, sheet = nil) + sheet ||= default_sheet + read_cells(sheet) + @cell_type[sheet][[row, col]] + end + + def sheets + ['my_sheet', 'blank sheet'] + end + end + end + + let(:spreadsheet_data) do + { + [3, 1] => 'Header', + + [5, 1] => Date.civil(1961, 11, 21), + + [8, 3] => 'thisisc8', + [8, 7] => 'thisisg8', + + [12, 1] => 41.0, + [12, 2] => 42.0, + [12, 3] => 43.0, + [12, 4] => 44.0, + [12, 5] => 45.0, + + [15, 3] => 43.0, + [15, 4] => 44.0, + [15, 5] => 45.0, + + [16, 2] => '"Hello world!"', + [16, 3] => 'forty-three', + [16, 4] => 'forty-four', + [16, 5] => 'forty-five' + } + end + + let(:spreadsheet) { klass.new('some_file', spreadsheet_data) } + + describe '#uri?' do + it 'should return true when passed a filename starting with http(s)://' do + expect(spreadsheet.send(:uri?, 'http://example.com/')).to be_truthy + expect(spreadsheet.send(:uri?, 'https://example.com/')).to be_truthy + end + + it 'should return false when passed a filename which does not start with http(s)://' do + expect(spreadsheet.send(:uri?, 'example.com')).to be_falsy + end + + it 'should return false when passed non-String object such as Tempfile' do + expect(spreadsheet.send(:uri?, Tempfile.new('test'))).to be_falsy + end + end + + describe '#set' do + it 'should not update cell when setting an invalid type' do + spreadsheet.set(1, 1, 1) + expect { spreadsheet.set(1, 1, :invalid_type) }.to raise_error(ArgumentError) + expect(spreadsheet.cell(1, 1)).to eq(1) + expect(spreadsheet.celltype(1, 1)).to eq(:float) + end + end + + describe '#first_row' do + it 'should return the first row' do + expect(spreadsheet.first_row).to eq(3) + end + end + + describe '#last_row' do + it 'should return the last row' do + expect(spreadsheet.last_row).to eq(16) + end + end + + describe '#first_column' do + it 'should return the first column' do + expect(spreadsheet.first_column).to eq(1) + end + end + + describe '#first_column_as_letter' do + it 'should return the first column as a letter' do + expect(spreadsheet.first_column_as_letter).to eq('A') + end + end + + describe '#last_column' do + it 'should return the last column' do + expect(spreadsheet.last_column).to eq(7) + end + end + + describe '#last_column_as_letter' do + it 'should return the last column as a letter' do + expect(spreadsheet.last_column_as_letter).to eq('G') + end + end + + describe '#row' do + it 'should return the specified row' do + expect(spreadsheet.row(12)).to eq([41.0, 42.0, 43.0, 44.0, 45.0, nil, nil]) + expect(spreadsheet.row(16)).to eq([nil, '"Hello world!"', 'forty-three', 'forty-four', 'forty-five', nil, nil]) + end + end + + describe '#row_with' do + context 'with a matching header row' do + it 'returns the row number' do + expect(spreadsheet.row_with([/Header/])). to eq 3 + end + end + + context 'without a matching header row' do + it 'raises an error' do + expect { spreadsheet.row_with([/Missing Header/]) }.to \ + raise_error(Roo::HeaderRowNotFoundError) + end + end + end + + describe '#empty?' do + it 'should return true when empty' do + expect(spreadsheet.empty?(1, 1)).to be_truthy + expect(spreadsheet.empty?(8, 3)).to be_falsy + expect(spreadsheet.empty?('A', 11)).to be_truthy + expect(spreadsheet.empty?('A', 12)).to be_falsy + end + end + + describe '#reload' do + it 'should return reinitialize the spreadsheet' do + spreadsheet.reload + expect(spreadsheet.instance_variable_get(:@cell).empty?).to be_truthy + end + end + + describe '#each' do + it 'should return an enumerator with all the rows' do + each = spreadsheet.each + expect(each).to be_a(Enumerator) + expect(each.to_a.last).to eq([nil, '"Hello world!"', 'forty-three', 'forty-four', 'forty-five', nil, nil]) + end + end + + describe '#to_yaml' do + it 'should convert the spreadsheet to yaml' do + expect(spreadsheet.to_yaml({}, 5, 1, 5, 1)).to eq("--- \n" + yaml_entry(5, 1, 'date', '1961-11-21')) + expect(spreadsheet.to_yaml({}, 8, 3, 8, 3)).to eq("--- \n" + yaml_entry(8, 3, 'string', 'thisisc8')) + + expect(spreadsheet.to_yaml({}, 12, 3, 12, 3)).to eq("--- \n" + yaml_entry(12, 3, 'float', 43.0)) + + expect(spreadsheet.to_yaml({}, 12, 3, 12)).to eq( + "--- \n" + yaml_entry(12, 3, 'float', 43.0) + + yaml_entry(12, 4, 'float', 44.0) + + yaml_entry(12, 5, 'float', 45.0)) + + expect(spreadsheet.to_yaml({}, 12, 3)).to eq( + "--- \n" + yaml_entry(12, 3, 'float', 43.0) + + yaml_entry(12, 4, 'float', 44.0) + + yaml_entry(12, 5, 'float', 45.0) + + yaml_entry(15, 3, 'float', 43.0) + + yaml_entry(15, 4, 'float', 44.0) + + yaml_entry(15, 5, 'float', 45.0) + + yaml_entry(16, 3, 'string', 'forty-three') + + yaml_entry(16, 4, 'string', 'forty-four') + + yaml_entry(16, 5, 'string', 'forty-five')) + end + end + + let(:expected_csv) do + < Date.civil(1961, 11, 21).to_s, - - [8, 3] => 'thisisc8', - [8, 7] => 'thisisg8', - - [12, 1] => 41.0, - [12, 2] => 42.0, - [12, 3] => 43.0, - [12, 4] => 44.0, - [12, 5] => 45.0, - - [15, 3] => 43.0, - [15, 4] => 44.0, - [15, 5] => 45.0, - - [16, 2] => '"Hello world!"', - [16, 3] => 'dreiundvierzig', - [16, 4] => 'vierundvierzig', - [16, 5] => 'fuenfundvierzig' - } - end - - def set_sheet_types(workbook) - workbook.instance_variable_get(:@cell_type)[workbook.default_sheet] = { - [5, 1] => :date, - - [8, 3] => :string, - [8, 7] => :string, - - [12, 1] => :float, - [12, 2] => :float, - [12, 3] => :float, - [12, 4] => :float, - [12, 5] => :float, - - [15, 3] => :float, - [15, 4] => :float, - [15, 5] => :float, - - [16, 2] => :string, - [16, 3] => :string, - [16, 4] => :string, - [16, 5] => :string - } - end - - def set_first_row(workbook) - row_hash = workbook.instance_variable_get(:@first_row) - row_hash[workbook.default_sheet] = workbook.instance_variable_get(:@cell)[workbook.default_sheet].map { |k, _v| k[0] }.min - end - - def set_last_row(workbook) - row_hash = workbook.instance_variable_get(:@last_row) - row_hash[workbook.default_sheet] = workbook.instance_variable_get(:@cell)[workbook.default_sheet].map { |k, _v| k[0] }.max - end - - def set_first_col(workbook) - col_hash = workbook.instance_variable_get(:@first_column) - col_hash[workbook.default_sheet] = workbook.instance_variable_get(:@cell)[workbook.default_sheet].map { |k, _v| k[1] }.min - end - - def set_last_col(workbook) - col_hash = workbook.instance_variable_get(:@last_column) - col_hash[workbook.default_sheet] = workbook.instance_variable_get(:@cell)[workbook.default_sheet].map { |k, _v| k[1] }.max - end - - def set_cells_read(workbook) - read_hash = workbook.instance_variable_get(:@cells_read) - read_hash[workbook.default_sheet] = true - end - - def expected_csv - <