Skip to content

Commit

Permalink
Added support to load an excel file with the Kernel method Table. Exa…
Browse files Browse the repository at this point in the history
…mple: Table(excelfile.xls)

git-svn-id: http://stonecode.svnrepository.com/svn/ruport-util/trunk@40 8bb0c7d9-1235-0410-a803-a4cf5c4fc5c0
  • Loading branch information
wes committed Jan 31, 2008
1 parent c3aba2e commit 643a978
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/ruport/util.rb
Expand Up @@ -22,3 +22,4 @@ module Util
require "ruport/util/query"
require "ruport/util/xls"
require "ruport/util/ods_table"
require "ruport/util/xls_table"
136 changes: 136 additions & 0 deletions lib/ruport/util/xls_table.rb
@@ -0,0 +1,136 @@
require 'ruport'

# === Overview
#
# This class extends the core class Ruport::Data::Table and adds support for loading Excel
# spreadsheet files using roo. The idea is to get data from speadsheets that may contain
# already calculated values entered by non-programmers.
#
# Once your data is in a Table object, it can be manipulated
# to suit your needs, then used to build a report.
#
# Copyright (C) 2008, Wes Hays
# All Rights Reserved.
#
class Ruport::Data::Table

# === Overview
#
# This module provides facilities for creating tables from Excel spreadsheet file (xls).
#
module FromXLS
# Loads a xls file directly into a Table using the roo library.
#
# Example:
#
# # Load data from an Excel xls file with defaults
# table = Table.load_xls('myspreadsheet.xls')
#
# # do not assume the data has column names - default is false.
# table = Table.load_xls('myspreadsheet.xls',{:has_column_names => false})
#
# # Select sheet - default is the first sheet.
# table = Table.load_xls('myspreadsheet.xls', {:select_sheet => 1})
#
# # Start row - default is the first row. Use this to override where
# the first row should start.
# table = Table.load_xls('myspreadsheet.xls', {:start_row => 1})
#
def load_xls(xls_file, options={})
get_table_from_xls_file(xls_file, options)
end

# Creates a Table from an Excel object (from roo library).
#
# Example:
#
# # parse excel object with defaults.
# table = Table.parse_xls(excel_object)
#
# # do not assume the data has column names.
# table = Table.parse_xls(excel_object,{:has_column_names => false})
#
# # Select sheet - default is the first sheet.
# table = Table.parse_xls(excel_object, {:select_sheet => 1})
#
# # Start row - default is the first row. Use this to override where
# the first row should start.
# table = Table.parse_xls('myspreadsheet.xls', {:start_row => 1})
#
def parse_xls(xls_object, options={})
get_table_from_xls(xls_object, options)
end

private

def get_table_from_xls_file(xls_file, options) #:nodoc:
require 'roo'
oo = Excel.new(xls_file)
get_table_from_xls(oo, options)
end

def get_table_from_xls(oo, options) #:nodoc:
options = {:has_column_names => true,
:select_sheet => oo.sheets.first,
:start_row => 0}.merge(options)
oo.default_sheet = options[:select_sheet]

options[:start_row] = options[:start_row].to_i + 1 unless options[:start_row].nil?
start_row = options[:start_row]

raise 'start_row must be greater than or equal to zero' if options[:start_row].to_i < 0

last_row_index_zero = oo.last_row - 1
raise "start_row must be less than or equal to #{last_row_index_zero}" if !oo.last_row.nil? and
(options[:start_row].to_i > oo.last_row)

table = self.new(options) do |feeder|

if options[:has_column_names] == true
feeder.data.column_names = oo.row(start_row)
start_row = start_row + 1
end

unless oo.last_row.nil?
start_row.upto(oo.last_row) do |row|
tempArr = []
1.upto(oo.last_column) do |col|
tempArr << oo.cell(row,col)
end
feeder << tempArr
end
end

end

return table
end

end

extend FromXLS

end


module Kernel

alias :RuportTableMethod2 :Table

# Updates the Ruport interface for creating Data::Tables with
# the ability to pass in a XLS file or Roo Excel object.
#
# t = Table("myspreadsheet.xls")
# t = Table("myspreadsheet.xls", :has_column_names => true)
def Table(*args,&block)
table=
case(args[0])
when /\.xls/
Ruport::Data::Table.load_xls(*args)
else
RuportTableMethod2(*args,&block)
end

return table
end
end
Binary file added test/samples/people.xls
Binary file not shown.
236 changes: 236 additions & 0 deletions test/test_xls_table.rb
@@ -0,0 +1,236 @@
# Copyright (C) 2008, Wes Hays
# All Rights Reserved.

require 'test/helper'
testcase_requires 'roo'

describe 'Ruport::Data::TableFromXLS' do
before(:each) do
@xls_file = 'test/samples/people.xls'
@csv_file = 'test/samples/data.csv'

@xls_file_column_names = %w(Name Age DOB)
# This test will pass once Spreadsheet and Roo support
# formulas in an excel file.
# @rows = [ ['Andy', 27.0, Date.parse('01/20/1980')],
# ['Bob', 26.0, Date.parse('02/11/1981')],
# ['Charlie', 20.0, Date.parse('03/14/1987')],
# ['David', 73.0, Date.parse('04/26/1997')] ]

# Delete this once Roo supports formulas in an excel file.
@rows = [ ['Andy', 27.0, Date.parse('01/20/1980')],
['Bob', 26.0, Date.parse('02/11/1981')],
['Charlie', 20.0, Date.parse('03/14/1987')],
['David', nil, Date.parse('04/26/1997')] ]


@xls_file_column_names2 = %w(Name Age Pet_Type)
# This test will pass once Spreadsheet and Roo support
# formulas in an excel file.
# @rows2 = [ ['Tigger', 3.0, 'Cat'],
# ['Chai', 4.0, 'Dog'],
# ['Rusky', 6.0, 'Dog'],
# ['Sam', 13.0, 'Dog'] ]

# Delete this once Roo supports formulas in an excel file.
@xls_file_column_names2 = %w(Name Age Pet_Type)
@rows2 = [ ['Tigger', 3.0, 'Cat'],
['Chai', 4.0, 'Dog'],
['Rusky', 6.0, 'Dog'],
['Sam', nil, 'Dog'] ]
end

# ==== File check ====
# Raise error if file is not found
it "should raise if xls file is not found" do
lambda do
Ruport::Data::Table.load_xls('people.xls')
end.should raise_error
end

# Raise error if file is not found
it "shouldn't raise if xls file exists" do
lambda do
Ruport::Data::Table.load_xls(@xls_file)
end.should_not raise_error
end


# ==== Constructor check ====
it "shouldn't be nil if a xls file is passed" do
table = Table(@xls_file)
table.should_not be_nil
end

it "shouldn't be nil if a Excel object is passed" do
oo = Excel.new(@xls_file)
oo.default_sheet = oo.sheets.first
table = Table(oo) # This will be passed to the base Ruport::Data::Table class.
table.should_not be_nil
end

it "shouldn't be nil if a Ruport::Data::Table parameter is passed" do
table = Table(@csv_file) # Pass cs file
table.should_not be_nil
end


# ==== Constructor check with options params ====
it "shouldn't be nil if a xls file is passed with options params" do
table = Table(@xls_file, {:has_column_names => false})
table.should_not be_nil
end

it "shouldn't be nil if a Excel object is passed with options params using parse_xls method" do
oo = Excel.new(@xls_file)
oo.default_sheet = oo.sheets.first
table = Ruport::Data::Table.parse_xls(oo, {:has_column_names => false})
table.should_not be_nil
end

it "shouldn't be nil if a Ruport::Data::Table parameter is passed with options params" do
table = Table(@csv_file, {:has_column_names => false}) # Pass cs file
table.should_not be_nil
end

it "should raise if start_row is less than zero" do
lambda do
Table(@xls_file, {:start_row => -2})
end.should raise_error
end

it "should raise if start_row is greater than the number of rows (starting at 0) in the spreadsheet" do
lambda do
Table(@xls_file, {:start_row => 20})
end.should raise_error
end


# ==== Table load check ====

# Output:
# +-----------------------------+
# | Name | Age | DOB |
# | Andy | 27.0 | 1980-01-20 |
# | Bob | 26.0 | 1981-02-11 |
# | Charlie | 20.0 | 1987-03-14 |
# | David | 73.0 | 1997-04-26 |
# +-----------------------------+
it "table should be valid without column names loaded from xls file starting at the row 1 (index 0) - column names will be data" do
# Load data from xls file but do not load column headers.
table = Table(@xls_file, {:has_column_names => false, :start_row => 0})
table.should_not be_nil
table.column_names.should == []

# Add headers to the first position
@rows.insert(0, @xls_file_column_names)

table.each { |r| r.to_a.should == @rows.shift
r.attributes.should == [0, 1, 2] }
end

# Output:
# +-----------------------------+
# | Bob | 26.0 | 1981-02-11 |
# | Charlie | 20.0 | 1987-03-14 |
# | David | 73.0 | 1997-04-26 |
# +-----------------------------+
it "table should be valid without column names loaded from xls file starting at row 3 (index 2)" do
# Load data from xls file but do not load column headers.
# Will start at Row 3 (index 2): ['Bob', 26.0, Date.parse('02/11/1981')]
table = Table(@xls_file, {:has_column_names => false, :start_row => 2})
table.should_not be_nil
table.column_names.should == []

# The header row has not been included yet so don't worry about that one
# just delete the first row in @rows.
@rows.delete_at(0) # delete ['Andy', 27.0, Date.parse('01/20/1980')]

table.each { |r| r.to_a.should == @rows.shift
r.attributes.should == [0, 1, 2] }
end

# Output:
# +-----------------------------+
# | Name | Age | DOB |
# | Andy | 27.0 | 1980-01-20 |
# | Bob | 26.0 | 1981-02-11 |
# | Charlie | 20.0 | 1987-03-14 |
# | David | 73.0 | 1997-04-26 |
# +-----------------------------+
it "table should be valid without column names loaded from xls file" do
# Load data from xls file but do not load column headers.
table = Table(@xls_file, {:has_column_names => false})
table.should_not be_nil
table.column_names.should == []

# Add headers to the first position
@rows.insert(0, @xls_file_column_names)

table.each { |r| r.to_a.should == @rows.shift
r.attributes.should == [0, 1, 2] }
end

# Output:
# +-----------------------------+
# | Name | Age | DOB |
# +-----------------------------+
# | Andy | 27.0 | 1980-01-20 |
# | Bob | 26.0 | 1981-02-11 |
# | Charlie | 20.0 | 1987-03-14 |
# | David | 73.0 | 1997-04-26 |
# +-----------------------------+
it "table should be valid with column names loaded from xls file" do
# Load data from xls file but do not load column headers.
table = Table(@xls_file)
table.should_not be_nil
table.column_names.should == @xls_file_column_names

table.each { |r| r.to_a.should == @rows.shift
r.attributes.should == @xls_file_column_names }
end

# Output:
# +--------------------------+
# | Name | Age | Pet_Type |
# +--------------------------+
# | Tigger | 3.0 | Cat |
# | Chai | 4.0 | Dog |
# | Rusky | 6.0 | Dog |
# | Sam | 13.0 | Dog |
# +--------------------------+
it "table should be valid with column names loaded from xls file using Sheet2" do
# Load data from xls file but do not load column headers.
table = Table(@xls_file, {:select_sheet => 'Sheet2'})
table.should_not be_nil
table.column_names.should == @xls_file_column_names2

table.each { |r| r.to_a.should == @rows2.shift
r.attributes.should == @xls_file_column_names2 }
end

# Output:
# +--------------------------+
# | Name | Age | Pet_Type |
# +--------------------------+
# | Tigger | 3.0 | Cat |
# | Chai | 4.0 | Dog |
# | Rusky | 6.0 | Dog |
# | Sam | 13.0 | Dog |
# +--------------------------+
it "should be valid if an Excel object is passed using parse_xls method" do
oo = Excel.new(@xls_file)
oo.default_sheet = oo.sheets.first
table = Ruport::Data::Table.parse_xls(oo)
table.should_not be_nil

table.column_names.should == @xls_file_column_names

table.each { |r| r.to_a.should == @rows.shift
r.attributes.should == @xls_file_column_names }
end

end



0 comments on commit 643a978

Please sign in to comment.