Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit from roo-1.2.3

  • Loading branch information...
commit f01ee67fc29477ce12a5fbd5c4d0b4bf029b1a5c 0 parents
Hugh Mcgowan authored
Showing with 14,955 additions and 0 deletions.
  1. +225 −0 History.txt
  2. +20 −0 License.txt
  3. +68 −0 Manifest.txt
  4. +43 −0 README.txt
  5. +171 −0 Rakefile
  6. +149 −0 base64include.rb
  7. +53 −0 examples/roo_soap_client.rb
  8. +29 −0 examples/roo_soap_server.rb
  9. +33 −0 examples/write_me.rb
  10. +11 −0 lib/roo.rb
  11. +402 −0 lib/roo/excel.rb
  12. +601 −0 lib/roo/excelx.rb
  13. +636 −0 lib/roo/generic_spreadsheet.rb
  14. +411 −0 lib/roo/google.rb
  15. +458 −0 lib/roo/openoffice.rb
  16. +81 −0 lib/roo/roo_rails_helper.rb
  17. +9 −0 lib/roo/version.rb
  18. +67 −0 scripts/txt2html
  19. +1,585 −0 setup.rb
  20. +3,741 −0 test/Bibelbund.csv
  21. BIN  test/Bibelbund.ods
  22. BIN  test/Bibelbund.xls
  23. BIN  test/Bibelbund.xlsx
  24. BIN  test/Bibelbund1.ods
  25. BIN  test/bbu.ods
  26. BIN  test/bbu.xls
  27. BIN  test/bbu.xlsx
  28. BIN  test/bode-v1.ods.zip
  29. BIN  test/bode-v1.xls.zip
  30. BIN  test/borders.ods
  31. BIN  test/borders.xls
  32. BIN  test/borders.xlsx
  33. BIN  test/bug-row-column-fixnum-float.xls
  34. BIN  test/datetime.ods
  35. BIN  test/datetime.xls
  36. BIN  test/datetime.xlsx
  37. BIN  test/emptysheets.ods
  38. BIN  test/emptysheets.xls
  39. BIN  test/false_encoding.xls
  40. BIN  test/formula.ods
  41. BIN  test/formula.xls
  42. BIN  test/formula.xlsx
  43. +1 −0  test/no_spreadsheet_file.txt
  44. +18 −0 test/numbers1.csv
  45. BIN  test/numbers1.ods
  46. BIN  test/numbers1.xls
  47. BIN  test/numbers1.xlsx
  48. +18 −0 test/numbers1_excel.csv
  49. BIN  test/only_one_sheet.ods
  50. BIN  test/only_one_sheet.xls
  51. BIN  test/only_one_sheet.xlsx
  52. BIN  test/ric.ods
  53. BIN  test/simple_spreadsheet.ods
  54. BIN  test/simple_spreadsheet.xls
  55. BIN  test/simple_spreadsheet.xlsx
  56. BIN  test/simple_spreadsheet_from_italo.ods
  57. BIN  test/simple_spreadsheet_from_italo.xls
  58. +19 −0 test/test_helper.rb
  59. +4,833 −0 test/test_roo.rb
  60. +2 −0  test/time-test.csv
  61. BIN  test/time-test.ods
  62. BIN  test/time-test.xls
  63. BIN  test/time-test.xlsx
  64. +385 −0 website/index.html
  65. +423 −0 website/index.txt
  66. +285 −0 website/javascripts/rounded_corners_lite.inc.js
  67. +130 −0 website/stylesheets/screen.css
  68. +48 −0 website/template.rhtml
225 History.txt
@@ -0,0 +1,225 @@
+== 1.2.3 2009-01-04
+
+* bugfix
+ * fixed encoding in #cell at exported Google-spreadsheets (.xls)
+
+== 1.2.2 2008-12-14
+
+* 2 enhancements
+ * added celltype :datetime in Excelx
+ * added celltype :datetime in Google
+
+== 1.2.1 2008-11-13
+
+* 1 enhancement
+ * added celltype :datetime in Openoffice and Excel
+
+== 1.2.0 2008-08-24
+* 3 major enhancements
+ * Excelx: improved the detection of cell type and conversion into roo types
+ * All: to_csv: changed boundaries from first_row,1..last_row,last_column to 1,1..last_row,last_column
+ * All: Environment variable "ROO_TMP" indicate where temporary directories will be created (if not set the default is the current working directory)
+* 2 bugfixes
+ * Excel: improved the detection of last_row/last_column (parseexcel-gem bug?)
+ * Excel/Excelx/Openoffice: temporary directories were not removed at opening a file of the wrong type
+== 1.1.0 2008-07-26
+* 2 major enhancements
+ * Excel: speed improvements
+ * Changed the behavior of reading files with the wrong type
+* 3 bugfixes
+ * Google: added normalize in set_value method
+ * Excel: last_row in Excel class did not work properly under some circumstances
+ * all: fixed a bug in #to_xml if there is an empty sheet
+== 1.0.2 2008-07-04
+* 2 bugfixes
+ * Excelx: fixed a bug when there are .xml.rels files in the XLSX archive
+ * Excelx: fixed a bug with celltype recognition (see comment with "2008-07-03")
+== 1.0.1 2008-06-30
+* 1 bugfix
+ * Excel: row/column method Fixnum/Float confusion
+== 1.0.0 2008-05-28
+* 2 major enhancements
+ * support of Excel's new .xlsx file format
+ * method #to_xml for exporting a spreadsheet to an xml representation
+* 1 bugfix
+ * fixed a bug with excel-spreadsheet character conversion under Macintosh Darwin
+== 0.9.4 2008-04-22
+* 1 bugfix
+ * fixed a bug with excel-spreadsheet character conversion under Solaris
+== 0.9.3 2008-03-25
+* 1 bugfix
+ * no more tmp directories if an invalid spreadsheet file was openend
+== 0.9.2 2008-03-24
+* 1 enhancement
+ * new celltype :time
+* 1 bugfix
+ * time values like '23:15' are handled as seconds from midnight
+== 0.9.1 2008-03-23
+* 1 enhancement
+ * additional 'sheet' parameter in Google#set_value
+* 1 bugfix
+ * fixed a bug within Google#set_value. thanks to davecahill <dpcahill@gmail.com> for the patch.
+== 0.9.0 2008-01-24
+* 1 enhancement:
+ * better support of roo spreadsheets in rails views
+== 0.8.5 2008-01-16
+* 1 bugfix
+ * fixed a bug within #to_cvs and explicit call of a sheet
+== 0.8.4 2008-01-01
+* 1 bugfix
+ * fixed 'find_by_condition' for excel sheets (header_line= --> GenericSpredsheet)
+== 0.8.3 2007-12-31
+* 2 bugfixes
+ * another fix for the encoding issue in excel sheet-names
+ * reactived the Excel#find method which has been disappeared in the last restructoring, moved to GenericSpreadsheet
+== 0.8.2 2007-12-28
+* 1 enhancement:
+ * basename() only in method #info
+* 2 bugfixes
+ * changed logging-method to mysql-database in test code with AR, table column 'class' => 'class_name'
+ * reactived the Excel#to_csv method which has been disappeared in the last restructoring
+== 0.8.1 2007-12-22
+* 3 bugfixes
+ * fixed a bug with first/last-row/column in empty sheet
+ * #info prints now '- empty -' if a sheet within a document is empty
+ * tried to fix the iconv conversion problem
+== 0.8.0 2007-12-15
+* 2 enhancements:
+ * Google online spreadsheets were implemented
+ * some methods common to more than one class were factored out to the GenericSpreadsheet (virtual) class
+== 0.7.0 2007-11-23
+* 6 enhancements:
+ * Openoffice/Excel: the most methods can be called with an option 'sheet'
+ parameter which will be used instead of the default sheet
+ * Excel: improved the speed of CVS output
+ * Openoffice/Excel: new method #column
+ * Openoffice/Excel: new method #find
+ * Openoffice/Excel: new method #info
+ * better exception if a spreadsheet file does not exist
+== 0.6.1 2007-10-06
+* 2 enhancements:
+ * Openoffice: percentage-values are now treated as numbers (not strings)
+ * Openoffice: refactoring
+* 1 bugfix
+ * Openoffice: repeating date-values in a line are now handled correctly
+== 0.6.0 2007-10-06
+* 1 enhancement:
+ * csv-output to stdout or file
+== 0.5.4 2007-08-27
+* 1 bugfix
+ * Openoffice: fixed a bug with internal representation of a spreadsheet (thanks to Ric Kamicar for the patch)
+== 0.5.3 2007-08-26
+* 2 enhancements:
+ * Openoffice: can now read zip-ed files
+ * Openoffice: can now read files from http://-URL over the net
+== 0.5.2 2007-08-26
+* 1 bugfix
+ * excel: removed debugging output
+== 0.5.1 2007-08-26
+* 4 enhancements:
+ * Openoffice: Exception if an illegal sheet-name is selected
+ * Openoffice/Excel: no need to set a default_sheet if there is only one in
+ the document
+ * Excel: can now read zip-ed files
+ * Excel: can now read files from http://-URL over the net
+
+== 0.5.0 2007-07-20
+* 3 enhancements:
+ * Excel-objects: the methods default_sheet= and sheets can now handle names instead of numbers
+ * changed the celltype methods to return symbols, not strings anymore (possible values are :formula, :float, :string, :date, :percentage (if you need strings you can convert it with .to_s)
+ * tests can now run on the client machine (not only my machine), if there are not public released files involved these tests are skipped
+
+== 0.4.1 2007-06-27
+* 1 bugfix
+ * there was ONE false require-statement which led to misleading error messageswhen this gem was used
+
+== 0.4.0 2007-06-27
+* 7 enhancements:
+ * robustness: Exception if no default_sheet was set
+ * new method reload() implemented
+ * about 15 % more method documentation
+ * optimization: huge increase of speed (no need to use fixed borders anymore)
+ * added the method 'formulas' which gives you all formulas in a spreadsheet
+ * added the method 'set' which can set cells to a certain value
+ * added the method 'to_yaml' which can produce output for importing in a (rails) database
+* 4 bugfixes
+ * ..row_as_letter methods were nonsense - removed
+ * @cells_read should be reset if the default_sheet is changed
+ * error in excel-part: strings are now converted to utf-8 (the parsexcel-gem gave me an error with my test data, which could not converted to .to_s using latin1 encoding)
+ * fixed bug when default_sheet is changed
+
+== 0.3.0 2007-06-20
+* 1 enhancement:
+ * Openoffice: formula support
+
+== 0.2.7 2007-06-20
+* 1 bugfix:
+ * Excel: float-numbers were truncated to integer
+
+== 0.2.6 2007-06-19
+* 1 bugfix:
+ * Openoffice: two or more consecutive cells with string content failed
+
+== 0.2.5 2007-06-17
+
+* 2 enhancements:
+ * Excel: row method implemented
+ * more tests
+* 1 bugfix:
+ * Openoffice: row method fixed
+
+== 0.2.4 2007-06-16
+* 1 bugfix:
+ * ID 11605 Two cols with same value: crash roo (openoffice version only)
+
+== 0.2.3 2007-06-02
+* 3 enhancements:
+ * more robust call att Excel#default_sheet= when called with a name
+ * new method empty?
+ * refactoring
+* 1 bugfix:
+ * bugfix in Excel#celltype
+ * bugfix (running under windows only) in closing the temp file before removing it
+
+== 0.2.2 2007-06-01
+* 1 bugfix:
+ * correct pathname for running with windows
+
+
+== 0.2.2 2007-06-01
+* 1 bugfix:
+ * incorrect dependencies fixed
+
+== 0.2.0 2007-06-01
+* 1 major enhancement:
+ * support for MS-Excel Spreadsheets
+
+== 0.1.2 2007-05-31
+* 1 major enhancement:
+ * cells with more than one character, like 'AA' can now be handled
+
+== 0.1.1 2007-05-31
+* 1 Bugfix
+ * Bugfix in first/last methods
+
+== 0.1.0 2007-05-31
+
+* 1 major enhancement:
+ * new methods first/last row/column
+ * new method officeversion
+
+== 0.0.3 2007-05-30
+
+* 1 minor enhancement:
+ * new method row()
+
+== 0.0.2 2007-05-30
+
+* 2 major enhancement:
+ * fixed some bugs
+ * more ways to access a cell
+
+== 0.0.1 2007-05-25
+
+* 1 major enhancement:
+ * Initial release
20 License.txt
@@ -0,0 +1,20 @@
+Copyright (c) 2007 Thomas Preymesser
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
68 Manifest.txt
@@ -0,0 +1,68 @@
+History.txt
+License.txt
+Manifest.txt
+README.txt
+Rakefile
+base64include.rb
+examples/roo_soap_server.rb
+examples/roo_soap_client.rb
+examples/write_me.rb
+lib/roo.rb
+lib/roo/version.rb
+lib/roo/generic_spreadsheet.rb
+lib/roo/openoffice.rb
+lib/roo/excel.rb
+lib/roo/excelx.rb
+lib/roo/google.rb
+lib/roo/roo_rails_helper.rb
+scripts/txt2html
+setup.rb
+test/false_encoding.xls
+test/Bibelbund1.ods
+test/Bibelbund.ods
+test/Bibelbund.xls
+test/Bibelbund.xlsx
+test/Bibelbund.csv
+test/bbu.xls
+test/bbu.xlsx
+test/bbu.ods
+test/no_spreadsheet_file.txt
+test/simple_spreadsheet.ods
+test/simple_spreadsheet.xls
+test/simple_spreadsheet.xlsx
+test/simple_spreadsheet_from_italo.ods
+test/simple_spreadsheet_from_italo.xls
+test/test_helper.rb
+test/test_roo.rb
+test/time-test.ods
+test/time-test.xls
+test/time-test.xlsx
+test/time-test.csv
+test/numbers1.csv
+test/numbers1_excel.csv
+test/numbers1.ods
+test/numbers1.xls
+test/numbers1.xlsx
+test/borders.ods
+test/borders.xls
+test/borders.xlsx
+test/formula.ods
+test/formula.xls
+test/formula.xlsx
+test/only_one_sheet.ods
+test/only_one_sheet.xls
+test/only_one_sheet.xlsx
+test/bode-v1.xls.zip
+test/bode-v1.ods.zip
+test/ric.ods
+test/bug-row-column-fixnum-float.xls
+test/emptysheets.ods
+test/emptysheets.xls
+test/datetime.ods
+test/datetime.xls
+test/datetime.xlsx
+website/index.html
+website/index.txt
+website/javascripts/rounded_corners_lite.inc.js
+website/stylesheets/screen.css
+website/template.rhtml
43 README.txt
@@ -0,0 +1,43 @@
+README for roo
+==============
+
+Installation:
+
+ sudo gem install roo
+
+Usage:
+
+ require 'rubygems'
+ require 'roo'
+
+ s = Openoffice.new("myspreadsheet.ods") # creates an Openoffice Spreadsheet instance
+ s = Excel.new("myspreadsheet.xls") # creates an Excel Spreadsheet instance
+ s = Google.new("myspreadsheetkey_at_google") # creates an Google Spreadsheet instance
+ s = Excelx.new("myspreadsheet.xlsx") # creates an Excel Spreadsheet instance for Excel .xlsx files
+
+ s.default_sheet = s.sheets.first # first sheet in the spreadsheet file will be used
+
+ # s.sheet is an array which holds the names of the sheets within
+ # a spreadsheet.
+ # you can also write
+ # s.default_sheet = s.sheets[3] or
+ # s.default_sheet = 'Sheet 3'
+
+ s.cell(1,1) # returns the content of the first row/first cell in the sheet
+ s.cell('A',1) # same cell
+ s.cell(1,'A') # same cell
+ s.cell(1,'A',s.sheets[0]) # same cell
+
+ # almost all methods have an optional argument 'sheet'.
+ # If this parameter is ommitted, the default_sheet will be used.
+
+ s.info # prints infos about the spreadsheet file
+
+ s.first_row # the number of the first row
+ s.last_row # the number of the last row
+ s.first_column # the number of the first column
+ s.last_column # the number of the last column
+
+
+see http://roo.rubyforge.org for a more complete tutorial
+
171 Rakefile
@@ -0,0 +1,171 @@
+require 'rubygems'
+require 'rake'
+require 'rake/clean'
+require 'rake/testtask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/rdoctask'
+require 'rake/contrib/rubyforgepublisher'
+require 'fileutils'
+require 'hoe'
+
+include FileUtils
+require File.join(File.dirname(__FILE__), 'lib', 'roo', 'version')
+
+AUTHOR = 'Thomas Preymesser' # can also be an array of Authors
+EMAIL = "thopre@gmail.com"
+DESCRIPTION = "roo can access the contents of OpenOffice-, Excel- or Google-Spreadsheets"
+GEM_NAME = 'roo' # what ppl will type to install your gem
+
+@config_file = "~/.rubyforge/user-config.yml"
+@config = nil
+def rubyforge_username
+ unless @config
+ begin
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
+ rescue
+ puts <<-EOS
+ERROR: No rubyforge config file found: #{@config_file}"
+Run 'rubyforge setup' to prepare your env for access to Rubyforge
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
+ EOS
+ exit
+ end
+ end
+ @rubyforge_username ||= @config["username"]
+end
+
+RUBYFORGE_PROJECT = 'roo' # The unix name for your project
+HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
+DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
+
+NAME = "roo"
+REV = nil
+# UNCOMMENT IF REQUIRED:
+# REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
+VERS = Roo::VERSION::STRING + (REV ? ".#{REV}" : "")
+CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
+RDOC_OPTS = ['--quiet', '--title', 'roo documentation',
+ "--opname", "index.html",
+ "--line-numbers",
+ "--main", "README",
+ "--inline-source"]
+
+class Hoe
+ def extra_deps
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
+ end
+end
+
+# Generate all the Rake tasks
+# Run 'rake -T' to see list of generated tasks (from gem root directory)
+hoe = Hoe.new(GEM_NAME, VERS) do |p|
+ p.author = AUTHOR
+ p.description = DESCRIPTION
+ p.email = EMAIL
+ p.summary = DESCRIPTION
+ p.url = HOMEPATH
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
+ p.test_globs = ["test/**/test_*.rb"]
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
+
+ # == Optional
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
+ p.extra_deps = [
+ # ['ruport', '>= 1.0.0'],
+ # ['ruport-util', '>= 0.5.0'],
+ ['parseexcel', '>= 0.5.2'],
+ ['rubyzip', '>= 0.9.1'],
+ ['hpricot', '>= 0.5'],
+ ['hoe', '>= 0.0.0'],
+ ['GData', '>= 0.0.3'],
+ ]
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
+end
+
+CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")
+PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
+hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
+
+desc 'Generate website files'
+task :website_generate do
+ Dir['website/**/*.txt'].each do |txt|
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
+ end
+end
+
+desc 'Upload website files to rubyforge'
+task :website_upload do
+ host = "#{rubyforge_username}@rubyforge.org"
+ remote_dir = "/var/www/gforge-projects/#{PATH}/"
+ local_dir = 'website'
+ sh %{rsync -av #{local_dir}/ #{host}:#{remote_dir}}
+end
+
+desc 'Generate and upload website files'
+task :website => [:website_generate, :website_upload]
+
+#-- prey: BEGIN
+require 'fileutils'
+include FileUtils::Verbose
+desc 'Test the local installation'
+task :test_local_installation do
+ # gehe nach $GEM_PATH und starte dort rake test
+ # cd(ENV['GEM_PATH']+"roo-1.1.0") do
+ cd("/usr/lib/ruby/gems/1.8/gems/roo-1.1.0") do
+ sh %{sudo rake test}
+ end
+ puts 'local installation test done'
+end
+#-- prey: END
+
+desc 'Release the website and new gem version'
+task :deploy => [:check_log_params, :check_version, :test_local_installation, :website, :release ] do
+ puts "Remember to create SVN tag:"
+ puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
+ "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
+ puts "Suggested comment:"
+ puts "Tagging release #{CHANGES}"
+end
+
+desc 'Check to ensure the LOG_* constant in Test are set off'
+task :check_log_params do
+ require 'test/test_roo'
+ if DISPLAY_LOG
+ raise 'please turn off the DISPLAY_LOG constant for deployment!'
+ end
+ if DB_LOG
+ raise 'please turn off the DB_LOG constant for deployment!'
+ end
+end
+desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
+task :local_deploy => [:website_generate, :install_gem]
+
+task :check_version do
+ unless ENV['VERSION']
+ puts 'Must pass a VERSION=x.y.z release version'
+ exit
+ end
+ unless ENV['VERSION'] == VERS
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
+ exit
+ end
+end
+
+task 'stats' do
+ require '/home/tp/ruby-test/scriptlines'
+ files = FileList['lib/**/*.rb']
+ puts ScriptLines.headline
+ sum = ScriptLines.new("TOTAL (#{files.size} file(s))")
+ files.each do |fn|
+ File.open(fn) do |file|
+ script_lines = ScriptLines.new(fn)
+ script_lines.read(file)
+ sum += script_lines
+ puts script_lines
+ end
+ end
+
+ puts sum
+end
149 base64include.rb
@@ -0,0 +1,149 @@
+@@empty_spreadsheet=<<_ENDOFBASE64CODE_
+UEsDBBQAAAAAABWJ4jaFbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNh
+dGlvbi92bmQub2FzaXMub3BlbmRvY3VtZW50LnNwcmVhZHNoZWV0UEsDBBQA
+AAAAABWJ4jYAAAAAAAAAAAAAAAAaAAAAQ29uZmlndXJhdGlvbnMyL3N0YXR1
+c2Jhci9QSwMEFAAIAAgAFYniNgAAAAAAAAAAAAAAACcAAABDb25maWd1cmF0
+aW9uczIvYWNjZWxlcmF0b3IvY3VycmVudC54bWwDAFBLBwgAAAAAAgAAAAAA
+AABQSwMEFAAAAAAAFYniNgAAAAAAAAAAAAAAABgAAABDb25maWd1cmF0aW9u
+czIvZmxvYXRlci9QSwMEFAAAAAAAFYniNgAAAAAAAAAAAAAAABoAAABDb25m
+aWd1cmF0aW9uczIvcG9wdXBtZW51L1BLAwQUAAAAAAAVieI2AAAAAAAAAAAA
+AAAAHAAAAENvbmZpZ3VyYXRpb25zMi9wcm9ncmVzc2Jhci9QSwMEFAAAAAAA
+FYniNgAAAAAAAAAAAAAAABgAAABDb25maWd1cmF0aW9uczIvbWVudWJhci9Q
+SwMEFAAAAAAAFYniNgAAAAAAAAAAAAAAABgAAABDb25maWd1cmF0aW9uczIv
+dG9vbGJhci9QSwMEFAAAAAAAFYniNgAAAAAAAAAAAAAAAB8AAABDb25maWd1
+cmF0aW9uczIvaW1hZ2VzL0JpdG1hcHMvUEsDBBQACAAIABWJ4jYAAAAAAAAA
+AAAAAAALAAAAY29udGVudC54bWzlV7ty2zAQ7PMVHBbpIOjhSWxGkptMKjuN
+k0xaCDxKiPHgAKBo/X0AgqRBWZRZJw01Ouze7S2Op9H6/kXw5AjaMCU36WI2
+TxOQVOVM7jfpzx/f0G16v/2wVkXBKGS5opUAaRFV0rrPxLGlycLpJq20zBQx
+zGSSCDCZpZkqQXasLEZnTa0QMfbEJ9MbcMy28GKnkj12wCW76ZUbcMzONamn
+kj3WmRrTCzWV/GI4KpRzXZTEsjMVL5zJ5016sLbMMK7relavZkrv8eLu7g43
+p71g2uPKSvMGlVMMHHwxgxezBe6wAiyZqs9jY0myEjvQk60hlry5VXPcT56I
+437EGnogevJsNODh9a7y6de7ymOuIPYwcie3+NEdNo/Hh9dZ0GJqLY8dWEU1
+Kye3GdAxXynVS/WE8II2cpfz+Q0O3yN0fRVea2ZBR3B6FU4Jp73jSlwyzeEW
+2CEQHP2Y9oPvjTAjhCUOxz3Y5KOpfz8+PNEDCPIKZu+DEZPGEumdaVfaYI9u
+u6UZDDe4DxRueaKCUEA5UG626zD8fTgJ3/0lbtLvTOwqkzwRaRI3LW7WO6Bg
+/LRJP5JSmS8DVAilySCtR6M9SNDM3YepmTEDRMksdRN7JJr5NZfi67K+wh/y
+q2oKjoqKMFMknYwF8Z4mPOZhGyeVVe7dYxQ1eXpzm+egA6oWfbFWdrPg3Srh
+lZBpx4yDqNRuhLVlYJJCZTsN5BntwA2aS+hLdxlbeM1yvwaWs+Wnz1Q0+iM5
+49r0mDat6jNhLhKrCkc+eAC2P7itMJ/d3Lra1/VWBpAqLROEo5hsdQXTZVty
+WXYXFG7Ng0Yl2QPqxqggFbdnPUX9hN/cnJmSk1Orp83m94z7RUVC5S4T18ju
+3krFo3PRHuxUfnp9VUvnUG4OAHa7DqWbZysjaH7yx67REGvSoaj/EC41k86/
+gnAD6SBXO0oX6M1Etg0HXxAFzlGM6QzDw5zuzi4k9GN0Vtvl8x6dcYeRd1pf
+/r+tr/7p1vGltwAP3hM88kdg+xdQSwcIHNYNSv0CAABJDAAAUEsDBBQACAAI
+ABWJ4jYAAAAAAAAAAAAAAAAKAAAAc3R5bGVzLnhtbN1YX5ObNhB/76dguJlM
+MlMM+JKc7djcS9rpQ9PJJE3fZSGwGoEYSfhPH/t5+qn6SboSCIMBm6TtpM3d
+zd2x+u1q9dvV7uL14zFjzp4ISXm+ccNZ4DokxzymebpxP/z8vbdwH6Nv1jxJ
+KCarmOMyI7nypDoxIh1QzuWqWty4pchXHEkqVznKiFwpvOIFya3Sqo1ema0q
+iTE2Vd2A29qKHNVUZY3t6KLt9J0NuK0dC3SYqqyxwGlbPeFTlY+SeQn3MM8K
+pOiFF0dG848bd6dUsfL9w+EwO9zPuEj9cLlc+ma1cRg3uKIUzKBi7BNG9GbS
+D2ehb7EZUWiqfxrbdikvsy0Rk6lBCvWiKvfp5IzYpyPU4B0Sk3PDgLvhvY+n
+h/c+butmSO1GYrLw38Ci+fXmx3MuiGzqXhrboQoLWkw+ZoVu63POG1e1QnVB
+jbvzIHjuV88t9OEq/CCoIqIFx1fhGDHcMM6zIdIAF/qA8Mhep6nr1CWkU7Yi
+W6MSDvUpQZh4McFMRusqtxqxUz1rjjbuTzTbltJ5j3LpQDAglSwwo+y0cZ+g
+gstXHVQlcp2OWY32UpITQeG48kCl7CAKqjAkxB4JqquI61936zX5Ff1Smg1H
+nWphprh0kopkt3zyxzis5VXZt77HJEElq5uBtVz7aIqlhwljroWfRV4hIBWE
+otBCrClMMwQLDDaVG3d+ZqhAAqUCFbu+FliEzXnhxVQqlOseFM7mL3B21tYV
+v69ozjeYAQlfMZSnJUphLSZGgHmZKwGHev2de2nAgwuH8suIGYy1YyEktyu1
+Qbvw4X3frC5ojByvG25AfdPNEhjXgR2IWLSu6nRdrjthrLnR16oDcuqnjOYe
+zRVJQS+mKVUQs9BsNGCzsYFLIWC6OA1tFQYv3wb2FHvOoJrr1qxEScac6CfN
+dffsaip4WZgBx1j3G/M6WSKnOYN57Pt+yracWVvdZLHYVsJEf/7+R2Pxwkg0
+sPAphLljWW6SlnFowXdJEsDX5SG94UN+dfxW9GSoaK5HHlNlmsYesZI8ffYk
+Va82TeKhomA1214nMf3xWFWa/Qi9ri6bO14Z/XHld0Re13Wa6tjM41533yvJ
+YepMPXZThRjF1p4Bl3lMBAyOxGIkZzQegRxorGcdVCo+gqhT0Wxq/ncbHw6E
+pjuYXSBYcatO3WC2Imf+Wex0idWzZy/c1+LyA0H63eifCcyVhmgohMCkuSd5
+KXRrS+jRGhekIEiTCXcwB/oSxCS50TKB8rPRjYtBEca00UbZZAn9TbfVl4Vy
+xzLn7wWzZjT8LEptOKZQKrhC+up7UFK0+8ug76R/MejUjzq5YaSn2OtOQIXu
+wgydeKk6Zyqy0B3A9H3SwzL472U8BjUmPLU9h2QHhzv30I4s4RyidxEvXZV3
+dQyC2cOLEAYhI0cihSVGEr3QFYoa3pVuuVJ6Fg+aacofd6n25Qu4CaPfgI9d
+f/xeFG4Hb/41BU+vbLmI9Qt5MAsWC5w5pp47d4H5MogCxdVnPgAJF1YN4Y+6
+meexreF3ONDfDT8tBIwKKellSu+s/4NU+q8ydjux/dF6VS9kSDYm5Hk8MkJt
+6doA074L/TJXeR+tzUdsRf1X7gipwNHj4+PavxTWkuKCg4vQ6zDafk0lTKOn
+XserqGl2f6uPUj9or6vJLQrtfi1ZzwVrqsP5VRf8Ho23mH1HCi6uEjvvEWs7
+f6pbmPbnE7l2nlY4RRVrQ6rnZz0eOjt1ROYGXewOgxQZnalgUmtAnpm74WUi
+CB48/QMnDeaz4GGmJbUXGhh961iHwftwuQoWq+f3jdND2dP174ullOO3geZl
+JVou28BK9i+lnj981/3hz/CjvwBQSwcI7Cj9uWgFAAADGAAAUEsDBBQAAAAA
+ABWJ4jZx0iL8qgMAAKoDAAAIAAAAbWV0YS54bWw8P3htbCB2ZXJzaW9uPSIx
+LjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8b2ZmaWNlOmRvY3VtZW50LW1ldGEg
+eG1sbnM6b2ZmaWNlPSJ1cm46b2FzaXM6bmFtZXM6dGM6b3BlbmRvY3VtZW50
+OnhtbG5zOm9mZmljZToxLjAiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3Lncz
+Lm9yZy8xOTk5L3hsaW5rIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2Rj
+L2VsZW1lbnRzLzEuMS8iIHhtbG5zOm1ldGE9InVybjpvYXNpczpuYW1lczp0
+YzpvcGVuZG9jdW1lbnQ6eG1sbnM6bWV0YToxLjAiIHhtbG5zOm9vbz0iaHR0
+cDovL29wZW5vZmZpY2Uub3JnLzIwMDQvb2ZmaWNlIiBvZmZpY2U6dmVyc2lv
+bj0iMS4wIj48b2ZmaWNlOm1ldGE+PG1ldGE6Z2VuZXJhdG9yPk9wZW5PZmZp
+Y2Uub3JnLzIuMCRMaW51eCBPcGVuT2ZmaWNlLm9yZ19wcm9qZWN0LzY4MG01
+JEJ1aWxkLTkwNzM8L21ldGE6Z2VuZXJhdG9yPjxtZXRhOmluaXRpYWwtY3Jl
+YXRvcj5UaG9tYXMgUHJleW1lc3NlcjwvbWV0YTppbml0aWFsLWNyZWF0b3I+
+PG1ldGE6Y3JlYXRpb24tZGF0ZT4yMDA3LTA3LTAyVDE5OjA3OjUxPC9tZXRh
+OmNyZWF0aW9uLWRhdGU+PGRjOmxhbmd1YWdlPmRlLURFPC9kYzpsYW5ndWFn
+ZT48bWV0YTplZGl0aW5nLWN5Y2xlcz4xPC9tZXRhOmVkaXRpbmctY3ljbGVz
+PjxtZXRhOmVkaXRpbmctZHVyYXRpb24+UFQwUzwvbWV0YTplZGl0aW5nLWR1
+cmF0aW9uPjxtZXRhOnVzZXItZGVmaW5lZCBtZXRhOm5hbWU9IkluZm8gMSIv
+PjxtZXRhOnVzZXItZGVmaW5lZCBtZXRhOm5hbWU9IkluZm8gMiIvPjxtZXRh
+OnVzZXItZGVmaW5lZCBtZXRhOm5hbWU9IkluZm8gMyIvPjxtZXRhOnVzZXIt
+ZGVmaW5lZCBtZXRhOm5hbWU9IkluZm8gNCIvPjxtZXRhOmRvY3VtZW50LXN0
+YXRpc3RpYyBtZXRhOnRhYmxlLWNvdW50PSIzIi8+PC9vZmZpY2U6bWV0YT48
+L29mZmljZTpkb2N1bWVudC1tZXRhPlBLAwQUAAgACAAVieI2AAAAAAAAAAAA
+AAAAGAAAAFRodW1ibmFpbHMvdGh1bWJuYWlsLnBuZ+sM8HPn5ZLiYmBg4PX0
+cAkC0iVAvICDDUjeNfWQY2BgbPd0cQypmPP2oiMngwHPgQO2r+4XpScZcJRz
+Mc28a1ty5j3DPSOTYxcknJN5eg1HGTRizODOSmatE5azVwPGDIOnq5/LOqeE
+JgBQSwcItoVPfmoAAADAAQAAUEsDBBQACAAIABWJ4jYAAAAAAAAAAAAAAAAM
+AAAAc2V0dGluZ3MueG1s7Zltc6I6FIC/31/h8PWOFbXbVqe6g/i6Vdd3W78F
+SDVtSNgkiPTX3yC6Y1nZtYgzd3bqDCokec7h5OScE7j/urFxZg0ZR5RUlPyV
+qmQgMamFyLKiTCfN7J3ytfrPPX1+RiYsW9R0bUhElkMhZBeekcMJL4fNFcVl
+pEwBR7xMgA15WZhl6kCyH1Y+7F3eCguvbDAirxVlJYRTzuU8z7vyileULXP5
+UqmU27buu5qUPKPlqaLC3oeiKKU/BQUDQmW2wgqqep0Lz5XMTsl3pqnu7bC/
+/er9TkD4k0UC2oFtMrvLgWoVRYosrxH0flpNOTbu/ZgZ4sjAUGMQTKij7BuF
+78hGRIRSVe9zv0I+BO7CZ3EZ8hxZYnUMXSh8uTub3oZouTqq+fXt7an0rA2c
+LCIW3EArKgl6x6doO0Y6F/NP0Rd6HSuiJBdMzr9SDRrzH9I0gEb0nABpjz8p
++n7IeAWhyJ/gfrrLOGUDypGQ3v+Yope8Jz+lSG5Tht4oEQCPHYxEj1owav4V
+ZWf4N2QCmZeiR7TfGyjN9Xmo/wX4minQGm7pI0CWMeYpJIPv9U05Zu2xo7iI
+ciY33di9p9aoENT+APjIxcNYFte8DTqnTvw2HMXEuzDunL0wTEYxNgCLTy+3
+CS27oNSeSE6qSzqAzgB2o9Stqnk1qROAJQzyx2/pNwnh4xX1AgE1mWZfBwwG
+ZUuEb1CKISBK9RlgDpOLWUBGt/rzOAGCuWfw+1RcCt1iKJrZUyAHVJ1iyiJo
+TIMllC/cFGX1dJPCvF7AKm3ApequTUbUa0Ngyar5IkK2gURGmgvQO/y7K+RO
+A45926CYj2E0IaQiZEyAM6EjwAWMTnQaKysEd/iuXL6YhBHkcr5ji0MZ3RJG
+oCj+aIV4Ln7sGhZaIx6rfkrw48ondZ0Qr20QH/vEXDFK0BtMEIgSVgO7zdLx
+DnJre/pmOLzgMhBM8Ed2xZ954y/LG11EXqeOBQSM31AVP1PSZ0r6TEl/dUr6
+kyTNFVQH2HSxDBXp4wdymyrvoy9PYnawdX2QzRdUNSuP26x6k1UT2mwnSi5l
+N/powAAc3lzXEAHMV6qzYfvfnDqqT3V103vRvO6k4/XqpujVn+TR0P5vn2F+
+Npy+OY1hXhO9QFdd9Xt1rdidaAV5bA66zjWtp9leeJLL3dU0bTWWfxuW/Brb
+JTRqNdWnsbbRSc0H8y/q4rFTGjX6w+6kETBFYI++r3p9pHq9obM2bYwNMlpZ
+Lbw2XvJDo/hNNe2mY2nOi1GsOYvHXkn2e3sqNPliIu1Z1x6MedNfFLC7aDV/
+WI991ZjPXKuuet26xnu65+l2f220Sv6iNfOs1rLUexk+mO0+XzwusCH7HbQX
+wLyPBxPtYdqsNYaFkmu1ZtfbexjSSkKHdxzsTzlkdSBA+g6vrwADpvRDndoO
+gzyIDKk//+jwB8iIxhEgA5eYwgVHnjim8jgBrOEsfFHzneiY8kskmbBMaTJq
+T6Dt/C4OnSFEw1gWXEGI+EYNHRAT4gsUXRRYI1kRUYL9BPcQW/7nfnk7lot7
+b1j9D1BLBwhS6dOMhwQAAHkcAABQSwMEFAAIAAgAFYniNgAAAAAAAAAAAAAA
+ABUAAABNRVRBLUlORi9tYW5pZmVzdC54bWy1lUtqwzAQQPc9hdHeVttVMXEC
+LfQE6QEm8tgR6IdmFJLbVw7k0zaUplg7DUjvjUYaabHaW1PtMJL2rhNPzaOo
+0Cnfazd24mP9Xr+I1fJhYcHpAYnb06DK6xydw06k6FoPpKl1YJFaVq0P6Hqv
+kkXH7df57WRaPlQX8KAN1nliPFQXGfYaaj4E7ASEYLQCznnKneubo6u5VjQU
+IkJPW0QWF8iQjKkD8LYTUsi7nLcpb94NekzxmAs9S2LgRBuIZfCgFBrMoY9S
+pRinneZiFncVEQzGA2MhePAhhXwTUiF89GNEKnfSU+rF4Oy9KQbXFkYk+arZ
+QqCijjvZ358NSm7qnibpRl0L/pbDnXLGPcupV2/Cs5//18y/c4kPBml2rEWG
+2R6e9TbZjQNtSPJp2AQ3zg2ft7DInP/Fc2kX8se3uPwEUEsHCMwGTWZCAQAA
+UQcAAFBLAQIUABQAAAAAABWJ4jaFbDmKLgAAAC4AAAAIAAAAAAAAAAAAAAAA
+AAAAAABtaW1ldHlwZVBLAQIUABQAAAAAABWJ4jYAAAAAAAAAAAAAAAAaAAAA
+AAAAAAAAAAAAAFQAAABDb25maWd1cmF0aW9uczIvc3RhdHVzYmFyL1BLAQIU
+ABQACAAIABWJ4jYAAAAAAgAAAAAAAAAnAAAAAAAAAAAAAAAAAIwAAABDb25m
+aWd1cmF0aW9uczIvYWNjZWxlcmF0b3IvY3VycmVudC54bWxQSwECFAAUAAAA
+AAAVieI2AAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAADjAAAAQ29uZmlndXJh
+dGlvbnMyL2Zsb2F0ZXIvUEsBAhQAFAAAAAAAFYniNgAAAAAAAAAAAAAAABoA
+AAAAAAAAAAAAAAAAGQEAAENvbmZpZ3VyYXRpb25zMi9wb3B1cG1lbnUvUEsB
+AhQAFAAAAAAAFYniNgAAAAAAAAAAAAAAABwAAAAAAAAAAAAAAAAAUQEAAENv
+bmZpZ3VyYXRpb25zMi9wcm9ncmVzc2Jhci9QSwECFAAUAAAAAAAVieI2AAAA
+AAAAAAAAAAAAGAAAAAAAAAAAAAAAAACLAQAAQ29uZmlndXJhdGlvbnMyL21l
+bnViYXIvUEsBAhQAFAAAAAAAFYniNgAAAAAAAAAAAAAAABgAAAAAAAAAAAAA
+AAAAwQEAAENvbmZpZ3VyYXRpb25zMi90b29sYmFyL1BLAQIUABQAAAAAABWJ
+4jYAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAPcBAABDb25maWd1cmF0aW9u
+czIvaW1hZ2VzL0JpdG1hcHMvUEsBAhQAFAAIAAgAFYniNhzWDUr9AgAASQwA
+AAsAAAAAAAAAAAAAAAAANAIAAGNvbnRlbnQueG1sUEsBAhQAFAAIAAgAFYni
+Nuwo/bloBQAAAxgAAAoAAAAAAAAAAAAAAAAAagUAAHN0eWxlcy54bWxQSwEC
+FAAUAAAAAAAVieI2cdIi/KoDAACqAwAACAAAAAAAAAAAAAAAAAAKCwAAbWV0
+YS54bWxQSwECFAAUAAgACAAVieI2toVPfmoAAADAAQAAGAAAAAAAAAAAAAAA
+AADaDgAAVGh1bWJuYWlscy90aHVtYm5haWwucG5nUEsBAhQAFAAIAAgAFYni
+NlLp04yHBAAAeRwAAAwAAAAAAAAAAAAAAAAAig8AAHNldHRpbmdzLnhtbFBL
+AQIUABQACAAIABWJ4jbMBk1mQgEAAFEHAAAVAAAAAAAAAAAAAAAAAEsUAABN
+RVRBLUlORi9tYW5pZmVzdC54bWxQSwUGAAAAAA8ADwDuAwAA0BUAAAAA
+_ENDOFBASE64CODE_
53 examples/roo_soap_client.rb
@@ -0,0 +1,53 @@
+require 'soap/rpc/driver'
+
+ def ferien_fuer_region(proxy, region, year=nil)
+ proxy.first_row.upto(proxy.last_row) { |row|
+ if proxy.cell(row, 2) == region
+ jahr = proxy.cell(row,1).to_i
+ if year == nil || jahr == year
+ bis_datum = proxy.cell(row,5)
+ if DateTime.now > bis_datum
+ print '('
+ end
+ print jahr.to_s+" "
+ print proxy.cell(row,2)+" "
+ print proxy.cell(row,3)+" "
+ print proxy.cell(row,4).to_s+" "
+ print bis_datum.to_s+" "
+ print (proxy.cell(row,6) || '')+" "
+ if DateTime.now > bis_datum
+ print ')'
+ end
+ puts
+ end
+ end
+ }
+ end
+
+proxy = SOAP::RPC::Driver.new("http://localhost:12321","spreadsheetserver")
+proxy.add_method('cell','row','col')
+proxy.add_method('officeversion')
+proxy.add_method('last_row')
+proxy.add_method('last_column')
+proxy.add_method('first_row')
+proxy.add_method('first_column')
+proxy.add_method('sheets')
+proxy.add_method('set_default_sheet','s')
+proxy.add_method('ferien_fuer_region', 'region')
+
+sheets = proxy.sheets
+proxy.set_default_sheet(sheets.first)
+
+puts "first row: #{proxy.first_row}"
+puts "first column: #{proxy.first_column}"
+puts "last row: #{proxy.last_row}"
+puts "last column: #{proxy.last_column}"
+puts "cell: #{proxy.cell('C',8)}"
+puts "cell: #{proxy.cell('F',12)}"
+puts "officeversion: #{proxy.officeversion}"
+puts "Berlin:"
+
+ferien_fuer_region(proxy, "Berlin")
+
+
+
29 examples/roo_soap_server.rb
@@ -0,0 +1,29 @@
+require 'rubygems'
+require 'roo'
+require 'soap/rpc/standaloneServer'
+
+NS = "spreadsheetserver" # name of your service = namespace
+class Server2 < SOAP::RPC::StandaloneServer
+
+ def on_init
+ spreadsheet = Openoffice.new("./Ferien-de.ods")
+ add_method(spreadsheet, 'cell', 'row', 'col')
+ add_method(spreadsheet, 'officeversion')
+ add_method(spreadsheet, 'first_row')
+ add_method(spreadsheet, 'last_row')
+ add_method(spreadsheet, 'first_column')
+ add_method(spreadsheet, 'last_column')
+ add_method(spreadsheet, 'sheets')
+ #add_method(spreadsheet, 'default_sheet=', 's')
+ # method with '...=' did not work? alias method 'set_default_sheet' created
+ add_method(spreadsheet, 'set_default_sheet', 's')
+ end
+
+end
+
+PORT = 12321
+puts "serving at port #{PORT}"
+svr = Server2.new('Roo', NS, '0.0.0.0', PORT)
+
+trap('INT') { svr.shutdown }
+svr.start
33 examples/write_me.rb
@@ -0,0 +1,33 @@
+require 'rubygems'
+require 'roo'
+
+#-- create a new spreadsheet within your google-spreadsheets and paste
+#-- the 'key' parameter in the spreadsheet URL
+MAXTRIES = 1000
+print "what's your name? "
+my_name = gets.chomp
+print "where do you live? "
+my_location = gets.chomp
+print "your message? (if left blank, only your name and location will be inserted) "
+my_message = gets.chomp
+spreadsheet = Google.new('ptu6bbahNZpY0N0RrxQbWdw')
+spreadsheet.default_sheet = 'Sheet1'
+success = false
+MAXTRIES.times do
+ col = rand(10)+1
+ row = rand(10)+1
+ if spreadsheet.empty?(row,col)
+ if my_message.empty?
+ text = Time.now.to_s+" "+"Greetings from #{my_name} (#{my_location})"
+ else
+ text = Time.now.to_s+" "+"#{my_message} from #{my_name} (#{my_location})"
+ end
+ spreadsheet.set_value(row,col,text)
+ puts "message written to row #{row}, column #{col}"
+ success = true
+ break
+ end
+ puts "Row #{row}, column #{col} already occupied, trying again..."
+end
+puts "no empty cell found within #{MAXTRIES} tries" if !success
+
11 lib/roo.rb
@@ -0,0 +1,11 @@
+module Roo
+end
+
+require 'roo/version'
+# require 'roo/spreadsheetparser' TODO:
+require 'roo/generic_spreadsheet'
+require 'roo/openoffice'
+require 'roo/excel'
+require 'roo/excelx'
+require 'roo/google'
+require 'roo/roo_rails_helper'
402 lib/roo/excel.rb
@@ -0,0 +1,402 @@
+require 'rubygems'
+gem 'parseexcel', '>= 0.5.2'
+require 'parseexcel'
+CHARGUESS = false
+require 'charguess' if CHARGUESS
+
+module Spreadsheet # :nodoc
+ module ParseExcel
+ class Worksheet
+ include Enumerable
+ attr_reader :min_row, :max_row, :min_col, :max_col
+ end
+ end
+end
+
+# Class for handling Excel-Spreadsheets
+class Excel < GenericSpreadsheet
+
+ EXCEL_NO_FORMULAS = 'formulas are not supported for excel spreadsheets'
+
+ # Creates a new Excel spreadsheet object.
+ # Parameter packed: :zip - File is a zip-file
+ def initialize(filename, packed = nil, file_warning = :error)
+ super()
+ @file_warning = file_warning
+ @tmpdir = "oo_"+$$.to_s
+ @tmpdir = File.join(ENV['ROO_TMP'], @tmpdir) if ENV['ROO_TMP']
+ unless File.exists?(@tmpdir)
+ FileUtils::mkdir(@tmpdir)
+ end
+ filename = open_from_uri(filename) if filename[0,7] == "http://"
+ filename = open_from_stream(filename[7..-1]) if filename[0,7] == "stream:"
+ filename = unzip(filename) if packed and packed == :zip
+ begin
+ file_type_check(filename,'.xls','an Excel')
+ @filename = filename
+ unless File.file?(@filename)
+ raise IOError, "file #{@filename} does not exist"
+ end
+ @workbook = Spreadsheet::ParseExcel.parse(filename)
+ @default_sheet = nil
+ # no need to set default_sheet if there is only one sheet in the document
+ if self.sheets.size == 1
+ @default_sheet = self.sheets.first
+ end
+ ensure
+ #if ENV["roo_local"] != "thomas-p"
+ FileUtils::rm_r(@tmpdir)
+ #end
+ end
+ @cell = Hash.new
+ @cell_type = Hash.new
+ @formula = Hash.new
+ @first_row = Hash.new
+ @last_row = Hash.new
+ @first_column = Hash.new
+ @last_column = Hash.new
+ @header_line = 1
+ @cells_read = Hash.new
+ end
+
+ # returns an array of sheet names in the spreadsheet
+ def sheets
+ result = []
+ #0.upto(@workbook.worksheets.size - 1) do |i| # spreadsheet
+ 0.upto(@workbook.sheet_count - 1) do |i| # parseexcel
+ # TODO: is there a better way to do conversion?
+ if CHARGUESS
+ encoding = CharGuess::guess(@workbook.worksheet(i).name)
+ encoding = 'unicode' unless encoding
+
+
+ result << Iconv.new('utf-8',encoding).iconv(
+ @workbook.worksheet(i).name
+ )
+ else
+ result << platform_specific_iconv(@workbook.worksheet(i).name)
+ end
+ end
+ return result
+ end
+
+ # returns the content of a cell. The upper left corner is (1,1) or ('A',1)
+ def cell(row,col,sheet=nil)
+ sheet = @default_sheet unless sheet
+ raise ArgumentError unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ raise "should be read" unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ if celltype(row,col,sheet) == :date
+ yyyy,mm,dd = @cell[sheet][[row,col]].split('-')
+ return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
+ end
+ if celltype(row,col,sheet) == :string
+ return platform_specific_iconv(@cell[sheet][[row,col]])
+ else
+ return @cell[sheet][[row,col]]
+ end
+ end
+
+ # returns the type of a cell:
+ # * :float
+ # * :string,
+ # * :date
+ # * :percentage
+ # * :formula
+ # * :time
+ # * :datetime
+ def celltype(row,col,sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ begin
+ if @formula[sheet][[row,col]]
+ return :formula
+ else
+ @cell_type[sheet][[row,col]]
+ end
+ rescue
+ puts "Error in sheet #{sheet}, row #{row}, col #{col}"
+ raise
+ end
+ end
+
+ # returns the first non empty column
+ def first_column(sheet=nil)
+ sheet = @default_sheet unless sheet
+ return @first_column[sheet] if @first_column[sheet]
+ fr, lr, fc, lc = get_firsts_lasts(sheet)
+ fc
+ end
+
+ # returns the last non empty column
+ def last_column(sheet=nil)
+ sheet = @default_sheet unless sheet
+ return @last_column[sheet] if @last_column[sheet]
+ fr, lr, fc, lc = get_firsts_lasts(sheet)
+ lc
+ end
+
+ # returns the first non empty row
+ def first_row(sheet=nil)
+ sheet = @default_sheet unless sheet
+ return @first_row[sheet] if @first_row[sheet]
+ fr, lr, fc, lc = get_firsts_lasts(sheet)
+ fr
+ end
+
+ # returns the last non empty row
+ def last_row(sheet=nil)
+ sheet = @default_sheet unless sheet
+ return @last_row[sheet] if @last_row[sheet]
+ fr, lr, fc, lc = get_firsts_lasts(sheet)
+ lr
+ end
+
+ # returns NO formula in excel spreadsheets
+ def formula(row,col,sheet=nil)
+ raise EXCEL_NO_FORMULAS
+ end
+
+ # raises an exception because formulas are not supported for excel files
+ def formula?(row,col,sheet=nil)
+ raise EXCEL_NO_FORMULAS
+ end
+
+ # returns NO formulas in excel spreadsheets
+ def formulas(sheet=nil)
+ raise EXCEL_NO_FORMULAS
+ end
+
+ # shows the internal representation of all cells
+ # mainly for debugging purposes
+ def to_s(sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ @cell[sheet].inspect
+ end
+
+ private
+ # determine the first and last boundaries
+ def get_firsts_lasts(sheet=nil)
+
+ # 2008-09-14 BEGINf
+ fr=lr=fc=lc=nil
+ sheet = @default_sheet unless sheet
+ if ! @cells_read[sheet]
+ read_cells(sheet)
+ end
+ if @cell[sheet] # nur wenn ueberhaupt Zellen belegt sind
+ @cell[sheet].each {|cellitem|
+ key = cellitem.first
+ y,x = key
+
+ if cellitem[1].class != String or
+ (cellitem[1].class == String and cellitem[1] != "")
+ fr = y unless fr
+ fr = y if y < fr
+
+ lr = y unless lr
+ lr = y if y > lr
+
+ fc = x unless fc
+ fc = x if x < fc
+
+ lc = x unless lc
+ lc = x if x > lc
+ end
+ }
+ end
+ @first_row[sheet] = fr
+ @last_row[sheet] = lr
+ @first_column[sheet] = fc
+ @last_column[sheet] = lc
+ return fr, lr, fc, lc
+ end
+
+ # converts name of a sheet to index (0,1,2,..)
+ def sheet_no(name)
+ return name-1 if name.kind_of?(Fixnum)
+ 0.upto(@workbook.sheet_count - 1) do |i|
+ #0.upto(@workbook.worksheets.size - 1) do |i|
+ # TODO: is there a better way to do conversion?
+ return i if name == platform_specific_iconv(
+ @workbook.worksheet(i).name)
+ #Iconv.new('utf-8','unicode').iconv(
+ # @workbook.worksheet(i).name
+ # )
+ end
+ raise StandardError, "sheet '#{name}' not found"
+ end
+
+ def empty_row?(row)
+ content = false
+ row.compact.each {|elem|
+ if elem != ''
+ content = true
+ end
+ }
+ ! content
+ end
+
+ def empty_column?(col)
+ content = false
+ col.compact.each {|elem|
+ if elem != ''
+ content = true
+ end
+ }
+ ! content
+ end
+
+ def platform_specific_iconv(value)
+ case RUBY_PLATFORM.downcase
+ when /darwin/
+ result = Iconv.new('utf-8','utf-8').iconv(value)
+ when /solaris/
+ result = Iconv.new('utf-8','utf-8').iconv(value)
+ when /mswin32/
+ result = Iconv.new('utf-8','iso-8859-1').iconv(value)
+ else
+ result = value
+ end # case
+ if every_second_null?(result)
+ result = remove_every_second_null(result)
+ end
+ result
+ end
+
+ def every_second_null?(str)
+ result = true
+ return false if str.length < 2
+ 0.upto(str.length/2-1) do |i|
+ c = str[i*2,1]
+ n = str[i*2+1,1]
+ if n != "\000"
+ result = false
+ break
+ end
+ end
+ result
+ end
+
+ def remove_every_second_null(str)
+ result = ''
+ 0.upto(str.length/2-1) do |i|
+ c = str[i*2,1]
+ result += c
+ end
+ result
+ end
+
+ # helper function to set the internal representation of cells
+ def set_cell_values(sheet,x,y,i,v,vt,formula,tr,str_v)
+ #key = "#{y},#{x+i}"
+ key = [y,x+i]
+ @cell_type[sheet] = {} unless @cell_type[sheet]
+ @cell_type[sheet][key] = vt
+ @formula[sheet] = {} unless @formula[sheet]
+ @formula[sheet][key] = formula if formula
+ @cell[sheet] = {} unless @cell[sheet]
+ case vt # @cell_type[sheet][key]
+ when :float
+ @cell[sheet][key] = v.to_f
+ when :string
+ @cell[sheet][key] = str_v
+ when :date
+ @cell[sheet][key] = v
+ when :datetime
+ @cell[sheet][key] = DateTime.new(v.year,v.month,v.day,v.hour,v.min,v.sec)
+ when :percentage
+ @cell[sheet][key] = v.to_f
+ when :time
+ @cell[sheet][key] = v
+ else
+ @cell[sheet][key] = v
+ end
+ end
+
+ # read all cells in the selected sheet
+ def read_cells(sheet=nil)
+ sheet = @default_sheet unless sheet
+ raise ArgumentError, "Error: sheet '#{sheet||'nil'}' not valid" if @default_sheet == nil and sheet==nil
+ raise RangeError unless self.sheets.include? sheet
+
+ if @cells_read[sheet]
+ raise "sheet #{sheet} already read"
+ end
+
+ worksheet = @workbook.worksheet(sheet_no(sheet))
+ skip = 0
+ x =1
+ y=1
+ i=0
+ worksheet.each(skip) { |row_par|
+ if row_par
+ x =1
+ row_par.each do # |void|
+ cell = row_par.at(x-1)
+ if cell
+ case cell.type
+ when :numeric
+ vt = :float
+ v = cell.to_f
+ when :text
+ vt = :string
+ str_v = cell.to_s('utf-8')
+ when :date
+ if cell.to_s.to_f < 1.0
+ vt = :time
+ f = cell.to_s.to_f*24.0*60.0*60.0
+ secs = f.round
+ h = (secs / 3600.0).floor
+ secs = secs - 3600*h
+ m = (secs / 60.0).floor
+ secs = secs - 60*m
+ s = secs
+ v = h*3600+m*60+s
+ else
+ if cell.datetime.hour != 0 or
+ cell.datetime.min != 0 or
+ cell.datetime.sec != 0 or
+ cell.datetime.msec != 0
+ vt = :datetime
+ v = cell.datetime
+ else
+ vt = :date
+ v = cell.date
+ v = sprintf("%04d-%02d-%02d",v.year,v.month,v.day)
+ end
+ end
+ else
+ vt = cell.type.to_s.downcase.to_sym
+ v = nil
+ end # case
+ formula = tr = nil #TODO:???
+ set_cell_values(sheet,x,y,i,v,vt,formula,tr,str_v)
+ end # if cell
+
+ x += 1
+ end
+ end
+ y += 1
+ }
+ @cells_read[sheet] = true
+ end
+
+ #TODO: testing only
+ # def inject_null_characters(str)
+ # if str.class != String
+ # return str
+ # end
+ # new_str=''
+ # 0.upto(str.size-1) do |i|
+ # new_str += str[i,1]
+ # new_str += "\000"
+ # end
+ # new_str
+ # end
+ #
+
+end
601 lib/roo/excelx.rb
@@ -0,0 +1,601 @@
+
+require 'rubygems'
+require 'rexml/document'
+require 'fileutils'
+require 'zip/zipfilesystem'
+require 'date'
+
+class String
+ def end_with?(str)
+ self[-str.length,str.length] == str
+ end
+end
+
+class Excelx < GenericSpreadsheet
+ FORMATS = {
+ 'General' => :float,
+ '0' => :float,
+ '0.00' => :float,
+ '#,##0' => :float,
+ '#,##0.00' => :float,
+ '0%' => :percentage,
+ '0.00%' => :percentage,
+ '0.00E+00' => :float,
+ '# ?/?' => :float, #??? TODO:
+ '# ??/??' => :float, #??? TODO:
+ 'mm-dd-yy' => :date,
+ 'd-mmm-yy' => :date,
+ 'd-mmm' => :date,
+ 'mmm-yy' => :date,
+ 'h:mm AM/PM' => :date,
+ 'h:mm:ss AM/PM' => :date,
+ 'h:mm' => :time,
+ 'h:mm:ss' => :time,
+ 'm/d/yy h:mm' => :date,
+ '#,##0 ;(#,##0)' => :float,
+ '#,##0 ;[Red](#,##0)' => :float,
+ '#,##0.00;(#,##0.00)' => :float,
+ '#,##0.00;[Red](#,##0.00)' => :float,
+ 'mm:ss' => :time,
+ '[h]:mm:ss' => :time,
+ 'mmss.0' => :time,
+ '##0.0E+0' => :float,
+ '@' => :float,
+ #-- zusaetzliche Formate, die nicht standardmaessig definiert sind:
+ "yyyy\\-mm\\-dd" => :date,
+ 'dd/mm/yy' => :date,
+ 'hh:mm:ss' => :time,
+ "dd/mm/yy\\ hh:mm" => :datetime,
+ }
+ STANDARD_FORMATS = {
+ 0 => 'General',
+ 1 => '0',
+ 2 => '0.00',
+ 3 => '#,##0',
+ 4 => '#,##0.00',
+ 9 => '0%',
+ 10 => '0.00%',
+ 11 => '0.00E+00',
+ 12 => '# ?/?',
+ 13 => '# ??/??',
+ 14 => 'mm-dd-yy',
+ 15 => 'd-mmm-yy',
+ 16 => 'd-mmm',
+ 17 => 'mmm-yy',
+ 18 => 'h:mm AM/PM',
+ 19 => 'h:mm:ss AM/PM',
+ 20 => 'h:mm',
+ 21 => 'h:mm:ss',
+ 22 => 'm/d/yy h:mm',
+ 37 => '#,##0 ;(#,##0)',
+ 38 => '#,##0 ;[Red](#,##0)',
+ 39 => '#,##0.00;(#,##0.00)',
+ 40 => '#,##0.00;[Red](#,##0.00)',
+ 45 => 'mm:ss',
+ 46 => '[h]:mm:ss',
+ 47 => 'mmss.0',
+ 48 => '##0.0E+0',
+ 49 => '@',
+ }
+ @@nr = 0
+
+ # initialization and opening of a spreadsheet file
+ # values for packed: :zip
+ def initialize(filename, packed=nil, file_warning = :error) #, create = false)
+ super()
+ @file_warning = file_warning
+ @tmpdir = "oo_"+$$.to_s
+ @tmpdir = File.join(ENV['ROO_TMP'], @tmpdir) if ENV['ROO_TMP']
+ unless File.exists?(@tmpdir)
+ FileUtils::mkdir(@tmpdir)
+ end
+ filename = open_from_uri(filename) if filename[0,7] == "http://"
+ filename = unzip(filename) if packed and packed == :zip
+ begin
+ file_type_check(filename,'.xlsx','an Excel-xlsx')
+ @cells_read = Hash.new
+ @filename = filename
+ unless File.file?(@filename)
+ raise IOError, "file #{@filename} does not exist"
+ end
+ @@nr += 1
+ @file_nr = @@nr
+ extract_content(@filename)
+ file = File.new(File.join(@tmpdir, @file_nr.to_s+"_roo_workbook.xml"))
+ @workbook_doc = REXML::Document.new file
+ file.close
+ @shared_table = []
+ if File.exist?(File.join(@tmpdir, @file_nr.to_s+'_roo_sharedStrings.xml'))
+ file = File.new(File.join(@tmpdir, @file_nr.to_s+'_roo_sharedStrings.xml'))
+ @sharedstring_doc = REXML::Document.new file
+ file.close
+ read_shared_strings(@sharedstring_doc)
+ end
+ @styles_table = []
+ if File.exist?(File.join(@tmpdir, @file_nr.to_s+'_roo_styles.xml'))
+ file = File.new(File.join(@tmpdir, @file_nr.to_s+'_roo_styles.xml'))
+ @styles_doc = REXML::Document.new file
+ file.close
+ read_styles(@styles_doc)
+ end
+ @sheet_doc = []
+ @sheet_files.each_with_index do |item, i|
+ file = File.new(item)
+ @sheet_doc[i] = REXML::Document.new file
+ file.close
+ end
+ ensure
+ #if ENV["roo_local"] != "thomas-p"
+ FileUtils::rm_r(@tmpdir)
+ #end
+ end
+ @default_sheet = nil
+ # no need to set default_sheet if there is only one sheet in the document
+ if self.sheets.size == 1
+ @default_sheet = self.sheets.first
+ end
+ @cell = Hash.new
+ @cell_type = Hash.new
+ @formula = Hash.new
+ @first_row = Hash.new
+ @last_row = Hash.new
+ @first_column = Hash.new
+ @last_column = Hash.new
+ @header_line = 1
+ @excelx_type = Hash.new
+ @excelx_value = Hash.new
+ @s_attribute = Hash.new # TODO: ggf. wieder entfernen nur lokal benoetigt
+ end
+
+ # Returns the content of a spreadsheet-cell.
+ # (1,1) is the upper left corner.
+ # (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 unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ if celltype(row,col,sheet) == :date
+ yyyy,mm,dd = @cell[sheet][[row,col]].split('-')
+ return Date.new(yyyy.to_i,mm.to_i,dd.to_i)
+ elsif celltype(row,col,sheet) == :datetime
+ date_part,time_part = @cell[sheet][[row,col]].split(' ')
+ yyyy,mm,dd = date_part.split('-')
+ hh,mi,ss = time_part.split(':')
+ return DateTime.civil(yyyy.to_i,mm.to_i,dd.to_i,hh.to_i,mi.to_i,ss.to_i)
+
+ end
+ @cell[sheet][[row,col]]
+ 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 unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ if @formula[sheet][[row,col]] == nil
+ return nil
+ else
+ return @formula[sheet][[row,col]]
+ end
+ end
+
+ # true, if there is a formula
+ def formula?(row,col,sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ formula(row,col) != nil
+ 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:
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ set_value(row,col,value,sheet)
+ if value.class == Fixnum
+ set_type(row,col,:float,sheet)
+ elsif value.class == String
+ set_type(row,col,:string,sheet)
+ elsif value.class == Float
+ set_type(row,col,:string,sheet)
+ else
+ raise ArgumentError, "Type for "+value.to_s+" not set"
+ end
+ end
+
+ # returns the type of a cell:
+ # * :float
+ # * :string,
+ # * :date
+ # * :percentage
+ # * :formula
+ # * :time
+ # * :datetime
+ def celltype(row,col,sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ if @formula[sheet][[row,col]]
+ return :formula
+ else
+ @cell_type[sheet][[row,col]]
+ end
+ end
+
+ # returns the internal type of an excel cell
+ # * :numeric_or_formula
+ # * :string
+ # Note: this is only available within the Excelx class
+ def excelx_type(row,col,sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ return @excelx_type[sheet][[row,col]]
+ 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 unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ return @excelx_value[sheet][[row,col]]
+ end
+
+ # returns the internal format of an excel cell
+ def excelx_format(row,col,sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ row,col = normalize(row,col)
+ s = @s_attribute[sheet][[row,col]]
+ result = attribute2format(s)
+ result
+ end
+
+ # returns an array of sheet names in the spreadsheet
+ def sheets
+ return_sheets = []
+ @workbook_doc.each_element do |workbook|
+ workbook.each_element do |el|
+ if el.name == "sheets"
+ el.each_element do |sheet|
+ return_sheets << sheet.attributes['name']
+ end
+ end
+ end
+ end
+ return_sheets
+ end
+
+ # shows the internal representation of all cells
+ # for debugging purposes
+ def to_s(sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ @cell[sheet].inspect
+ end
+
+ # returns each formula in the selected sheet as an array of elements
+ # [row, col, formula]
+ def formulas(sheet=nil)
+ theformulas = Array.new
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ first_row(sheet).upto(last_row(sheet)) {|row|
+ first_column(sheet).upto(last_column(sheet)) {|col|
+ if formula?(row,col,sheet)
+ f = [row, col, formula(row,col,sheet)]
+ theformulas << f
+ end
+ }
+ }
+ theformulas
+ end
+
+ private
+
+ # helper function to set the internal representation of cells
+ def set_cell_values(sheet,x,y,i,v,vt,formula,tr,str_v,
+ excelx_type=nil,
+ excelx_value=nil,
+ s_attribute=nil)
+ key = [y,x+i]
+ @cell_type[sheet] = {} unless @cell_type[sheet]
+ @cell_type[sheet][key] = vt
+ @formula[sheet] = {} unless @formula[sheet]
+ @formula[sheet][key] = formula if formula
+ @cell[sheet] = {} unless @cell[sheet]
+ case @cell_type[sheet][key]
+ when :float
+ @cell[sheet][key] = v.to_f
+ when :string
+ @cell[sheet][key] = str_v
+ when :date
+ @cell[sheet][key] = (Date.new(1899,12,30)+v.to_i).strftime("%Y-%m-%d")
+ when :datetime
+ @cell[sheet][key] = (DateTime.new(1899,12,30)+v.to_f).strftime("%Y-%m-%d %H:%M:%S")
+ when :percentage
+ @cell[sheet][key] = v.to_f
+ when :time
+ @cell[sheet][key] = v.to_f*(24*60*60)
+ else
+ @cell[sheet][key] = v
+ end
+ @excelx_type[sheet] = {} unless @excelx_type[sheet]
+ @excelx_type[sheet][key] = excelx_type
+ @excelx_value[sheet] = {} unless @excelx_value[sheet]
+ @excelx_value[sheet][key] = excelx_value
+ @s_attribute[sheet] = {} unless @s_attribute[sheet]
+ @s_attribute[sheet][key] = s_attribute
+ end
+
+ # splits a coordinate like "AA12" into the parts "AA" (String) and 12 (Fixnum)
+ def split_coord(s)
+ letter = ""
+ number = 0
+ i = 0
+ while i<s.length and "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".include?(s[i,1])
+ letter += s[i,1]
+ i+=1
+ end
+ while i<s.length and "0123456789".include?(s[i,1])
+ number = number*10 + s[i,1].to_i
+ i+=1
+ end
+ if letter=="" or number==0
+ raise ArgumentError
+ end
+ return letter,number
+ end
+
+ def split_coordinate(str)
+ letter,number = split_coord(str)
+ x = GenericSpreadsheet.letter_to_number(letter)
+ y = number
+ return x,y
+ end
+
+ # read all cells in the selected sheet
+ def format2type(format)
+ if FORMATS.has_key? format
+ FORMATS[format]
+ else
+ :float
+ end
+ end
+
+ # read all cells in the selected sheet
+ def read_cells(sheet=nil)
+ sheet = @default_sheet unless sheet
+ sheet_found = false
+ raise ArgumentError, "Error: sheet '#{sheet||'nil'}' not valid" if @default_sheet == nil and sheet==nil
+ raise RangeError unless self.sheets.include? sheet
+ n = self.sheets.index(sheet)
+ @sheet_doc[n].each_element do |worksheet|
+ worksheet.each_element do |elem|
+ if elem.name == 'sheetData'
+ elem.each_element do |sheetdata|
+ if sheetdata.name == 'row'
+ sheetdata.each_element do |row|
+ if row.name == 'c'
+ if row.attributes['t'] == 's'
+ tmp_type = :shared
+ else
+ s_attribute = row.attributes['s']
+ format = attribute2format(s_attribute)
+ tmp_type = format2type(format)
+ end
+ formula = nil
+ row.each_element do |cell|
+# puts "cell.name: #{cell.name}" if cell.text.include? "22606.5120"
+# puts "cell.text: #{cell.text}" if cell.text.include? "22606.5120"
+ if cell.name == 'f'
+ formula = cell.text
+ end
+ if cell.name == 'v'
+ #puts "tmp_type: #{tmp_type}" if cell.text.include? "22606.5120"
+ #puts cell.name
+ if tmp_type == :time or tmp_type == :datetime #2008-07-26
+ #p cell.text
+ # p cell.text.to_f if cell.text.include? "22606.5120"
+ if cell.text.to_f >= 1.0 # 2008-07-26
+ # puts ">= 1.0" if cell.text.include? "22606.5120"
+ # puts "cell.text.to_f: #{cell.text.to_f}" if cell.text.include? "22606.5120"
+ #puts "cell.text.to_f.floor: #{cell.text.to_f.floor}" if cell.text.include? "22606.5120"
+ if (cell.text.to_f - cell.text.to_f.floor).abs > 0.000001 #TODO:
+ # puts "abs ist groesser" if cell.text.include? "22606.5120"
+ # @cell[sheet][key] = DateTime.parse(tr.attributes['date-value'])
+ tmp_type = :datetime
+
+ else
+ #puts ":date"
+ tmp_type = :date # 2008-07-26
+ end
+ else
+ #puts "<1.0"
+ end # 2008-07-26
+ end # 2008-07-26
+ excelx_type = [:numeric_or_formula,format]
+ excelx_value = cell.text
+ if tmp_type == :shared
+ vt = :string
+ str_v = @shared_table[cell.text.to_i]
+ excelx_type = :string
+ elsif tmp_type == :date
+ vt = :date
+ v = cell.text
+ elsif tmp_type == :time
+ vt = :time
+ v = cell.text
+ elsif tmp_type == :datetime
+ vt = :datetime
+ v = cell.text
+ elsif tmp_type == :formula
+ vt = :formula
+ v = cell.text.to_f #TODO: !!!!
+ else
+ vt = :float
+ v = cell.text
+ end
+ #puts "vt: #{vt}" if cell.text.include? "22606.5120"
+ x,y = split_coordinate(row.attributes['r'])
+ tr=nil #TODO: ???s
+ set_cell_values(sheet,x,y,0,v,vt,formula,tr,str_v,excelx_type,excelx_value,s_attribute)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ sheet_found = true #TODO:
+ if !sheet_found
+ raise RangeError
+ end
+ @cells_read[sheet] = true
+ end
+
+ # Checks if the default_sheet exists. If not an RangeError exception is
+ # raised
+ def check_default_sheet
+ sheet_found = false
+ raise ArgumentError, "Error: default_sheet not set" if @default_sheet == nil
+ @workbook_doc.each_element do |workbook|
+ workbook.each_element do |el|
+ if el.name == "sheets"
+ el.each_element do |sheet|
+ if @default_sheet == sheet.attributes['name']
+ sheet_found = true
+ end
+ end
+ end
+ end
+ end
+ if ! sheet_found
+ raise RangeError, "sheet '#{@default_sheet}' not found"
+ end
+ end
+
+ # extracts all needed files from the zip file
+ def process_zipfile(zipfilename, zip, path='')
+ @sheet_files = []
+ Zip::ZipFile.open(zipfilename) {|zf|
+ zf.entries.each {|entry|
+ #entry.extract
+ if entry.to_s.end_with?('workbook.xml')
+ open(@tmpdir+'/'+@file_nr.to_s+'_roo_workbook.xml','wb') {|f|
+ f << zip.read(entry)
+ }
+ end
+ if entry.to_s.end_with?('sharedStrings.xml')
+ open(@tmpdir+'/'+@file_nr.to_s+'_roo_sharedStrings.xml','wb') {|f|
+ f << zip.read(entry)
+ }
+ end
+ if entry.to_s.end_with?('styles.xml')
+ open(@tmpdir+'/'+@file_nr.to_s+'_roo_styles.xml','wb') {|f|
+ f << zip.read(entry)
+ }
+ end
+ if entry.to_s =~ /sheet([0-9]+).xml$/
+ nr = $1
+ open(@tmpdir+'/'+@file_nr.to_s+"_roo_sheet#{nr}",'wb') {|f|
+ f << zip.read(entry)
+ }
+ @sheet_files[nr.to_i-1] = @tmpdir+'/'+@file_nr.to_s+"_roo_sheet#{nr}"
+ end
+ }
+ }
+ return
+ end
+
+ # extract files from the zip file
+ def extract_content(zipfilename)
+ Zip::ZipFile.open(@filename) do |zip|
+ process_zipfile(zipfilename,zip)
+ end
+ end
+
+ # sets the value of a cell
+ def set_value(row,col,value,sheet=nil)
+ sheet = @default_value unless sheet
+ @cell[sheet][[row,col]] = value
+ end
+
+ # sets the type of a cell
+ def set_type(row,col,type,sheet=nil)
+ sheet = @default_value unless sheet
+ @cell_type[sheet][[row,col]] = type
+ end
+
+ # read the shared strings xml document
+ def read_shared_strings(doc)
+ doc.each_element do |sst|
+ if sst.name == 'sst'
+ sst.each_element do |si|
+ if si.name == 'si'
+ si.each_element do |elem|
+ if elem.name == 't'
+ @shared_table << elem.text
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ # read the styles elements of an excelx document
+ def read_styles(doc)
+ @numFmts = []
+ @cellXfs = []
+ doc.each_element do |e1|
+ if e1.name == "styleSheet"
+ e1.each_element do |e2|
+ if e2.name == "numFmts"
+ e2.each_element do |e3|
+ if e3.name == 'numFmt'
+ numFmtId = e3.attributes['numFmtId']
+ formatCode = e3.attributes['formatCode']
+ @numFmts << [numFmtId, formatCode]
+ end
+ end
+ elsif e2.name == "cellXfs"
+ e2.each_element do |e3|
+ if e3.name == 'xf'
+ numFmtId = e3.attributes['numFmtId']
+ @cellXfs << [numFmtId]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ # convert internal excelx attribute to a format
+ def attribute2format(s)
+ result = nil
+ @numFmts.each {|nf|
+ if nf.first == @cellXfs[s.to_i].first
+ result = nf[1]
+ break
+ end
+ }
+ unless result
+ id = @cellXfs[s.to_i].first.to_i
+ if STANDARD_FORMATS.has_key? id
+ result = STANDARD_FORMATS[id]
+ end
+ end
+ result
+ end
+
+end # class
636 lib/roo/generic_spreadsheet.rb
@@ -0,0 +1,636 @@
+require 'rubygems'
+require 'builder'
+
+# Base class for all other types of spreadsheets
+class GenericSpreadsheet
+
+ attr_reader :default_sheet
+
+ # sets the line with attribute names (default: 1)
+ attr_accessor :header_line
+
+ def initialize
+ end
+
+ # set the working sheet in the document
+ def default_sheet=(sheet)
+ if sheet.kind_of? Fixnum
+ if sheet >= 0 and sheet <= sheets.length
+ sheet = self.sheets[sheet-1]
+ else
+ raise RangeError
+ end
+ elsif sheet.kind_of?(String)
+ raise RangeError if ! self.sheets.include?(sheet)
+ else
+ raise TypeError, "what are you trying to set as default sheet?"
+ end
+ @default_sheet = sheet
+ check_default_sheet
+ @first_row[sheet] = @last_row[sheet] = @first_column[sheet] = @last_column[sheet] = nil
+ @cells_read[sheet] = false
+ end
+
+ # first non-empty column as a letter
+ def first_column_as_letter(sheet=nil)
+ GenericSpreadsheet.number_to_letter(first_column(sheet))
+ end
+
+ # last non-empty column as a letter
+ def last_column_as_letter(sheet=nil)
+ GenericSpreadsheet.number_to_letter(last_column(sheet))
+ end
+
+ # returns the number of the first non-empty row
+ def first_row(sheet=nil)
+ if sheet == nil
+ sheet = @default_sheet
+ end
+ read_cells(sheet) unless @cells_read[sheet]
+ if @first_row[sheet]
+ return @first_row[sheet]
+ end
+ impossible_value = 999_999 # more than a spreadsheet can hold
+ result = impossible_value
+ @cell[sheet].each_pair {|key,value|
+ y,x = key # _to_string(key).split(',')
+ y = y.to_i
+ result = [result, y].min if value
+ } if @cell[sheet]
+ result = nil if result == impossible_value
+ @first_row[sheet] = result
+ result
+ end
+
+ # returns the number of the last non-empty row
+ def last_row(sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ if @last_row[sheet]
+ return @last_row[sheet]
+ end
+ impossible_value = 0
+ result = impossible_value
+ @cell[sheet].each_pair {|key,value|
+ y,x = key # _to_string(key).split(',')
+ y = y.to_i
+ result = [result, y].max if value
+ } if @cell[sheet]
+ result = nil if result == impossible_value
+ @last_row[sheet] = result
+ result
+ end
+
+ # returns the number of the first non-empty column
+ def first_column(sheet=nil)
+ if sheet == nil
+ sheet = @default_sheet
+ end
+ read_cells(sheet) unless @cells_read[sheet]
+ if @first_column[sheet]
+ return @first_column[sheet]
+ end
+ impossible_value = 999_999 # more than a spreadsheet can hold
+ result = impossible_value
+ @cell[sheet].each_pair {|key,value|
+ y,x = key # _to_string(key).split(',')
+ x = x # .to_i
+ result = [result, x].min if value
+ } if @cell[sheet]
+ result = nil if result == impossible_value
+ @first_column[sheet] = result
+ result
+ end
+
+ # returns the number of the last non-empty column
+ def last_column(sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ if @last_column[sheet]
+ return @last_column[sheet]
+ end
+ impossible_value = 0
+ result = impossible_value
+ @cell[sheet].each_pair {|key,value|
+ y,x = key # _to_string(key).split(',')
+ x = x.to_i
+ result = [result, x].max if value
+ } if @cell[sheet]
+ result = nil if result == impossible_value
+ @last_column[sheet] = result
+ result
+ end
+
+ # returns a rectangular area (default: all cells) as yaml-output
+ # you can add additional attributes with the prefix parameter like:
+ # oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
+ def to_yaml(prefix={}, from_row=nil, from_column=nil, to_row=nil, to_column=nil,sheet=nil)
+ sheet = @default_sheet unless sheet
+ result = "--- \n"
+ (from_row||first_row(sheet)).upto(to_row||last_row(sheet)) do |row|
+ (from_column||first_column(sheet)).upto(to_column||last_column(sheet)) do |col|
+ unless empty?(row,col,sheet)
+ result << "cell_#{row}_#{col}: \n"
+ prefix.each {|k,v|
+ result << " #{k}: #{v} \n"
+ }
+ result << " row: #{row} \n"
+ result << " col: #{col} \n"
+ result << " celltype: #{self.celltype(row,col,sheet)} \n"
+ if self.celltype(row,col,sheet) == :time
+ result << " value: #{GenericSpreadsheet.integer_to_timestring( self.cell(row,col,sheet))} \n"
+ else
+ result << " value: #{self.cell(row,col,sheet)} \n"
+ end
+ end
+ end
+ end
+ result
+ end
+
+ # write the current spreadsheet to stdout or into a file
+ def to_csv(filename=nil,sheet=nil)
+ sheet = @default_sheet unless sheet
+ if filename
+ file = File.open(filename,"w") # do |file|
+ write_csv_content(file,sheet)
+ file.close
+ else
+ write_csv_content(STDOUT,sheet)
+ end
+ true
+ end
+
+ # find a row either by row number or a condition
+ # Caution: this works only within the default sheet -> set default_sheet before you call this method
+ # (experimental. see examples in the test_roo.rb file)
+ def find(*args) # :nodoc
+ result_array = false
+ args.each {|arg,val|
+ if arg.class == Hash
+ arg.each { |hkey,hval|
+ if hkey == :array and hval == true
+ result_array = true
+ end
+ }
+ end
+ }
+ column_with = {}
+ 1.upto(last_column) do |col|
+ column_with[cell(@header_line,col)] = col
+ end
+ result = Array.new
+ #-- id
+ if args[0].class == Fixnum
+ rownum = args[0]
+ if @header_line
+ tmp = {}
+ else
+ tmp = []
+ end
+ 1.upto(self.row(rownum).size) {|j|
+ x = ''
+ column_with.each { |key,val|
+ if val == j
+ x = key
+ end
+ }
+ if @header_line
+ tmp[x] = cell(rownum,j)
+ else
+ tmp[j-1] = cell(rownum,j)
+ end
+
+ }
+ if @header_line
+ result = [ tmp ]
+ else
+ result = tmp
+ end
+ #-- :all
+ elsif args[0] == :all
+ if args[1].class == Hash
+ args[1].each {|key,val|
+ if key == :conditions
+ column_with = {}
+ 1.upto(last_column) do |col|
+ column_with[cell(@header_line,col)] = col
+ end
+ conditions = val
+ first_row.upto(last_row) do |i|
+ # are all conditions met?
+ found = 1
+ conditions.each { |key,val|
+ if cell(i,column_with[key]) == val
+ found *= 1
+ else
+ found *= 0
+ end
+ }
+ if found > 0
+ tmp = {}
+ 1.upto(self.row(i).size) {|j|
+ x = ''
+ column_with.each { |key,val|
+ if val == j
+ x = key
+ end
+ }
+ tmp[x] = cell(i,j)
+ }
+ if result_array
+ result << self.row(i)
+ else
+ result << tmp
+ end
+ end
+ end
+ end # :conditions
+ }
+ end
+ end
+ result
+ end
+
+ # returns all values in this row as an array
+ # row numbers are 1,2,3,... like in the spreadsheet
+ def row(rownumber,sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ result = []
+ tmp_arr = []
+ @cell[sheet].each_pair {|key,value|
+ y,x = key # _to_string(key).split(',')
+ x = x.to_i
+ y = y.to_i
+ if y == rownumber
+ tmp_arr[x] = value
+ end
+ }
+ result = tmp_arr[1..-1]
+ while result && result[-1] == nil
+ result = result[0..-2]
+ end
+ result
+ end
+
+ # returns all values in this column as an array
+ # column numbers are 1,2,3,... like in the spreadsheet
+ def column(columnnumber,sheet=nil)
+ if columnnumber.class == String
+ columnnumber = Excel.letter_to_number(columnnumber)
+ end
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet]
+ result = []
+ first_row(sheet).upto(last_row(sheet)) do |row|
+ result << cell(row,columnnumber,sheet)
+ end
+ result
+ end
+
+ # reopens and read a spreadsheet document
+ def reload
+ ds = @default_sheet
+ initialize(@filename) if self.class == Openoffice or
+ self.class == Excel
+ initialize(@spreadsheetkey,@user,@password) if self.class == Google
+ self.default_sheet = ds
+ #@first_row = @last_row = @first_column = @last_column = nil
+ end
+
+ # true if cell is empty
+ def empty?(row, col, sheet=nil)
+ sheet = @default_sheet unless sheet
+ read_cells(sheet) unless @cells_read[sheet] or self.class == Excel
+ row,col = normalize(row,col)
+ return true unless cell(row, col, sheet)
+ return true if celltype(row, col, sheet) == :string && cell(row, col, sheet).empty?
+ return true if row < first_row(sheet) || row > last_row(sheet) || col < first_column(sheet) || col > last_column(sheet)
+ false
+ end
+
+ # recursively removes the current temporary directory
+ # this is only needed if you work with zipped files or files via the web
+ def remove_tmp
+ if File.exists?(@tmpdir)
+ FileUtils::rm_r(@tmpdir)
+ end
+ end
+
+ # Returns information of the spreadsheet document and all sheets within
+ # this document.
+ def info
+ result = "File: #{File.basename(@filename)}\n"+
+ "Number of sheets: #{sheets.size}\n"+
+ "Sheets: #{sheets.map{|sheet| sheet+", "}.to_s[0..-3]}\n"
+ n = 1
+ sheets.each {|sheet|
+ self.default_sheet = sheet
+ result << "Sheet " + n.to_s + ":\n"
+ unless first_row
+ result << " - empty -"
+ else
+ result << " First row: #{first_row}\n"
+ result << " Last row: #{last_row}\n"
+ result << " First column: #{GenericSpreadsheet.number_to_letter(first_column)}\n"
+ result << " Last column: #{GenericSpreadsheet.number_to_letter(last_column)}"
+ end
+ result << "\n" if sheet != sheets.last
+ n += 1
+ }
+ result
+ end
+
+ def to_xml
+ xml_document = ''
+ xml = Builder::XmlMarkup.new(:target => xml_document, :indent => 2)
+ xml.instruct! :xml, :version =>"1.0", :encoding => "utf-8"
+ xml.spreadsheet {
+ self.sheets.each do |sheet|
+ self.default_sheet = sheet
+ xml.sheet(:name => sheet) { |x|
+ if first_row and last_row and first_column and last_column
+ # 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),
+ :row =>row,
+ :column => col,
+ :type => celltype(row,col))
+ end
+ end
+ end
+ end
+ }
+ end
+ }
+ xml_document
+ end
+
+ protected
+
+ def file_type_check(filename, ext, name)
+ new_expression = {
+ '.ods' => 'Openoffice.new',
+ '.xls' => 'Excel.new',
+ '.xlsx' => 'Excelx.new',
+ }
+ case ext
+ when '.ods', '.xls', '.xlsx'
+ correct_class = "use #{new_expression[ext]} to handle #{ext} spreadsheet files"
+ else
+ raise "unknown file type: #{ext}"
+ end
+ if File.extname(filename).downcase != ext
+ case @file_warning
+ when :error
+ warn correct_class
+ raise TypeError, "#{filename} is not #{name} file"
+ when :warning
+ warn "are you sure, this is #{name} spreadsheet file?"
+ warn correct_class
+ when :ignore
+ # ignore
+ else
+ raise "#{@file_warning} illegal state of file_warning"
+ end
+ end
+ end
+
+ # konvertiert einen Key in der Form "12,45" (=row,column) in
+ # ein Array mit numerischen Werten ([12,45])
+ # Diese Methode ist eine temp. Loesung, um zu erforschen, ob der
+ # Zugriff mit numerischen Keys schneller ist.
+ def key_to_num(str)
+ r,c = str.split(',')
+ r = r.to_i
+ c = c.to_i
+ [r,c]
+ end
+
+ # siehe: key_to_num
+ def key_to_string(arr)
+ "#{arr[0]},#{arr[1]}"
+ end
+
+ private
+
+ # converts cell coordinate to numeric values of row,col
+ def normalize(row,col)
+ if row.class == String
+ if col.class == Fixnum
+ # ('A',1):
+ # ('B', 5) -> (5, 2)
+ row, col = col, row
+ else
+ raise ArgumentError
+ end
+ end
+ if col.class == String
+ col = GenericSpreadsheet.letter_to_number(col)
+ end
+ return row,col
+ end
+
+ # def open_from_uri(uri)
+ # require 'open-uri' ;
+ # tempfilename = File.join(@tmpdir, File.basename(uri))
+ # f = File.open(tempfilename,"wb")
+ # begin
+ # open(uri) do |net|
+ # f.write(net.read)
+ # end
+ # rescue
+ # raise "could not open #{uri}"
+ # end
+ # f.close
+ # File.join(@tmpdir, File.basename(uri))
+ # end
+
+ # OpenURI::HTTPError
+ # def open_from_uri(uri)
+ # require 'open-uri'
+ # #existiert URL?
+ # r = Net::HTTP.get_response(URI.parse(uri))
+ # raise "URL nicht verfuegbar" unless r.is_a? Net::HTTPOK
+ # tempfilename = File.join(@tmpdir, File.basename(uri))
+ # f = File.open(tempfilename,"wb")
+ # open(uri) do |net|
+ # f.write(net.read)
+ # end
+ # # rescue
+ # # raise "could not open #{uri}"
+ # # end
+ # f.close
+ # File.join(@tmpdir, File.basename(uri))
+ # end
+
+ def open_from_uri(uri)
+ require 'open-uri'
+ response = ''
+ begin
+ open(uri, "User-Agent" => "Ruby/#{RUBY_VERSION}") { |net|
+ response = net.read
+ tempfilename = File.join(@tmpdir, File.basename(uri))
+ f = File.open(tempfilename,"wb")
+ f.write(response)
+ f.close
+ }
+ rescue OpenURI::HTTPError
+ raise "could not open #{uri}"
+ end
+ File.join(@tmpdir, File.basename(uri))
+ end
+
+ def open_from_stream(stream)
+ tempfilename = File.join(@tmpdir, "spreadsheet")
+ f = File.open(tempfilename,"wb")
+ f.write(stream[7..-1])
+ f.close
+ File.join(@tmpdir, "spreadsheet")
+ end
+
+ # convert a number to something like 'AB' (1 => 'A', 2 => 'B', ...)
+ def self.number_to_letter(n)
+ letters=""
+ while n > 0
+ num = n%26
+ letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num-1,1] + letters
+ n = n.div(26)
+ end
+ letters
+ end
+
+ # convert letters like 'AB' to a number ('A' => 1, 'B' => 2, ...)
+ def self.letter_to_number(letters)
+ result =<