google_drive2 3.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,770 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
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'
11
+
12
+ module GoogleDrive
13
+ # A worksheet (i.e. a tab) in a spreadsheet.
14
+ # Use GoogleDrive::Spreadsheet#worksheets to get GoogleDrive::Worksheet
15
+ # object.
16
+ class Worksheet
17
+ include(Util)
18
+
19
+ # A few default color instances that match the colors from the Google Sheets web UI.
20
+ #
21
+ # TODO: Add more colors from
22
+ # https://github.com/denilsonsa/gimp-palettes/blob/master/palettes/Google-Drive.gpl
23
+ module Colors
24
+ RED = Google::Apis::SheetsV4::Color.new(red: 1.0)
25
+ DARK_RED_1 = Google::Apis::SheetsV4::Color.new(red: 0.8)
26
+ RED_BERRY = Google::Apis::SheetsV4::Color.new(red: 0.596)
27
+ DARK_RED_BERRY_1 = Google::Apis::SheetsV4::Color.new(red: 0.659, green: 0.11)
28
+ ORANGE = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 0.6)
29
+ DARK_ORANGE_1 = Google::Apis::SheetsV4::Color.new(red: 0.9, green: 0.569, blue: 0.22)
30
+ YELLOW = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 1.0)
31
+ DARK_YELLOW_1 = Google::Apis::SheetsV4::Color.new(red: 0.945, green: 0.76, blue: 0.196)
32
+ GREEN = Google::Apis::SheetsV4::Color.new(green: 1.0)
33
+ DARK_GREEN_1 = Google::Apis::SheetsV4::Color.new(red: 0.416, green: 0.659, blue: 0.31)
34
+ CYAN = Google::Apis::SheetsV4::Color.new(green: 1.0, blue: 1.0)
35
+ DARK_CYAN_1 = Google::Apis::SheetsV4::Color.new(red: 0.27, green: 0.506, blue: 0.557)
36
+ CORNFLOWER_BLUE = Google::Apis::SheetsV4::Color.new(red: 0.29, green: 0.525, blue: 0.91)
37
+ DARK_CORNFLOWER_BLUE_1 = Google::Apis::SheetsV4::Color.new(red: 0.235, green: 0.47, blue: 0.847)
38
+ BLUE = Google::Apis::SheetsV4::Color.new(blue: 1.0)
39
+ DARK_BLUE_1 = Google::Apis::SheetsV4::Color.new(red: 0.239, green: 0.522, blue: 0.776)
40
+ PURPLE = Google::Apis::SheetsV4::Color.new(red: 0.6, blue: 1.0)
41
+ DARK_PURPLE_1 = Google::Apis::SheetsV4::Color.new(red: 0.404, green: 0.306, blue: 0.655)
42
+ MAGENTA = Google::Apis::SheetsV4::Color.new(red: 1.0, blue: 1.0)
43
+ DARK_MAGENTA_1 = Google::Apis::SheetsV4::Color.new(red: 0.651, green: 0.302, blue: 0.475)
44
+ WHITE = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 1.0, blue: 1.0)
45
+ BLACK = Google::Apis::SheetsV4::Color.new(red: 0.0, green: 0.0, blue: 0.0)
46
+ GRAY = Google::Apis::SheetsV4::Color.new(red: 0.8, green: 0.8, blue: 0.8)
47
+ DARK_GRAY_1 = Google::Apis::SheetsV4::Color.new(red: 0.714, green: 0.714, blue: 0.714)
48
+ end
49
+
50
+ # @api private
51
+ # A regexp which matches an invalid character in XML 1.0:
52
+ # https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0
53
+ XML_INVALID_CHAR_REGEXP =
54
+ /[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd\u{10000}-\u{10ffff}]/
55
+
56
+ # @api private
57
+ def initialize(session, spreadsheet, properties)
58
+ @session = session
59
+ @spreadsheet = spreadsheet
60
+ set_properties(properties)
61
+ @cells = nil
62
+ @input_values = nil
63
+ @numeric_values = nil
64
+ @modified = Set.new
65
+ @list = nil
66
+ @v4_requests = []
67
+ end
68
+
69
+ # Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
70
+ #
71
+ # DEPRECATED: This method is deprecated, and now requires additional
72
+ # network fetch. Consider using properties instead.
73
+ def worksheet_feed_entry
74
+ @worksheet_feed_entry ||= @session.request(:get, worksheet_feed_url).root
75
+ end
76
+
77
+ # Google::Apis::SheetsV4::SheetProperties object for this worksheet.
78
+ attr_reader :properties
79
+
80
+ # Title of the worksheet (shown as tab label in Web interface).
81
+ attr_reader :title
82
+
83
+ # Index of the worksheet (affects tab order in web interface).
84
+ attr_reader :index
85
+
86
+ # GoogleDrive::Spreadsheet which this worksheet belongs to.
87
+ attr_reader :spreadsheet
88
+
89
+ # Time object which represents the time the worksheet was last updated.
90
+ #
91
+ # DEPRECATED: From google_drive 3.0.0, it returns the time the
92
+ # *spreadsheet* was last updated, instead of the worksheet. This is because
93
+ # it looks the information is not available in Sheets v4 API.
94
+ def updated
95
+ spreadsheet.modified_time.to_time
96
+ end
97
+
98
+ # URL of cell-based feed of the worksheet.
99
+ #
100
+ # DEPRECATED: This method is deprecated, and now requires additional
101
+ # network fetch.
102
+ def cells_feed_url
103
+ worksheet_feed_entry.css(
104
+ "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']"
105
+ )[0]['href']
106
+ end
107
+
108
+ # URL of worksheet feed URL of the worksheet.
109
+ def worksheet_feed_url
110
+ return '%s/%s' % [spreadsheet.worksheets_feed_url, worksheet_feed_id]
111
+ end
112
+
113
+ # URL to export the worksheet as CSV.
114
+ def csv_export_url
115
+ 'https://docs.google.com/spreadsheets/d/%s/export?gid=%s&format=csv' %
116
+ [spreadsheet.id, gid]
117
+ end
118
+
119
+ # Exports the worksheet as String in CSV format.
120
+ def export_as_string
121
+ @session.request(:get, csv_export_url, response_type: :raw)
122
+ end
123
+
124
+ # Exports the worksheet to +path+ in CSV format.
125
+ def export_as_file(path)
126
+ data = export_as_string
127
+ open(path, 'wb') { |f| f.write(data) }
128
+ end
129
+
130
+ # ID of the worksheet.
131
+ def sheet_id
132
+ @properties.sheet_id
133
+ end
134
+
135
+ # Returns sheet_id.to_s.
136
+ def gid
137
+ sheet_id.to_s
138
+ end
139
+
140
+ # URL to view/edit the worksheet in a Web browser.
141
+ def human_url
142
+ format("%s\#gid=%s", spreadsheet.human_url, gid)
143
+ end
144
+
145
+ # Copy worksheet to specified spreadsheet.
146
+ # This method can take either instance of GoogleDrive::Spreadsheet or its id.
147
+ def copy_to(spreadsheet_or_id)
148
+ destination_spreadsheet_id =
149
+ spreadsheet_or_id.respond_to?(:id) ?
150
+ spreadsheet_or_id.id : spreadsheet_or_id
151
+ request = Google::Apis::SheetsV4::CopySheetToAnotherSpreadsheetRequest.new(
152
+ destination_spreadsheet_id: destination_spreadsheet_id,
153
+ )
154
+ @session.sheets_service.copy_spreadsheet(spreadsheet.id, sheet_id, request)
155
+ nil
156
+ end
157
+
158
+ # Copy worksheet to owner spreadsheet.
159
+ def duplicate
160
+ copy_to(spreadsheet)
161
+ end
162
+
163
+ # Returns content of the cell as String. Arguments must be either
164
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
165
+ #
166
+ # e.g.
167
+ # worksheet[2, 1] #=> "hoge"
168
+ # worksheet["A2"] #=> "hoge"
169
+ def [](*args)
170
+ (row, col) = parse_cell_args(args)
171
+ cells[[row, col]] || ''
172
+ end
173
+
174
+ # Updates content of the cell.
175
+ # Arguments in the bracket must be either (row number, column number) or
176
+ # cell name. Note that update is not sent to the server until you call
177
+ # save().
178
+ # Top-left cell is [1, 1].
179
+ #
180
+ # e.g.
181
+ # worksheet[2, 1] = "hoge"
182
+ # worksheet["A2"] = "hoge"
183
+ # worksheet[1, 3] = "=A1+B1"
184
+ def []=(*args)
185
+ (row, col) = parse_cell_args(args[0...-1])
186
+ value = args[-1].to_s
187
+ validate_cell_value(value)
188
+ reload_cells unless @cells
189
+ @cells[[row, col]] = value
190
+ @input_values[[row, col]] = value
191
+ @numeric_values[[row, col]] = nil
192
+ @modified.add([row, col])
193
+ self.max_rows = row if row > @max_rows
194
+ self.max_cols = col if col > @max_cols
195
+ if value.empty?
196
+ @num_rows = nil
197
+ @num_cols = nil
198
+ else
199
+ @num_rows = row if @num_rows && row > @num_rows
200
+ @num_cols = col if @num_cols && col > @num_cols
201
+ end
202
+ end
203
+
204
+ # Updates cells in a rectangle area by a two-dimensional Array.
205
+ # +top_row+ and +left_col+ specifies the top-left corner of the area.
206
+ #
207
+ # e.g.
208
+ # worksheet.update_cells(2, 3, [["1", "2"], ["3", "4"]])
209
+ def update_cells(top_row, left_col, darray)
210
+ darray.each_with_index do |array, y|
211
+ array.each_with_index do |value, x|
212
+ self[top_row + y, left_col + x] = value
213
+ end
214
+ end
215
+ end
216
+
217
+ # Returns the value or the formula of the cell. Arguments must be either
218
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
219
+ #
220
+ # If user input "=A1+B1" to cell [1, 3]:
221
+ # worksheet[1, 3] #=> "3" for example
222
+ # worksheet.input_value(1, 3) #=> "=RC[-2]+RC[-1]"
223
+ def input_value(*args)
224
+ (row, col) = parse_cell_args(args)
225
+ reload_cells unless @cells
226
+ @input_values[[row, col]] || ''
227
+ end
228
+
229
+ # Returns the numeric value of the cell. Arguments must be either
230
+ # (row number, column number) or cell name. Top-left cell is [1, 1].
231
+ #
232
+ # e.g.
233
+ # worksheet[1, 3]
234
+ # #=> "3,0" # it depends on locale, currency...
235
+ # worksheet.numeric_value(1, 3)
236
+ # #=> 3.0
237
+ #
238
+ # Returns nil if the cell is empty or contains non-number.
239
+ #
240
+ # If you modify the cell, its numeric_value is nil until you call save()
241
+ # and reload().
242
+ #
243
+ # For details, see:
244
+ # https://developers.google.com/google-apps/spreadsheets/#working_with_cell-based_feeds
245
+ def numeric_value(*args)
246
+ (row, col) = parse_cell_args(args)
247
+ reload_cells unless @cells
248
+ @numeric_values[[row, col]]
249
+ end
250
+
251
+ # Row number of the bottom-most non-empty row.
252
+ def num_rows
253
+ reload_cells unless @cells
254
+ # Memoizes it because this can be bottle-neck.
255
+ # https://github.com/gimite/google-drive-ruby/pull/49
256
+ @num_rows ||=
257
+ @input_values
258
+ .reject { |(_r, _c), v| v.empty? }
259
+ .map { |(r, _c), _v| r }
260
+ .max ||
261
+ 0
262
+ end
263
+
264
+ # Column number of the right-most non-empty column.
265
+ def num_cols
266
+ reload_cells unless @cells
267
+ # Memoizes it because this can be bottle-neck.
268
+ # https://github.com/gimite/google-drive-ruby/pull/49
269
+ @num_cols ||=
270
+ @input_values
271
+ .reject { |(_r, _c), v| v.empty? }
272
+ .map { |(_r, c), _v| c }
273
+ .max ||
274
+ 0
275
+ end
276
+
277
+ # Number of rows including empty rows.
278
+ attr_reader :max_rows
279
+
280
+ # Updates number of rows.
281
+ # Note that update is not sent to the server until you call save().
282
+ def max_rows=(rows)
283
+ @max_rows = rows
284
+ @meta_modified = true
285
+ end
286
+
287
+ # Number of columns including empty columns.
288
+ attr_reader :max_cols
289
+
290
+ # Updates number of columns.
291
+ # Note that update is not sent to the server until you call save().
292
+ def max_cols=(cols)
293
+ @max_cols = cols
294
+ @meta_modified = true
295
+ end
296
+
297
+ # Updates title of the worksheet.
298
+ # Note that update is not sent to the server until you call save().
299
+ def title=(title)
300
+ @title = title
301
+ @meta_modified = true
302
+ end
303
+
304
+ # Updates index of the worksheet.
305
+ # Note that update is not sent to the server until you call save().
306
+ def index=(index)
307
+ @index = index
308
+ @meta_modified = true
309
+ end
310
+
311
+ # @api private
312
+ def cells
313
+ reload_cells unless @cells
314
+ @cells
315
+ end
316
+
317
+ # An array of spreadsheet rows. Each row contains an array of
318
+ # columns. Note that resulting array is 0-origin so:
319
+ #
320
+ # worksheet.rows[0][0] == worksheet[1, 1]
321
+ def rows(skip = 0)
322
+ nc = num_cols
323
+ result = ((1 + skip)..num_rows).map do |row|
324
+ (1..nc).map { |col| self[row, col] }.freeze
325
+ end
326
+ result.freeze
327
+ end
328
+
329
+ # Inserts rows.
330
+ #
331
+ # e.g.
332
+ # # Inserts 2 empty rows before row 3.
333
+ # worksheet.insert_rows(3, 2)
334
+ # # Inserts 2 rows with values before row 3.
335
+ # worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])
336
+ #
337
+ # Note that this method is implemented by shifting all cells below the row.
338
+ # Its behavior is different from inserting rows on the web interface if the
339
+ # worksheet contains inter-cell reference.
340
+ def insert_rows(row_num, rows)
341
+ rows = Array.new(rows, []) if rows.is_a?(Integer)
342
+
343
+ # Shifts all cells below the row.
344
+ self.max_rows += rows.size
345
+ num_rows.downto(row_num) do |r|
346
+ (1..num_cols).each do |c|
347
+ self[r + rows.size, c] = input_value(r, c)
348
+ end
349
+ end
350
+
351
+ # Fills in the inserted rows.
352
+ num_cols = self.num_cols
353
+ rows.each_with_index do |row, r|
354
+ (0...[row.size, num_cols].max).each do |c|
355
+ self[row_num + r, 1 + c] = row[c] || ''
356
+ end
357
+ end
358
+ end
359
+
360
+ # Deletes rows.
361
+ #
362
+ # e.g.
363
+ # # Deletes 2 rows starting from row 3 (i.e., deletes row 3 and 4).
364
+ # worksheet.delete_rows(3, 2)
365
+ #
366
+ # Note that this method is implemented by shifting all cells below the row.
367
+ # Its behavior is different from deleting rows on the web interface if the
368
+ # worksheet contains inter-cell reference.
369
+ def delete_rows(row_num, rows)
370
+ if row_num + rows - 1 > self.max_rows
371
+ raise(ArgumentError, 'The row number is out of range')
372
+ end
373
+ for r in row_num..(self.max_rows - rows)
374
+ for c in 1..num_cols
375
+ self[r, c] = input_value(r + rows, c)
376
+ end
377
+ end
378
+ self.max_rows -= rows
379
+ end
380
+
381
+ # Reloads content of the worksheets from the server.
382
+ # Note that changes you made by []= etc. is discarded if you haven't called
383
+ # save().
384
+ def reload
385
+ api_spreadsheet =
386
+ @session.sheets_service.get_spreadsheet(
387
+ spreadsheet.id,
388
+ ranges: "'%s'" % @title,
389
+ fields:
390
+ 'sheets(properties,data.rowData.values' \
391
+ '(formattedValue,userEnteredValue,effectiveValue))'
392
+ )
393
+ api_sheet = api_spreadsheet.sheets[0]
394
+ set_properties(api_sheet.properties)
395
+ update_cells_from_api_sheet(api_sheet)
396
+ @v4_requests = []
397
+ @worksheet_feed_entry = nil
398
+ true
399
+ end
400
+
401
+ # Saves your changes made by []=, etc. to the server.
402
+ def save
403
+ sent = false
404
+
405
+ if @meta_modified
406
+ add_request({
407
+ update_sheet_properties: {
408
+ properties: {
409
+ sheet_id: sheet_id,
410
+ title: title,
411
+ index: index,
412
+ grid_properties: {row_count: max_rows, column_count: max_cols},
413
+ },
414
+ fields: '*',
415
+ },
416
+ })
417
+ end
418
+
419
+ if !@v4_requests.empty?
420
+ self.spreadsheet.batch_update(@v4_requests)
421
+ @v4_requests = []
422
+ sent = true
423
+ end
424
+
425
+ @remote_title = @title
426
+
427
+ unless @modified.empty?
428
+ min_modified_row = 1.0 / 0.0
429
+ max_modified_row = 0
430
+ min_modified_col = 1.0 / 0.0
431
+ max_modified_col = 0
432
+ @modified.each do |r, c|
433
+ min_modified_row = r if r < min_modified_row
434
+ max_modified_row = r if r > max_modified_row
435
+ min_modified_col = c if c < min_modified_col
436
+ max_modified_col = c if c > max_modified_col
437
+ end
438
+
439
+ # Uses update_spreadsheet_value instead batch_update_spreadsheet with
440
+ # update_cells. batch_update_spreadsheet has benefit that the request
441
+ # can be batched with other requests. But it has drawback that the
442
+ # type of the value (string_value, number_value, etc.) must be
443
+ # explicitly specified in user_entered_value. Since I don't know exact
444
+ # logic to determine the type from text, I chose to use
445
+ # update_spreadsheet_value here.
446
+ range = "'%s'!R%dC%d:R%dC%d" %
447
+ [@title, min_modified_row, min_modified_col, max_modified_row, max_modified_col]
448
+ values = (min_modified_row..max_modified_row).map do |r|
449
+ (min_modified_col..max_modified_col).map do |c|
450
+ @modified.include?([r, c]) ? (@cells[[r, c]] || '') : nil
451
+ end
452
+ end
453
+ value_range = Google::Apis::SheetsV4::ValueRange.new(values: values)
454
+ @session.sheets_service.update_spreadsheet_value(
455
+ spreadsheet.id, range, value_range, value_input_option: 'USER_ENTERED')
456
+
457
+ @modified.clear
458
+ sent = true
459
+ end
460
+
461
+ sent
462
+ end
463
+
464
+ # Calls save() and reload().
465
+ def synchronize
466
+ save
467
+ reload
468
+ end
469
+
470
+ # Deletes this worksheet. Deletion takes effect right away without calling
471
+ # save().
472
+ def delete
473
+ spreadsheet.batch_update([{
474
+ delete_sheet: Google::Apis::SheetsV4::DeleteSheetRequest.new(sheet_id: sheet_id),
475
+ }])
476
+ end
477
+
478
+ # Returns true if you have changes made by []= etc. which haven't been saved.
479
+ def dirty?
480
+ !@modified.empty? || !@v4_requests.empty?
481
+ end
482
+
483
+ # List feed URL of the worksheet.
484
+ #
485
+ # DEPRECATED: This method is deprecated, and now requires additional
486
+ # network fetch.
487
+ def list_feed_url
488
+ @worksheet_feed_entry.css(
489
+ "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']"
490
+ )[0]['href']
491
+ end
492
+
493
+ # Provides access to cells using column names, assuming the first row
494
+ # contains column
495
+ # names. Returned object is GoogleDrive::List which you can use mostly as
496
+ # Array of Hash.
497
+ #
498
+ # e.g. Assuming the first row is ["x", "y"]:
499
+ # worksheet.list[0]["x"] #=> "1" # i.e. worksheet[2, 1]
500
+ # worksheet.list[0]["y"] #=> "2" # i.e. worksheet[2, 2]
501
+ # worksheet.list[1]["x"] = "3" # i.e. worksheet[3, 1] = "3"
502
+ # worksheet.list[1]["y"] = "4" # i.e. worksheet[3, 2] = "4"
503
+ # worksheet.list.push({"x" => "5", "y" => "6"})
504
+ #
505
+ # Note that update is not sent to the server until you call save().
506
+ def list
507
+ @list ||= List.new(self)
508
+ end
509
+
510
+ # Returns a [row, col] pair for a cell name string.
511
+ # e.g.
512
+ # worksheet.cell_name_to_row_col("C2") #=> [2, 3]
513
+ def cell_name_to_row_col(cell_name)
514
+ unless cell_name.is_a?(String)
515
+ raise(
516
+ ArgumentError, format('Cell name must be a string: %p', cell_name)
517
+ )
518
+ end
519
+ unless cell_name.upcase =~ /^([A-Z]+)(\d+)$/
520
+ raise(
521
+ ArgumentError,
522
+ format(
523
+ 'Cell name must be only letters followed by digits with no ' \
524
+ 'spaces in between: %p',
525
+ cell_name
526
+ )
527
+ )
528
+ end
529
+ col = 0
530
+ Regexp.last_match(1).each_byte do |b|
531
+ # 0x41: "A"
532
+ col = col * 26 + (b - 0x41 + 1)
533
+ end
534
+ row = Regexp.last_match(2).to_i
535
+ [row, col]
536
+ end
537
+
538
+ def inspect
539
+ fields = { spreadsheet_id: spreadsheet.id, gid: gid }
540
+ fields[:title] = @title if @title
541
+ format(
542
+ "\#<%p %s>",
543
+ self.class,
544
+ fields.map { |k, v| format('%s=%p', k, v) }.join(', ')
545
+ )
546
+ end
547
+
548
+ # Merges a range of cells together. "MERGE_COLUMNS" is another option for merge_type
549
+ def merge_cells(top_row, left_col, num_rows, num_cols, merge_type: 'MERGE_ALL')
550
+ range = v4_range_object(top_row, left_col, num_rows, num_cols)
551
+ add_request({
552
+ merge_cells:
553
+ Google::Apis::SheetsV4::MergeCellsRequest.new(
554
+ range: range, merge_type: merge_type),
555
+ })
556
+ end
557
+
558
+ # Changes the formatting of a range of cells to match the given number format.
559
+ # For example to change A1 to a percentage with 1 decimal point:
560
+ # worksheet.set_number_format(1, 1, 1, 1, "##.#%")
561
+ # Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#numberformat
562
+ def set_number_format(top_row, left_col, num_rows, num_cols, pattern, type: "NUMBER")
563
+ number_format = Google::Apis::SheetsV4::NumberFormat.new(type: type, pattern: pattern)
564
+ format = Google::Apis::SheetsV4::CellFormat.new(number_format: number_format)
565
+ fields = 'userEnteredFormat(numberFormat)'
566
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
567
+ end
568
+
569
+ # Changes text alignment of a range of cells.
570
+ # Horizontal alignment can be "LEFT", "CENTER", or "RIGHT".
571
+ # Vertical alignment can be "TOP", "MIDDLE", or "BOTTOM".
572
+ # Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#HorizontalAlign
573
+ def set_text_alignment(
574
+ top_row, left_col, num_rows, num_cols,
575
+ horizontal: nil, vertical: nil)
576
+ return if horizontal.nil? && vertical.nil?
577
+
578
+ format = Google::Apis::SheetsV4::CellFormat.new(
579
+ horizontal_alignment: horizontal, vertical_alignment: vertical)
580
+ subfields =
581
+ (horizontal.nil? ? [] : ['horizontalAlignment']) +
582
+ (vertical.nil? ? [] : ['verticalAlignment'])
583
+
584
+ fields = 'userEnteredFormat(%s)' % subfields.join(',')
585
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
586
+ end
587
+
588
+ # Changes the background color on a range of cells. e.g.:
589
+ # worksheet.set_background_color(1, 1, 1, 1, GoogleDrive::Worksheet::Colors::DARK_YELLOW_1)
590
+ #
591
+ # background_color is an instance of Google::Apis::SheetsV4::Color.
592
+ def set_background_color(top_row, left_col, num_rows, num_cols, background_color)
593
+ format = Google::Apis::SheetsV4::CellFormat.new(background_color: background_color)
594
+ fields = 'userEnteredFormat(backgroundColor)'
595
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
596
+ end
597
+
598
+ # Change the text formatting on a range of cells. e.g., To set cell
599
+ # A1 to have red text that is bold and italic:
600
+ # worksheet.set_text_format(
601
+ # 1, 1, 1, 1,
602
+ # bold: true,
603
+ # italic: true,
604
+ # foreground_color: GoogleDrive::Worksheet::Colors::RED_BERRY)
605
+ #
606
+ # foreground_color is an instance of Google::Apis::SheetsV4::Color.
607
+ # Google API reference:
608
+ # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#textformat
609
+ def set_text_format(top_row, left_col, num_rows, num_cols, bold: false,
610
+ italic: false, strikethrough: false, font_size: nil,
611
+ font_family: nil, foreground_color: nil)
612
+ text_format = Google::Apis::SheetsV4::TextFormat.new(
613
+ bold: bold,
614
+ italic: italic,
615
+ strikethrough: strikethrough,
616
+ font_size: font_size,
617
+ font_family: font_family,
618
+ foreground_color: foreground_color
619
+ )
620
+
621
+ format = Google::Apis::SheetsV4::CellFormat.new(text_format: text_format)
622
+ fields = 'userEnteredFormat(textFormat)'
623
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
624
+ end
625
+
626
+ # Update the border styles for a range of cells.
627
+ # borders is a Hash of Google::Apis::SheetsV4::Border keyed with the
628
+ # following symbols: :top, :bottom, :left, :right, :innerHorizontal, :innerVertical
629
+ # e.g., To set a black double-line on the bottom of A1:
630
+ # update_borders(
631
+ # 1, 1, 1, 1,
632
+ # {bottom: Google::Apis::SheetsV4::Border.new(
633
+ # style: "DOUBLE", color: GoogleDrive::Worksheet::Colors::BLACK)})
634
+ def update_borders(top_row, left_col, num_rows, num_cols, borders)
635
+ request = Google::Apis::SheetsV4::UpdateBordersRequest.new(borders)
636
+ request.range = v4_range_object(top_row, left_col, num_rows, num_cols)
637
+ add_request({update_borders: request})
638
+ end
639
+
640
+ # Add an instance of Google::Apis::SheetsV4::Request (or its Hash
641
+ # equivalent) which will be applied on the next call to the save method.
642
+ def add_request(request)
643
+ @v4_requests.push(request)
644
+ end
645
+
646
+ # @api private
647
+ def worksheet_feed_id
648
+ gid_int = sheet_id
649
+ xor_val = gid_int > 31578 ? 474 : 31578
650
+ letter = gid_int > 31578 ? 'o' : ''
651
+ letter + (gid_int ^ xor_val).to_s(36)
652
+ end
653
+
654
+ private
655
+
656
+ def format_cells(top_row, left_col, num_rows, num_cols, format, fields)
657
+ add_request({
658
+ repeat_cell:
659
+ Google::Apis::SheetsV4::RepeatCellRequest.new(
660
+ range: v4_range_object(top_row, left_col, num_rows, num_cols),
661
+ cell: Google::Apis::SheetsV4::CellData.new(user_entered_format: format),
662
+ fields: fields
663
+ ),
664
+ })
665
+ end
666
+
667
+ def set_properties(properties)
668
+ @properties = properties
669
+ @title = @remote_title = properties.title
670
+ @index = properties.index
671
+ if properties.grid_properties.nil?
672
+ @max_rows = @max_cols = 0
673
+ else
674
+ @max_rows = properties.grid_properties.row_count
675
+ @max_cols = properties.grid_properties.column_count
676
+ end
677
+ @meta_modified = false
678
+ end
679
+
680
+ def reload_cells
681
+ response =
682
+ @session.sheets_service.get_spreadsheet(
683
+ spreadsheet.id,
684
+ ranges: "'%s'" % @remote_title,
685
+ fields: 'sheets.data.rowData.values(formattedValue,userEnteredValue,effectiveValue)'
686
+ )
687
+ update_cells_from_api_sheet(response.sheets[0])
688
+ end
689
+
690
+ def update_cells_from_api_sheet(api_sheet)
691
+ rows_data = api_sheet.data[0].row_data || []
692
+
693
+ @num_rows = rows_data.size
694
+ @num_cols = 0
695
+ @cells = {}
696
+ @input_values = {}
697
+ @numeric_values = {}
698
+
699
+ rows_data.each_with_index do |row_data, r|
700
+ next if !row_data.values
701
+ @num_cols = row_data.values.size if row_data.values.size > @num_cols
702
+ row_data.values.each_with_index do |cell_data, c|
703
+ k = [r + 1, c + 1]
704
+ @cells[k] = cell_data.formatted_value || ''
705
+ @input_values[k] = extended_value_to_str(cell_data.user_entered_value)
706
+ @numeric_values[k] =
707
+ cell_data.effective_value && cell_data.effective_value.number_value ?
708
+ cell_data.effective_value.number_value.to_f : nil
709
+ end
710
+ end
711
+
712
+ @modified.clear
713
+ end
714
+
715
+ def parse_cell_args(args)
716
+ if args.size == 1 && args[0].is_a?(String)
717
+ cell_name_to_row_col(args[0])
718
+ elsif args.size == 2 && args[0].is_a?(Integer) && args[1].is_a?(Integer)
719
+ if args[0] >= 1 && args[1] >= 1
720
+ args
721
+ else
722
+ raise(
723
+ ArgumentError,
724
+ format(
725
+ 'Row/col must be >= 1 (1-origin), but are %d/%d',
726
+ args[0], args[1]
727
+ )
728
+ )
729
+ end
730
+ else
731
+ raise(
732
+ ArgumentError,
733
+ format(
734
+ "Arguments must be either one String or two Integer's, but are %p",
735
+ args
736
+ )
737
+ )
738
+ end
739
+ end
740
+
741
+ def validate_cell_value(value)
742
+ if value =~ XML_INVALID_CHAR_REGEXP
743
+ raise(
744
+ ArgumentError,
745
+ format('Contains invalid character %p for XML 1.0: %p', $&, value)
746
+ )
747
+ end
748
+ end
749
+
750
+ def v4_range_object(top_row, left_col, num_rows, num_cols)
751
+ Google::Apis::SheetsV4::GridRange.new(
752
+ sheet_id: sheet_id,
753
+ start_row_index: top_row - 1,
754
+ start_column_index: left_col - 1,
755
+ end_row_index: top_row + num_rows - 1,
756
+ end_column_index: left_col + num_cols - 1
757
+ )
758
+ end
759
+
760
+ def extended_value_to_str(extended_value)
761
+ return '' if !extended_value
762
+ value =
763
+ extended_value.number_value ||
764
+ extended_value.string_value ||
765
+ extended_value.bool_value ||
766
+ extended_value.formula_value
767
+ value.to_s
768
+ end
769
+ end
770
+ end