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.
@@ -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