gozes / rods

Ruby OpenDocument Spreadsheet

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

 = RODS - Ruby Open Document Spreadsheet
 This class provides a convenient interface for reading and writing
 spreadsheets conforming to Open Document Format v1.1.
 Installiation of an office-application (LibreOffice, OpenOffice.org) is not required as the code directly
 manipulates the XML-files in the zipped *.ods-container.

 On my website http://ruby.homelinux.com/ruby/rods/ you can see the output of the script below.
 You can contact me at rods.ruby@online.de (and tell me about experiences, problems you encountered or drop me a line, if you like it ;-).

 link:images/Rods.jpg
 
 = Copyright
 Copyright (c) <em>Dr. Heinz Breinlinger</em> (2011).
 Licensed under the same terms as Ruby. No warranty is provided.
 
 = What you must know
 When you open a (new) spreadsheet in an application like OpenOffice.org, you'll see an infinity of empty cells and
 might be tempted to assume that all these cells actually "exist" in the underlying file format/XML-structure.
 This is not the case ! A "newly born" spreadsheet does not contain even a single cell ! They're created on demand.
 That is: If you write a cell with coordinates (10,10) into a new sheet only 2 rows and 2 cells are created
 * an empty row with a repetition-attribute of 9 ( -> visible as row 1 to 9)
 * a second row (visible on line 10)
 * a single cell in the second row with a repetition-attribute of 9 ( -> visible as cells 1 to 9 in line 10)
 * a single cell after the previous cell ( -> visible as cell 10 in line 10)
 Things get complicated when you insert one or more rows, but we won't delve into implementation details here.
 Suffice it to say, that
 * if a single cell in a row has a visible value, the row as well as the cell 'exist' in the former sense, i.e.
   they're definitely found by the methods get(Next|Previous)Existent(Row|Cell).
 * the opposite does not apply: If a cell or row is visibly empty it COULD exist (and therefore be detectetd by
   the former methods) or not. That is: When using the 'fast methods' you will from time to time stumble over
   cells and rows not bearing any value, but nevertheless existing and are well advised to verify the return-values !
 
 This is very important for using the interface appropriately: All methods matching 'Existent' merely return "living"
 cells, while all other methods bearing a "get|Get" create a row or cell if necessary.
 In other words: Use
 * getNextExistentRow, getPreviousExistentRow, getNextExistentCell, getPreviousExistentCell
   whenever you just want to read values you already have (seemlessly skipping most but not all visibly emtpy lines and columns) and
 * getCell, writeGetCell and writeGetCellFromRow whenever you want a cell to be created if it does not exist (which
   is undoubtedly the case, when you want to write to it or assign it a style) !
 The first return nil, if an element does not exist yet, the second always return the desired element.
 
 = Usage Examples
    
    Open a spreadsheet in the working directory.
    sheet = Rods.new "Template.ods" # empty or prefilled sheet

    Rename, insert and delete some tables
    sheet.rename_table "Table1","not needed"
    sheet.insert_table "example"

    Very important: tell RODS, what table you want to work with subsequently,
    if the table is not the first in the spreadsheet.

    sheet.set_current_table "example"
    sheet.delete_table "Table2"
    sheet.delete_table "Table3"

    Fill the sheet with values. All values are strings at the 
    user-level and are accompanied by a type-token.
    The type affects the internal representation and the style of
    the corresponding cell.
    When the type is a formula, the resulting type is appended.
    Valid types are
     "string","time","date","float","percent","currency","formula",
     "formula:float","formula:time","formula:date","formula:currency".

    sheet.write_cell 1,1, "date", "31.12.2010" 
    sheet.write_cell 2,1, "formula:date", " = A1+1" 

    Alter the data-style for the following 2 date-values.
   
    Data-styles affect, how values are displayed, i.e. their "meaning".
    Within that "meaning" styles (not data-styles !) affect the look
    (color, indentation, 'italic', etc.)
    Data-styles are predefined by RODS and (so far) unique for every type.
    Only date-values have 2 different data-styles implemented.

   sheet.setDateFormat("dateDay")  # "05.01.2011" -> "Mi" (if German)

    Add 2 formulas with date-values as results.

   sheet.writeCell(1,2,"formula:date"," = A1")
   sheet.writeCell(2,2,"formula:date"," = A2")

    Reset the data-style for date-values

   sheet.setDateFormat("date")     # back to "DD.MM.YYYY"

    Continue with 2 time-values

   sheet.writeCell(1,3,"time","13:37") 
   sheet.writeCell(2,3,"time","20:15")

    Write 2 currency-values

   sheet.writeCell(1,4,"currency","19,99") 
   sheet.writeCell(2,4,"currency","-7,78")

    Insert a formula for the time-difference

   cell = sheet.writeGetCell(3,3,"formula:time"," = C2-C1")

    Set a border, center the text, change background- and font-color.

   sheet.setAttributes(cell,{ "border"  = > "0.01cm solid turquoise",
                                "text-align"  = > "center",           
                                "background-color"  = > "yellow2",   
                                "color"  = > "blue"})               

    Inset a formula for the sum of the above currency-values

   cell = sheet.writeGetCell(3,4,"formula:currency"," = D2+D1")

    Apply different borders and display the font as italic and bold.

   sheet.setAttributes(cell,{ "border-right"  = > "0.05cm solid magenta4",
                                "border-bottom"  = > "0.03cm solid lightgreen",
                                "border-top"  = > "0.08cm solid salmon",
                                "font-style"  = > "italic",
                                "font-weight"  = > "bold"})

    Create a new style for percent-values and apply it to a new
    percent-value.
    Be aware that a valid data-style is chosen ("percentFormat"
    is RODS' default-data-style for percent-values.)

   sheet.writeStyleAbbr({"name"  = > "newPercentStyle",        
                           "margin-left"  = > "0.3cm",
                           "text-align"  = > "start",
                           "color"  = > "blue",
                           "border"  = > "0.01cm solid black",
                           "font-style"  = > "italic",
                           "data-style-name"  = > "percentFormat", # <- data-style !
                           "font-weight"  = > "bold"})
   cell = sheet.writeGetCell(4,2,"percent","4,71")
   sheet.setStyle(cell,"newPercentStyle")

    Add a comment-field, change the font-color and font-style.

   sheet.writeComment(cell,"by Dr. Heinz Breinlinger")
   cell = sheet.writeGetCell(4,3,"formula:time"," = B4*C3")
   sheet.setAttributes(cell,{ "color"  = > "lightmagenta",
                                "font-style"  = > "italic"})

    Insert a formula for the percentage of a currency-value.

   cell = sheet.writeGetCell(4,4,"formula:currency"," = B4*D3")

    Change some attributes of the cell.

   sheet.setAttributes(cell,{ "color"  = > "turquoise7",
                                "text-align"  = > "center",
                                "font-style"  = > "bold"})

    Create a new style and apply it to 2 new text-values.

   sheet.writeStyleAbbr({"name"  = > "bold",
                          "text-align"  = > "end",
                          "font-weight"  = > "bold",
                          "background-color"  = > "purple"})
   cell = sheet.writeGetCell(3,1,"string","Diff/Sum")
   sheet.setStyle(cell,"bold")
   cell = sheet.writeGetCell(4,1,"string","Percent")
   sheet.setStyle(cell,"bold")

    Insert a text with an annotation.

   cell = sheet.writeGetCell(6,1,"string","annotation")
   sheet.writeComment(cell,"C3,C4,D3,D4 are formulas")

    Draw a long green vertical line to frame what we created.

   1.upto(7){ |row|
     cell = sheet.getCell(row,5)
     sheet.setAttributes(cell,{ "border-right"  = > "0.07cm solid green6" }) 
   }

    Complete it with a red horicontal line right across.

   1.upto(5){ |col|
     cell = sheet.getCell(7,col)
     sheet.setAttributes(cell,{ "border-bottom"  = > "0.085cm solid red5" }) # 
   }

    Read and calculate 2 currency-values.

   amount = 0.0
   1.upto(2){ |i|
     row = sheet.getRow(i)
     text,type = sheet.readCellFromRow(row,4)
     if(type  = = "currency")
       amount += text.to_f
     end
   }

    Delete the table we do not need.

   sheet.deleteTable("not needed")

    Save the sheet under a different name (if you don not want to
    override the original).

   sheet.saveAs("Example.ods")

    Print the result of the former computation.

   puts("Sums up to: #{amount}")  # -> "Sums up to: 12.21"

    Open the file we wrote in the previous step again.

   sheet = Rods.new("Example.ods") 

    Set the current table for subsequent operations.
    This is not necessary here as the table of interest is the 
    first in the spreadsheet and automatically becomes the default-table.

   sheet.setCurrentTable("example") 

    Read a currency-value from the sheet and print it.
    Remember that all values are passed to and from the spreadsheet
    as strings which get their meaning by the accompanying types.

   text,type = sheet.readCell(2,4)
   if(text && type)
     if(type  == "currency")
       puts("not so much: #{text} bucks") # -> "not so much: -7.78 bucks"
     end
   end

 = Example 0.9.0 a

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Template.ods")
   1.upto(3){ |row|
     1.upto(4){ |col|
       sheet.writeCell(row,col,"time","13:47")
     }
   }
   sheet.insertColumn(3)
   1.upto(3){ |row|
     sheet.writeCell(row,3,"string","o' clock")
   }
   sheet.insertRow(2)
   cell = sheet.insertCell(3,3)
   sheet.writeText(cell,"string","insertCell")
   row = sheet.getRow(4)
   row = sheet.insertRowAbove(row)
   sheet.writeCell(4,1,"string","Willi")
   cell = sheet.insertCellFromRow(row,1)
   sheet.writeText(cell,"string","Supi")
   sheet.saveAs("Test8.ods")

 = Example 0.9.0 b

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Template.ods")
   sheet.writeCell(1,1,"string","oneone")
   cell = sheet.writeGetCell(1,2,"string","onetwo")
   sheet.deleteCellBefore(cell)
   cell = sheet.writeGetCell(1,5,"string","onefive")
   sheet.deleteCellBefore(cell)

   cellOne = sheet.writeGetCell(5,1,"string","fiveone")
   cellFive = sheet.writeGetCell(5,5,"string","fivefive")
   cellSix = sheet.writeGetCell(5,6,"string","fivesix")
   cellNine = sheet.writeGetCell(5,9,"string","fivenine")
   sheet.deleteCellAfter(cellOne)
   sheet.deleteCellAfter(cellFive)
   cell = sheet.getCell(5,4)
   sheet.deleteCellBefore(cell)
   cell = sheet.getCell(5,4)
   sheet.deleteCellAfter(cell)
   sheet.deleteCell(5,2)
   sheet.deleteCell(5,3)
   row = sheet.getRow(5)
   sheet.deleteRowAbove(row)
   sheet.insertRow(2)
   sheet.insertRow(2)
   row = sheet.getRow(2)
   sheet.deleteRowBelow(row)
   row = sheet.getRow(1)
   sheet.deleteRowBelow(row)
   row = sheet.getRow(2)
   sheet.deleteRowBelow(row)
   sheet.deleteRow(2)
   sheet.deleteRow(1)
   sheet.deleteRow(1)
   sheet.saveAs("Test9.ods")

 = Example 0.9.0 c

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Konten.ods")
   startRow = sheet.getRow(11)
   i = 0
   while(row = sheet.getNextExistentRow(startRow))
     i += 1
     puts("#{i}")
     sheet.deleteRow2(row)
   end
   sheet.saveAs("Test10.ods")

 = Example 0.9.0 d

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Konten.ods")
   sheet.deleteColumn(8)
   sheet.saveAs("Test11.ods")

 = Example 0.9.0 e

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Konten.ods")
   startCell = sheet.getCell(12,1)
   i = 0
   while(cell = sheet.getNextExistentCell(startCell))
     i += 1
     puts("#{i}")
     sheet.deleteCell2(cell)
   end
   sheet.saveAs("Test12.ods")

 = Example 0.8.0

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'
 
   sheet = Rods.new("Template.ods")
   1.upto(3){ |row|
     1.upto(4){ |col|
       sheet.writeCell(row,col,"time","13:47")
     }
   }

    inserting new column -> shifting existing columns

   sheet.insertColumn(3) 
   1.upto(3){ |row|
     sheet.writeCell(row,3,"string","o' clock")
   }
   sheet.saveAs("Test7.ods")

 = Example 0.7.5

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'
 
   sheet = Rods.new("Template.ods")
   sheet.writeCell(1,1,"string","oneOne")
   sheet.writeCell(1,2,"string","oneTwo")
   sheet.writeCell(2,1,"string","twoOne") # finally becomes cell 3,1
   sheet.writeCell(2,2,"string","twoTwo") # finally becomes cell 3,2
   row = sheet.getRow(1)
   newRow = sheet.insertRowBelow(row)
   sheet.writeCell(2,1,"string","twoNewOne")
   cell = sheet.writeGetCell(2,2,"string","moved") # finally becomes cell "2,3"
   sheet.insertCellBefore(cell)
   sheet.writeCell(2,2,"string","twoNewTwoNew")  # new cell "2,2"
   sheet.saveAs("Test6.ods")

 = Example 0.7.0

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Konten.ods")
    Finds all cells with content 'content' and returns them along with the
    indices of row and column as an array of hashes.
    [{ :cell  = > cell,
       :row   = > rowIndex,
       :col   = > colIndex},
     { :cell  = > cell,
       :row   = > rowIndex,
       :col   = > colIndex}]
     
    Regular expressions for 'content' are allowed but must be enclosed in 
    single (not double) quotes !
   
    In case of no matches at all, an empty array is returned.
    
    Keep in mind that the content of a cell with a formula is not the formula, but the
    current value of the computed result.
   
    Also consider that you have to search for the external (i.e. visible)
    represenation of a cell's content, not it's internal computational value.
    For instance, when looking for a currency value of 1525 (that is shown as
    '1.525 $'), you'll have to code
   
      result = sheet.getCellsAndIndicesFor('1[.,]525')
   
    The following finds all occurences of a comma- or dot-separated number,
    consisting of 1 digit before and 2 digits behind the decimal-separator.

   result = sheet.getCellsAndIndicesFor('\d{1}[.,]\d{2}')
   result.each{ |cellHash|
     puts("----------------------------------------------")
     puts("Node: #{cellHash[:cell]}") # Be aware: Prints out whole node ! ;-)
     puts("Row: #{cellHash[:row]}")
     puts("Column: #{cellHash[:col]}")
   }
   puts("done")

 = Example for additions in 0.6.2

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Template.ods")
   cell = sheet.writeGetCell(3,3,"string","Underline")
   sheet.setAttributes(cell,{ "style:text-underline-color"  = > "blue",
                                "style:text-underline-style"  = > "solid",
                                "style:text-underline-width"  = > "auto"})
   cell = sheet.writeGetCell(4,4,"string","Underline_Default_with_Black")

    if not specified otherwise, width and color are set to default
    - black
    - solid

   sheet.setAttributes(cell,{ "style:text-underline-style"  = > "solid" })
   sheet.saveAs("Test3.ods")
   puts("done")
 
 = Example for additions in 0.6.1

   !/usr/bin/ruby
    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Template.ods")
   sheet.insertTableAfter("Table1","Neue Table")
   sheet.insertTableAfter("Neue Table","Neue Table2")
   sheet.insertTableAfter("Neue Table2","Neue Table3")
   sheet.insertTableAfter("Table3","Neue Table4")
   sheet.insertTableBefore("Table1","Vor1")
   sheet.insertTableBefore("Neue Table4","Vor4")
   sheet.saveAs("Test2.ods")
   puts("done")
 
 = Caveat

 The XML-structure of a <file>.ods is 
 * first rows 
 * then cells
 
 As a result
 
   1.upto(500){ |i|
     text1,type1 = readCell(i,3)  # XML-Parser starts from the top-node 
     text2,type2 = readCell(i,4)  # XML-Parser starts from the top-node
     puts("Read #{text1} of #{type1} and #{text2} of #{type2}")
   }

 is significantly slower than the slight variation

   1.upto(500){ |i|
     row = getRow(i)                      # XML-Parser starts from top-node
     text1,type1 = readCellFromRow(row,3) # XML-Parser continues from row-node
     text2,type2 = readCellFromRow(row,4) # XML-Parser continues from row-node
     puts("Read #{text1} of #{type1} and #{text2} of #{type2}
   }

 This difference hardly matters while dealing with small documents, but degrades performance significantly when you 
 move up or down a sheet with a lot of rows and process several cells on the same row !

 Provided you just want to read exisiting cells (cf. explanation above), the following is a real speed-buster.

    coding: UTF-8
   
    Author: Dr. Heinz Breinlinger
   
   require 'rubygems'
   require 'rods'

   sheet = Rods.new("Konten.ods")
   sum = 0.0
   i = 0
   row = sheet.getRow(1)

    The methods
    - getNextExistentRow(row)
    - getPreviousExistentRow(row)
    - getNextExistentCell(cell)
    - getPreviousExistentCell(cell)
    allow the XML-Parser to just continue from the "adjacent" node and 
    return the previsous/next element without having to start from the top-node 
    of the document over and over again !
    Caveat: According to the explanation above, you definitely know, that a cell/row
    exist, if they bear a value; if they don't, it depends on the sheets history,
    whether these are 'alive' and therefore accessible by the afore functions or not !

   while(row = sheet.getNextExistentRow(row))  
     i += 1
     puts("#{i}")
     confirmed,type = sheet.readCellFromRow(row,1)
     next if (! confirmed || confirmed.empty?())
     account,type = sheet.readCellFromRow(row,9)
     next if (! account || account.empty?())
     transfer,type = sheet.readCellFromRow(row,7)
     next if (! transfer || transfer.empty?())
     if(confirmed.match(/x/i) && account.match(/Hauskonto/))
       puts("#{i}: --> #{transfer} €")
       sum += transfer.to_f
     end
   end
   puts("------------")
   puts("Sum: #{sum}")

 On the ATOM-Nettop I developed the gem, even the first script above took just 2-3 seconds and on my Core-i7-Notebook it was finished so quickly
 that I supposed it had halted on an immediate error, so: don't be concerned and just experiment :-).
 
 The same considerations apply for the family of 'delete'-functions added in version 0.9.0 ! See the examples above.

About

Ruby OpenDocument Spreadsheet

License:Other


Languages

Language:Ruby 100.0%