spreadbase 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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