google_drive2 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,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