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,273 @@
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 table and its contents.
24
+ #
25
+ # The max width of the table is 1024 cells - the last one being 'AMJ'.
26
+ #
27
+ # Row indexing follows the ruby semantics:
28
+ # - negative indexes represent an access starting from the end of an array
29
+ # - out-of-bounds access will return nil where a row is accessed as a whole, and raise an error when a cell has to be accessed.
30
+ #
31
+ class Table
32
+
33
+ include SpreadBase::Helpers
34
+
35
+ attr_accessor :name, :data
36
+
37
+ # Array of style names; nil when not associated to any column width.
38
+ #
39
+ attr_accessor :column_width_styles # :nodoc:
40
+
41
+ # _params_:
42
+ #
43
+ # +name+:: (required) Name of the table
44
+ # +data+:: (Array.new) 2d matrix of the data. if not empty, the rows need to be all of the same size
45
+ #
46
+ def initialize( name, data=[] )
47
+ raise "Table name required" if name.nil? || name == ''
48
+
49
+ @name = name
50
+ @data = data
51
+ @column_width_styles = []
52
+ end
53
+
54
+ # Access a cell value.
55
+ #
56
+ # _params_:
57
+ #
58
+ # +column_indentifier+:: either an int (0-based) or the excel-format identifier (AA...); limited to the given row size.
59
+ # +row_index+:: int (0-based). see notes about the rows indexing.
60
+ #
61
+ # _returns_ the value, which is automatically converted to the Ruby data type.
62
+ #
63
+ def []( column_identifier, row_index )
64
+ row = row( row_index )
65
+ column_index = decode_column_identifier( column_identifier )
66
+
67
+ check_column_index( row, column_index )
68
+
69
+ row[ column_index ]
70
+ end
71
+
72
+ # Writes a value in a cell.
73
+ #
74
+ # _params_:
75
+ #
76
+ # +column_indentifier+:: either an int (0-based) or the excel-format identifier (AA...); limited to the given row size.
77
+ # +row_index+:: int (0-based). see notes about the rows indexing.
78
+ # +value+:: value
79
+ #
80
+ def []=( column_identifier, row_index, value )
81
+ row = row( row_index )
82
+ column_index = decode_column_identifier( column_identifier )
83
+
84
+ check_column_index( row, column_index )
85
+
86
+ row[ column_index ] = value
87
+ end
88
+
89
+ # Returns an array containing the values of a single row.
90
+ #
91
+ # _params_:
92
+ #
93
+ # +row_index+:: int (0-based). see notes about the rows indexing.
94
+ #
95
+ def row( row_index )
96
+ check_row_index( row_index )
97
+
98
+ @data[ row_index ]
99
+ end
100
+
101
+ # Deletes a row.
102
+ #
103
+ # This operation won't modify the column width styles in any case.
104
+ #
105
+ # _params_:
106
+ #
107
+ # +row_index+:: int (0-based). see notes about the rows indexing.
108
+ #
109
+ # _returns_ the deleted row
110
+ #
111
+ def delete_row( row_index )
112
+ check_row_index( row_index )
113
+
114
+ @data.slice!( row_index )
115
+ end
116
+
117
+ # Inserts a row.
118
+ #
119
+ # This operation won't modify the column width styles in any case.
120
+ #
121
+ # _params_:
122
+ #
123
+ # +row_index+:: int (0-based). must be between 0 and (including) the table rows size.
124
+ # +row+:: array of values. if the table is not empty, must have the same size of the table width.
125
+ #
126
+ def insert_row( row_index, row )
127
+ check_row_index( row_index, :allow_append => true )
128
+
129
+ @data.insert( row_index, row )
130
+ end
131
+
132
+ # This operation won't modify the column width styles in any case.
133
+ #
134
+ def append_row( row )
135
+ insert_row( @data.size, row )
136
+ end
137
+
138
+ # Returns an array containing the values of a single column.
139
+ #
140
+ # WATCH OUT! This method doesn't have the range restrictions that axis indexes generally has, that is, it's possible to access a column outside the boundaries of the rows - it will return nil for each of those values.
141
+ #
142
+ # _params_:
143
+ #
144
+ # +column_indentifier+:: either an int (0-based) or the excel-format identifier (AA...).
145
+ # when int, follow the same idea of the rows indexing (ruby semantics).
146
+ #
147
+ def column( column_identifier )
148
+ column_index = decode_column_identifier( column_identifier )
149
+
150
+ @data.map do | row |
151
+ row[ column_index ]
152
+ end
153
+ end
154
+
155
+ # Deletes a column.
156
+ #
157
+ # WATCH OUT! This method doesn't have the range restrictions that axis indexes generally has, that is, it's possible to delete a column outside the boundaries of the rows - it will return nil for each of those values.
158
+ #
159
+ # _params_:
160
+ #
161
+ # +column_indentifier+:: either an int (0-based) or the excel-format identifier (AA...).
162
+ # when int, follow the same idea of the rows indexing (ruby semantics).
163
+ #
164
+ # _returns_ the deleted column
165
+ #
166
+ def delete_column( column_identifier )
167
+ column_index = decode_column_identifier( column_identifier )
168
+
169
+ @column_width_styles.slice!( column_index )
170
+
171
+ @data.map do | row |
172
+ row.slice!( column_index )
173
+ end
174
+ end
175
+
176
+ # Inserts a column.
177
+ #
178
+ # WATCH OUT! This method doesn't have the range restrictions that axis indexes generally has, that is, it's possible to insert a column outside the boundaries of the rows - it will fill the cells in the middle with nils..
179
+ #
180
+ # _params_:
181
+ #
182
+ # +column_indentifier+:: either an int (0-based) or the excel-format identifier (AA...).
183
+ # when int, follow the same idea of the rows indexing (ruby semantics).
184
+ # +column+:: array of values. if the table is not empty, it must have the same size of the table height.
185
+ #
186
+ def insert_column( column_identifier, column )
187
+ raise "Inserting column size (#{ column.size }) different than existing columns size (#{ @data.size })" if @data.size > 0 && column.size != @data.size
188
+
189
+ column_index = decode_column_identifier( column_identifier )
190
+
191
+ @column_width_styles.insert( column_index, nil )
192
+
193
+ if @data.size > 0
194
+ @data.zip( column ).each do | row, value |
195
+ row.insert( column_index, value )
196
+ end
197
+ else
198
+ @data = column.map { | value | [ value ] }
199
+ end
200
+
201
+ end
202
+
203
+ def append_column( column )
204
+ column_index = @data.size > 0 ? @data.first.size : 0
205
+
206
+ insert_column( column_index, column )
207
+ end
208
+
209
+ # _returns_ a matrix representation of the tables, with the values being separated by commas.
210
+ #
211
+ def to_s( options={} )
212
+ pretty_print_rows( @data, options )
213
+ end
214
+
215
+ private
216
+
217
+ # Check that row index points to an existing record, or, in case of :allow_append,
218
+ # point to one unit above the last row.
219
+ #
220
+ # _options_:
221
+ #
222
+ # +allow_append+:: Allow pointing to one unit above the last row.
223
+ #
224
+ def check_row_index( row_index, options={} )
225
+ allow_append = options [ :allow_append ]
226
+
227
+ positive_limit = allow_append ? @data.size : @data.size - 1
228
+
229
+ raise "Invalid row index (#{ row_index }) - allowed 0 to #{ positive_limit }" if row_index < 0 || row_index > positive_limit
230
+ end
231
+
232
+ def check_column_index( row, column_index )
233
+ raise "Invalid column index (#{ column_index }) for the given row - allowed 0 to #{ row.size - 1 }" if column_index >= row.size
234
+ end
235
+
236
+ # Accepts either an integer, or a MoFoBase26BisexNumber.
237
+ #
238
+ # Raises an error for invalid identifiers/indexes.
239
+ #
240
+ # _returns_ a 0-based decimal number.
241
+ #--
242
+ # Motherf#### base-26 bijective numeration - I would have gladly saved my f* time. At least
243
+ # there were a few cute ladies at the Charleston lesson.
244
+ #
245
+ def decode_column_identifier( column_identifier )
246
+ if column_identifier.is_a?( Fixnum )
247
+ raise "Negative column indexes not allowed: #{ column_identifier }" if column_identifier < 0
248
+
249
+ column_identifier
250
+ else
251
+ letters = column_identifier.upcase.chars.to_a
252
+ upcase_a_ord = 65
253
+
254
+ raise "Invalid letter for in column identifier (allowed 'a/A' to 'z/Z')" if letters.any? { | letter | letter < 'A' || letter > 'Z' }
255
+
256
+ base_10_value = letters.inject( 0 ) do | sum, letter |
257
+ letter_ord = letter.unpack( 'C' ).first
258
+ sum * 26 + ( letter_ord - upcase_a_ord + 1 )
259
+ end
260
+
261
+ base_10_value -= 1
262
+
263
+ # -1 is an empty string
264
+ #
265
+ raise "Invalid literal column identifier (allowed 'A' to 'AMJ')" if base_10_value < 0 || 1023 < base_10_value
266
+
267
+ base_10_value
268
+ end
269
+ end
270
+
271
+ end
272
+
273
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module SpreadBase
4
+ VERSION = "0.1.2"
5
+ end
data/lib/spreadbase.rb ADDED
@@ -0,0 +1,37 @@
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 'rubygems'
22
+
23
+ require File.expand_path( '../spreadbase/helpers/helpers', __FILE__ )
24
+
25
+ require File.expand_path( '../spreadbase/document', __FILE__ )
26
+ require File.expand_path( '../spreadbase/table', __FILE__ )
27
+
28
+ require File.expand_path( '../spreadbase/codecs/open_document_12_modules/encoding', __FILE__ )
29
+ require File.expand_path( '../spreadbase/codecs/open_document_12_modules/decoding', __FILE__ )
30
+ require File.expand_path( '../spreadbase/codecs/open_document_12', __FILE__ )
31
+
32
+ # = Spreadbase
33
+ #
34
+ # See https://github.com/saveriomiroddi/spreadbase for usage.
35
+ #
36
+ module SpreadBase
37
+ end
@@ -0,0 +1,137 @@
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 File.expand_path( '../../../lib/spreadbase', __FILE__ )
22
+ require File.expand_path( '../../spec_helpers', __FILE__ )
23
+
24
+ require 'date'
25
+ require 'bigdecimal'
26
+
27
+ include SpecHelpers
28
+
29
+ # See testing notes.
30
+ #
31
+ describe SpreadBase::Codecs::OpenDocument12 do
32
+
33
+ before :each do
34
+ table_1 = SpreadBase::Table.new(
35
+ 'abc', [
36
+ [ 1, 1.1, T_BIGDECIMAL ],
37
+ [ T_DATE, T_DATETIME, T_TIME ],
38
+ [ nil, 'a', nil ]
39
+ ]
40
+ )
41
+
42
+ table_2 = SpreadBase::Table.new( 'cde' )
43
+
44
+ @sample_document = SpreadBase::Document.new
45
+
46
+ @sample_document.tables << table_1 << table_2
47
+ end
48
+
49
+ # :encode/:decode
50
+ #
51
+ it "should encode and decode the sample document" do
52
+ document_archive = SpreadBase::Codecs::OpenDocument12.new.encode_to_archive( @sample_document )
53
+
54
+ document = SpreadBase::Codecs::OpenDocument12.new.decode_archive( document_archive, :floats_as_bigdecimal => true )
55
+
56
+ assert_size( document.tables, 2 ) do | table_1, table_2 |
57
+
58
+ table_1.name.should == 'abc'
59
+
60
+ assert_size( table_1.data, 3 ) do | row_1, row_2, row_3 |
61
+
62
+ assert_size( row_1, 3 ) do | value_1, value_2, value_3 |
63
+ value_1.should == 1
64
+ value_1.should be_a( Fixnum )
65
+ value_2.should == 1.1
66
+ value_2.should be_a( BigDecimal )
67
+ value_3.should == T_BIGDECIMAL
68
+ value_3.should be_a( BigDecimal )
69
+ end
70
+
71
+ assert_size( row_2, 3 ) do | value_1, value_2, value_3 |
72
+ value_1.should == T_DATE
73
+ value_2.should == T_DATETIME
74
+ value_3.should == T_DATETIME
75
+ end
76
+
77
+ assert_size( row_3, 3 ) do | value_1, value_2, value_3 |
78
+ value_1.should == nil
79
+ value_2.should == 'a'
80
+ value_3.should == nil
81
+ end
82
+
83
+ end
84
+
85
+ table_2.name.should == 'cde'
86
+
87
+ assert_size( table_2.data, 0 )
88
+ end
89
+ end
90
+
91
+ # Not worth testing in detail; just ensure that the pref
92
+ #
93
+ it "should encode the document with makeup (:prettify) - SMOKE" do
94
+ formatter = stub_initializer( REXML::Formatters::Pretty )
95
+
96
+ formatter.should_receive( :write )
97
+
98
+ SpreadBase::Codecs::OpenDocument12.new.encode_to_archive( @sample_document, :prettify => true )
99
+ end
100
+
101
+ # Those methods are actually "utility" (read: testing) methods.
102
+ #
103
+ it "should encode/decode the content.xml - SMOKE" do
104
+ content_xml = SpreadBase::Codecs::OpenDocument12.new.encode_to_content_xml( @sample_document )
105
+
106
+ document = SpreadBase::Codecs::OpenDocument12.new.decode_content_xml( content_xml )
107
+
108
+ assert_size( document.tables, 2 )
109
+ end
110
+
111
+ # If values are not converted to UTF-8, some encodings cause an error to be
112
+ # raised when assigning a value to a cell.
113
+ #
114
+ # 1.8 tests can't be done, since the official platform is 1.9
115
+ #
116
+ it "should convert to utf-8 before saving" do
117
+ string = "à".encode( 'UTF-16' )
118
+
119
+ @sample_document.tables[ 0 ][ 0, 0 ] = string
120
+
121
+ # Doesn't encode correctly if the value is not converted
122
+ #
123
+ SpreadBase::Codecs::OpenDocument12.new.encode_to_content_xml( @sample_document )
124
+ end
125
+
126
+ it "should decode as BigDecimal" do
127
+ content_xml = SpreadBase::Codecs::OpenDocument12.new.encode_to_content_xml( @sample_document )
128
+
129
+ document = SpreadBase::Codecs::OpenDocument12.new.decode_content_xml( content_xml, :floats_as_bigdecimal => true )
130
+
131
+ value = document.tables[ 0 ][ 2, 0 ]
132
+
133
+ value.should be_a( BigDecimal )
134
+ value.should == T_BIGDECIMAL
135
+ end
136
+
137
+ end
@@ -0,0 +1,134 @@
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 File.expand_path( '../../../lib/spreadbase', __FILE__ )
22
+ require File.expand_path( '../../spec_helpers', __FILE__ )
23
+
24
+ include SpecHelpers
25
+
26
+ describe SpreadBase::Document do
27
+
28
+ before :each do
29
+ @sample_document = SpreadBase::Document.new
30
+ @sample_document.tables = [
31
+ SpreadBase::Table.new(
32
+ 'abc', [
33
+ [ 1, 1.1, T_BIGDECIMAL ],
34
+ [ T_DATE, T_DATETIME, T_TIME ],
35
+ [ true, 'a', nil ]
36
+ ]
37
+ )
38
+ ]
39
+ end
40
+
41
+ # :-D
42
+ #
43
+ it "should initialize out of thin air" do
44
+ document = SpreadBase::Document.new
45
+
46
+ document.document_path.should == nil
47
+
48
+ document.tables.should be_empty
49
+ end
50
+
51
+ # A lazy use of stubs
52
+ #
53
+ it "should initialize from a file" do
54
+ codec = stub_initializer( SpreadBase::Codecs::OpenDocument12 )
55
+
56
+ File.should_receive( :'exists?' ).with( '/pizza/margerita.txt' ).and_return( true )
57
+ IO.should_receive( :read ).with( '/pizza/margerita.txt' ).and_return( 'abc' )
58
+ codec.should_receive( :decode_archive ).with( 'abc', {} ).and_return( @sample_document )
59
+
60
+ document = SpreadBase::Document.new( '/pizza/margerita.txt' )
61
+
62
+ assert_size( document.tables, 1 ) do | table |
63
+ table.name.should == 'abc'
64
+ table.data.size.should == 3
65
+ end
66
+ end
67
+
68
+ it "should initialize with a non-existing file" do
69
+ codec = stub_initializer( SpreadBase::Codecs::OpenDocument12 )
70
+
71
+ document = SpreadBase::Document.new( '/pizza/margerita.txt' )
72
+
73
+ assert_size( document.tables, 0 )
74
+ end
75
+
76
+ it "should save to a file" do
77
+ codec = stub_initializer( SpreadBase::Codecs::OpenDocument12 )
78
+
79
+ document = SpreadBase::Document.new
80
+ document.tables << SpreadBase::Table.new( 'Ya-ha!' )
81
+ document.document_path = '/tmp/abc.ods'
82
+
83
+ codec.should_receive( :encode_to_archive ).with( document, :prettify => 'abc' ).and_return( 'sob!' )
84
+ File.should_receive( :open ).with( '/tmp/abc.ods', 'wb' )
85
+
86
+ document.save( :prettify => 'abc' )
87
+ end
88
+
89
+ it "should raise an error when trying to save without a filename" do
90
+ document = SpreadBase::Document.new
91
+ document.tables << SpreadBase::Table.new( 'Ya-ha!' )
92
+
93
+ lambda { document.save }.should raise_error( RuntimeError, "Document path not specified" )
94
+ end
95
+
96
+ it "should raise an error when trying to save without tables" do
97
+ document = SpreadBase::Document.new
98
+ document.document_path = 'abc.ods'
99
+
100
+ lambda { document.save }.should raise_error( RuntimeError, "At least one table must be present" )
101
+ end
102
+
103
+ it "should return the data as string (:to_s)" do
104
+ expected_string = "\
105
+ abc:
106
+
107
+ +------------+---------------------------+---------------------------+
108
+ | 1 | 1.1 | 0.133E1 |
109
+ | 2012-04-10 | 2012-04-11T23:33:42+00:00 | 2012-04-11 23:33:42 +0200 |
110
+ | true | a | |
111
+ +------------+---------------------------+---------------------------+
112
+
113
+ "
114
+
115
+ @sample_document.to_s.should == expected_string
116
+ end
117
+
118
+ it "should return the data as string, with headers (:to_s)" do
119
+ expected_string = "\
120
+ abc:
121
+
122
+ +------------+---------------------------+---------------------------+
123
+ | 1 | 1.1 | 0.133E1 |
124
+ +------------+---------------------------+---------------------------+
125
+ | 2012-04-10 | 2012-04-11T23:33:42+00:00 | 2012-04-11 23:33:42 +0200 |
126
+ | true | a | |
127
+ +------------+---------------------------+---------------------------+
128
+
129
+ "
130
+
131
+ @sample_document.to_s( :with_headers => true ).should == expected_string
132
+ end
133
+
134
+ end