sheets_v4 0.6.0 → 0.8.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60f10846ff0c3b62cb29c487499eb717897a2b28916606de7d3199fe0424435f
4
- data.tar.gz: a6bb345b4379f94dbaa82b266943c144321e3bcf3404db34c1788e766813a766
3
+ metadata.gz: aaee6f486ac124cdefd974acfe3f18357ade8e9f90655a46e746ebfad53a391b
4
+ data.tar.gz: 0f3c1e513b45a841052be82817e5bcf157aba492282999f401bb069e048e2263
5
5
  SHA512:
6
- metadata.gz: 6914986ccca02b55b260b2a39e6777ce5e2cbca03f6fd00053d8ed96a960456831ee008bd175448da343eeaf0bcfc8c67309e489035b9792ec4ce51aba29d917
7
- data.tar.gz: f76c250d9b9bec531ca6185ef3b2b351df6e0a12eccb1d6f862cf6a63d4b81305798630d992d3ec22fb44a94cbd488f3736e85da6ff72302eb7b7cf87017e176
6
+ metadata.gz: c3762e42b88065e6bc76e5e749d629dc02073c2584053a9c979fd8b46196e70e155c2f06d87a768b511a93b30d38d09c5da6592ef875a9fb70cb7bd97b67ed7a
7
+ data.tar.gz: 64bed7345bcc3ee05d204b47770b297e8a53d05cc1deb6611bd114197cb46690df2683549e772b7b9453fbf158eba492efc05faf93e966047f57055e9d203d87
data/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ Changes for each release are listed in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/) for its releases.
6
6
 
7
+ ## v0.8.0 (2023-10-15)
8
+
9
+ [Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.7.0..v0.8.0)
10
+
11
+ Changes since v0.7.0:
12
+
13
+ * d8f695c Add extensions to Google::Apis::SheetsV4 classes (#26)
14
+ * ed2dc0e Show the cop names on Rubocop offenses when Rubocop is run from Rake (#25)
15
+ * c5bfcc1 Group the SheetsV4 methods in the Yard Docs to make them easier to find (#24)
16
+
17
+ ## v0.7.0 (2023-10-08)
18
+
19
+ [Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.6.0..v0.7.0)
20
+
21
+ Changes since v0.6.0:
22
+
23
+ * 616fe1f Add conversions bewteen Date/DateTime and spreadsheet values (#22)
24
+ * 6f37337 Rename SheetsV4::ValidateApiObjects to SheetsV4::ApiObjectValidation (#21)
25
+ * 0f76992 Rename SheetsV4::ValidateApiObjects::Validate to SheetsV4::ValidateApiObjects::ValidateApiObject (#20)
26
+ * e80040c Rename SheetsV4::CredentialCreator to SheetsV4::CreateCredential (#19)
27
+
7
28
  ## v0.6.0 (2023-10-03)
8
29
 
9
30
  [Full Changelog](https://github.com/main-branch/sheets_v4/compare/v0.5.0..v0.6.0)
data/README.md CHANGED
@@ -25,6 +25,11 @@ Unofficial helpers for the Google Sheets V4 API
25
25
  * [Method 2: constructing requests using hashes](#method-2-constructing-requests-using-hashes)
26
26
  * [Which method should be used?](#which-method-should-be-used)
27
27
  * [Validating requests](#validating-requests)
28
+ * [Google Extensions](#google-extensions)
29
+ * [SheetsService Extensions](#sheetsservice-extensions)
30
+ * [Spreadsheet Extensions](#spreadsheet-extensions)
31
+ * [Sheet Extensions](#sheet-extensions)
32
+ * [Working with dates and times](#working-with-dates-and-times)
28
33
  * [Colors](#colors)
29
34
  * [Development](#development)
30
35
  * [Contributing](#contributing)
@@ -125,7 +130,7 @@ sheets_service = File.open('credential.json') do |credential_source|
125
130
  end
126
131
  ```
127
132
 
128
- or an already constructed `Google::Auth::*`` object.
133
+ or an already constructed `Google::Auth::*` object.
129
134
 
130
135
  ### Building a request
131
136
 
@@ -273,6 +278,99 @@ request:
273
278
  SheetsV4.validate_api_object(schema: 'batch_update_spreadsheet_request', object: requests)
274
279
  ```
275
280
 
281
+ ### Google Extensions
282
+
283
+ The `SheetsV4::GoogleExtensions` module provides extensions to the `Google::Apis::SheetsV4`
284
+ classes to simplify use of the SheetsV4 API.
285
+
286
+ These extensions are not loaded by default and are not required to use other parts
287
+ of this Gem. To enable these extension, you must:
288
+
289
+ ```Ruby
290
+ require 'sheets_v4/google_extensions'
291
+ ```
292
+
293
+ #### SheetsService Extensions
294
+
295
+ Functionality is added to `get_spreadsheet` to set the `sheets_service` attribute on
296
+ the returned spreadsheet and set the `sheets_service` and `spreadsheet` attributes
297
+ on the sheets contained in the spreadsheet.
298
+
299
+ This can simplify complex spreadsheet updates because you won't have to pass a
300
+ sheets_service, spreadsheet, and sheet objects separately.
301
+
302
+ #### Spreadsheet Extensions
303
+
304
+ The `sheets_service` attribute is added and is set by `SheetsService#get_spreadsheet`.
305
+
306
+ #### Sheet Extensions
307
+
308
+ The `sheets_service` and `spreadsheet` attributes are added. Both are set when the
309
+ sheet's spreadsheet is loaded by `SheetsService#get_spreadsheet`.
310
+
311
+ ### Working with dates and times
312
+
313
+ Google Sheets, similar to other spreadsheet programs, stores dates and date-time
314
+ values as numbers. This system makes it easier to perform calculations with
315
+ dates and times.
316
+
317
+ This gem provides two sets of equavalent conversion methods. The first set is defined
318
+ as class methods on the `SheetsV4` class.
319
+
320
+ * `SheetsV4.date_to_gs(date)` returns a numeric cell value
321
+ * `SheetsV4.gs_to_date(cell_value)` returns a Date object
322
+ * `SheetsV4.datetime_to_gs(datetime)` returns a numeric cell value
323
+ * `SheetsV4.gs_to_datetime(cell_value)` returns a DateTime object
324
+
325
+ In order to convert to and from spreadsheet values, the spreadsheet timezone must
326
+ be known. A spreadsheet's timezone is found in the Google Sheets spreadsheet object's
327
+ properties:
328
+
329
+ ```Ruby
330
+ SheetsV4.default_spreadsheet_tz = spreadsheet.properties.time_zone
331
+ ```
332
+
333
+ If a time zone is not set using `SheetsV4.default_spreadsheet_tz`, a RuntimeError
334
+ will be raised when any of the above methods are used.
335
+
336
+ Here is an example of how the timezone can change the values fetched from the
337
+ spreadsheet:
338
+
339
+ ```Ruby
340
+ cell_value = 44333.191666666666
341
+
342
+ SheetsV4.default_spreadsheet_tz = 'America/New_York'
343
+ datetime = SheetsV4.gs_to_datetime(cell_value) #=> Mon, 17 May 2021 04:36:00 -0400
344
+ datetime.utc #=> 2021-05-17 08:36:00 UTC
345
+
346
+ SheetsV4.default_spreadsheet_tz = 'America/Los_Angeles'
347
+ datetime = SheetsV4.gs_to_datetime(cell_value) #=> Mon, 17 May 2021 04:36:00 -0700
348
+ datetime.utc #=> 2021-05-17 11:36:00 UTC
349
+ ```
350
+
351
+ Valid time zone names are those listed in one of these two sources:
352
+
353
+ * `ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.name }`
354
+ * `ActiveSupport::TimeZone.all.map(&:name)`
355
+
356
+ The `SheetsV4` methods works well if the spreadsheet timezone is constant through
357
+ the run of the program. If this is not the case -- for instance when working with
358
+ multiple spreadsheets whose timezones may be different -- then use
359
+ `SheetsV4::ConvertDatesAndTimes`.
360
+
361
+ Each instance of `SheetsV4::ConvertDatesAndTimes` has it's own spreadsheet timezone
362
+ used in the conversions. Instance methods for this class are the same as the
363
+ date conversion methods on the SheetsV4 class.
364
+
365
+ Example:
366
+
367
+ ```Ruby
368
+ cell_value = 44333.191666666666
369
+ converter = SheetsV4::ConvertDatesAndTimes.new('America/Los_Angeles')
370
+ datetime = SheetsV4.gs_to_datetime(cell_value) #=> Mon, 17 May 2021 04:36:00 -0700
371
+ datetime.utc #=> 2021-05-17 11:36:00 UTC
372
+ ```
373
+
276
374
  ### Colors
277
375
 
278
376
  Color objects (with appropriate :red, :green, :blue values) can be retrieved by name
data/Rakefile CHANGED
@@ -50,6 +50,9 @@ require 'rubocop/rake_task'
50
50
 
51
51
  RuboCop::RakeTask.new do |t|
52
52
  t.options = %w[
53
+ --display-cop-names
54
+ --display-style-guide
55
+ --extra-details
53
56
  --format progress
54
57
  --format json --out rubocop-report.json
55
58
  ]
data/examples/README.md CHANGED
@@ -46,3 +46,4 @@
46
46
  * [ ] Protected ranges
47
47
  * [ ] Resize a sheet
48
48
  * [ ] Retrying on error
49
+ * [ ] Set a custom datetime or decimal format for a range [1](https://developers.google.com/sheets/api/samples/formatting#set_a_custom_datetime_or_decimal_format_for_a_range)
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SheetsV4
4
- module ValidateApiObjects
4
+ module ApiObjectValidation
5
5
  # Load the Google Discovery API description for the Sheets V4 API
6
6
  #
7
7
  # @example
8
8
  # logger = Logger.new(STDOUT, :level => Logger::ERROR)
9
- # schemas = SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call
9
+ # schemas = SheetsV4::ApiObjectValidation::LoadSchemas.new(logger:).call
10
10
  #
11
11
  # @api private
12
12
  #
@@ -18,7 +18,7 @@ module SheetsV4
18
18
  # The schemas are only loaded once and cached.
19
19
  #
20
20
  # @example
21
- # schema_loader = SheetsV4::ValidateApiObjects::LoadSchemas.new
21
+ # schema_loader = SheetsV4::ApiObjectValidation::LoadSchemas.new
22
22
  #
23
23
  # @param logger [Logger] the logger to use
24
24
  #
@@ -30,8 +30,8 @@ module SheetsV4
30
30
  #
31
31
  # @example
32
32
  # logger = Logger.new(STDOUT, :level => Logger::INFO)
33
- # validator = SheetsV4::ValidateApiObjects::LoadSchemas.new(logger)
34
- # validator.logger == logger # => true
33
+ # schema_loader = SheetsV4::ApiObjectValidation::LoadSchemas.new(logger)
34
+ # schema_loader.logger == logger # => true
35
35
  #
36
36
  # @return [Logger]
37
37
  #
@@ -84,7 +84,7 @@ module SheetsV4
84
84
  # Log an error and raise a RuntimeError based on the HTTP response code
85
85
  # @param http_response [Net::HTTPResponse] the HTTP response
86
86
  # @return [void]
87
- # @raises [RuntimeError]
87
+ # @raise [RuntimeError]
88
88
  # @api private
89
89
  def raise_error(http_response)
90
90
  message = "HTTP Error '#{http_response.code}' loading schemas from '#{http_response.uri}'"
@@ -146,7 +146,7 @@ module SheetsV4
146
146
  # @return [void]
147
147
  # @api private
148
148
  def post_process_schemas(schemas)
149
- SheetsV4::ValidateApiObjects::TraverseObjectTree.call(
149
+ SheetsV4::ApiObjectValidation::TraverseObjectTree.call(
150
150
  object: schemas, visitor: ->(path:, object:) { schema_visitor(path:, object:) }
151
151
  )
152
152
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SheetsV4
4
- module ValidateApiObjects
4
+ module ApiObjectValidation
5
5
  # Resolve a JSON schema reference to a Google Sheets API schema
6
6
  #
7
7
  # This class uses the Google Discovery API to get the schemas. Any schema reference
@@ -9,14 +9,14 @@ module SheetsV4
9
9
  # name in the Google Discovery API and returning the schema object (as a Hash).
10
10
  #
11
11
  # This means that `{ "$ref": "cell_data" }` is resolved by returning
12
- # `SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call['cell_data']`.
12
+ # `SheetsV4::ApiObjectValidation::LoadSchemas.new(logger:).call['cell_data']`.
13
13
  #
14
- # An RuntimeError is raised if `SheetsV4::ValidateApiObjects::LoadSchemas.new.call`
14
+ # An RuntimeError is raised if `SheetsV4::ApiObjectValidation::LoadSchemas.new.call`
15
15
  # does not have a key matching the schema name.
16
16
  #
17
17
  # @example
18
18
  # logger = Logger.new(STDOUT, level: Logger::INFO)
19
- # ref_resolver = SheetsV4::ValidateApiObjects::ResolveSchemaRef.new(logger:)
19
+ # ref_resolver = SheetsV4::ApiObjectValidation::ResolveSchemaRef.new(logger:)
20
20
  # people_schema = { 'type' => 'array', 'items' => { '$ref' => 'person' } }
21
21
  # json_validator = JSONSchemer.schema(people_schema, ref_resolver:)
22
22
  # people_json = [{ 'name' => { 'first' => 'John', 'last' => 'Doe' } }]
@@ -53,7 +53,7 @@ module SheetsV4
53
53
 
54
54
  # Resolve a JSON schema reference
55
55
  #
56
- # @param ref [URI] the reference to resolve usually in the form "json-schemer://schema/{name}"
56
+ # @param ref [URI] the reference to resolve usually in the form "json-schemer://schema/[name]"
57
57
  #
58
58
  # @return [Hash] the schema object as a hash
59
59
  #
@@ -62,7 +62,7 @@ module SheetsV4
62
62
  def call(ref)
63
63
  schema_name = ref.path[1..]
64
64
  logger.debug { "Reading schema #{schema_name}" }
65
- schemas = SheetsV4::ValidateApiObjects::LoadSchemas.new(logger:).call
65
+ schemas = SheetsV4::ApiObjectValidation::LoadSchemas.new(logger:).call
66
66
  schemas[schema_name].tap do |schema_object|
67
67
  raise "Schema for #{ref} not found" unless schema_object
68
68
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SheetsV4
4
- module ValidateApiObjects
4
+ module ApiObjectValidation
5
5
  # Visit all objects in arbitrarily nested object tree of hashes and/or arrays
6
6
  #
7
7
  # @api public
@@ -16,7 +16,7 @@ module SheetsV4
16
16
  #
17
17
  # ```Ruby
18
18
  # visitor = -> (path:, object:) { puts "path: #{path}, object: #{obj}" }
19
- # SheetsV4::ValidateApiObjects::TraverseObjectTree.call(object:, visitor:)
19
+ # SheetsV4::ApiObjectValidation::TraverseObjectTree.call(object:, visitor:)
20
20
  # ```
21
21
  #
22
22
  # @example Given a simple object (not very exciting)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/inflector'
5
+ require 'json_schemer'
6
+
7
+ module SheetsV4
8
+ module ApiObjectValidation
9
+ # Validate objects against a Google Sheets API request object schema
10
+ #
11
+ # @api public
12
+ #
13
+ class ValidateApiObject
14
+ # Create a new api object validator
15
+ #
16
+ # By default, a nil logger is used. This means that no messages are logged.
17
+ #
18
+ # @example
19
+ # validator = SheetsV4::ApiObjectValidation::ValidateApiObject.new
20
+ #
21
+ # @param logger [Logger] the logger to use
22
+ #
23
+ def initialize(logger: Logger.new(nil))
24
+ @logger = logger
25
+ end
26
+
27
+ # The logger to use internally
28
+ #
29
+ # Validation errors are logged at the error level. Other messages are logged
30
+ # at the debug level.
31
+ #
32
+ # @example
33
+ # logger = Logger.new(STDOUT, :level => Logger::INFO)
34
+ # validator = SheetsV4::ApiObjectValidation::ValidateApiObject.new(logger)
35
+ # validator.logger == logger # => true
36
+ # validator.logger.debug { "Debug message" }
37
+ #
38
+ # @return [Logger]
39
+ #
40
+ attr_reader :logger
41
+
42
+ # Validate the object using the JSON schema named schema_name
43
+ #
44
+ # @example
45
+ # schema_name = 'batch_update_spreadsheet_request'
46
+ # object = { 'requests' => [] }
47
+ # validator = SheetsV4::ApiObjectValidation::ValidateApiObject.new
48
+ # validator.call(schema_name:, object:)
49
+ #
50
+ # @param schema_name [String] the name of the schema to validate against
51
+ # @param object [Object] the object to validate
52
+ #
53
+ # @raise [RuntimeError] if the object does not conform to the schema
54
+ #
55
+ # @return [void]
56
+ #
57
+ def call(schema_name:, object:)
58
+ logger.debug { "Validating #{object} against #{schema_name}" }
59
+
60
+ schema = { '$ref' => schema_name }
61
+ schemer = JSONSchemer.schema(schema, ref_resolver:)
62
+ errors = schemer.validate(object)
63
+ raise_error!(schema_name, object, errors) if errors.any?
64
+
65
+ logger.debug { "Object #{object} conforms to #{schema_name}" }
66
+ end
67
+
68
+ private
69
+
70
+ # The resolver to use to resolve JSON schema references
71
+ # @return [ResolveSchemaRef]
72
+ # @api private
73
+ def ref_resolver = @ref_resolver ||= SheetsV4::ApiObjectValidation::ResolveSchemaRef.new(logger:)
74
+
75
+ # Raise an error when the object does not conform to the schema
76
+ # @return [void]
77
+ # @raise [RuntimeError]
78
+ # @api private
79
+ def raise_error!(schema_name, object, errors)
80
+ error = errors.first['error']
81
+ error_message = "Object #{object} does not conform to #{schema_name}: #{error}"
82
+ logger.error(error_message)
83
+ raise error_message
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SheetsV4
4
+ # Validate API Objects against the Google Discovery API
5
+ #
6
+ # @example
7
+ # logger = Logger.new(STDOUT, :level => Logger::ERROR)
8
+ # schema_name = 'batch_update_spreadsheet_request'
9
+ # object = { 'requests' => [] }
10
+ # SheetsV4::ApiObjectValidation::ValidateApiObject.new(logger:).call(schema_name:, object:)
11
+ #
12
+ # @api public
13
+ #
14
+ module ApiObjectValidation; end
15
+ end
16
+
17
+ require_relative 'api_object_validation/load_schemas'
18
+ require_relative 'api_object_validation/resolve_schema_ref'
19
+ require_relative 'api_object_validation/traverse_object_tree'
20
+ require_relative 'api_object_validation/validate_api_object'
@@ -23,7 +23,6 @@ module SheetsV4
23
23
  #
24
24
  # @param method_name [#to_sym] the name of the color
25
25
  # @param arguments [Array] ignored
26
- # @param block [Proc] ignored
27
26
  # @return [Hash] the color object
28
27
  # @api private
29
28
  def method_missing(method_name, *arguments, &)
@@ -0,0 +1,243 @@
1
+ # Copyright (c) 2023 Yahoo
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'active_support'
6
+ require 'active_support/values/time_zone'
7
+ require 'active_support/core_ext/numeric/time'
8
+ require 'active_support/core_ext/string/zones'
9
+
10
+ module SheetsV4
11
+ # Convert between Ruby Date and DateTime objects and Google Sheets values
12
+ #
13
+ # Google Sheets uses decimal values to represent dates and times. These
14
+ # conversion routines allows converting from Ruby Date and DateTime objects
15
+ # and values that Google Sheets recognize.
16
+ #
17
+ # DateTime objects passed to `datetime_to_gs` or `date_to_gs` are converted to
18
+ # the time zone of the spreadsheet given in the initializer. DateTime objects
19
+ # returned by `gs_to_datetime` are always in the spreadsheet's time zone.
20
+ #
21
+ # Valid time zone names are those listed in one of these two sources:
22
+ # * `ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.name }`
23
+ # * `ActiveSupport::TimeZone.all.map(&:name)`
24
+ #
25
+ # @example
26
+ # tz = spreadsheet.properties.time_zone #=> e.g. 'America/Los_Angeles'
27
+ # converter = SheetsV4::ConvertDatesAndTimes.new(tz)
28
+ # date = Date.parse('1967-03-15')
29
+ # converter.date_to_gs(date) #=> 24546
30
+ # converter.gs_to_date(24546) #=> #<Date: 1967-03-15 ((2439565j,0s,0n),+0s,2299161j)>
31
+ #
32
+ # date_time = DateTime.parse('2021-05-17 11:36:00 UTC')
33
+ # converter.datetime_to_gs(date_time) #=> 44333.191666666666
34
+ # converter.gs_to_datetime(44333.191666666666)
35
+ # #=> #<DateTime: 2021-05-17T11:36:00+00:00 ((2459352j,41760s,0n),+0s,2299161j)>
36
+ #
37
+ # @api public
38
+ #
39
+ class ConvertDatesAndTimes
40
+ # The time zone passed into the initializer
41
+ #
42
+ # @example
43
+ # time_zone = 'UTC'
44
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
45
+ # converter.spreadsheet_tz
46
+ # #=> #<ActiveSupport::TimeZone:0x00007fe39000f908 @name="America/Los_Angeles", ...>
47
+ #
48
+ # @return [ActiveSupport::TimeZone] the time zone
49
+ #
50
+ attr_reader :spreadsheet_tz
51
+
52
+ # Initialize the conversion routines for a spreadsheet
53
+ #
54
+ # @example
55
+ # time_zone = 'America/Los_Angeles'
56
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
57
+ #
58
+ # @param spreadsheet_tz [String] the time zone set in the spreadsheet properties
59
+ #
60
+ # @raise [RuntimeError] if the time zone is not valid
61
+ #
62
+ def initialize(spreadsheet_tz)
63
+ @spreadsheet_tz = ActiveSupport::TimeZone.new(spreadsheet_tz)
64
+ raise "Invalid time zone '#{spreadsheet_tz}'" unless @spreadsheet_tz
65
+ end
66
+
67
+ # Convert a Ruby DateTime object to a Google Sheets date time value
68
+ #
69
+ # @example
70
+ # time_zone = 'America/Los_Angeles'
71
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
72
+ # date_time = DateTime.parse('2021-05-17 11:36:00 UTC')
73
+ # converter.datetime_to_gs(date_time) #=> 44333.191666666666
74
+ #
75
+ # @param datetime [DateTime, nil] the date and time to convert
76
+ #
77
+ # @return [Float, String] the value to store in a Google Sheets cell
78
+ # Returns a Float if datetime is not nil; otherwise, returns an empty string.
79
+ #
80
+ def datetime_to_gs(datetime)
81
+ return '' unless datetime
82
+
83
+ time = datetime.to_time.in_time_zone(spreadsheet_tz)
84
+ unix_to_gs_epoch(replace_time_zone(time, 'UTC').to_i)
85
+ end
86
+
87
+ # Convert a Google Sheets date time value to a DateTime object
88
+ #
89
+ # Time is rounded to the nearest second. The DateTime object returned is in
90
+ # the spreadsheet's time zone given in the initiaizer.
91
+ #
92
+ # @example
93
+ # time_zone = 'America/Los_Angeles'
94
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
95
+ # gs_value = 44333.191666666666
96
+ # converter.gs_to_datetime(gs_value) #=> #<DateTime: 2021-05-17T04:35:59-07:00 ...>
97
+ #
98
+ # @param gs_datetime [Float, "", nil] the value from the Google Sheets cell
99
+ #
100
+ # @return [DateTime, nil] the value represented by gs_datetime
101
+ # Returns a DateTime object if a Float was given; otherwise, returns nil if an
102
+ # empty string or nil was given.
103
+ #
104
+ def gs_to_datetime(gs_datetime)
105
+ return nil if gs_datetime.nil? || gs_datetime == ''
106
+
107
+ raise 'gs_datetime is a string' if gs_datetime.is_a?(String)
108
+
109
+ unix_epoch_datetime = gs_to_unix_epoch(gs_datetime.to_f)
110
+ time = Time.at_without_coercion(unix_epoch_datetime, in: 'UTC')
111
+ replace_time_zone(time, spreadsheet_tz).to_datetime
112
+ end
113
+
114
+ # Convert a Ruby Date object to a Google Sheets date value
115
+ #
116
+ # The Google Sheets date value is a float.
117
+ #
118
+ # @example with a Date object
119
+ # time_zone = 'America/Los_Angeles'
120
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
121
+ # date = Date.parse('2021-05-17')
122
+ # converter.date_to_gs(date) #=> 44333
123
+ #
124
+ # @example with a DateTime object
125
+ # time_zone = 'America/Los_Angeles'
126
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
127
+ # date_time = DateTime.parse('2021-05-17 11:36:00 UTC')
128
+ # converter.date_to_gs(date_time) #=> 44333
129
+ #
130
+ # @param date [DateTime, Date, nil] the date to convert
131
+ #
132
+ # @return [Float, String] the value to sstore in a Google Sheets cell
133
+ # Returns a Float if date is not nil; otherwise, returns an empty string
134
+ #
135
+ def date_to_gs(date)
136
+ return datetime_to_gs(date).to_i if date.is_a?(DateTime)
137
+
138
+ return (date - gs_epoch_start_date).to_i if date.is_a?(Date)
139
+
140
+ ''
141
+ end
142
+
143
+ # Convert a Google Sheets date value to a Ruby Date object
144
+ #
145
+ # @example with a Date value
146
+ # time_zone = 'America/Los_Angeles'
147
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
148
+ # gs_value = 44333
149
+ # converter.gs_to_date(gs_value) #=> #<Date: 2021-05-17 ...>
150
+ #
151
+ # @example with a Date and Time value
152
+ # time_zone = 'America/Los_Angeles'
153
+ # converter = SheetsV4::ConvertDatesAndTimes.new(time_zone)
154
+ # gs_value = 44333.191666666666
155
+ # converter.gs_to_date(gs_value) #=> #<Date: 2021-05-17 ...>
156
+ #
157
+ # @param gs_date [Float, "", nil] the value from the Google Sheets cell
158
+ #
159
+ # @return [Date, nil] the value represented by gs_date
160
+ # Returns a Date object if a Float was given; otherwise, returns nil if an
161
+ # empty string or nil was given.
162
+ #
163
+ def gs_to_date(gs_date)
164
+ return nil if gs_date.nil? || gs_date == ''
165
+
166
+ raise 'gs_date is a string' if gs_date.is_a?(String)
167
+
168
+ (gs_epoch_start_date + gs_date.to_i)
169
+ end
170
+
171
+ private
172
+
173
+ # The Google Sheets epoch start Date to use when calculating dates
174
+ # @return [Date] the 'zero' Date
175
+ # @api private
176
+ def gs_epoch_start_date
177
+ @gs_epoch_start_date ||= Date.parse('1899-12-30')
178
+ end
179
+
180
+ # The number of seconds in a day
181
+ #
182
+ # @return [Integer] number of seconds in a day
183
+ #
184
+ SECONDS_PER_DAY = 86_400
185
+
186
+ # The number of seconds between the Google Sheets Epoch and Unix Epoch
187
+ #
188
+ # Effectively the number of seconds between 1899-12-30 00:00:00 UTC and
189
+ # 1970-01-01 00:00:00 UTC.
190
+ #
191
+ # @return [Integer] the number of seconds
192
+ #
193
+ SECONDS_BETWEEN_GS_AND_UNIX_EPOCHS = 25_569 * 86_400
194
+
195
+ # Convert a gs_datetime to unix time
196
+ #
197
+ # @param gs_datetime [Float] time relative to the Google Sheets Epoch
198
+ #
199
+ # gs_datetime is a float representing the number of days since the start of
200
+ # the Google Sheets epoch (1899-12-30 00:00:00 UTC).
201
+ #
202
+ # @return [Integer] the same datetime in Unix Time rounded to the nearest second
203
+ #
204
+ # Unix time is an integer representing the number of seconds since the start of
205
+ # the Unix Epoch (1970-01-01 00:00:00 UTC). The number returned is rounded
206
+ # to the nearest second.
207
+ #
208
+ # @api private
209
+ #
210
+ def gs_to_unix_epoch(gs_datetime)
211
+ (gs_datetime * SECONDS_PER_DAY).round - SECONDS_BETWEEN_GS_AND_UNIX_EPOCHS
212
+ end
213
+
214
+ # Convert a unix time to gs_datetime
215
+ #
216
+ # @param unix_time [Integer] seconds since the Unix epoch 1970-01-01 00:00:00 UTC
217
+ #
218
+ # @return [Float] days since the Google Sheets epoch 1899-12-30 00:00:00 UTC
219
+ #
220
+ # @api private
221
+ #
222
+ def unix_to_gs_epoch(unix_time)
223
+ (unix_time + SECONDS_BETWEEN_GS_AND_UNIX_EPOCHS).to_f / SECONDS_PER_DAY
224
+ end
225
+
226
+ # Given a time, change the time zone without impacting the displayed date/time
227
+ #
228
+ # @example
229
+ # replace_time_zone(Time.parse('2021-05-21 11:40 UTC'), 'America/Los_Angeles')
230
+ # #=> '2021-05-21 11:40 -0700'
231
+ #
232
+ # @param time [Time] the time object to adjust
233
+ # @param time_zone_name [String] the desired time zone
234
+ #
235
+ # @return [Time] the resulting time object
236
+ #
237
+ # @api private
238
+ #
239
+ def replace_time_zone(time, time_zone_name)
240
+ time.asctime.in_time_zone(time_zone_name)
241
+ end
242
+ end
243
+ end
@@ -5,7 +5,7 @@ require 'json_schemer'
5
5
  module SheetsV4
6
6
  # Creates a Google API credential with an access token
7
7
  #
8
- class CredentialCreator
8
+ class CreateCredential
9
9
  # Creates a Google API credential with an access token
10
10
  #
11
11
  # This wraps the boiler plate code into one function to make constructing a
@@ -14,7 +14,7 @@ module SheetsV4
14
14
  # @example Constructing a credential from the contents of ~/.credential
15
15
  # credential_source = File.read(File.join(Dir.home, '.credential'))
16
16
  # scope = Google::Apis::SheetsV4::AUTH_SPREADSHEETS
17
- # credential = GoogleApisHelpers.credential(credential_source, scope)
17
+ # credential = SheetsV4::CreateCredential.call(credential_source, scope)
18
18
  #
19
19
  # @param [Google::Auth::*, String, IO, nil] credential_source may be one of four things:
20
20
  # (1) a previously created credential that you want to reuse, (2) a credential read