spreadbase 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|