google_drive_maintained 3.0.8

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