google_drive 1.0.6 → 2.0.0.pre1

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.
@@ -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