spreadbase 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/COPYING.LESSER +165 -0
- data/README.md +140 -0
- data/lib/spreadbase/codecs/open_document_12.rb +148 -0
- data/lib/spreadbase/codecs/open_document_12_modules/decoding.rb +169 -0
- data/lib/spreadbase/codecs/open_document_12_modules/encoding.rb +200 -0
- data/lib/spreadbase/document.rb +99 -0
- data/lib/spreadbase/helpers/helpers.rb +138 -0
- data/lib/spreadbase/table.rb +273 -0
- data/lib/spreadbase/version.rb +5 -0
- data/lib/spreadbase.rb +37 -0
- data/spec/codecs/open_document_12_spec.rb +137 -0
- data/spec/elements/document_spec.rb +134 -0
- data/spec/elements/table_spec.rb +267 -0
- data/spec/spec_helpers.rb +47 -0
- data/spreadbase.gemspec +24 -0
- data/utils/convert_sqlite_to_ods.rb +154 -0
- data/utils/prettify_file.rb +46 -0
- data/utils/test_ods_folder.rb +40 -0
- data/utils/test_recoding_file.rb +38 -0
- data/utils/test_recoding_from_content.rb +43 -0
- data/utils/utils_helpers.rb +69 -0
- metadata +89 -0
@@ -0,0 +1,200 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
=begin
|
4
|
+
Copyright 2012 Saverio Miroddi saverio.pub2 <a-hat!> gmail.com
|
5
|
+
|
6
|
+
This file is part of SpreadBase.
|
7
|
+
|
8
|
+
SpreadBase is free software: you can redistribute it and/or modify it under the
|
9
|
+
terms of the GNU Lesser General Public License as published by the Free Software
|
10
|
+
Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
version.
|
12
|
+
|
13
|
+
SpreadBase is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
15
|
+
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
16
|
+
|
17
|
+
You should have received a copy of the GNU Lesser General Public License along
|
18
|
+
with SpreadBase. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
=end
|
20
|
+
|
21
|
+
require 'rexml/document'
|
22
|
+
require 'date'
|
23
|
+
require 'bigdecimal'
|
24
|
+
require 'iconv' if RUBY_VERSION < '1.9'
|
25
|
+
|
26
|
+
module SpreadBase # :nodoc:
|
27
|
+
|
28
|
+
module Codecs # :nodoc:
|
29
|
+
|
30
|
+
module OpenDocument12Modules # :nodoc:
|
31
|
+
|
32
|
+
# Module containing the encoding routines of the OpenDocument12 format.
|
33
|
+
#
|
34
|
+
module Encoding
|
35
|
+
|
36
|
+
# Actually a document can be opened even without the office:body element, but we simplify the code
|
37
|
+
# by assuming that at least this tree is present.
|
38
|
+
#
|
39
|
+
BASE_CONTENT_XML = %Q[\
|
40
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
41
|
+
<office:document-content
|
42
|
+
xmlns:office='urn:oasis:names:tc:opendocument:xmlns:office:1.0'
|
43
|
+
xmlns:style='urn:oasis:names:tc:opendocument:xmlns:style:1.0'
|
44
|
+
xmlns:table='urn:oasis:names:tc:opendocument:xmlns:table:1.0'
|
45
|
+
xmlns:text='urn:oasis:names:tc:opendocument:xmlns:text:1.0'
|
46
|
+
xmlns:fo='urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'
|
47
|
+
xmlns:number='urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'
|
48
|
+
xmlns:of='urn:oasis:names:tc:opendocument:xmlns:of:1.2'
|
49
|
+
office:version='1.2'>
|
50
|
+
<office:automatic-styles>
|
51
|
+
<number:date-style style:name='N37'>
|
52
|
+
<number:month number:style='long'/>
|
53
|
+
<number:text>/</number:text>
|
54
|
+
<number:day number:style='long'/>
|
55
|
+
<number:text>/</number:text>
|
56
|
+
<number:year/>
|
57
|
+
</number:date-style>
|
58
|
+
<number:date-style style:name='N5050'>
|
59
|
+
<number:month/>
|
60
|
+
<number:text>/</number:text>
|
61
|
+
<number:day/>
|
62
|
+
<number:text>/</number:text>
|
63
|
+
<number:year/>
|
64
|
+
<number:text> </number:text>
|
65
|
+
<number:hours number:style='long'/>
|
66
|
+
<number:text>:</number:text>
|
67
|
+
<number:minutes number:style='long'/>
|
68
|
+
<number:text> </number:text>
|
69
|
+
<number:am-pm/>
|
70
|
+
</number:date-style>
|
71
|
+
<style:style style:name='date' style:family='table-cell' style:data-style-name='N37'/>
|
72
|
+
<style:style style:name='datetime' style:family='table-cell' style:data-style-name='N5050'/>
|
73
|
+
<style:style style:name='boolean' style:family='table-cell' style:data-style-name='N99'/>
|
74
|
+
</office:automatic-styles>
|
75
|
+
<office:body>
|
76
|
+
<office:spreadsheet/>
|
77
|
+
</office:body>
|
78
|
+
</office:document-content>] # :nodoc:
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Returns the XML root node
|
83
|
+
#
|
84
|
+
def encode_to_document_node( el_document, options={} )
|
85
|
+
root_node = REXML::Document.new( BASE_CONTENT_XML )
|
86
|
+
spreadsheet_node = root_node.elements[ '//office:document-content/office:body/office:spreadsheet' ]
|
87
|
+
styles_node = root_node.elements[ '//office:document-content/office:automatic-styles' ]
|
88
|
+
|
89
|
+
el_document.column_width_styles.each do | style_name, column_width |
|
90
|
+
encode_style( styles_node, style_name, column_width )
|
91
|
+
end
|
92
|
+
|
93
|
+
el_document.tables.each do | table |
|
94
|
+
encode_table( table, spreadsheet_node, options )
|
95
|
+
end
|
96
|
+
|
97
|
+
root_node
|
98
|
+
end
|
99
|
+
|
100
|
+
# Currently only encodes column width styles
|
101
|
+
#
|
102
|
+
def encode_style( styles_node, style_name, column_width )
|
103
|
+
style_node = styles_node.add_element( 'style:style', 'style:name' => style_name, 'style:family' => 'table-column' )
|
104
|
+
|
105
|
+
style_node.add_element( 'style:table-column-properties', 'style:column-width' => column_width )
|
106
|
+
end
|
107
|
+
|
108
|
+
def encode_table( table, spreadsheet_node, options={} )
|
109
|
+
table_node = spreadsheet_node.add_element( 'table:table' )
|
110
|
+
|
111
|
+
table_node.attributes[ 'table:name' ] = table.name
|
112
|
+
|
113
|
+
table.column_width_styles.each do | style_name |
|
114
|
+
encode_column( table_node, style_name ) if style_name
|
115
|
+
end
|
116
|
+
|
117
|
+
# At least one column element is required
|
118
|
+
#
|
119
|
+
table_node.add_element( 'table:table-column' ) if table.column_width_styles.size == 0
|
120
|
+
|
121
|
+
table.data.each do | row |
|
122
|
+
encode_row( row, table_node, options )
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Currently only encodes column width styles
|
127
|
+
#
|
128
|
+
def encode_column( table_node, style_name )
|
129
|
+
table_node.add_element( 'table:table-column', 'table:style-name' => style_name )
|
130
|
+
end
|
131
|
+
|
132
|
+
def encode_row( row, table_node, options={} )
|
133
|
+
row_node = table_node.add_element( 'table:table-row' )
|
134
|
+
|
135
|
+
row.each do | value |
|
136
|
+
encode_cell( value, row_node, options )
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def encode_cell( value, row_node, options={} )
|
141
|
+
force_18_strings_encoding = options[ :force_18_strings_encoding ] || 'UTF-8'
|
142
|
+
|
143
|
+
cell_node = row_node.add_element( 'table:table-cell' )
|
144
|
+
|
145
|
+
# WATCH OUT!!! DateTime.new.is_a?( Date )!!!
|
146
|
+
#
|
147
|
+
case value
|
148
|
+
when String
|
149
|
+
cell_node.attributes[ 'office:value-type' ] = 'string'
|
150
|
+
|
151
|
+
cell_value_node = cell_node.add_element( 'text:p' )
|
152
|
+
|
153
|
+
if RUBY_VERSION >= '1.9'
|
154
|
+
value = value.encode( 'UTF-8' )
|
155
|
+
else
|
156
|
+
value = Iconv.conv( 'UTF-8', force_18_strings_encoding, value )
|
157
|
+
end
|
158
|
+
|
159
|
+
cell_value_node.text = value
|
160
|
+
when Time, DateTime
|
161
|
+
cell_node.attributes[ 'office:value-type' ] = 'date'
|
162
|
+
cell_node.attributes[ 'table:style-name' ] = 'datetime'
|
163
|
+
|
164
|
+
encoded_value = value.strftime( '%Y-%m-%dT%H:%M:%S' )
|
165
|
+
|
166
|
+
cell_node.attributes[ 'office:date-value' ] = encoded_value
|
167
|
+
when Date
|
168
|
+
cell_node.attributes[ 'office:value-type' ] = 'date'
|
169
|
+
cell_node.attributes[ 'table:style-name' ] = 'date'
|
170
|
+
|
171
|
+
encoded_value = value.strftime( '%Y-%m-%d' )
|
172
|
+
|
173
|
+
cell_node.attributes[ 'office:date-value' ] = encoded_value
|
174
|
+
when BigDecimal
|
175
|
+
cell_node.attributes[ 'office:value-type' ] = 'float'
|
176
|
+
|
177
|
+
cell_node.attributes[ 'office:value' ] = value.to_s( 'F' )
|
178
|
+
when Float, Fixnum
|
179
|
+
cell_node.attributes[ 'office:value-type' ] = 'float'
|
180
|
+
|
181
|
+
cell_node.attributes[ 'office:value' ] = value.to_s
|
182
|
+
when true, false
|
183
|
+
cell_node.attributes[ 'office:value-type' ] = 'boolean'
|
184
|
+
cell_node.attributes[ 'table:style-name' ] = 'boolean'
|
185
|
+
|
186
|
+
cell_node.attributes[ 'office:boolean-value' ] = value.to_s
|
187
|
+
when nil
|
188
|
+
# do nothing
|
189
|
+
else
|
190
|
+
raise "Unrecognized value class: #{ value.class }"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
=begin
|
4
|
+
Copyright 2012 Saverio Miroddi saverio.pub2 <a-hat!> gmail.com
|
5
|
+
|
6
|
+
This file is part of SpreadBase.
|
7
|
+
|
8
|
+
SpreadBase is free software: you can redistribute it and/or modify it under the
|
9
|
+
terms of the GNU Lesser General Public License as published by the Free Software
|
10
|
+
Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
version.
|
12
|
+
|
13
|
+
SpreadBase is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
15
|
+
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
16
|
+
|
17
|
+
You should have received a copy of the GNU Lesser General Public License along
|
18
|
+
with SpreadBase. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
=end
|
20
|
+
|
21
|
+
module SpreadBase # :nodoc:
|
22
|
+
|
23
|
+
# Represents the abstraction of a document, merging both the file and the
|
24
|
+
# document metadata concepts.
|
25
|
+
#
|
26
|
+
class Document
|
27
|
+
|
28
|
+
attr_accessor :document_path
|
29
|
+
|
30
|
+
attr_accessor :tables
|
31
|
+
|
32
|
+
# Currently contains only the style includining column widths
|
33
|
+
# Format:
|
34
|
+
#
|
35
|
+
# { '<name>' => '<width>' }
|
36
|
+
#
|
37
|
+
attr_accessor :column_width_styles # :nodoc:
|
38
|
+
|
39
|
+
# _params_:
|
40
|
+
#
|
41
|
+
# +document_path+:: (nil) Document path; if not passed, an empty document is created.
|
42
|
+
#
|
43
|
+
# _options_:
|
44
|
+
#
|
45
|
+
# +force_18_strings_encoding+:: ('UTF-8') on ruby 1.8, when converting to UTF-8, assume the strings are using the specified format.
|
46
|
+
# +floats_as_bigdecimal+:: (false) decode floats as BigDecimal instead of Float
|
47
|
+
#
|
48
|
+
def initialize( document_path=nil, options={} )
|
49
|
+
@document_path = document_path
|
50
|
+
@options = options.clone
|
51
|
+
|
52
|
+
if @document_path && File.exists?( document_path )
|
53
|
+
document_archive = IO.read( document_path )
|
54
|
+
decoded_document = Codecs::OpenDocument12.new.decode_archive( document_archive, options )
|
55
|
+
|
56
|
+
@column_width_styles = decoded_document.column_width_styles
|
57
|
+
@tables = decoded_document.tables
|
58
|
+
else
|
59
|
+
@column_width_styles = []
|
60
|
+
@tables = []
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Saves the document to the disk; before saving, it's required:
|
65
|
+
# - to have at least one table
|
66
|
+
# - to have set the documenth path, either during the initialization, or using the #document_path accessor.
|
67
|
+
#
|
68
|
+
# _options_:
|
69
|
+
#
|
70
|
+
# +prettify+:: Prettifies the content.xml file before saving.
|
71
|
+
#
|
72
|
+
def save( options={} )
|
73
|
+
options = @options.merge( options )
|
74
|
+
|
75
|
+
raise "At least one table must be present" if @tables.empty?
|
76
|
+
raise "Document path not specified" if @document_path.nil?
|
77
|
+
|
78
|
+
document_archive = Codecs::OpenDocument12.new.encode_to_archive( self, options )
|
79
|
+
|
80
|
+
File.open( @document_path, 'wb' ) { | file | file << document_archive }
|
81
|
+
end
|
82
|
+
|
83
|
+
# _options_:
|
84
|
+
#
|
85
|
+
# +with_headers+:: Print the tables with headers.
|
86
|
+
#
|
87
|
+
def to_s( options={} )
|
88
|
+
options.merge!( :row_prefix => ' ' )
|
89
|
+
|
90
|
+
tables.inject( '' ) do | output, table |
|
91
|
+
output << "#{ table.name }:" << "\n" << "\n"
|
92
|
+
|
93
|
+
output << table.to_s( options ) << "\n"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
=begin
|
4
|
+
Copyright 2012 Saverio Miroddi saverio.pub2 <a-hat!> gmail.com
|
5
|
+
|
6
|
+
This file is part of SpreadBase.
|
7
|
+
|
8
|
+
SpreadBase is free software: you can redistribute it and/or modify it under the
|
9
|
+
terms of the GNU Lesser General Public License as published by the Free Software
|
10
|
+
Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
version.
|
12
|
+
|
13
|
+
SpreadBase is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
15
|
+
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
16
|
+
|
17
|
+
You should have received a copy of the GNU Lesser General Public License along
|
18
|
+
with SpreadBase. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
=end
|
20
|
+
|
21
|
+
module SpreadBase # :nodoc:
|
22
|
+
|
23
|
+
# Currently generic helper class
|
24
|
+
#
|
25
|
+
module Helpers
|
26
|
+
|
27
|
+
# Safe alternative to "[ instance ] * repeats", which returns an array filled with the same instance, which is a recipe for a disaster
|
28
|
+
#
|
29
|
+
# The instance is duplicated Object#clone, when necessary - note that this method is not meant to do a deep copy.
|
30
|
+
#
|
31
|
+
def make_array_from_repetitions( instance, repetitions )
|
32
|
+
( 1 .. repetitions ).inject( [] ) do | cumulative_result, i |
|
33
|
+
case instance
|
34
|
+
when Fixnum, Float, BigDecimal, Date, Time, TrueClass, FalseClass, NilClass #, DateTime is a Date
|
35
|
+
cumulative_result << instance
|
36
|
+
when String, Array
|
37
|
+
cumulative_result << instance.clone
|
38
|
+
else
|
39
|
+
raise "Unsupported class: #{ }"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Prints the 2d-array in a nice, fixed-space table
|
45
|
+
#
|
46
|
+
# _params_:
|
47
|
+
#
|
48
|
+
# +rows+:: 2d-array of values.
|
49
|
+
# Empty arrays generate empty strings.
|
50
|
+
# Entries can be of different sizes; nils are used as filling values to normalize the rows to the same length.
|
51
|
+
#
|
52
|
+
# _options_:
|
53
|
+
#
|
54
|
+
# +row_prefix+:: Prefix this string to each row.
|
55
|
+
# +with_header+:: First row will be separated from the remaining ones.
|
56
|
+
#
|
57
|
+
# +formatting_block+:: If passed, values will be formatted by the block.
|
58
|
+
# If no block is passed, or it returns nil or :standard, the standard formatting is used.
|
59
|
+
#
|
60
|
+
def pretty_print_rows( rows, options={}, &formatting_block )
|
61
|
+
row_prefix = options[ :row_prefix ] || ''
|
62
|
+
with_headers = options[ :with_headers ]
|
63
|
+
|
64
|
+
output = ""
|
65
|
+
|
66
|
+
formatting_block = lambda { | value | value.to_s } if ! block_given?
|
67
|
+
|
68
|
+
if rows.size > 0
|
69
|
+
max_column_sizes = [ 0 ] * rows.map( &:size ).max
|
70
|
+
|
71
|
+
# Compute maximum widths
|
72
|
+
|
73
|
+
rows.each do | values |
|
74
|
+
values.each_with_index do | value, i |
|
75
|
+
formatted_value = pretty_print_value( value, &formatting_block )
|
76
|
+
formatted_value_width = formatted_value.chars.to_a.size
|
77
|
+
|
78
|
+
max_column_sizes[ i ] = formatted_value_width if formatted_value_width > max_column_sizes[ i ]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Print!
|
83
|
+
|
84
|
+
output << row_prefix << '+-' + max_column_sizes.map { | size | '-' * size }.join( '-+-' ) + '-+' << "\n"
|
85
|
+
|
86
|
+
print_pattern = '| ' + max_column_sizes.map { | size | "%-#{ size }s" }.join( ' | ' ) + ' |'
|
87
|
+
|
88
|
+
rows.each_with_index do | row, row_index |
|
89
|
+
# Ensure that we always have a number of values equal to the max width
|
90
|
+
#
|
91
|
+
formatted_row_values = ( 0 ... max_column_sizes.size ).map do | column_index |
|
92
|
+
value = row[ column_index ]
|
93
|
+
|
94
|
+
pretty_print_value( value, &formatting_block )
|
95
|
+
end
|
96
|
+
|
97
|
+
output << row_prefix << print_pattern % formatted_row_values << "\n"
|
98
|
+
|
99
|
+
if with_headers && row_index == 0
|
100
|
+
output << row_prefix << '+-' + max_column_sizes.map { | size | '-' * size }.join( '-+-' ) + '-+' << "\n"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
output << row_prefix << '+-' + max_column_sizes.map { | size | '-' * size }.join( '-+-' ) + '-+' << "\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
output
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def pretty_print_value( value, &formatting_block )
|
113
|
+
custom_result = block_given? && yield( value )
|
114
|
+
|
115
|
+
if custom_result && custom_result != :standard
|
116
|
+
custom_result
|
117
|
+
else
|
118
|
+
case value
|
119
|
+
when BigDecimal
|
120
|
+
value.to_s( 'F' )
|
121
|
+
when Time, DateTime
|
122
|
+
# Time#to_s renders differently between 1.8.7 and 1.9.3; 1.8.7's rendering is bizarrely
|
123
|
+
# inconsistent with the Date and DateTime ones.
|
124
|
+
#
|
125
|
+
value.strftime( '%Y-%m-%d %H:%M:%S %z' )
|
126
|
+
when String, Date, Numeric, TrueClass, FalseClass
|
127
|
+
value.to_s
|
128
|
+
when nil
|
129
|
+
"NIL"
|
130
|
+
else
|
131
|
+
value.inspect
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|