Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
craigulliott committed Jul 2, 2011
1 parent c9c02a4 commit 3cb4efb
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 35 deletions.
33 changes: 3 additions & 30 deletions .gitignore
Expand Up @@ -14,35 +14,8 @@ doc
# jeweler generated
pkg

# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
#
# * Create a file at ~/.gitignore
# * Include files you want ignored
# * Run: git config --global core.excludesfile ~/.gitignore
#
# After doing this, these files will be ignored in all your git projects,
# saving you from having to 'pollute' every project you touch with them
#
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
#
# For MacOS:
#
#.DS_Store
.DS_Store

# For TextMate
#*.tmproj
#tmtags

# For emacs:
#*~
#\#*
#.\#*

# For vim:
#*.swp

# For redcar:
#.redcar

# For rubinius:
#*.rbc
# For TextMate:
.tmproj
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -10,4 +10,5 @@ group :development do
gem "bundler", "~> 1.0.0"
gem "jeweler", "~> 1.6.2"
gem "rcov", ">= 0"
gem "zipruby", "~> 0.3.6"
end
30 changes: 30 additions & 0 deletions Gemfile.lock
@@ -0,0 +1,30 @@
GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.2)
git (1.2.5)
jeweler (1.6.3)
bundler (~> 1.0)
git (>= 1.2.5)
rake
rake (0.9.2)
rcov (0.9.9)
rspec (2.3.0)
rspec-core (~> 2.3.0)
rspec-expectations (~> 2.3.0)
rspec-mocks (~> 2.3.0)
rspec-core (2.3.1)
rspec-expectations (2.3.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.3.0)
zipruby (0.3.6)

PLATFORMS
ruby

DEPENDENCIES
bundler (~> 1.0.0)
jeweler (~> 1.6.2)
rcov
rspec (~> 2.3.0)
zipruby (~> 0.3.6)
10 changes: 9 additions & 1 deletion README.rdoc
@@ -1,6 +1,14 @@
= spreadsheetx

Description goes here.
This gem facilitates a templatized approach to working with Excel files. It only supports Microsoft's new .xlsx format (because it's xml based and somewhat sane to work with).

We use this gem to generate reports for clients, where the reports have substantial charts and styling throughout.

The work flow goes a little something like this:
* Create a report in Excel that has all your charts and styling
* Save the report with placeholder data
* Programmatically replace or add rows and cells
* When the new file is opened, formatting and charts are preserved

== Contributing to spreadsheetx

Expand Down
4 changes: 2 additions & 2 deletions Rakefile
Expand Up @@ -17,8 +17,8 @@ Jeweler::Tasks.new do |gem|
gem.name = "spreadsheetx"
gem.homepage = "http://github.com/craigulliott/spreadsheetx"
gem.license = "MIT"
gem.summary = %Q{TODO: one-line summary of your gem}
gem.description = %Q{TODO: longer description of your gem}
gem.summary = %Q{Facilitates opening and modifying existing xlsx excel spreadsheets}
gem.description = %Q{Using an existing xlsx file as a template, it allows you to modify cell values and add rows and columns. Facilitating a templateized approach to creating a new xlsx spreadsheet}
gem.email = "craigulliott@gmail.com"
gem.authors = ["Craig Ulliott"]
# dependencies defined in Gemfile
Expand Down
21 changes: 21 additions & 0 deletions lib/spreadsheetx.rb
@@ -0,0 +1,21 @@
# zipruby is nice as it can modify an existing zip file, perfect for our usecase
require 'zipruby'
# we use this because it comes with ruby
require 'rexml/document'
# for copying files
require 'fileutils'
#
require 'spreadsheetx/workbook'
require 'spreadsheetx/worksheet'

module SpreadsheetX

class << self

def open(path)
SpreadsheetX::Workbook.new(path)
end

end

end
55 changes: 55 additions & 0 deletions lib/spreadsheetx/workbook.rb
@@ -0,0 +1,55 @@
module SpreadsheetX

# This class represents an XLSX Document on disk
class Workbook

attr_reader :path
attr_reader :worksheets

# return a Workbook object which relates to an existing xlsx file on disk
def initialize(path)
@path = path
Zip::Archive.open(path) do |archive|

# open the workbook
archive.fopen('xl/workbook.xml') do |f|

# read contents of this file
file_contents = f.read

#parse the XML and build the worksheets
@worksheets = []
REXML::Document.new(file_contents).elements.each('workbook/sheets/sheet') do |node|
sheet_id = node.attributes['sheetId'].to_i
r_id = node.attributes['r:id'].gsub('rId','').to_i
name = node.attributes['name'].to_s
@worksheets.push SpreadsheetX::Worksheet.new(archive, sheet_id, r_id, name)
end

end

end
end

# saves the binary form of the complete xlsx file to a new xlsx file
def save(destination_path)

# copy the xlsx file to the destination
FileUtils.cp(@path, destination_path)

# replace the xlsx files with the new workbooks
Zip::Archive.open(destination_path) do |ar|

# replace with the new worksheets
@worksheets.each do |worksheet|
ar.replace_buffer("xl/worksheets/sheet#{worksheet.r_id}.xml", worksheet.to_s)
end

end

end

end

end

88 changes: 88 additions & 0 deletions lib/spreadsheetx/worksheet.rb
@@ -0,0 +1,88 @@
module SpreadsheetX

# Workbooks are made up of N Worksheets, this class represents a specific Worksheet.
class Worksheet

attr_reader :sheet_id
attr_reader :r_id
attr_reader :name

# return a Worksheet object which relates to a specific Worksheet
def initialize(archive, sheet_id, r_id, name)
@sheet_id = sheet_id
@r_id = r_id
@name = name

# open the workbook
archive.fopen("xl/worksheets/sheet#{@r_id}.xml") do |f|

# read contents of this file
file_contents = f.read
#parse the XML and hold the doc
@xml_doc = REXML::Document.new(file_contents)

end

end

# update the value of a particular cell, if the row or cell doesnt exist in the XML, then it will be created
def update_cell(col_number, row_number, val)

cell_id = SpreadsheetX::Worksheet.cell_id(col_number, row_number)

rows = @xml_doc.get_elements("worksheet/sheetData/row[@r=#{row_number}]")
# was this row found
if rows.empty?
# build a new row
row = @xml_doc.elements['worksheet'].elements['sheetData'].add_element('row', {'r' => row_number})
else
# x path returns an array, but we know there is only one row with this number
row = rows.first
end

cells = row.get_elements("c[@r='#{cell_id}']")
if cells.empty?
cell = row.add_element('c', {'r' => cell_id})
else
# x path returns an array, but we know there is only one row with this number
cell = cells.first
end

# first clear out any existing values
cell.delete_element('*')

# now we put the value in the cell
if val.kind_of? String
cell.attributes['t'] = 'inlineStr'
cell.add_element('is').add_element('t').add_text(val)
else
cell.attributes['t'] = nil
cell.add_element('v').add_text(val.to_s)
end

end

# the number of rows containing data this sheet has
# NOTE: this is the count of those rows, not the length of the document
def row_count
count = 0
@xml_doc.elements.each('worksheet/sheetData/row'){ count+=1 }
count
end

# returns the xml representation of this worksheet
def to_s
@xml_doc.to_s
end

# turns a cell address into its excel name, 1,1 = A1 2,3 = C2 etc.
def self.cell_id(col_number, row_number)
letter = 'A'
# some day, speed this up
(col_number.to_i-1).times{letter = letter.succ}
"#{letter}#{row_number}"
end

end

end
82 changes: 80 additions & 2 deletions spec/spreadsheetx_spec.rb
@@ -1,7 +1,85 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')

describe "Spreadsheetx" do
it "fails" do
fail "hey buddy, you should probably rename this file and start specing for real"

it "opens xlsx files successfully" do

# a valid xlsx file used for testing
empty_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec.xlsx"
workbook = SpreadsheetX.open(empty_xlsx_file)

end

it "allow accessing worksheets" do

# a valid xlsx file used for testing
empty_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec.xlsx"
workbook = SpreadsheetX.open(empty_xlsx_file)

workbook.worksheets.length.should == 2
workbook.worksheets.last.name.should == 'Test'

end

it "allow accessing row counts" do

# a valid xlsx file used for testing
empty_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec.xlsx"
workbook = SpreadsheetX.open(empty_xlsx_file)

workbook.worksheets.last.row_count.should == 4

end

it "can be saved" do

# a valid xlsx file used for testing
empty_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec.xlsx"
workbook = SpreadsheetX.open(empty_xlsx_file)

new_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec_out.xlsx"
workbook.save(new_xlsx_file)

end

it "can convert an address of a cell to a cell name" do

SpreadsheetX::Worksheet.cell_id(1, 1).should == 'A1'
SpreadsheetX::Worksheet.cell_id(2, 1).should == 'B1'
SpreadsheetX::Worksheet.cell_id(27, 9).should == 'AA9'
SpreadsheetX::Worksheet.cell_id(26, 4).should == 'Z4'
SpreadsheetX::Worksheet.cell_id(820, 496).should == 'AEN496'


end

it "allows cell values to be updated" do

# a valid xlsx file used for testing
empty_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec.xlsx"
workbook = SpreadsheetX.open(empty_xlsx_file)

workbook.worksheets.last.update_cell(1, 1, 9)
workbook.worksheets.last.update_cell(1, 2, 'A')
workbook.worksheets.last.update_cell(1, 3, nil)

new_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec_changed_out.xlsx"
workbook.save(new_xlsx_file)

end

it "allows cells to be added" do

# a valid xlsx file used for testing
empty_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec.xlsx"
workbook = SpreadsheetX.open(empty_xlsx_file)

workbook.worksheets.last.update_cell(9, 9, 9)
workbook.worksheets.last.update_cell(9, 10, 'A')

new_xlsx_file = "#{File.dirname(__FILE__)}/../templates/spec_added_out.xlsx"
workbook.save(new_xlsx_file)

end

end
Binary file added templates/spec.xlsx
Binary file not shown.

0 comments on commit 3cb4efb

Please sign in to comment.