spreadbase 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+ temp/
data/COPYING.LESSER ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ SpreadBase!!
2
+ ============
3
+
4
+ ... because Excel IS a database.
5
+
6
+ What is SpreadBase©?
7
+ --------------------
8
+
9
+ SpreadBase© is a set of APIs for programmatically accessing spreadsheets (currently, only OpenDocument 1.2).
10
+
11
+ Usage
12
+ -----
13
+
14
+ Install/require the gem:
15
+
16
+ gem install spreadbase
17
+
18
+ require 'spreadbase'
19
+
20
+ Create/open a document:
21
+
22
+ document = SpreadBase::Document.new( "Today's menu.ods" )
23
+
24
+ Add a table:
25
+
26
+ document.tables << SpreadBase::Table.new(
27
+ 'Transistors', [
28
+ [ 'Roasted 6502', 38.911 ],
29
+ [ '65000 with side dishes of Copper and Blitter', 512.0 ],
30
+ ]
31
+ )
32
+
33
+ Modify an existing table; can be done also directly on the array:
34
+
35
+ table = document.tables.first
36
+
37
+ table.insert_row( 0, [ 'Dish', 'Price' ] )
38
+ table.insert_row( 2, [ '8080, with an 8-bit bus', 8 ] )
39
+
40
+ table.insert_column( 2, [ 'Availability', Date.today, Time.now + 42, 'Never!!' ] )
41
+
42
+ Add another (empty) table:
43
+
44
+ table_2 = SpreadBase::Table.new( 'Loud and annoying customers' )
45
+
46
+ document.tables << table_2
47
+
48
+ Append a column:
49
+
50
+ table_2.append_column( [ 'Name' ] )
51
+
52
+ Append a row:
53
+
54
+ table_2.append_row( [ 'Fabrizio F.' ] )
55
+
56
+ Read a cell:
57
+
58
+ price_8080 = document.tables[ 0 ][ 1, 2 ]
59
+
60
+ When a cell value is read from an existing file, the data type is directly converted to the closest ruby one.
61
+
62
+ Write to a cell:
63
+
64
+ document.tables[ 0 ][ 1, 2 ] = price_8080 + 0.080
65
+
66
+ Print a table:
67
+
68
+ puts document.tables[ 0 ].to_s( :with_headers => true )
69
+
70
+ +----------------------------------------------+--------+---------------------------+
71
+ | Dish | Price | Availability |
72
+ +----------------------------------------------+--------+---------------------------+
73
+ | Roasted 6502 | 38.911 | 2012-04-21 |
74
+ | 8080, with an 8-bit bus | 8.08 | 2012-04-21 11:45:08 +0200 |
75
+ | 65000 with side dishes of Copper and Blitter | 512.0 | Never!! |
76
+ +----------------------------------------------+--------+---------------------------+
77
+
78
+ Print a document:
79
+
80
+ puts document.to_s( :with_headers => true )
81
+
82
+ Transistors:
83
+
84
+ +----------------------------------------------+--------+---------------------------+
85
+ | Dish | Price | Availability |
86
+ +----------------------------------------------+--------+---------------------------+
87
+ | Roasted 6502 | 38.911 | 2012-04-21 |
88
+ | 8080, with an 8-bit bus | 8.08 | 2012-04-21 11:45:08 +0200 |
89
+ | 65000 with side dishes of Copper and Blitter | 512.0 | Never!! |
90
+ +----------------------------------------------+--------+---------------------------+
91
+
92
+ Loud and annoying customers:
93
+
94
+ +-------------+
95
+ | Name |
96
+ +-------------+
97
+ | Fabrizio F. |
98
+ +-------------+
99
+
100
+ Save the document:
101
+
102
+ document.save
103
+
104
+ Notes
105
+ -----
106
+
107
+ - Numbers are decoded to Fixnum or Float, depending on the existence of the fractional part.
108
+ Alternatively, numbers with a fractional part can be decoded as Bigdecimal, using the option:
109
+
110
+ `SpreadBase::Document.new( "Random numbers für alle!.ods", :floats_as_bigdecimal => true )`
111
+
112
+ - The archives are always encoded in UTF-8. In Ruby 1.8.7, input strings are assumed to be UTF-8; if not, it's possible to open a document as:
113
+
114
+ `SpreadBase::Document.new( "Today's menu.ods", :force_18_strings_encoding => '<encoding>' )`
115
+
116
+ in order to override the input encoding.
117
+ - The gem has been tested on Ruby 1.8.7 and 1.9.3-p125, on Linux and Mac OS X.
118
+ - The column widths are retained (decoding/encoding), but at the current version, they're not [officially] accessible via any API.
119
+
120
+ Currently unsupported features
121
+ ------------------------------
122
+
123
+ - Styles; Date and and [Date]Times are formatted as, respectively, '%Y-%m-%d' and '%Y-%m-%d %H:%M:%S %z'
124
+ - Percentage data type - they're handled using their float value (e.g. 50% = 0.5)
125
+
126
+ Supporting SpreadBase
127
+ ---------------------
128
+
129
+ If you find SpreadBase useful for any reason, I invite you to join Kiva.org, using this invitation:
130
+
131
+ http://www.kiva.org/invitedby/saveriomiroddi
132
+
133
+ it will cost you **nothing** (zero/0 €/£/$), it will take three to five minutes of your time, and you will have actively done something for economically disadvantaged countries.
134
+
135
+ If you want to do more, in addition to accepting the invitation, you can donate to my Paypal account (saverio.pub2 \<a-hat!\> gmail.com) - I will publish your donation and use the entire amount for making loans using the mentioned website.
136
+
137
+ Roadmap/Todo
138
+ ------------
139
+
140
+ https://github.com/saveriomiroddi/spreadbase/wiki/Todo-%28roadmap%29
@@ -0,0 +1,148 @@
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 'zipruby'
22
+ require 'rexml/document'
23
+
24
+ module SpreadBase # :nodoc:
25
+
26
+ module Codecs # :nodoc:
27
+
28
+ # Interface for OpenDocument 1. encoding/decoding.
29
+ #
30
+ class OpenDocument12
31
+
32
+ MANIFEST_XML = %Q[\
33
+ <?xml version="1.0" encoding="UTF-8"?>
34
+ <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
35
+ <manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.spreadsheet" manifest:version="1.2" manifest:full-path="/"/>
36
+ <manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml"/>
37
+ </manifest:manifest>] # :nodoc:
38
+
39
+ include OpenDocument12Modules::Encoding
40
+ include OpenDocument12Modules::Decoding
41
+
42
+ # Encode a Document to an OpenDocument archive.
43
+ #
44
+ # The generated archive contains the strictly necessary data required to have a consistent archive.
45
+ #
46
+ # _params_:
47
+ #
48
+ # +el_document+:: SpreadBase::Document instance
49
+ #
50
+ # _options_:
51
+ #
52
+ # +force_18_strings_encoding+:: ('UTF-8') on ruby 1.8, when converting to UTF-8, assume the strings are using the specified format.
53
+ # +prettify+:: (false )prettifies the content.xml to be human readable.
54
+ #
55
+ # _returns_ the archive as binary string.
56
+ #
57
+ def encode_to_archive( el_document, options={} )
58
+ document_buffer = encode_to_content_xml( el_document, options )
59
+ zip_buffer = ''
60
+
61
+ Zip::Archive.open_buffer( zip_buffer, Zip::CREATE ) do | zip_file |
62
+ zip_file.add_dir( 'META-INF' )
63
+
64
+ zip_file.add_buffer( 'META-INF/manifest.xml', MANIFEST_XML );
65
+ zip_file.add_buffer( 'content.xml', document_buffer );
66
+ end
67
+
68
+ zip_buffer
69
+ end
70
+
71
+ # Decode an OpenDocument archive.
72
+ #
73
+ # _params_:
74
+ #
75
+ # +zip_buffer+:: archive as binary string.
76
+ # if it's been read from the disk, don't forget to read in binary fmode.
77
+ #
78
+ # _options_:
79
+ #
80
+ # +floats_as_bigdecimal+:: (false) decode floats as BigDecimal instead of Float
81
+ #
82
+ # _returns_ the SpreadBase::Document instance.
83
+ #
84
+ def decode_archive( zip_buffer, options={} )
85
+ content_xml_data = Zip::Archive.open_buffer( zip_buffer ) do | zip_file |
86
+ zip_file.fopen( 'content.xml' ) { | file | file.read }
87
+ end
88
+
89
+ decode_content_xml( content_xml_data, options )
90
+ end
91
+
92
+ # Utility method; encodes the Document to the content.xml format.
93
+ #
94
+ # _params_:
95
+ #
96
+ # +el_document+:: SpreadBase::Document instance
97
+ #
98
+ # _options_:
99
+ #
100
+ # +force_18_strings_encoding+:: ('UTF-8') on ruby 1.8, when converting to UTF-8, assume the strings are using the specified format.
101
+ # +prettify+:: (false ) prettifies the content.xml to be human readable.
102
+ #
103
+ # _returns_ content.xml as string.
104
+ #--
105
+ # "utility" is a fancy name for testing/utils helper.
106
+ #
107
+ def encode_to_content_xml( el_document, options={} )
108
+ prettify = options[ :prettify ]
109
+
110
+ document_xml_root = encode_to_document_node( el_document, options )
111
+ document_buffer = prettify ? pretty_xml( document_xml_root ) : document_xml_root.to_s
112
+
113
+ document_buffer
114
+ end
115
+
116
+ # Utility method; decode the content.xml belonging to an OpenDocument archive.
117
+ #
118
+ # _options_:
119
+ #
120
+ # +floats_as_bigdecimal+:: (false) decode floats as BigDecimal instead of Float
121
+ #
122
+ # _returns_ the SpreadBase::Document instance.
123
+ #--
124
+ # "utility" is a fancy name for testing/utils helper.
125
+ #
126
+ def decode_content_xml( content_xml_data, options={} )
127
+ root_node = REXML::Document.new( content_xml_data )
128
+
129
+ decode_document_node( root_node, options )
130
+ end
131
+
132
+ private
133
+
134
+ def pretty_xml( document )
135
+ buffer = ""
136
+
137
+ xml_formatter = REXML::Formatters::Pretty.new
138
+ xml_formatter.compact = true
139
+ xml_formatter.write( document, buffer )
140
+
141
+ buffer
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ end
@@ -0,0 +1,169 @@
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 'date'
22
+ require 'bigdecimal'
23
+
24
+ module SpreadBase # :nodoc:
25
+
26
+ module Codecs # :nodoc:
27
+
28
+ module OpenDocument12Modules # :nodoc:
29
+
30
+ # Module containing the decoding routines of the OpenDocument12 format.
31
+ #
32
+ module Decoding
33
+
34
+ include SpreadBase::Helpers
35
+
36
+ private
37
+
38
+ # Returns a Document instance.
39
+ #
40
+ def decode_document_node( root_node, options={} )
41
+ document = Document.new
42
+
43
+ style_nodes = root_node.elements.to_a( '//office:document-content/office:automatic-styles/style:style' )
44
+ table_nodes = root_node.elements.to_a( '//office:document-content/office:body/office:spreadsheet/table:table' )
45
+
46
+ document.column_width_styles = decode_column_width_styles( style_nodes )
47
+
48
+ document.tables = table_nodes.map { | node | decode_table_node( node, options ) }
49
+
50
+ document
51
+ end
52
+
53
+ # Currently it has only the purpose of decoding the column widths (for this reason it has a different naming convention).
54
+ #
55
+ def decode_column_width_styles( style_nodes )
56
+ style_nodes.inject( {} ) do | column_width_styles, style_node |
57
+ column_node = style_node.elements[ 'style:table-column-properties' ]
58
+
59
+ if column_node
60
+ column_width = column_node.attributes[ 'style:column-width' ]
61
+
62
+ if column_width
63
+ style_name = style_node.attributes[ 'style:name' ]
64
+
65
+ column_width_styles[ style_name] = column_width
66
+ end
67
+ end
68
+
69
+ column_width_styles
70
+ end
71
+ end
72
+
73
+ def decode_table_node( table_node, options )
74
+ table = Table.new( table_node.attributes[ 'table:name' ] )
75
+
76
+ column_nodes = table_node.elements.to_a( 'table:table-column' )
77
+ row_nodes = table_node.elements.to_a( 'table:table-row' )
78
+
79
+ # A single column/row can represent multiple columns (table:number-(columns|rows)-repeated)
80
+ #
81
+ table.column_width_styles = column_nodes.inject( [] ) { | current_styles, node | current_styles + decode_column_width_style( node ) }
82
+ table.data = row_nodes.inject( [] ) { | current_rows, node | current_rows + decode_row_node( node, options ) }
83
+
84
+ table
85
+ end
86
+
87
+ def decode_column_width_style( column_node )
88
+ repetitions = ( column_node.attributes[ 'table:number-columns-repeated' ] || '1' ).to_i
89
+ style_name = column_node.attributes[ 'table:style-name' ]
90
+
91
+ # WATCH OUT! See module note
92
+ #
93
+ make_array_from_repetitions( style_name, repetitions )
94
+ end
95
+
96
+ def decode_row_node( row_node, options )
97
+ repetitions = ( row_node.attributes[ 'table:number-rows-repeated' ] || '1' ).to_i
98
+ cell_nodes = row_node.elements.to_a( 'table:table-cell' )
99
+
100
+ # Watch out the :flatten; a single cell can represent multiple cells (table:number-columns-repeated)
101
+ #
102
+ values = cell_nodes.map { | node | decode_cell_node( node, options ) }.flatten
103
+
104
+ make_array_from_repetitions( values, repetitions )
105
+ end
106
+
107
+ def decode_cell_node( cell_node, options )
108
+ floats_as_bigdecimal = options[ :floats_as_bigdecimal ]
109
+
110
+ value_type = cell_node.attributes[ 'office:value-type' ]
111
+
112
+ value = \
113
+ case value_type
114
+ when 'string'
115
+ value_node = cell_node.elements[ 'text:p' ]
116
+
117
+ value_node.text
118
+ when 'date'
119
+ date_string = cell_node.attributes[ 'office:date-value' ]
120
+
121
+ if date_string =~ /T/
122
+ DateTime.strptime( date_string, '%Y-%m-%dT%H:%M:%S' )
123
+ else
124
+ Date.strptime( date_string, '%Y-%m-%d' )
125
+ end
126
+ when 'float', 'percentage'
127
+ float_string = cell_node.attributes[ 'office:value' ]
128
+
129
+ if float_string.include?( '.' )
130
+ if floats_as_bigdecimal
131
+ BigDecimal.new( float_string )
132
+ else
133
+ float_string.to_f
134
+ end
135
+ else
136
+ float_string.to_i
137
+ end
138
+ when 'boolean'
139
+ boolean_string = cell_node.attributes[ 'office:boolean-value' ]
140
+
141
+ case boolean_string
142
+ when 'true'
143
+ true
144
+ when 'false'
145
+ false
146
+ else
147
+ raise "Invalid boolean value: #{ boolean_string }"
148
+ end
149
+ when nil
150
+ nil
151
+ else
152
+ raise "Unrecognized value type found in a cell: #{ value_type }"
153
+ end
154
+
155
+ repetitions = ( cell_node.attributes[ 'table:number-columns-repeated' ] || '1' ).to_i
156
+
157
+ make_array_from_repetitions( value, repetitions )
158
+ end
159
+
160
+ end
161
+
162
+ private
163
+
164
+
165
+ end
166
+
167
+ end
168
+
169
+ end