spreadbase 0.1.2

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