csv_plus_plus 0.1.2 → 0.2.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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  6. data/lib/csv_plus_plus/cell.rb +29 -41
  7. data/lib/csv_plus_plus/cli.rb +53 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +71 -71
  9. data/lib/csv_plus_plus/color.rb +32 -7
  10. data/lib/csv_plus_plus/compiler.rb +98 -66
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
  12. data/lib/csv_plus_plus/entities/boolean.rb +26 -10
  13. data/lib/csv_plus_plus/entities/builtins.rb +66 -24
  14. data/lib/csv_plus_plus/entities/date.rb +42 -6
  15. data/lib/csv_plus_plus/entities/entity.rb +17 -69
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
  17. data/lib/csv_plus_plus/entities/function.rb +34 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +49 -10
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +30 -11
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
  23. data/lib/csv_plus_plus/entities/string.rb +23 -7
  24. data/lib/csv_plus_plus/entities.rb +7 -16
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +25 -2
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  33. data/lib/csv_plus_plus/error.rb +5 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +25 -10
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
  38. data/lib/csv_plus_plus/lexer.rb +64 -1
  39. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  40. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  41. data/lib/csv_plus_plus/modifier/expand.rb +78 -0
  42. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  43. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  44. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  45. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  46. data/lib/csv_plus_plus/modifier.rb +89 -160
  47. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  48. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  49. data/lib/csv_plus_plus/options/options.rb +97 -0
  50. data/lib/csv_plus_plus/options.rb +34 -77
  51. data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
  52. data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
  53. data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
  54. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  55. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  56. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  57. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  58. data/lib/csv_plus_plus/reader.rb +14 -0
  59. data/lib/csv_plus_plus/row.rb +53 -12
  60. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  61. data/lib/csv_plus_plus/runtime/position.rb +242 -0
  62. data/lib/csv_plus_plus/runtime/references.rb +115 -0
  63. data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
  64. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  65. data/lib/csv_plus_plus/runtime.rb +34 -191
  66. data/lib/csv_plus_plus/source_code.rb +71 -0
  67. data/lib/csv_plus_plus/template.rb +71 -39
  68. data/lib/csv_plus_plus/version.rb +2 -1
  69. data/lib/csv_plus_plus/writer/csv.rb +37 -8
  70. data/lib/csv_plus_plus/writer/excel.rb +25 -5
  71. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
  72. data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
  73. data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
  74. data/lib/csv_plus_plus/writer/merger.rb +31 -0
  75. data/lib/csv_plus_plus/writer/open_document.rb +21 -2
  76. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
  77. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  78. data/lib/csv_plus_plus/writer.rb +79 -10
  79. data/lib/csv_plus_plus.rb +47 -18
  80. metadata +50 -21
  81. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  82. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  83. data/lib/csv_plus_plus/data_validation.rb +0 -138
  84. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
  85. data/lib/csv_plus_plus/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
  87. data/lib/csv_plus_plus/expand.rb +0 -20
  88. data/lib/csv_plus_plus/google_options.rb +0 -27
  89. data/lib/csv_plus_plus/graph.rb +0 -62
  90. data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
  91. data/lib/csv_plus_plus/references.rb +0 -68
  92. data/lib/csv_plus_plus/scope.rb +0 -196
  93. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  94. data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
  95. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
  96. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  97. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,62 +1,96 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
5
  # Contains the data from a parsed csvpp template.
5
6
  #
6
7
  # @attr_reader rows [Array<Row>] The +Row+s that comprise this +Template+
7
- # @attr_reader scope [Scope] The +Scope+ containing all function and variable references
8
+ # @attr_reader runtime [Runtime] The +Runtime+ containing all function and variable references
8
9
  class Template
9
- attr_reader :rows, :scope
10
+ extend ::T::Sig
10
11
 
12
+ sig { returns(::T::Array[::CSVPlusPlus::Row]) }
13
+ attr_reader :rows
14
+
15
+ sig { returns(::CSVPlusPlus::Runtime::Runtime) }
16
+ attr_reader :runtime
17
+
18
+ sig { params(rows: ::T::Array[::CSVPlusPlus::Row], runtime: ::CSVPlusPlus::Runtime::Runtime).void }
11
19
  # @param rows [Array<Row>] The +Row+s that comprise this +Template+
12
- # @param scope [Scope] The +Scope+ containing all function and variable references
13
- def initialize(rows:, scope:)
14
- @scope = scope
20
+ # @param runtime [Runtime] The +Runtime+ containing all function and variable references
21
+ def initialize(rows:, runtime:)
15
22
  @rows = rows
23
+ @runtime = runtime
16
24
  end
17
25
 
18
- # @return [String]
19
- def to_s
20
- "Template(rows: #{@rows}, scope: #{@scope})"
26
+ sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).void }
27
+ # Only run after expanding all rows, now we can bind all [[var=]] modifiers to a variable. There are two distinct
28
+ # types of variable bindings here:
29
+ #
30
+ # * Binding to a cell: for this we just make an +A1Reference+ to the cell itself (A1, B4, etc)
31
+ # * Binding to a cell within an expand: the variable can only be resolved within that expand and needs to be
32
+ # relative to it's row (it can't be an absolute cell reference like above)
33
+ #
34
+ # @param runtime [Runtime] The current runtime
35
+ # rubocop:disable Metrics/MethodLength
36
+ def bind_all_vars!(runtime)
37
+ runtime.position.map_rows(@rows) do |row|
38
+ # rubocop:disable Style/MissingElse
39
+ if row.unexpanded?
40
+ # rubocop:enable Style/MissingElse
41
+ raise(
42
+ ::CSVPlusPlus::Error::CompilerError,
43
+ 'Template#expand_rows! must be called before Template#bind_all_vars!'
44
+ )
45
+ end
46
+
47
+ runtime.position.map_row(row.cells) do |cell|
48
+ bind_vars(cell, row.modifier.expand)
49
+ end
50
+ end
21
51
  end
52
+ # rubocop:enable Metrics/MethodLength
22
53
 
23
- # Apply any expand= modifiers to the parsed template
54
+ sig { returns(::T::Array[::CSVPlusPlus::Row]) }
55
+ # Apply expand= (adding rows to the results) modifiers to the parsed template. This happens in towards the end of
56
+ # compilation because expanding rows will change the relative rownums as rows are added, and variables can't be
57
+ # bound until the rows have been assigned their final rownums.
24
58
  #
25
59
  # @return [Array<Row>]
26
60
  def expand_rows!
27
- expanded_rows = []
28
- row_index = 0
29
- expand_rows(
30
- lambda do |new_row|
31
- new_row.index = row_index
32
- expanded_rows << new_row
33
- row_index += 1
61
+ # TODO: make it so that an infinite expand will not overwrite the rows below it, but instead merge with them
62
+ @rows =
63
+ rows.reduce([]) do |expanded_rows, row|
64
+ if row.modifier.expand
65
+ row.expand_rows(starts_at: expanded_rows.length, into: expanded_rows)
66
+ else
67
+ expanded_rows << row.tap { |r| r.index = expanded_rows.length }
68
+ end
34
69
  end
35
- )
36
-
37
- @rows = expanded_rows
38
70
  end
39
71
 
72
+ sig { void }
40
73
  # Make sure that the template has a valid amount of infinite expand modifiers
41
- #
42
- # @param runtime [Runtime] The compiler's current runtime
43
- def validate_infinite_expands(runtime)
74
+ def validate_infinite_expands
44
75
  infinite_expand_rows = @rows.filter { |r| r.modifier.expand&.infinite? }
45
76
  return unless infinite_expand_rows.length > 1
46
77
 
47
- runtime.raise_formula_syntax_error(
48
- 'You can only have one infinite expand= (on all others you must specify an amount)',
49
- infinite_expand_rows[1]
78
+ raise(
79
+ ::CSVPlusPlus::Error::ModifierSyntaxError.new(
80
+ 'You can only have one infinite expand= (on all others you must specify an amount)',
81
+ bad_input: infinite_expand_rows[1].to_s
82
+ )
50
83
  )
51
84
  end
52
85
 
53
- # Provide a summary of the state of the template (and it's +@scope+)
86
+ sig { returns(::String) }
87
+ # Provide a summary of the state of the template (and it's +@runtime+)
54
88
  #
55
- # @return [String]
89
+ # @return [::String]
56
90
  def verbose_summary
57
91
  # TODO: we can probably include way more stats in here
58
92
  <<~SUMMARY
59
- #{@scope.verbose_summary}
93
+ #{@runtime.scope.verbose_summary}
60
94
 
61
95
  > #{@rows.length} rows to be written
62
96
  SUMMARY
@@ -64,17 +98,15 @@ module CSVPlusPlus
64
98
 
65
99
  private
66
100
 
67
- def expand_rows(push_row_fn)
68
- # TODO: make it so that an infinite expand will not overwrite the rows below it, but
69
- # instead merge with them
70
- rows.each do |row|
71
- if row.modifier.expand
72
- row.expand_amount.times do
73
- push_row_fn.call(row.deep_clone)
74
- end
75
- else
76
- push_row_fn.call(row)
77
- end
101
+ sig { params(cell: ::CSVPlusPlus::Cell, expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)).void }
102
+ def bind_vars(cell, expand)
103
+ var = cell.modifier.var
104
+ return unless var
105
+
106
+ if expand
107
+ @runtime.bind_variable_in_expand(var, expand)
108
+ else
109
+ @runtime.bind_variable_to_cell(var)
78
110
  end
79
111
  end
80
112
  end
@@ -1,6 +1,7 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
- VERSION = '0.1.2'
5
+ VERSION = '0.2.0'
5
6
  public_constant :VERSION
6
7
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative './file_backer_upper'
@@ -5,29 +6,57 @@ require_relative './file_backer_upper'
5
6
  module CSVPlusPlus
6
7
  module Writer
7
8
  # A class that can output a +Template+ to CSV
8
- class CSV < ::CSVPlusPlus::Writer::BaseWriter
9
+ class CSV < ::CSVPlusPlus::Writer::Writer
10
+ extend ::T::Sig
9
11
  include ::CSVPlusPlus::Writer::FileBackerUpper
12
+ include ::CSVPlusPlus::Writer::Merger
10
13
 
11
- # write a +template+ to CSV
14
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
15
+ # @param options [Options::FileOptions]
16
+ # @param position [Runtime::Position]
17
+ def initialize(options, position)
18
+ super(position)
19
+
20
+ @reader = ::T.let(::CSVPlusPlus::Reader::CSV.new(options), ::CSVPlusPlus::Reader::CSV)
21
+ @options = options
22
+ end
23
+
24
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
25
+ # Write a +template+ to CSV
26
+ #
27
+ # @param template [Template] The template to use as input to be written. It should have been compiled by calling
28
+ # Compiler#compile_template
12
29
  def write(template)
13
- # TODO: also read it and merge the results
14
30
  ::CSV.open(@options.output_filename, 'wb') do |csv|
15
- template.rows.each do |row|
31
+ @position.map_rows(template.rows) do |row|
16
32
  csv << build_row(row)
17
33
  end
18
34
  end
19
35
  end
20
36
 
21
- protected
37
+ sig { override.void }
38
+ # Write a backup of the current spreadsheet.
39
+ def write_backup
40
+ backup_file(@options)
41
+ end
22
42
 
23
- def load_requires
24
- require('csv')
43
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::String)) }
44
+ # Turn the cell into a CSV-
45
+ def evaluate_cell(cell)
46
+ if (ast = cell.ast)
47
+ "=#{ast.evaluate(@position)}"
48
+ else
49
+ cell.value
50
+ end
25
51
  end
26
52
 
27
53
  private
28
54
 
55
+ sig { params(row: ::CSVPlusPlus::Row).returns(::T::Array[::T.nilable(::String)]) }
29
56
  def build_row(row)
30
- row.cells.map(&:to_csv)
57
+ @position.map_row(row.cells) do |cell, _i|
58
+ merge_cell_value(existing_value: @reader.value_at(cell), new_value: evaluate_cell(cell), options: @options)
59
+ end
31
60
  end
32
61
  end
33
62
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative './file_backer_upper'
@@ -6,17 +7,36 @@ require_relative './rubyxl_builder'
6
7
  module CSVPlusPlus
7
8
  module Writer
8
9
  # A class that can output a +Template+ to an Excel file
9
- class Excel < ::CSVPlusPlus::Writer::BaseWriter
10
+ class Excel < ::CSVPlusPlus::Writer::Writer
11
+ extend ::T::Sig
10
12
  include ::CSVPlusPlus::Writer::FileBackerUpper
11
13
 
12
- # write the +template+ to an Excel file
14
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
15
+ # @param options [Options::FileOptions]
16
+ # @param position [Runtime::Position]
17
+ def initialize(options, position)
18
+ super(position)
19
+
20
+ @options = options
21
+ end
22
+
23
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
24
+ # Write the +template+ to an Excel file
25
+ #
26
+ # @param template [Template] The template to write
13
27
  def write(template)
14
28
  ::CSVPlusPlus::Writer::RubyXLBuilder.new(
15
- input_filename: @options.output_filename,
16
- rows: template.rows,
17
- sheet_name: @options.sheet_name
29
+ options: @options,
30
+ position: @position,
31
+ rows: template.rows
18
32
  ).build_workbook.write(@options.output_filename)
19
33
  end
34
+
35
+ sig { override.void }
36
+ # Write a backup of the current spreadsheet.
37
+ def write_backup
38
+ backup_file(@options)
39
+ end
20
40
  end
21
41
  end
22
42
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -5,32 +6,44 @@ module CSVPlusPlus
5
6
  # A module that can be mixed into any Writer that needs to back up it's @output_filename (all of them except Google
6
7
  # Sheets)
7
8
  module FileBackerUpper
9
+ include ::Kernel
10
+ extend ::T::Sig
11
+
8
12
  # I don't want to include a bunch of second/millisecond stuff in the filename unless we
9
13
  # really need to. so try a less specifically formatted filename then get more specific
10
- DESIRED_BACKUP_FORMATS = [%(%Y_%m_%d-%I_%M%p), %(%Y_%m_%d-%I_%M_%S%p), %(%Y_%m_%d-%I_%M_%S_%L%p)].freeze
14
+ DESIRED_BACKUP_FORMATS = ::T.let(
15
+ [
16
+ %(%Y_%m_%d-%I_%M%p),
17
+ %(%Y_%m_%d-%I_%M_%S%p),
18
+ %(%Y_%m_%d-%I_%M_%S_%L%p)
19
+ ].freeze,
20
+ ::T::Array[::String]
21
+ )
11
22
  private_constant :DESIRED_BACKUP_FORMATS
12
23
 
24
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions).returns(::T.nilable(::Pathname)) }
13
25
  # Assuming the underlying spreadsheet is file-based, create a backup of it
14
- def write_backup
15
- return unless ::File.exist?(@options.output_filename)
26
+ def backup_file(options)
27
+ return unless ::File.exist?(options.output_filename)
16
28
 
17
29
  # TODO: also don't do anything if the current backups contents haven't changed (do a md5sum or something)
18
30
 
19
- attempt_backups.tap do |backed_up_to|
20
- warn("Backed up #{@options.output_filename} to #{backed_up_to}") if @options.verbose
31
+ attempt_backups(options).tap do |backed_up_to|
32
+ puts("Backed up #{options.output_filename} to #{backed_up_to}") if options.verbose
21
33
  end
22
34
  end
23
35
 
24
36
  private
25
37
 
38
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions).returns(::Pathname) }
26
39
  # rubocop:disable Metrics/MethodLength
27
- def attempt_backups
40
+ def attempt_backups(options)
28
41
  attempted =
29
42
  # rubocop:disable Lint/ConstantResolution
30
43
  DESIRED_BACKUP_FORMATS.map do |file_format|
31
44
  # rubocop:enable Lint/ConstantResolution
32
- filename = format_backup_filename(file_format)
33
- backed_up_to = backup(filename)
45
+ filename = format_backup_filename(file_format, options.output_filename)
46
+ backed_up_to = backup(filename, options.output_filename)
34
47
 
35
48
  next filename unless backed_up_to
36
49
 
@@ -44,16 +57,17 @@ module CSVPlusPlus
44
57
  end
45
58
  # rubocop:enable Metrics/MethodLength
46
59
 
47
- def backup(filename)
60
+ sig { params(filename: ::Pathname, output_filename: ::Pathname).returns(::T.nilable(::Pathname)) }
61
+ def backup(filename, output_filename)
48
62
  return if ::File.exist?(filename)
49
63
 
50
- ::FileUtils.cp(@options.output_filename, filename)
64
+ ::FileUtils.cp(output_filename, filename)
51
65
  filename
52
66
  end
53
67
 
54
- def format_backup_filename(file_format)
55
- pn = ::Pathname.new(@options.output_filename)
56
- pn.sub_ext("-#{::Time.now.strftime(file_format)}" + pn.extname)
68
+ sig { params(file_format: ::String, output_filename: ::Pathname).returns(::Pathname) }
69
+ def format_backup_filename(file_format, output_filename)
70
+ output_filename.sub_ext("-#{::Time.now.strftime(file_format)}" + output_filename.extname)
57
71
  end
58
72
  end
59
73
  end
@@ -1,118 +1,62 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative '../google_api_client'
4
- require_relative 'base_writer'
5
- require_relative 'google_sheet_builder'
6
-
7
4
  module CSVPlusPlus
8
5
  module Writer
9
6
  # A class that can write a +Template+ to Google Sheets (via their API)
10
- class GoogleSheets < ::CSVPlusPlus::Writer::BaseWriter
11
- # TODO: it would be nice to raise this but we shouldn't expand out more than necessary for our data
12
- SPREADSHEET_INFINITY = 1000
13
- public_constant :SPREADSHEET_INFINITY
7
+ class GoogleSheets < ::CSVPlusPlus::Writer::Writer
8
+ extend ::T::Sig
9
+ include ::CSVPlusPlus::GoogleApiClient
14
10
 
15
- # @param options [Options]
16
- def initialize(options)
17
- super(options)
11
+ sig do
12
+ params(options: ::CSVPlusPlus::Options::GoogleSheetsOptions, position: ::CSVPlusPlus::Runtime::Position).void
13
+ end
14
+ # @param options [Options::GoogleSheetsOptions]
15
+ # @param position [Runtime::Position]
16
+ def initialize(options, position)
17
+ super(position)
18
18
 
19
- @sheet_id = options.google.sheet_id
20
- @sheet_name = options.sheet_name
19
+ @options = ::T.let(options, ::CSVPlusPlus::Options::GoogleSheetsOptions)
20
+ @reader = ::T.let(::CSVPlusPlus::Reader::GoogleSheets.new(options), ::CSVPlusPlus::Reader::GoogleSheets)
21
21
  end
22
22
 
23
- # write a +template+ to Google Sheets
23
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
24
+ # Write a +template+ to Google Sheets
24
25
  #
25
26
  # @param template [Template]
26
27
  def write(template)
27
- @sheets_client = ::CSVPlusPlus::GoogleApiClient.sheets_client
28
-
29
- fetch_spreadsheet!
30
- fetch_spreadsheet_values!
31
-
32
28
  create_sheet! if @options.create_if_not_exists
33
29
 
34
30
  update_cells!(template)
35
31
  end
36
32
 
37
- # write a backup of the google sheet
33
+ sig { override.void }
34
+ # Write a backup of the Google Sheet that is about to be written
38
35
  def write_backup
39
- drive_client = ::CSVPlusPlus::GoogleApiClient.drive_client
40
- drive_client.copy_file(@sheet_id)
36
+ drive_client.copy_file(@options.sheet_id)
41
37
  end
42
38
 
43
39
  private
44
40
 
45
- def format_range(range)
46
- @sheet_name ? "'#{@sheet_name}'!#{range}" : range
47
- end
48
-
49
- def full_range
50
- format_range('A1:Z1000')
51
- end
52
-
53
- def fetch_spreadsheet_values!
54
- formatted_values = get_all_spreadsheet_values('FORMATTED_VALUE')
55
- formula_values = get_all_spreadsheet_values('FORMULA')
56
-
57
- return if formula_values.values.nil? || formatted_values.values.nil?
58
-
59
- @current_values = extract_current_values(formatted_values, formula_values)
60
- end
61
-
62
- def extract_current_values(formatted_values, formula_values)
63
- formatted_values.values.map.each_with_index do |row, x|
64
- row.map.each_with_index do |_cell, y|
65
- formula_value = formula_values.values[x][y]
66
- if formula_value.is_a?(::String) && formula_value.start_with?('=')
67
- formula_value
68
- else
69
- strip_to_nil(formatted_values.values[x][y])
70
- end
71
- end
72
- end
73
- end
74
-
75
- def strip_to_nil(str)
76
- str.strip.empty? ? nil : str
77
- end
78
-
79
- def get_all_spreadsheet_values(render_option)
80
- @sheets_client.get_spreadsheet_values(@sheet_id, full_range, value_render_option: render_option)
81
- end
82
-
83
- def sheet
84
- return unless @sheet_name
85
-
86
- @spreadsheet.sheets.find { |s| s.properties.title.strip == @sheet_name.strip }
87
- end
88
-
89
- def fetch_spreadsheet!
90
- @spreadsheet = @sheets_client.get_spreadsheet(@sheet_id)
91
-
92
- return unless @sheet_name.nil?
93
-
94
- @sheet_name = @spreadsheet.sheets&.first&.properties&.title
95
- end
96
-
41
+ sig { void }
97
42
  def create_sheet!
98
- return if sheet
43
+ return if @reader.sheet
99
44
 
100
- @sheets_client.create_spreadsheet(@sheet_name)
101
- fetch_spreadsheet!
102
- @sheet_name = @spreadsheet.sheets.last.properties.title
45
+ sheets_client.create_spreadsheet(@options.sheet_name)
103
46
  end
104
47
 
48
+ sig { params(template: ::CSVPlusPlus::Template).void }
105
49
  def update_cells!(template)
106
- @sheets_client.batch_update_spreadsheet(@sheet_id, builder(template).batch_update_spreadsheet_request)
50
+ sheets_client.batch_update_spreadsheet(@options.sheet_id, builder(template).batch_update_spreadsheet_request)
107
51
  end
108
52
 
53
+ sig { params(template: ::CSVPlusPlus::Template).returns(::CSVPlusPlus::Writer::GoogleSheetsBuilder) }
109
54
  def builder(template)
110
- ::CSVPlusPlus::Writer::GoogleSheetBuilder.new(
111
- rows: template.rows,
112
- sheet_id: sheet&.properties&.sheet_id,
113
- column_index: @options.offset[1],
114
- row_index: @options.offset[0],
115
- current_sheet_values: @current_values
55
+ ::CSVPlusPlus::Writer::GoogleSheetsBuilder.new(
56
+ options: @options,
57
+ position: @position,
58
+ reader: @reader,
59
+ rows: template.rows
116
60
  )
117
61
  end
118
62
  end