Skip to content

Commit 3984c00

Browse files
gorngorn
gorn
authored and
gorn
committed
Implementing FODS format, see issue #39.
Needs to be tested. Merge branch 'issue-40-fods'
2 parents a9f012a + 80c5b0a commit 3984c00

11 files changed

+1249
-22
lines changed

GUIDE.md

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
You can open ODS file (OpenDocument Spreadsheet) like this
55
````ruby
66
workbook = Rspreadsheet.open('./test.ods')
7+
workbook = Rspreadsheet.open('./test.fods') # gem supports flast OpenDocument format
78
````
89
and access its first sheet like this
910
````ruby
@@ -42,6 +43,19 @@ workbook.save(any_io_object) # file can be saved to any IO like object a
4243
workbook.to_io # coverts it to IO object which can be used to
4344
anotherIO.write(workbook.to_io.read) # send file over internet without saving it first
4445
````
46+
47+
### Creating fresh new file
48+
You may name the spreadsheet on creation or at first save.
49+
50+
````ruby
51+
workbook = Rspreadsheet.new
52+
workbook.save('./filename.ods') # filename nust be provided at least on first save
53+
workbook2 = Rspreadsheet.new('./filename2.fods', format: :flat)
54+
workbook2.save
55+
```
56+
57+
If you want to use the fods flat format, you must create it as such.
58+
4559
### Date and Time
4660
OpenDocument and ruby have different models of date, time and datetime. Ruby containg three different objects. Time and DateTime cover all cases, Date covers dates only. OpenDocument distinguishes two groups - time of a day (time) and everything else (date). To simplify things a little we return cell values containg time of day as Time object and cell values containg datetime of date as DateTime. I am aware that this is very arbitrary choice, but it is very practical. This way and to some extend the types of values from OpenDocument are preserved when read from files, beeing acted upon and written back to spreadshhet.
4761

lib/helpers/class_extensions.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ def elements
119119
# end
120120
end
121121

122-
end
122+
end

lib/rspreadsheet.rb

+27-4
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,36 @@
77

88
module Rspreadsheet
99
extend Configuration
10+
1011
define_setting :raise_on_negative_coordinates, true
1112

1213
# makes creating new workbooks as easy as `Rspreadsheet.new` or `Rspreadsheet.open('filename.ods')
13-
def self.new(filename=nil)
14-
Workbook.new(filename)
14+
def self.new(*params)
15+
raise ArgumentError.new("wrong number of arguments (given #{params.size}, expected 0-2)") if params.size >2
16+
17+
case params.last
18+
when Hash then options = params.pop
19+
else options = {}
20+
end
21+
22+
if options[:format].nil? # automatické heuristické rozpoznání formátu
23+
options[:format] = :standard
24+
unless params.first.nil?
25+
begin
26+
Zip::File.open(params.first)
27+
rescue
28+
options[:format] = :flat
29+
end
30+
end
31+
end
32+
33+
case options[:format]
34+
when :flat , :fods then WorkbookFlat.new(*params)
35+
when :standard then Workbook.new(*params)
36+
else raise 'format of the file not recognized'
37+
end
1538
end
16-
def self.open(filename)
17-
Workbook.new(filename)
39+
def self.open(filename, options = {})
40+
self.new(filename, options)
1841
end
1942
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rpt="http://openoffice.org/2005/report" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:css3t="http://www.w3.org/TR/css3-text/" office:version="1.2" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
4+
<office:meta><meta:generator>Rspreadshhet gem by Gorn</meta:generator></office:meta>
5+
<office:automatic-styles />
6+
<office:body>
7+
<office:spreadsheet />
8+
</office:body>
9+
</office:document>

lib/rspreadsheet/tools.rb

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
module Rspreadsheet
1+
require 'pry'
2+
3+
module Rspreadsheet
24

35
# this module contains methods used bz several objects
46
module Tools
7+
using ClassExtensions if RUBY_VERSION > '2.1'
8+
59
def self.only_letters?(x); x.kind_of?(String) and x.match(/^[A-Za-z]*$/) != nil end
610
def self.kind_of_integer?(x)
711
(x.kind_of?(Numeric) and x.to_i==x) or
@@ -189,7 +193,7 @@ def self.new_time_value(h,m,s)
189193
Time.new(StartOfEpoch.year,StartOfEpoch.month,StartOfEpoch.day,h,m,s)
190194
end
191195

192-
def self.output_to_stream(io,&block)
196+
def self.output_to_zip_stream(io,&block)
193197
if io.kind_of? File or io.kind_of? String
194198
Zip::File.open(io, 'br+') do |zip|
195199
yield zip
@@ -201,6 +205,32 @@ def self.output_to_stream(io,&block)
201205
end
202206
end
203207

208+
def self.content_xml_diff(filename1,filename2)
209+
content_xml1 = Zip::File.open(filename1) do |zip|
210+
LibXML::XML::Document.io zip.get_input_stream('content.xml')
211+
end
212+
content_xml2 = Zip::File.open(filename2) do |zip|
213+
LibXML::XML::Document.io zip.get_input_stream('content.xml')
214+
end
215+
216+
return xml_diff(content_xml1.root,content_xml2.root)
217+
end
218+
219+
def self.xml_file_diff(filename1,filename2)
220+
content_xml1 = LibXML::XML::Document.file(filename1).root
221+
content_xml2 = LibXML::XML::Document.file(filename2).root
222+
return xml_diff(content_xml1, content_xml2)
223+
end
224+
225+
def self.xml_diff(xml_node1,xml_node2)
226+
message = []
227+
message << xml_node2.first_diff(xml_node1)
228+
message << xml_node1.first_diff(xml_node2)
229+
message << 'content XML not equal' unless xml_node1.to_s.should == xml_node2.to_s
230+
message = message.compact.join('; ')
231+
message = nil if message == ''
232+
message
233+
end
204234
end
205235

206236
end

lib/rspreadsheet/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module Rspreadsheet
2-
VERSION = "0.4.9"
2+
VERSION = "0.5.0"
33
end

lib/rspreadsheet/workbook.rb

+53-11
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
require 'libxml'
33

44
module Rspreadsheet
5+
56
class Workbook
67
attr_reader :filename
78
attr_reader :xmlnode # debug
89
def xmldoc; @xmlnode.doc end
910

10-
#@!group Worskheets methods
11+
#@!group Worksheet methods
1112
def create_worksheet_from_node(source_node)
1213
sheet = Worksheet.new(source_node,self)
1314
register_worksheet(sheet)
@@ -21,10 +22,11 @@ def create_worksheet(name = "Sheet#{worksheets_count+1}")
2122
alias :add_worksheet :create_worksheet
2223
# @return [Integer] number of sheets in the workbook
2324
def worksheets_count; @worksheets.length end
25+
alias :worksheet_count :worksheets_count
2426
# @return [String] names of sheets in the workbook
2527
def worksheet_names; @worksheets.collect{ |ws| ws.name } end
2628
# @param [Integer,String]
27-
# @return [Worskheet] worksheet with given index or name
29+
# @return [Worksheet] worksheet with given index or name
2830
def worksheets(index_or_name)
2931
case index_or_name
3032
when Integer then begin
@@ -61,8 +63,7 @@ def initialize(afilename=nil)
6163
@xmlnode.find('./table:table').each do |node|
6264
create_worksheet_from_node(node)
6365
end
64-
end
65-
66+
end
6667

6768
# @param [String] Optional new filename
6869
# Saves the worksheet. Optionally you can provide new filename or IO stream to which the file should be saved.
@@ -71,16 +72,16 @@ def save(io=nil)
7172
when @filename.nil? && io.nil?
7273
raise 'New file should be named on first save.'
7374
when @filename.kind_of?(String) && io.nil?
74-
Tools.output_to_stream(@filename) do |input_and_output_zip| # open old file
75-
update_manifest_and_content_xml(input_and_output_zip,input_and_output_zip) # input and output are identical
75+
Tools.output_to_zip_stream(@filename) do |input_and_output_zip| # open old file
76+
update_zip_manifest_and_content_xml(input_and_output_zip,input_and_output_zip) # input and output are identical
7677
end
77-
when @filename.kind_of?(String) && (io.kind_of?(String) || io.kind_of?(File))
78+
when (@filename.kind_of?(String) && (io.kind_of?(String) || io.kind_of?(File)))
7879
io = io.path if io.kind_of?(File) # convert file to its filename
7980
FileUtils.cp(@filename , io) # copy file externally
8081
@filename = io # remember new name
8182
save_to_io(nil) # continue modyfying file on spot
8283
when io.kind_of?(IO) || io.kind_of?(String) || io.kind_of?(StringIO)
83-
Tools.output_to_stream(io) do |output_io| # open output stream of file
84+
Tools.output_to_zip_stream(io) do |output_io| # open output stream of file
8485
write_ods_to_io(output_io)
8586
end
8687
io.rewind if io.kind_of?(StringIO)
@@ -97,19 +98,22 @@ def write_ods_to_io(io)
9798
if @filename.nil?
9899
Zip::File.open(TEMPLATE_FILE_NAME) do |empty_template_zip| # open empty_template file
99100
copy_internally_without_content(empty_template_zip,io) # copy empty_template internals
100-
update_manifest_and_content_xml(empty_template_zip,io) # update xmls + pictures
101+
update_zip_manifest_and_content_xml(empty_template_zip,io) # update xmls + pictures
101102
end
102103
else
103104
Zip::File.open(@filename) do | old_zip | # open old file
104105
copy_internally_without_content(old_zip,io) # copy the old internals
105-
update_manifest_and_content_xml(old_zip,io) # update xmls + pictures
106+
update_zip_manifest_and_content_xml(old_zip,io) # update xmls + pictures
106107
end
107108
end
108109
end
109110

111+
def flat_format?; false end
112+
def normal_format?; true end
113+
110114
private
111115

112-
def update_manifest_and_content_xml(input_zip,output_zip)
116+
def update_zip_manifest_and_content_xml(input_zip,output_zip)
113117
update_manifest_xml(input_zip,output_zip)
114118
update_content_xml(output_zip)
115119
end
@@ -182,6 +186,44 @@ def register_worksheet(worksheet)
182186
@worksheets[index-1]=worksheet
183187
@xmlnode << worksheet.xmlnode if worksheet.xmlnode.doc != @xmlnode.doc
184188
end
189+
190+
end
191+
192+
class WorkbookFlat < Workbook
193+
def initialize(afilename=nil)
194+
@worksheets=[]
195+
@filename = afilename
196+
@xml_doc = LibXML::XML::Document.file(@filename || FLAT_TEMPLATE_FILE_NAME)
197+
@xmlnode = @xml_doc.find_first('//office:spreadsheet')
198+
@xmlnode.find('./table:table').each do |node|
199+
create_worksheet_from_node(node)
200+
end
201+
end
202+
203+
def save(io=nil)
204+
case
205+
when @filename.nil? && io.nil?
206+
raise 'New file should be named on first save, please provide filename (or IO).'
207+
when @filename.kind_of?(String) && io.nil?
208+
@xml_doc.save(@filename)
209+
when (@filename.kind_of?(String) && (io.kind_of?(String) || io.kind_of?(File)))
210+
@filename = (io.kind_of?(File)) ? io.path : io
211+
@xml_doc.save(@filename)
212+
when io.kind_of?(IO) || io.kind_of?(String) || io.kind_of?(StringIO)
213+
IO.write(io,@xml_doc.to_s)
214+
io.rewind if io.kind_of?(StringIO)
215+
else raise 'Invalid combinations of parameter types in save'
216+
end
217+
end
218+
alias :save_to_io :save
219+
alias :save_as :save
220+
221+
def flat_format?; true end
222+
def normal_format?; false end
223+
224+
private
225+
FLAT_TEMPLATE_FILE_NAME = (File.dirname(__FILE__)+'/empty_file_template.fods').freeze
226+
185227
end
186228

187229
class WorkbookIO

spec/fods_spec.rb

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
require 'spec_helper'
2+
using ClassExtensions if RUBY_VERSION > '2.1'
3+
4+
describe 'Rspreadsheet flat ODS format' do
5+
before do
6+
delete_tmpfile(@tmp_filename_fods = '/tmp/testfile.fods') # delete temp file before tests
7+
delete_tmpfile(@tmp_filename_ods = '/tmp/testfile.ods')
8+
end
9+
after do
10+
delete_tmpfile(@tmp_filename_fods)
11+
delete_tmpfile(@tmp_filename_ods)
12+
end
13+
14+
it 'can open flat ods testfile and reads its content correctly' do
15+
book = Rspreadsheet.open($test_filename_fods, format: :fods )
16+
s = book.worksheets(1)
17+
(1..10).each do |i|
18+
s[i,1].should === i
19+
end
20+
s[1,2].should === 'text'
21+
s[2,2].should === Date.new(2014,1,1)
22+
23+
cell = s.cell(6,3)
24+
cell.format.bold.should == true
25+
cell = s.cell(6,4)
26+
cell.format.bold.should == false
27+
cell.format.italic.should == true
28+
cell = s.cell(6,5)
29+
cell.format.italic.should == false
30+
cell.format.color.should == '#ff3333'
31+
cell = s.cell(6,6)
32+
cell.format.color.should_not == '#ff3333'
33+
cell.format.background_color.should == '#6666ff'
34+
cell = s.cell(6,7)
35+
cell.format.font_size.should == '7pt'
36+
end
37+
38+
it 'does not change when opened and saved again' do
39+
book = Rspreadsheet.new($test_filename_fods, format: :flat) # open test file
40+
book.save(@tmp_filename_fods) # and save it as temp file
41+
Rspreadsheet::Tools.xml_file_diff($test_filename_fods, @tmp_filename_fods).should be_nil
42+
end
43+
44+
it 'can be converted to normal format with convert_format_to_normal', :pending do
45+
book = Rspreadsheet.open($test_filename_fods, format: :flat)
46+
book.convert_format_to_normal
47+
book.save_as(@tmp_filename_ods)
48+
Rspreadsheet::Tools.content_xml_diff($test_filename_fods, @tmp_filename_ods).should be_nil
49+
end
50+
51+
it 'pick format automaticaaly' do
52+
book = Rspreadsheet.open($test_filename_fods)
53+
book.flat_format?.should == true
54+
book.save_as(@tmp_filename_fods)
55+
expect {book = Rspreadsheet.open(@tmp_filename_fods)}.not_to raise_error
56+
book.normal_format?.should == false
57+
58+
book = Rspreadsheet.open($test_filename_ods)
59+
book.normal_format?.should == true
60+
book.save_as(@tmp_filename_ods)
61+
expect {book = Rspreadsheet.open(@tmp_filename_ods)}.not_to raise_error
62+
book.flat_format?.should == false
63+
end
64+
65+
private
66+
def delete_tmpfile(afile)
67+
File.delete(afile) if File.exist?(afile)
68+
end
69+
70+
end

spec/rspreadsheet_spec.rb

+2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@
7373
end
7474
it 'can create new worksheet' do
7575
book = Rspreadsheet.new
76+
book.worksheet_count.should == 0
7677
book.create_worksheet
78+
book.worksheet_count.should == 1
7779
end
7880
it 'examples from README file are working' do
7981
Rspreadsheet.open($test_filename).save(@tmp_filename)

spec/spec_helper.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@
3030

3131
# some variables used everywhere
3232
$test_filename = './spec/testfile1.ods'
33+
$test_filename_fods = './spec/testfile1.fods'
3334
$test_filename_images = './spec/testfile2-images.ods'
3435

3536
# require my gem
3637
require 'rspreadsheet'
37-
38-
39-

0 commit comments

Comments
 (0)