google_drive 1.0.6 → 2.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,377 +1,366 @@
1
1
  # Author: Hiroshi Ichikawa <http://gimite.net/>
2
2
  # The license of this source is "New BSD Licence"
3
3
 
4
- require "cgi"
5
- require "set"
6
- require "uri"
7
-
8
- require "google_drive/util"
9
- require "google_drive/error"
10
- require "google_drive/list"
4
+ require 'cgi'
5
+ require 'set'
6
+ require 'uri'
11
7
 
8
+ require 'google_drive/util'
9
+ require 'google_drive/error'
10
+ require 'google_drive/list'
12
11
 
13
12
  module GoogleDrive
13
+ # A worksheet (i.e. a tab) in a spreadsheet.
14
+ # Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet object.
15
+ class Worksheet
16
+ include(Util)
17
+
18
+ def initialize(session, spreadsheet, worksheet_feed_entry) #:nodoc:
19
+ @session = session
20
+ @spreadsheet = spreadsheet
21
+ set_worksheet_feed_entry(worksheet_feed_entry)
22
+
23
+ @cells = nil
24
+ @input_values = nil
25
+ @numeric_values = nil
26
+ @modified = Set.new
27
+ @list = nil
28
+ end
14
29
 
15
- # A worksheet (i.e. a tab) in a spreadsheet.
16
- # Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet object.
17
- class Worksheet
18
-
19
- include(Util)
20
-
21
- def initialize(session, spreadsheet, worksheet_feed_entry) #:nodoc:
22
-
23
- @session = session
24
- @spreadsheet = spreadsheet
25
- set_worksheet_feed_entry(worksheet_feed_entry)
26
-
27
- @cells = nil
28
- @input_values = nil
29
- @numeric_values = nil
30
- @modified = Set.new()
31
- @list = nil
32
-
33
- end
34
-
35
- # Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
36
- attr_reader(:worksheet_feed_entry)
37
-
38
- # Title of the worksheet (shown as tab label in Web interface).
39
- attr_reader(:title)
40
-
41
- # Time object which represents the time the worksheet was last updated.
42
- attr_reader(:updated)
30
+ # Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
31
+ attr_reader(:worksheet_feed_entry)
43
32
 
44
- # URL of cell-based feed of the worksheet.
45
- def cells_feed_url
46
- return @worksheet_feed_entry.css(
47
- "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]["href"]
48
- end
33
+ # Title of the worksheet (shown as tab label in Web interface).
34
+ attr_reader(:title)
49
35
 
50
- # URL of worksheet feed URL of the worksheet.
51
- def worksheet_feed_url
52
- return @worksheet_feed_entry.css("link[rel='self']")[0]["href"]
53
- end
36
+ # Time object which represents the time the worksheet was last updated.
37
+ attr_reader(:updated)
54
38
 
55
- # URL to export the worksheet as CSV.
56
- def csv_export_url
57
- return @worksheet_feed_entry.css(
58
- "link[rel='http://schemas.google.com/spreadsheets/2006#exportcsv']")[0]["href"]
59
- end
39
+ # URL of cell-based feed of the worksheet.
40
+ def cells_feed_url
41
+ @worksheet_feed_entry.css(
42
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']")[0]['href']
43
+ end
60
44
 
61
- # Exports the worksheet as String in CSV format.
62
- def export_as_string()
63
- api_result = @session.execute!(:uri => self.csv_export_url)
64
- return api_result.body
65
- end
45
+ # URL of worksheet feed URL of the worksheet.
46
+ def worksheet_feed_url
47
+ @worksheet_feed_entry.css("link[rel='self']")[0]['href']
48
+ end
66
49
 
67
- # Exports the worksheet to +path+ in CSV format.
68
- def export_as_file(path)
69
- data = export_as_string()
70
- open(path, "wb"){ |f| f.write(data) }
71
- end
50
+ # URL to export the worksheet as CSV.
51
+ def csv_export_url
52
+ @worksheet_feed_entry.css(
53
+ "link[rel='http://schemas.google.com/spreadsheets/2006#exportcsv']")[0]['href']
54
+ end
72
55
 
73
- # gid of the worksheet.
74
- def gid
75
- # A bit tricky but couldn't find a better way.
76
- return CGI.parse(URI.parse(self.csv_export_url).query)["gid"].last
77
- end
56
+ # Exports the worksheet as String in CSV format.
57
+ def export_as_string
58
+ @session.request(:get, csv_export_url, response_type: :raw)
59
+ end
78
60
 
79
- # URL to view/edit the worksheet in a Web browser.
80
- def human_url
81
- return "%s\#gid=%s" % [self.spreadsheet.human_url, self.gid]
82
- end
61
+ # Exports the worksheet to +path+ in CSV format.
62
+ def export_as_file(path)
63
+ data = export_as_string
64
+ open(path, 'wb') { |f| f.write(data) }
65
+ end
83
66
 
84
- # GoogleDrive::Spreadsheet which this worksheet belongs to.
85
- def spreadsheet
86
- if !@spreadsheet
87
- if !(self.worksheet_feed_url =~ %r{https?://spreadsheets\.google\.com/feeds/worksheets/(.*)/(.*)$})
88
- raise(GoogleDrive::Error,
89
- "Worksheet feed URL is in unknown format: #{self.worksheet_feed_url}")
90
- end
91
- @spreadsheet = @session.file_by_id($1)
92
- end
93
- return @spreadsheet
94
- end
67
+ # gid of the worksheet.
68
+ def gid
69
+ # A bit tricky but couldn't find a better way.
70
+ CGI.parse(URI.parse(csv_export_url).query)['gid'].last
71
+ end
95
72
 
96
- # Returns content of the cell as String. Arguments must be either
97
- # (row number, column number) or cell name. Top-left cell is [1, 1].
98
- #
99
- # e.g.
100
- # worksheet[2, 1] #=> "hoge"
101
- # worksheet["A2"] #=> "hoge"
102
- def [](*args)
103
- (row, col) = parse_cell_args(args)
104
- return self.cells[[row, col]] || ""
105
- end
73
+ # URL to view/edit the worksheet in a Web browser.
74
+ def human_url
75
+ "%s\#gid=%s" % [spreadsheet.human_url, gid]
76
+ end
106
77
 
107
- # Updates content of the cell.
108
- # Arguments in the bracket must be either (row number, column number) or cell name.
109
- # Note that update is not sent to the server until you call save().
110
- # Top-left cell is [1, 1].
111
- #
112
- # e.g.
113
- # worksheet[2, 1] = "hoge"
114
- # worksheet["A2"] = "hoge"
115
- # worksheet[1, 3] = "=A1+B1"
116
- def []=(*args)
117
- (row, col) = parse_cell_args(args[0...-1])
118
- value = args[-1].to_s()
119
- validate_cell_value(value)
120
- reload_cells() if !@cells
121
- @cells[[row, col]] = value
122
- @input_values[[row, col]] = value
123
- @numeric_values[[row, col]] = nil
124
- @modified.add([row, col])
125
- self.max_rows = row if row > @max_rows
126
- self.max_cols = col if col > @max_cols
127
- if value.empty?
128
- @num_rows = nil
129
- @num_cols = nil
130
- else
131
- @num_rows = row if row > num_rows
132
- @num_cols = col if col > num_cols
133
- end
78
+ # GoogleDrive::Spreadsheet which this worksheet belongs to.
79
+ def spreadsheet
80
+ unless @spreadsheet
81
+ unless worksheet_feed_url =~ %r{https?://spreadsheets\.google\.com/feeds/worksheets/(.*)/(.*)$}
82
+ fail(GoogleDrive::Error,
83
+ "Worksheet feed URL is in unknown format: #{worksheet_feed_url}")
134
84
  end
85
+ @spreadsheet = @session.file_by_id(Regexp.last_match(1))
86
+ end
87
+ @spreadsheet
88
+ end
135
89
 
136
- # Updates cells in a rectangle area by a two-dimensional Array.
137
- # +top_row+ and +left_col+ specifies the top-left corner of the area.
138
- #
139
- # e.g.
140
- # worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
141
- def update_cells(top_row, left_col, darray)
142
- darray.each_with_index() do |array, y|
143
- array.each_with_index() do |value, x|
144
- self[top_row + y, left_col + x] = value
145
- end
146
- end
147
- end
90
+ # Returns content of the cell as String. Arguments must be either
91
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
92
+ #
93
+ # e.g.
94
+ # worksheet[2, 1] #=> "hoge"
95
+ # worksheet["A2"] #=> "hoge"
96
+ def [](*args)
97
+ (row, col) = parse_cell_args(args)
98
+ cells[[row, col]] || ''
99
+ end
148
100
 
149
- # Returns the value or the formula of the cell. Arguments must be either
150
- # (row number, column number) or cell name. Top-left cell is [1, 1].
151
- #
152
- # If user input "=A1+B1" to cell [1, 3]:
153
- # worksheet[1, 3] #=> "3" for example
154
- # worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
155
- def input_value(*args)
156
- (row, col) = parse_cell_args(args)
157
- reload_cells() if !@cells
158
- return @input_values[[row, col]] || ""
159
- end
101
+ # Updates content of the cell.
102
+ # Arguments in the bracket must be either (row number, column number) or cell name.
103
+ # Note that update is not sent to the server until you call save().
104
+ # Top-left cell is [1, 1].
105
+ #
106
+ # e.g.
107
+ # worksheet[2, 1] = "hoge"
108
+ # worksheet["A2"] = "hoge"
109
+ # worksheet[1, 3] = "=A1+B1"
110
+ def []=(*args)
111
+ (row, col) = parse_cell_args(args[0...-1])
112
+ value = args[-1].to_s
113
+ validate_cell_value(value)
114
+ reload_cells unless @cells
115
+ @cells[[row, col]] = value
116
+ @input_values[[row, col]] = value
117
+ @numeric_values[[row, col]] = nil
118
+ @modified.add([row, col])
119
+ self.max_rows = row if row > @max_rows
120
+ self.max_cols = col if col > @max_cols
121
+ if value.empty?
122
+ @num_rows = nil
123
+ @num_cols = nil
124
+ else
125
+ @num_rows = row if row > num_rows
126
+ @num_cols = col if col > num_cols
127
+ end
128
+ end
160
129
 
161
- # Returns the numeric value of the cell. Arguments must be either
162
- # (row number, column number) or cell name. Top-left cell is [1, 1].
163
- #
164
- # e.g.
165
- # worksheet[1, 3] #=> "3,0" # it depends on locale, currency...
166
- # worksheet.numeric_value(1, 3) #=> 3.0
167
- #
168
- # Returns nil if the cell is empty or contains non-number.
169
- #
170
- # If you modify the cell, its numeric_value is nil until you call save() and reload().
171
- #
172
- # For details, see:
173
- # https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
174
- def numeric_value(*args)
175
- (row, col) = parse_cell_args(args)
176
- reload_cells() if !@cells
177
- return @numeric_values[[row, col]]
178
- end
179
-
180
- # Row number of the bottom-most non-empty row.
181
- def num_rows
182
- reload_cells() if !@cells
183
- # Memoizes it because this can be bottle-neck.
184
- # https://github.com/gimite/google-drive-ruby/pull/49
185
- return @num_rows ||= @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| r }.max || 0
186
- end
130
+ # Updates cells in a rectangle area by a two-dimensional Array.
131
+ # +top_row+ and +left_col+ specifies the top-left corner of the area.
132
+ #
133
+ # e.g.
134
+ # worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
135
+ def update_cells(top_row, left_col, darray)
136
+ darray.each_with_index do |array, y|
137
+ array.each_with_index do |value, x|
138
+ self[top_row + y, left_col + x] = value
139
+ end
140
+ end
141
+ end
187
142
 
188
- # Column number of the right-most non-empty column.
189
- def num_cols
190
- reload_cells() if !@cells
191
- # Memoizes it because this can be bottle-neck.
192
- # https://github.com/gimite/google-drive-ruby/pull/49
193
- return @num_cols ||= @input_values.select(){ |(r, c), v| !v.empty? }.map(){ |(r, c), v| c }.max || 0
194
- end
143
+ # Returns the value or the formula of the cell. Arguments must be either
144
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
145
+ #
146
+ # If user input "=A1+B1" to cell [1, 3]:
147
+ # worksheet[1, 3] #=> "3" for example
148
+ # worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
149
+ def input_value(*args)
150
+ (row, col) = parse_cell_args(args)
151
+ reload_cells unless @cells
152
+ @input_values[[row, col]] || ''
153
+ end
195
154
 
196
- # Number of rows including empty rows.
197
- def max_rows
198
- reload_cells() if !@cells
199
- return @max_rows
200
- end
155
+ # Returns the numeric value of the cell. Arguments must be either
156
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
157
+ #
158
+ # e.g.
159
+ # worksheet[1, 3] #=> "3,0" # it depends on locale, currency...
160
+ # worksheet.numeric_value(1, 3) #=> 3.0
161
+ #
162
+ # Returns nil if the cell is empty or contains non-number.
163
+ #
164
+ # If you modify the cell, its numeric_value is nil until you call save() and reload().
165
+ #
166
+ # For details, see:
167
+ # https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
168
+ def numeric_value(*args)
169
+ (row, col) = parse_cell_args(args)
170
+ reload_cells unless @cells
171
+ @numeric_values[[row, col]]
172
+ end
201
173
 
202
- # Updates number of rows.
203
- # Note that update is not sent to the server until you call save().
204
- def max_rows=(rows)
205
- reload_cells() if !@cells
206
- @max_rows = rows
207
- @meta_modified = true
208
- end
174
+ # Row number of the bottom-most non-empty row.
175
+ def num_rows
176
+ reload_cells unless @cells
177
+ # Memoizes it because this can be bottle-neck.
178
+ # https://github.com/gimite/google-drive-ruby/pull/49
179
+ @num_rows ||= @input_values.select { |(_r, _c), v| !v.empty? }.map { |(r, _c), _v| r }.max || 0
180
+ end
209
181
 
210
- # Number of columns including empty columns.
211
- def max_cols
212
- reload_cells() if !@cells
213
- return @max_cols
214
- end
182
+ # Column number of the right-most non-empty column.
183
+ def num_cols
184
+ reload_cells unless @cells
185
+ # Memoizes it because this can be bottle-neck.
186
+ # https://github.com/gimite/google-drive-ruby/pull/49
187
+ @num_cols ||= @input_values.select { |(_r, _c), v| !v.empty? }.map { |(_r, c), _v| c }.max || 0
188
+ end
215
189
 
216
- # Updates number of columns.
217
- # Note that update is not sent to the server until you call save().
218
- def max_cols=(cols)
219
- reload_cells() if !@cells
220
- @max_cols = cols
221
- @meta_modified = true
222
- end
190
+ # Number of rows including empty rows.
191
+ def max_rows
192
+ reload_cells unless @cells
193
+ @max_rows
194
+ end
223
195
 
224
- # Updates title of the worksheet.
225
- # Note that update is not sent to the server until you call save().
226
- def title=(title)
227
- @title = title
228
- @meta_modified = true
229
- end
196
+ # Updates number of rows.
197
+ # Note that update is not sent to the server until you call save().
198
+ def max_rows=(rows)
199
+ reload_cells unless @cells
200
+ @max_rows = rows
201
+ @meta_modified = true
202
+ end
230
203
 
231
- def cells #:nodoc:
232
- reload_cells() if !@cells
233
- return @cells
234
- end
204
+ # Number of columns including empty columns.
205
+ def max_cols
206
+ reload_cells unless @cells
207
+ @max_cols
208
+ end
235
209
 
236
- # An array of spreadsheet rows. Each row contains an array of
237
- # columns. Note that resulting array is 0-origin so:
238
- #
239
- # worksheet.rows[0][0] == worksheet[1, 1]
240
- def rows(skip = 0)
241
- nc = self.num_cols
242
- result = ((1 + skip)..self.num_rows).map() do |row|
243
- (1..nc).map(){ |col| self[row, col] }.freeze()
244
- end
245
- return result.freeze()
246
- end
210
+ # Updates number of columns.
211
+ # Note that update is not sent to the server until you call save().
212
+ def max_cols=(cols)
213
+ reload_cells unless @cells
214
+ @max_cols = cols
215
+ @meta_modified = true
216
+ end
247
217
 
248
- # Inserts rows.
249
- #
250
- # e.g.
251
- # # Inserts 2 empty rows before row 3.
252
- # worksheet.insert_rows(3, 2)
253
- # # Inserts 2 rows with values before row 3.
254
- # worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])
255
- #
256
- # Note that this method is implemented by shifting all cells below the row.
257
- # Its behavior is different from inserting rows on the web interface if the
258
- # worksheet contains inter-cell reference.
259
- def insert_rows(row_num, rows)
260
-
261
- if rows.is_a?(Integer)
262
- rows = Array.new(rows, [])
263
- end
218
+ # Updates title of the worksheet.
219
+ # Note that update is not sent to the server until you call save().
220
+ def title=(title)
221
+ @title = title
222
+ @meta_modified = true
223
+ end
264
224
 
265
- # Shifts all cells below the row.
266
- self.max_rows += rows.size
267
- r = self.num_rows
268
- while r >= row_num
269
- for c in 1..self.num_cols
270
- self[r + rows.size, c] = self[r, c]
271
- end
272
- r -= 1
273
- end
225
+ def cells #:nodoc:
226
+ reload_cells unless @cells
227
+ @cells
228
+ end
274
229
 
275
- # Fills in the inserted rows.
276
- num_cols = self.num_cols
277
- rows.each_with_index() do |row, r|
278
- for c in 0...[row.size, num_cols].max
279
- self[row_num + r, 1 + c] = row[c] || ""
280
- end
281
- end
230
+ # An array of spreadsheet rows. Each row contains an array of
231
+ # columns. Note that resulting array is 0-origin so:
232
+ #
233
+ # worksheet.rows[0][0] == worksheet[1, 1]
234
+ def rows(skip = 0)
235
+ nc = num_cols
236
+ result = ((1 + skip)..num_rows).map do |row|
237
+ (1..nc).map { |col| self[row, col] }.freeze
238
+ end
239
+ result.freeze
240
+ end
282
241
 
283
- end
242
+ # Inserts rows.
243
+ #
244
+ # e.g.
245
+ # # Inserts 2 empty rows before row 3.
246
+ # worksheet.insert_rows(3, 2)
247
+ # # Inserts 2 rows with values before row 3.
248
+ # worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])
249
+ #
250
+ # Note that this method is implemented by shifting all cells below the row.
251
+ # Its behavior is different from inserting rows on the web interface if the
252
+ # worksheet contains inter-cell reference.
253
+ def insert_rows(row_num, rows)
254
+ rows = Array.new(rows, []) if rows.is_a?(Integer)
255
+
256
+ # Shifts all cells below the row.
257
+ self.max_rows += rows.size
258
+ r = num_rows
259
+ while r >= row_num
260
+ for c in 1..num_cols
261
+ self[r + rows.size, c] = self[r, c]
262
+ end
263
+ r -= 1
264
+ end
265
+
266
+ # Fills in the inserted rows.
267
+ num_cols = self.num_cols
268
+ rows.each_with_index do |row, r|
269
+ for c in 0...[row.size, num_cols].max
270
+ self[row_num + r, 1 + c] = row[c] || ''
271
+ end
272
+ end
273
+ end
284
274
 
285
- # Deletes rows.
286
- #
287
- # e.g.
288
- # # Deletes 2 rows starting from row 3 (i.e., deletes row 3 and 4).
289
- # worksheet.delete_rows(3, 2)
290
- #
291
- # Note that this method is implemented by shifting all cells below the row.
292
- # Its behavior is different from deleting rows on the web interface if the
293
- # worksheet contains inter-cell reference.
294
- def delete_rows(row_num, rows)
295
- if row_num + rows - 1 > self.max_rows
296
- raise(ArgumentError, "The row number is out of range")
297
- end
298
- for r in row_num..(self.max_rows - rows)
299
- for c in 1..self.num_cols
300
- self[r, c] = self[r + rows, c]
301
- end
302
- end
303
- self.max_rows -= rows
304
- end
275
+ # Deletes rows.
276
+ #
277
+ # e.g.
278
+ # # Deletes 2 rows starting from row 3 (i.e., deletes row 3 and 4).
279
+ # worksheet.delete_rows(3, 2)
280
+ #
281
+ # Note that this method is implemented by shifting all cells below the row.
282
+ # Its behavior is different from deleting rows on the web interface if the
283
+ # worksheet contains inter-cell reference.
284
+ def delete_rows(row_num, rows)
285
+ if row_num + rows - 1 > self.max_rows
286
+ fail(ArgumentError, 'The row number is out of range')
287
+ end
288
+ for r in row_num..(self.max_rows - rows)
289
+ for c in 1..num_cols
290
+ self[r, c] = self[r + rows, c]
291
+ end
292
+ end
293
+ self.max_rows -= rows
294
+ end
305
295
 
306
- # Reloads content of the worksheets from the server.
307
- # Note that changes you made by []= etc. is discarded if you haven't called save().
308
- def reload()
309
- set_worksheet_feed_entry(@session.request(:get, self.worksheet_feed_url).root)
310
- reload_cells()
311
- return true
312
- end
296
+ # Reloads content of the worksheets from the server.
297
+ # Note that changes you made by []= etc. is discarded if you haven't called save().
298
+ def reload
299
+ set_worksheet_feed_entry(@session.request(:get, worksheet_feed_url).root)
300
+ reload_cells
301
+ true
302
+ end
313
303
 
314
- # Saves your changes made by []=, etc. to the server.
315
- def save()
316
-
317
- sent = false
304
+ # Saves your changes made by []=, etc. to the server.
305
+ def save
306
+ sent = false
318
307
 
319
- if @meta_modified
308
+ if @meta_modified
320
309
 
321
- edit_url = @worksheet_feed_entry.css("link[rel='edit']")[0]["href"]
322
- xml = <<-"EOS"
310
+ edit_url = @worksheet_feed_entry.css("link[rel='edit']")[0]['href']
311
+ xml = <<-"EOS"
323
312
  <entry xmlns='http://www.w3.org/2005/Atom'
324
313
  xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
325
- <title>#{h(self.title)}</title>
314
+ <title>#{h(title)}</title>
326
315
  <gs:rowCount>#{h(self.max_rows)}</gs:rowCount>
327
- <gs:colCount>#{h(self.max_cols)}</gs:colCount>
316
+ <gs:colCount>#{h(max_cols)}</gs:colCount>
328
317
  </entry>
329
318
  EOS
330
319
 
331
- result = @session.request(
332
- :put, edit_url, :data => xml,
333
- :header => {"Content-Type" => "application/atom+xml;charset=utf-8", "If-Match" => "*"})
334
- set_worksheet_feed_entry(result.root)
320
+ result = @session.request(
321
+ :put, edit_url, data: xml,
322
+ header: { 'Content-Type' => 'application/atom+xml;charset=utf-8', 'If-Match' => '*' })
323
+ set_worksheet_feed_entry(result.root)
335
324
 
336
- sent = true
325
+ sent = true
337
326
 
338
- end
327
+ end
328
+
329
+ unless @modified.empty?
330
+
331
+ # Gets id and edit URL for each cell.
332
+ # Note that return-empty=true is required to get those info for empty cells.
333
+ cell_entries = {}
334
+ rows = @modified.map { |r, _c| r }
335
+ cols = @modified.map { |_r, c| c }
336
+ url = concat_url(cells_feed_url,
337
+ "?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" \
338
+ "&min-col=#{cols.min}&max-col=#{cols.max}")
339
+ doc = @session.request(:get, url)
339
340
 
340
- if !@modified.empty?
341
-
342
- # Gets id and edit URL for each cell.
343
- # Note that return-empty=true is required to get those info for empty cells.
344
- cell_entries = {}
345
- rows = @modified.map(){ |r, c| r }
346
- cols = @modified.map(){ |r, c| c }
347
- url = concat_url(self.cells_feed_url,
348
- "?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" +
349
- "&min-col=#{cols.min}&max-col=#{cols.max}")
350
- doc = @session.request(:get, url)
351
-
352
- for entry in doc.css("entry")
353
- row = entry.css("gs|cell")[0]["row"].to_i()
354
- col = entry.css("gs|cell")[0]["col"].to_i()
355
- cell_entries[[row, col]] = entry
356
- end
357
-
358
- xml = <<-EOS
341
+ doc.css('entry').each do |entry|
342
+ row = entry.css('gs|cell')[0]['row'].to_i
343
+ col = entry.css('gs|cell')[0]['col'].to_i
344
+ cell_entries[[row, col]] = entry
345
+ end
346
+
347
+ xml = <<-EOS
359
348
  <feed xmlns="http://www.w3.org/2005/Atom"
360
349
  xmlns:batch="http://schemas.google.com/gdata/batch"
361
350
  xmlns:gs="http://schemas.google.com/spreadsheets/2006">
362
- <id>#{h(self.cells_feed_url)}</id>
351
+ <id>#{h(cells_feed_url)}</id>
363
352
  EOS
364
- for row, col in @modified
365
- value = @cells[[row, col]]
366
- entry = cell_entries[[row, col]]
367
- id = entry.css("id").text
368
- edit_link = entry.css("link[rel='edit']")[0]
369
- if !edit_link
370
- raise(GoogleDrive::Error,
371
- "The user doesn't have write permission to the spreadsheet: %p" % self.spreadsheet)
372
- end
373
- edit_url = edit_link["href"]
374
- xml << <<-EOS
353
+ @modified.each do |row, col|
354
+ value = @cells[[row, col]]
355
+ entry = cell_entries[[row, col]]
356
+ id = entry.css('id').text
357
+ edit_link = entry.css("link[rel='edit']")[0]
358
+ unless edit_link
359
+ fail(GoogleDrive::Error,
360
+ "The user doesn't have write permission to the spreadsheet: %p" % spreadsheet)
361
+ end
362
+ edit_url = edit_link['href']
363
+ xml << <<-EOS
375
364
  <entry>
376
365
  <batch:id>#{h(row)},#{h(col)}</batch:id>
377
366
  <batch:operation type="update"/>
@@ -380,162 +369,157 @@ module GoogleDrive
380
369
  href="#{h(edit_url)}"/>
381
370
  <gs:cell row="#{h(row)}" col="#{h(col)}" inputValue="#{h(value)}"/>
382
371
  </entry>
383
- EOS
384
- end
385
- xml << <<-"EOS"
386
- </feed>
387
- EOS
388
-
389
- batch_url = concat_url(self.cells_feed_url, "/batch")
390
- result = @session.request(
391
- :post,
392
- batch_url,
393
- :data => xml,
394
- :header => {"Content-Type" => "application/atom+xml;charset=utf-8", "If-Match" => "*"})
395
- for entry in result.css("entry")
396
- interrupted = entry.css("batch|interrupted")[0]
397
- if interrupted
398
- raise(GoogleDrive::Error, "Update has failed: %s" %
399
- interrupted["reason"])
400
- end
401
- if !(entry.css("batch|status").first["code"] =~ /^2/)
402
- raise(GoogleDrive::Error, "Updating cell %s has failed: %s" %
403
- [entry.css("id").text, entry.css("batch|status")[0]["reason"]])
404
- end
405
- end
406
-
407
- @modified.clear()
408
- sent = true
409
-
372
+ EOS
373
+ end
374
+ xml << <<-"EOS"
375
+ </feed>
376
+ EOS
377
+
378
+ batch_url = concat_url(cells_feed_url, '/batch')
379
+ result = @session.request(
380
+ :post,
381
+ batch_url,
382
+ data: xml,
383
+ header: { 'Content-Type' => 'application/atom+xml;charset=utf-8', 'If-Match' => '*' })
384
+ result.css('entry').each do |entry|
385
+ interrupted = entry.css('batch|interrupted')[0]
386
+ if interrupted
387
+ fail(GoogleDrive::Error, 'Update has failed: %s' %
388
+ interrupted['reason'])
389
+ end
390
+ unless entry.css('batch|status').first['code'] =~ /^2/
391
+ fail(GoogleDrive::Error, 'Updating cell %s has failed: %s' %
392
+ [entry.css('id').text, entry.css('batch|status')[0]['reason']])
410
393
  end
411
-
412
- return sent
413
-
414
394
  end
415
395
 
416
- # Calls save() and reload().
417
- def synchronize()
418
- save()
419
- reload()
420
- end
396
+ @modified.clear
397
+ sent = true
421
398
 
422
- # Deletes this worksheet. Deletion takes effect right away without calling save().
423
- def delete()
424
- ws_doc = @session.request(:get, self.worksheet_feed_url)
425
- edit_url = ws_doc.css("link[rel='edit']")[0]["href"]
426
- @session.request(:delete, edit_url)
427
- end
399
+ end
428
400
 
429
- # Returns true if you have changes made by []= which haven't been saved.
430
- def dirty?
431
- return !@modified.empty?
432
- end
401
+ sent
402
+ end
433
403
 
434
- # List feed URL of the worksheet.
435
- def list_feed_url
436
- return @worksheet_feed_entry.css(
437
- "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']")[0]["href"]
438
- end
439
-
440
- # Provides access to cells using column names, assuming the first row contains column
441
- # names. Returned object is GoogleDrive::List which you can use mostly as
442
- # Array of Hash.
443
- #
444
- # e.g. Assuming the first row is ["x", "y"]:
445
- # worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
446
- # worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
447
- # worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
448
- # worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
449
- # worksheet.list.push({"x" => "5", "y" => "6"})
450
- #
451
- # Note that update is not sent to the server until you call save().
452
- def list
453
- return @list ||= List.new(self)
454
- end
455
-
456
- # Returns a [row, col] pair for a cell name string.
457
- # e.g.
458
- # worksheet.cell_name_to_row_col("C2") #=> [2, 3]
459
- def cell_name_to_row_col(cell_name)
460
- if !cell_name.is_a?(String)
461
- raise(ArgumentError, "Cell name must be a string: %p" % cell_name)
462
- end
463
- if !(cell_name.upcase =~ /^([A-Z]+)(\d+)$/)
464
- raise(ArgumentError,
465
- "Cell name must be only letters followed by digits with no spaces in between: %p" %
466
- cell_name)
467
- end
468
- col = 0
469
- $1.each_byte() do |b|
470
- # 0x41: "A"
471
- col = col * 26 + (b - 0x41 + 1)
472
- end
473
- row = $2.to_i()
474
- return [row, col]
475
- end
404
+ # Calls save() and reload().
405
+ def synchronize
406
+ save
407
+ reload
408
+ end
476
409
 
477
- def inspect
478
- fields = {:worksheet_feed_url => self.worksheet_feed_url}
479
- fields[:title] = @title if @title
480
- return "\#<%p %s>" % [self.class, fields.map(){ |k, v| "%s=%p" % [k, v] }.join(", ")]
481
- end
482
-
483
- private
484
-
485
- def set_worksheet_feed_entry(entry)
486
- @worksheet_feed_entry = entry
487
- @title = entry.css("title").text
488
- @updated = Time.parse(entry.css("updated").text)
489
- @meta_modified = false
490
- end
410
+ # Deletes this worksheet. Deletion takes effect right away without calling save().
411
+ def delete
412
+ ws_doc = @session.request(:get, worksheet_feed_url)
413
+ edit_url = ws_doc.css("link[rel='edit']")[0]['href']
414
+ @session.request(:delete, edit_url)
415
+ end
491
416
 
492
- def reload_cells()
493
-
494
- doc = @session.request(:get, self.cells_feed_url)
495
- @max_rows = doc.css("gs|rowCount").text.to_i()
496
- @max_cols = doc.css("gs|colCount").text.to_i()
497
-
498
- @num_cols = nil
499
- @num_rows = nil
500
-
501
- @cells = {}
502
- @input_values = {}
503
- @numeric_values = {}
504
- doc.css("feed > entry").each() do |entry|
505
- cell = entry.css("gs|cell")[0]
506
- row = cell["row"].to_i()
507
- col = cell["col"].to_i()
508
- @cells[[row, col]] = cell.inner_text
509
- @input_values[[row, col]] = cell["inputValue"] || cell.inner_text
510
- numeric_value = cell["numericValue"]
511
- @numeric_values[[row, col]] = numeric_value ? numeric_value.to_f() : nil
512
- end
513
- @modified.clear()
417
+ # Returns true if you have changes made by []= which haven't been saved.
418
+ def dirty?
419
+ !@modified.empty?
420
+ end
514
421
 
515
- end
422
+ # List feed URL of the worksheet.
423
+ def list_feed_url
424
+ @worksheet_feed_entry.css(
425
+ "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']")[0]['href']
426
+ end
516
427
 
517
- def parse_cell_args(args)
518
- if args.size == 1 && args[0].is_a?(String)
519
- return cell_name_to_row_col(args[0])
520
- elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
521
- if args[0] >= 1 && args[1] >= 1
522
- return args
523
- else
524
- raise(ArgumentError,
525
- "Row/col must be >= 1 (1-origin), but are %d/%d" % [args[0], args[1]])
526
- end
527
- else
528
- raise(ArgumentError,
529
- "Arguments must be either one String or two Integer's, but are %p" % [args])
530
- end
531
- end
532
-
533
- def validate_cell_value(value)
534
- if value.include?("\x1a")
535
- raise(ArgumentError, "Contains invalid character \\x1a for xml: %p" % value)
536
- end
537
- end
428
+ # Provides access to cells using column names, assuming the first row contains column
429
+ # names. Returned object is GoogleDrive::List which you can use mostly as
430
+ # Array of Hash.
431
+ #
432
+ # e.g. Assuming the first row is ["x", "y"]:
433
+ # worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
434
+ # worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
435
+ # worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
436
+ # worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
437
+ # worksheet.list.push({"x" => "5", "y" => "6"})
438
+ #
439
+ # Note that update is not sent to the server until you call save().
440
+ def list
441
+ @list ||= List.new(self)
442
+ end
443
+
444
+ # Returns a [row, col] pair for a cell name string.
445
+ # e.g.
446
+ # worksheet.cell_name_to_row_col("C2") #=> [2, 3]
447
+ def cell_name_to_row_col(cell_name)
448
+ unless cell_name.is_a?(String)
449
+ fail(ArgumentError, 'Cell name must be a string: %p' % cell_name)
450
+ end
451
+ unless cell_name.upcase =~ /^([A-Z]+)(\d+)$/
452
+ fail(ArgumentError,
453
+ 'Cell name must be only letters followed by digits with no spaces in between: %p' %
454
+ cell_name)
455
+ end
456
+ col = 0
457
+ Regexp.last_match(1).each_byte do |b|
458
+ # 0x41: "A"
459
+ col = col * 26 + (b - 0x41 + 1)
460
+ end
461
+ row = Regexp.last_match(2).to_i
462
+ [row, col]
463
+ end
464
+
465
+ def inspect
466
+ fields = { worksheet_feed_url: worksheet_feed_url }
467
+ fields[:title] = @title if @title
468
+ "\#<%p %s>" % [self.class, fields.map { |k, v| '%s=%p' % [k, v] }.join(', ')]
469
+ end
470
+
471
+ private
472
+
473
+ def set_worksheet_feed_entry(entry)
474
+ @worksheet_feed_entry = entry
475
+ @title = entry.css('title').text
476
+ @updated = Time.parse(entry.css('updated').text)
477
+ @meta_modified = false
478
+ end
479
+
480
+ def reload_cells
481
+ doc = @session.request(:get, cells_feed_url)
482
+ @max_rows = doc.css('gs|rowCount').text.to_i
483
+ @max_cols = doc.css('gs|colCount').text.to_i
484
+
485
+ @num_cols = nil
486
+ @num_rows = nil
487
+
488
+ @cells = {}
489
+ @input_values = {}
490
+ @numeric_values = {}
491
+ doc.css('feed > entry').each do |entry|
492
+ cell = entry.css('gs|cell')[0]
493
+ row = cell['row'].to_i
494
+ col = cell['col'].to_i
495
+ @cells[[row, col]] = cell.inner_text
496
+ @input_values[[row, col]] = cell['inputValue'] || cell.inner_text
497
+ numeric_value = cell['numericValue']
498
+ @numeric_values[[row, col]] = numeric_value ? numeric_value.to_f : nil
499
+ end
500
+ @modified.clear
501
+ end
502
+
503
+ def parse_cell_args(args)
504
+ if args.size == 1 && args[0].is_a?(String)
505
+ return cell_name_to_row_col(args[0])
506
+ elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
507
+ if args[0] >= 1 && args[1] >= 1
508
+ return args
509
+ else
510
+ fail(ArgumentError,
511
+ 'Row/col must be >= 1 (1-origin), but are %d/%d' % [args[0], args[1]])
512
+ end
513
+ else
514
+ fail(ArgumentError,
515
+ "Arguments must be either one String or two Integer's, but are %p" % [args])
516
+ end
517
+ end
538
518
 
519
+ def validate_cell_value(value)
520
+ if value.include?("\x1a")
521
+ fail(ArgumentError, 'Contains invalid character \\x1a for xml: %p' % value)
522
+ end
539
523
  end
540
-
524
+ end
541
525
  end