Skip to content

Commit

Permalink
Excel Exporter - support for options (#37)
Browse files Browse the repository at this point in the history
* Passes user-given options to Spreadsheet::Format

* Removes unnecessary test.xls

test.xls was produced while trying out in pry, and was mistakenly added into git history

* Adds support to export DataFrame's Index / MultiIndex to .xls file

* Merges formatting and displaying options, improves code quality

YARD Docs have been updated. Using the :data, :index, :header keyword arguments seems to be much more easier and intutive than using something like options[:display][:data]. Code quality has been improved, as per review comments.

* Adds generic formatting method
  • Loading branch information
athityakumar authored Jul 30, 2017
1 parent b6ff7da commit 67f0d6b
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 28 deletions.
131 changes: 111 additions & 20 deletions lib/daru/io/exporters/excel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,28 @@ class Excel < Base

# Exports +Daru::DataFrame+ to an Excel Spreadsheet.
#
# @note For giving formatting options as hashes to the +:data+, +:index+ or +header+
# keyword argument(s), please have a look at the
# {http://www.rubydoc.info/gems/ruby-spreadsheet/Spreadsheet/Font Spreadsheet::Font}
# and
# {http://www.rubydoc.info/gems/ruby-spreadsheet/Spreadsheet/Format Spreadsheet::Format}
# pages.
#
# @param dataframe [Daru::DataFrame] A dataframe to export
# @param path [String] Path of the file where the +Daru::DataFrame+
# should be written.
# @param options [Hash] A set of options containing user-preferences
# @param header [Hash or Boolean] Defaults to true. When set to false or nil,
# headers are not written. When given a hash of formatting options,
# headers are written with the specific formatting. When set to true,
# headers are written without any formatting.
# @param data [Hash or Boolean] Defaults to true. When set to false or nil,
# data values are not written. When given a hash of formatting options,
# data values are written with the specific formatting. When set to true,
# data values are written without any formatting.
# @param index [Hash or Boolean] Defaults to true. When set to false or nil,
# index values are not written. When given a hash of formatting options,
# index values are written with the specific formatting. When set to true,
# index values are written without any formatting.
#
# @example Writing to an Excel file without options
# df = Daru::DataFrame.new([[1,2],[3,4]], order: [:a, :b])
Expand All @@ -23,34 +41,107 @@ class Excel < Base
#
# Daru::IO::Exporters::Excel.new(df, "dataframe_df.xls").call
#
# @todo The +opts+ parameter isn't used while creating the Excel Spreadsheet
# yet. Implementing this feature will greatly allow the user to generate a
# Spreadsheet of their choice.
def initialize(dataframe, path, **options)
# @example Writing to an Excel file with formatting options
# df = Daru::DataFrame.new([[1,2],[3,4]], order: [:a, :b])
#
# #=> #<Daru::DataFrame(2x2)>
# # a b
# # 0 1 3
# # 1 2 4
#
# Daru::IO::Exporters::Excel.new(df,
# "dataframe_df.xls",
# header: { color: :red, weight: :bold },
# index: false,
# data: { color: :blue }
# ).call
#
# @example Writing a DataFrame with Multi-Index to an Excel file
# df = Daru::DataFrame.new [[1,2],[3,4]], order: [:x, :y], index: [[:a, :b, :c], [:d, :e, :f]]
#
# #=> #<Daru::DataFrame(2x2)>
# # x y
# # a b c 1 3
# # d e f 2 4
#
# Daru::IO::Exporters::Excel.new(df,
# "dataframe_df.xls",
# header: { color: :red, weight: :bold },
# index: { color: :green },
# data: { color: :blue }
# ).call
def initialize(dataframe, path, header: true, data: true, index: true)
optional_gem 'spreadsheet', '~> 1.1.1'

super(dataframe)
@path = path
@options = options
@path = path
@data = data
@index = index
@header = header
end

# @note
#
# The +format+ variable used in this method, has to be given
# as options by the user via the +options+ hash input.
#
# Signed off by @athityakumar on 03/06/2017 at 7:00PM
def call
book = Spreadsheet::Workbook.new
sheet = book.create_worksheet
@book = Spreadsheet::Workbook.new
@sheet = @book.create_worksheet

process_offsets
write_headers

format = Spreadsheet::Format.new color: :blue, weight: :bold
@dataframe.each_row_with_index.with_index do |(row, idx), r|
write_index(idx, r+@row_offset)
write_data(row, r+@row_offset)
end

@book.write(@path)
end

private

def process_offsets
@row_offset = @header ? 1 : 0
@col_offset = 0 unless @index
@col_offset ||= @dataframe.index.is_a?(Daru::MultiIndex) ? @dataframe.index.width : 1
end

def write_headers
formatting(
0...@col_offset + @dataframe.ncols,
0,
[' '] * @col_offset + @dataframe.vectors.map(&:to_s),
@header
)
end

def write_index(idx, row)
formatting(
0...@col_offset,
row,
idx,
@index
)
end

def write_data(row, idx)
formatting(
@col_offset...@col_offset + @dataframe.ncols,
idx,
row,
@data
)
end

sheet.row(0).concat(@dataframe.vectors.to_a.map(&:to_s)) # Unfreeze strings
sheet.row(0).default_format = format
@dataframe.each_row_with_index { |row, i| sheet.row(i+1).concat(row.to_a) }
def formatting(col_range, row, idx, format)
return unless format
@sheet.row(row).concat(
case idx
when Daru::Vector then idx.to_a
when Array then idx.map(&:to_s)
else [idx.to_s]
end
)

book.write(@path)
return unless format.is_a?(Hash)
col_range.each { |col| @sheet.row(row).set_format(col, Spreadsheet::Format.new(format)) }
end
end
end
Expand Down
51 changes: 43 additions & 8 deletions spec/daru/io/exporters/excel_spec.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
RSpec.describe Daru::IO::Exporters::Excel do
include_context 'exporter setup'

subject do
Daru::DataFrame.rows(
Spreadsheet.open(tempfile.path).worksheet(0).rows[1..-1].map(&:to_a),
order: Spreadsheet.open(tempfile.path).worksheet(0).rows[0].to_a
)
end

let(:filename) { 'test_write.xls' }
let(:content) { Spreadsheet.open tempfile.path }
let(:opts) { {header: {color: :blue}, data: {color: :red}, index: {color: :green}} }

before { described_class.new(df, tempfile.path, opts).call }
before { described_class.new(df, tempfile.path, **opts).call }

context 'writes to excel spreadsheet' do
subject do
Daru::DataFrame.rows(
Spreadsheet.open(tempfile.path).worksheet(0).rows[1..-1].map(&:to_a),
order: Spreadsheet.open(tempfile.path).worksheet(0).rows[0].to_a
)
end

let(:opts) { {index: false} }

it_behaves_like 'exact daru dataframe',
ncols: 4,
nrows: 5,
Expand All @@ -25,4 +28,36 @@
[nil, 23, 4,'a','ff']
]
end

context 'writes to excel spreadsheet with header formatting' do
subject { Spreadsheet.open(tempfile.path).worksheet(0).rows[0].format(0).font.color }

it { is_expected.to eq(:blue) }
end

context 'writes to excel spreadsheet with index formatting' do
subject { Spreadsheet.open(tempfile.path).worksheet(0).rows[1].format(0).font.color }

it { is_expected.to eq(:green) }
end

context 'writes to excel spreadsheet with data formatting' do
subject { Spreadsheet.open(tempfile.path).worksheet(0).rows[1].format(1).font.color }

it { is_expected.to eq(:red) }
end

context 'writes to excel spreadsheet with multi-index' do
subject { Spreadsheet.open(tempfile.path).worksheet(0).rows }

let(:df) do
Daru::DataFrame.new(
[[1,2],[3,4]],
order: %i[x y],
index: [%i[a b c], %i[d e f]]
)
end

it { is_expected.to eq([[' ', ' ', ' ', 'x', 'y'], ['a', 'b', 'c', 1, 3], ['d', 'e', 'f', 2, 4]]) }
end
end

0 comments on commit 67f0d6b

Please sign in to comment.