csv_plus_plus 0.1.3 → 0.2.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -3
  3. data/docs/CHANGELOG.md +18 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
  6. data/lib/csv_plus_plus/cell.rb +1 -35
  7. data/lib/csv_plus_plus/cli.rb +43 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +77 -70
  9. data/lib/csv_plus_plus/color.rb +1 -1
  10. data/lib/csv_plus_plus/compiler.rb +31 -21
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
  12. data/lib/csv_plus_plus/entities/boolean.rb +16 -9
  13. data/lib/csv_plus_plus/entities/builtins.rb +68 -40
  14. data/lib/csv_plus_plus/entities/date.rb +14 -11
  15. data/lib/csv_plus_plus/entities/entity.rb +11 -29
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
  17. data/lib/csv_plus_plus/entities/function.rb +22 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +35 -11
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +15 -10
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
  23. data/lib/csv_plus_plus/entities/string.rb +13 -10
  24. data/lib/csv_plus_plus/entities.rb +2 -18
  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 +18 -5
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +1 -1
  33. data/lib/csv_plus_plus/error.rb +4 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +18 -8
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
  38. data/lib/csv_plus_plus/lexer.rb +40 -1
  39. data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
  40. data/lib/csv_plus_plus/modifier/expand.rb +17 -0
  41. data/lib/csv_plus_plus/modifier.rb +6 -1
  42. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  43. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  44. data/lib/csv_plus_plus/options/options.rb +102 -0
  45. data/lib/csv_plus_plus/options.rb +22 -110
  46. data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
  47. data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
  48. data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
  49. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  50. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  51. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  52. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  53. data/lib/csv_plus_plus/reader.rb +14 -0
  54. data/lib/csv_plus_plus/runtime/graph.rb +6 -6
  55. data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
  56. data/lib/csv_plus_plus/runtime/references.rb +32 -27
  57. data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
  58. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  59. data/lib/csv_plus_plus/runtime.rb +9 -9
  60. data/lib/csv_plus_plus/source_code.rb +14 -9
  61. data/lib/csv_plus_plus/template.rb +17 -12
  62. data/lib/csv_plus_plus/version.rb +1 -1
  63. data/lib/csv_plus_plus/writer/csv.rb +32 -5
  64. data/lib/csv_plus_plus/writer/excel.rb +19 -6
  65. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
  66. data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
  67. data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
  68. data/lib/csv_plus_plus/writer/merger.rb +56 -0
  69. data/lib/csv_plus_plus/writer/open_document.rb +16 -2
  70. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
  71. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  72. data/lib/csv_plus_plus/writer.rb +58 -19
  73. data/lib/csv_plus_plus.rb +26 -14
  74. metadata +43 -18
  75. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
  76. data/lib/csv_plus_plus/entities/variable.rb +0 -37
  77. data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
  78. data/lib/csv_plus_plus/google_options.rb +0 -32
  79. data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
  80. data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
  81. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
  82. data/lib/csv_plus_plus/writer/base_writer.rb +0 -45
@@ -1,170 +1,64 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../google_api_client'
5
- require_relative 'base_writer'
6
- require_relative 'google_sheet_builder'
7
-
8
4
  module CSVPlusPlus
9
5
  module Writer
10
6
  # A class that can write a +Template+ to Google Sheets (via their API)
11
- # rubocop:disable Metrics/ClassLength
12
- class GoogleSheets < ::CSVPlusPlus::Writer::BaseWriter
7
+ class GoogleSheets < ::CSVPlusPlus::Writer::Writer
13
8
  extend ::T::Sig
9
+ include ::CSVPlusPlus::GoogleApiClient
14
10
 
15
- sig { returns(::String) }
16
- attr_reader :sheet_id
17
-
18
- sig { returns(::T.nilable(::String)) }
19
- attr_reader :sheet_name
20
-
21
- # TODO: it would be nice to raise this but we shouldn't expand out more than necessary for our data
22
- SPREADSHEET_INFINITY = 1000
23
- public_constant :SPREADSHEET_INFINITY
24
-
25
- sig { params(options: ::CSVPlusPlus::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void }
26
- # @param options [Options]
27
- # @param runtime [Runtime]
28
- def initialize(options, runtime)
29
- super(options, runtime)
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)
30
18
 
31
- # @current_values = ::T.let(nil, ::T.nilable(::T::Array
32
- @sheet_id = ::T.let(::T.must(options.google).sheet_id, ::String)
33
- @sheet_name = ::T.let(options.sheet_name, ::T.nilable(::String))
34
- @sheets_client = ::T.let(::CSVPlusPlus::GoogleApiClient.sheets_client, ::Google::Apis::SheetsV4::SheetsService)
19
+ @options = ::T.let(options, ::CSVPlusPlus::Options::GoogleSheetsOptions)
20
+ @reader = ::T.let(::CSVPlusPlus::Reader::GoogleSheets.new(options), ::CSVPlusPlus::Reader::GoogleSheets)
35
21
  end
36
22
 
37
23
  sig { override.params(template: ::CSVPlusPlus::Template).void }
38
- # write a +template+ to Google Sheets
24
+ # Write a +template+ to Google Sheets
39
25
  #
40
26
  # @param template [Template]
41
27
  def write(template)
42
- fetch_spreadsheet!
43
- fetch_spreadsheet_values!
44
-
45
28
  create_sheet! if @options.create_if_not_exists
46
29
 
47
30
  update_cells!(template)
48
31
  end
49
32
 
50
33
  sig { override.void }
51
- # write a backup of the google sheet
34
+ # Write a backup of the Google Sheet that is about to be written
52
35
  def write_backup
53
- drive_client = ::CSVPlusPlus::GoogleApiClient.drive_client
54
- drive_client.copy_file(@sheet_id)
36
+ drive_client.copy_file(@options.sheet_id)
55
37
  end
56
38
 
57
39
  private
58
40
 
59
- sig do
60
- params(
61
- formatted_values: ::Google::Apis::SheetsV4::ValueRange,
62
- formula_values: ::Google::Apis::SheetsV4::ValueRange
63
- ).returns(::T::Array[::T::Array[::T.nilable(::String)]])
64
- end
65
- def extract_current_values(formatted_values, formula_values)
66
- formatted_values.values.map.each_with_index do |row, x|
67
- row.map.each_with_index do |_cell, y|
68
- formula_value = formula_values.values[x][y]
69
- if formula_value.is_a?(::String) && formula_value.start_with?('=')
70
- formula_value
71
- else
72
- strip_to_nil(formatted_values.values[x][y])
73
- end
74
- end
75
- end
76
- end
77
-
78
- sig { void }
79
- # rubocop:disable Metrics/MethodLength
80
- def fetch_spreadsheet_values!
81
- formatted_values = get_all_spreadsheet_values('FORMATTED_VALUE')
82
- formula_values = get_all_spreadsheet_values('FORMULA')
83
-
84
- @current_values =
85
- ::T.let(
86
- if formula_values.values.nil? || formatted_values.values.nil?
87
- []
88
- else
89
- extract_current_values(formatted_values, formula_values)
90
- end,
91
- ::T.nilable(::T::Array[::T::Array[::T.nilable(::String)]])
92
- )
93
- end
94
- # rubocop:enable Metrics/MethodLength
95
-
96
- sig { params(range: ::String).returns(::String) }
97
- def format_range(range)
98
- @sheet_name ? "'#{@sheet_name}'!#{range}" : range
99
- end
100
-
101
- sig { returns(::String) }
102
- def full_range
103
- format_range('A1:Z1000')
104
- end
105
-
106
- sig { params(str: ::String).returns(::T.nilable(::String)) }
107
- def strip_to_nil(str)
108
- str.strip.empty? ? nil : str
109
- end
110
-
111
- sig { params(render_option: ::String).returns(::Google::Apis::SheetsV4::ValueRange) }
112
- def get_all_spreadsheet_values(render_option)
113
- @sheets_client.get_spreadsheet_values(@sheet_id, full_range, value_render_option: render_option)
114
- end
115
-
116
- sig { returns(::T.nilable(::Google::Apis::SheetsV4::Sheet)) }
117
- def sheet
118
- return unless @sheet_name
119
-
120
- spreadsheet.sheets.find { |s| s.properties.title.strip == @sheet_name.strip }
121
- end
122
-
123
- sig { returns(::Google::Apis::SheetsV4::Spreadsheet) }
124
- def spreadsheet
125
- @spreadsheet ||= ::T.let(
126
- @sheets_client.get_spreadsheet(@sheet_id),
127
- ::T.nilable(::Google::Apis::SheetsV4::Spreadsheet)
128
- )
129
-
130
- raise(::CSVPlusPlus::Error::WriterError, 'Unable to connect to google spreadsheet') unless @spreadsheet
131
-
132
- @spreadsheet
133
- end
134
-
135
- sig { void }
136
- def fetch_spreadsheet!
137
- return unless @sheet_name.nil?
138
-
139
- @sheet_name = spreadsheet.sheets&.first&.properties&.title
140
- end
141
-
142
41
  sig { void }
143
42
  def create_sheet!
144
- return if sheet
43
+ return if @reader.sheet
145
44
 
146
- @sheets_client.create_spreadsheet(@sheet_name)
147
- fetch_spreadsheet!
148
- @sheet_name = spreadsheet.sheets.last.properties.title
45
+ sheets_client.create_spreadsheet(@options.sheet_name)
149
46
  end
150
47
 
151
48
  sig { params(template: ::CSVPlusPlus::Template).void }
152
49
  def update_cells!(template)
153
- @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)
154
51
  end
155
52
 
156
- sig { params(template: ::CSVPlusPlus::Template).returns(::CSVPlusPlus::Writer::GoogleSheetBuilder) }
53
+ sig { params(template: ::CSVPlusPlus::Template).returns(::CSVPlusPlus::Writer::GoogleSheetsBuilder) }
157
54
  def builder(template)
158
- ::CSVPlusPlus::Writer::GoogleSheetBuilder.new(
159
- runtime: @runtime,
160
- rows: template.rows,
161
- sheet_id: sheet&.properties&.sheet_id,
162
- column_index: @options.offset[1],
163
- row_index: @options.offset[0],
164
- current_sheet_values: ::T.must(@current_values)
55
+ ::CSVPlusPlus::Writer::GoogleSheetsBuilder.new(
56
+ options: @options,
57
+ position: @position,
58
+ reader: @reader,
59
+ rows: template.rows
165
60
  )
166
61
  end
167
62
  end
168
- # rubocop:enable Metrics/ClassLength
169
63
  end
170
64
  end
@@ -5,35 +5,27 @@ module CSVPlusPlus
5
5
  module Writer
6
6
  # Given +rows+ from a +Template+, build requests compatible with Google Sheets Ruby API
7
7
  # rubocop:disable Metrics/ClassLength
8
- class GoogleSheetBuilder
8
+ class GoogleSheetsBuilder
9
9
  extend ::T::Sig
10
+ include ::CSVPlusPlus::Writer::Merger
10
11
 
11
12
  sig do
12
13
  params(
13
- current_sheet_values: ::T::Array[::T::Array[::T.nilable(::String)]],
14
- runtime: ::CSVPlusPlus::Runtime::Runtime,
15
- sheet_id: ::T.nilable(::Integer),
16
- rows: ::T::Array[::CSVPlusPlus::Row],
17
- column_index: ::T.nilable(::Integer),
18
- row_index: ::T.nilable(::Integer)
14
+ options: ::CSVPlusPlus::Options::GoogleSheetsOptions,
15
+ position: ::CSVPlusPlus::Runtime::Position,
16
+ reader: ::CSVPlusPlus::Reader::GoogleSheets,
17
+ rows: ::T::Array[::CSVPlusPlus::Row]
19
18
  ).void
20
19
  end
21
- # @param column_index [Integer] Offset the results by +column_index+
22
- # @param current_sheet_values [Array<Array<::String, nil>>]
23
- # @param sheet_id [::String] The sheet ID referencing the sheet in Google
24
- # @param row_index [Integer] Offset the results by +row_index+
20
+ # @param options [Options]
21
+ # @param position [Position] The current position.
22
+ # @param reader [::CSVPlusPlus::Reader::GoogleSheets]
25
23
  # @param rows [Array<Row>] The rows to render
26
- # @param runtime [Runtime] The current runtime.
27
- #
28
- # rubocop:disable Metrics/ParameterLists
29
- def initialize(current_sheet_values:, runtime:, sheet_id:, rows:, column_index: 0, row_index: 0)
30
- # rubocop:enable Metrics/ParameterLists
31
- @current_sheet_values = current_sheet_values
32
- @sheet_id = sheet_id
24
+ def initialize(options:, position:, reader:, rows:)
25
+ @options = options
26
+ @position = position
27
+ @reader = reader
33
28
  @rows = rows
34
- @column_index = column_index
35
- @row_index = row_index
36
- @runtime = runtime
37
29
  end
38
30
 
39
31
  sig { returns(::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest) }
@@ -46,19 +38,23 @@ module CSVPlusPlus
46
38
 
47
39
  private
48
40
 
49
- sig { params(extended_value: ::Google::Apis::SheetsV4::ExtendedValue, value: ::T.nilable(::String)).void }
50
- def set_extended_value_type!(extended_value, value)
51
- v = value || ''
52
- if v.start_with?('=')
53
- extended_value.formula_value = value
54
- elsif v.match(/^-?[\d.]+$/)
55
- extended_value.number_value = value
56
- elsif v.downcase == 'true' || v.downcase == 'false'
57
- extended_value.bool_value = value
58
- else
59
- extended_value.string_value = value
41
+ sig { params(value: ::T.nilable(::String)).returns(::Google::Apis::SheetsV4::ExtendedValue) }
42
+ # rubocop:disable Metrics/MethodLength
43
+ def build_extended_value(value)
44
+ ::Google::Apis::SheetsV4::ExtendedValue.new.tap do |xv|
45
+ v = value || ''
46
+ if v.start_with?('=')
47
+ xv.formula_value = value
48
+ elsif v.match(/^-?[\d.]+$/)
49
+ xv.number_value = value
50
+ elsif v.downcase == 'true' || v.downcase == 'false'
51
+ xv.bool_value = value
52
+ else
53
+ xv.string_value = value
54
+ end
60
55
  end
61
56
  end
57
+ # rubocop:enable Metrics/MethodLength
62
58
 
63
59
  sig { params(mod: ::CSVPlusPlus::Modifier::GoogleSheetModifier).returns(::Google::Apis::SheetsV4::CellFormat) }
64
60
  def build_cell_format(mod)
@@ -75,7 +71,6 @@ module CSVPlusPlus
75
71
  sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::GridRange) }
76
72
  def grid_range_for_cell(cell)
77
73
  ::Google::Apis::SheetsV4::GridRange.new(
78
- sheet_id: @sheet_id,
79
74
  start_column_index: cell.index,
80
75
  end_column_index: cell.index + 1,
81
76
  start_row_index: cell.row_index,
@@ -83,25 +78,15 @@ module CSVPlusPlus
83
78
  )
84
79
  end
85
80
 
86
- sig { params(row_index: ::Integer, cell_index: ::Integer).returns(::T.nilable(::String)) }
87
- def current_value(row_index, cell_index)
88
- ::T.must(@current_sheet_values[row_index])[cell_index]
89
- rescue ::StandardError
90
- nil
91
- end
92
-
93
81
  sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::ExtendedValue) }
94
82
  def build_cell_value(cell)
95
- ::Google::Apis::SheetsV4::ExtendedValue.new.tap do |xv|
96
- value =
97
- if cell.value.nil?
98
- current_value(cell.row_index, cell.index)
99
- else
100
- cell.evaluate(@runtime)
101
- end
102
-
103
- set_extended_value_type!(xv, value)
104
- end
83
+ build_extended_value(
84
+ merge_cell_value(
85
+ existing_value: @reader.value_at(cell),
86
+ new_value: (ast = cell.ast) ? ast.evaluate(@position) : cell.value,
87
+ options: @options
88
+ )
89
+ )
105
90
  end
106
91
 
107
92
  sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::CellData) }
@@ -127,9 +112,8 @@ module CSVPlusPlus
127
112
  ::Google::Apis::SheetsV4::UpdateCellsRequest.new(
128
113
  fields: '*',
129
114
  start: ::Google::Apis::SheetsV4::GridCoordinate.new(
130
- sheet_id: @sheet_id,
131
- column_index: @column_index,
132
- row_index: @row_index
115
+ column_index: @options.offset[1],
116
+ row_index: @options.offset[0]
133
117
  ),
134
118
  rows:
135
119
  )
@@ -159,7 +143,7 @@ module CSVPlusPlus
159
143
  def chunked_requests(rows)
160
144
  accum = []
161
145
  [].tap do |chunked|
162
- @runtime.map_rows(rows) do |row|
146
+ @position.map_rows(rows) do |row|
163
147
  accum << build_row_data(row)
164
148
  next unless accum.length == 1000
165
149
 
@@ -181,7 +165,7 @@ module CSVPlusPlus
181
165
  ::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new.tap do |bu|
182
166
  bu.requests = chunked_requests(rows)
183
167
 
184
- @runtime.map_rows(rows) do |row|
168
+ @position.map_rows(rows) do |row|
185
169
  row.cells.filter { |c| c.modifier.any_border? }
186
170
  .each do |cell|
187
171
  bu.requests << build_update_borders_request(cell)
@@ -0,0 +1,56 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Writer
6
+ # A merging strategy for when we want to write to a cell but it has a value
7
+ module Merger
8
+ extend ::T::Sig
9
+ include ::Kernel
10
+
11
+ sig do
12
+ type_parameters(:V)
13
+ .params(
14
+ existing_value: ::T.nilable(::T.all(::T.type_parameter(:V), ::BasicObject)),
15
+ new_value: ::T.nilable(::T.all(::T.type_parameter(:V), ::BasicObject)),
16
+ options: ::CSVPlusPlus::Options::Options
17
+ ).returns(::T.nilable(::T.type_parameter(:V)))
18
+ end
19
+ # Consistently enforce our strategy for resolving differences between new changes and existing. By default we
20
+ # overwrite values that are currently in the spreadsheet but you can override that with the --safe flag
21
+ def merge_cell_value(existing_value:, new_value:, options:)
22
+ merged_value = merge_with_strategy(existing_value:, new_value:, options:)
23
+
24
+ return merged_value unless options.verbose
25
+
26
+ if options.overwrite_values && merged_value != existing_value
27
+ warn("Overwriting existing value: \"#{existing_value}\" with \"#{new_value}\"")
28
+ # rubocop:disable Style/MissingElse
29
+ elsif !options.overwrite_values && new_value != merged_value
30
+ # rubocop:enable Style/MissingElse
31
+ warn("Keeping old value: \"#{existing_value}\" rather than new value: \"#{new_value}\"")
32
+ end
33
+
34
+ merged_value
35
+ end
36
+
37
+ private
38
+
39
+ sig do
40
+ type_parameters(:V)
41
+ .params(
42
+ existing_value: ::T.nilable(::T.all(::T.type_parameter(:V), ::BasicObject)),
43
+ new_value: ::T.nilable(::T.all(::T.type_parameter(:V), ::BasicObject)),
44
+ options: ::CSVPlusPlus::Options::Options
45
+ ).returns(::T.nilable(::T.type_parameter(:V)))
46
+ end
47
+ def merge_with_strategy(existing_value:, new_value:, options:)
48
+ if options.overwrite_values
49
+ new_value || existing_value
50
+ else
51
+ existing_value || new_value
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -4,16 +4,30 @@
4
4
  module CSVPlusPlus
5
5
  module Writer
6
6
  # A class that can output a +Template+ to an Excel file
7
- class OpenDocument < ::CSVPlusPlus::Writer::BaseWriter
7
+ class OpenDocument < ::CSVPlusPlus::Writer::Writer
8
8
  extend ::T::Sig
9
-
10
9
  include ::CSVPlusPlus::Writer::FileBackerUpper
11
10
 
11
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
12
+ # @param options [Options::FileOptions]
13
+ # @param position [Runtime::Position]
14
+ def initialize(options, position)
15
+ super(position)
16
+
17
+ @options = options
18
+ end
19
+
12
20
  sig { override.params(template: ::CSVPlusPlus::Template).void }
13
21
  # write a +template+ to an OpenDocument file
14
22
  def write(template)
15
23
  # TODO
16
24
  end
25
+
26
+ sig { override.void }
27
+ # write a backup of the google sheet
28
+ def write_backup
29
+ backup_file(@options)
30
+ end
17
31
  end
18
32
  end
19
33
  end
@@ -5,39 +5,38 @@ module CSVPlusPlus
5
5
  module Writer
6
6
  # Build a RubyXL workbook formatted according to the given +rows+
7
7
  #
8
- # @attr_reader input_filename [String] The filename being written to
8
+ # @attr_reader output_filename [Pathname, nil] The filename being written to
9
9
  # @attr_reader rows [Array<Row>] The rows being written
10
10
  # rubocop:disable Metrics/ClassLength
11
11
  class RubyXLBuilder
12
12
  extend ::T::Sig
13
+ include ::CSVPlusPlus::Writer::Merger
13
14
 
14
15
  RubyXLCell = ::T.type_alias { ::T.all(::RubyXL::Cell, ::RubyXL::CellConvenienceMethods) }
15
16
  public_constant :RubyXLCell
16
17
 
17
- sig { returns(::T.nilable(::String)) }
18
- attr_reader :input_filename
18
+ RubyXLValue = ::T.type_alias { ::T.any(::String, ::Numeric, ::Date) }
19
+ public_constant :RubyXLValue
19
20
 
20
21
  sig { returns(::T::Array[::CSVPlusPlus::Row]) }
21
22
  attr_reader :rows
22
23
 
23
24
  sig do
24
25
  params(
25
- input_filename: ::T.nilable(::String),
26
- rows: ::T::Array[::CSVPlusPlus::Row],
27
- runtime: ::CSVPlusPlus::Runtime::Runtime,
28
- sheet_name: ::T.nilable(::String)
26
+ options: ::CSVPlusPlus::Options::FileOptions,
27
+ position: ::CSVPlusPlus::Runtime::Position,
28
+ rows: ::T::Array[::CSVPlusPlus::Row]
29
29
  ).void
30
30
  end
31
- # @param input_filename [::String] The file to write to
31
+ # @param options [Options::FileOptions]
32
+ # @param position [Position] The current position
32
33
  # @param rows [Array<Row>] The rows to write
33
- # @param runtime [Runtime] The current runtime
34
- # @param sheet_name [::String] The name of the sheet within the workbook to write to
35
- def initialize(input_filename:, rows:, runtime:, sheet_name: nil)
34
+ def initialize(options:, position:, rows:)
35
+ @options = options
36
+ @position = position
36
37
  @rows = rows
37
- @input_filename = input_filename
38
- @runtime = runtime
39
- @sheet_name = sheet_name
40
38
  @worksheet = ::T.let(open_worksheet, ::RubyXL::Worksheet)
39
+ @reader = ::T.let(::CSVPlusPlus::Reader::RubyXL.new(@options, @worksheet), ::CSVPlusPlus::Reader::RubyXL)
41
40
  end
42
41
 
43
42
  sig { returns(::RubyXL::Workbook) }
@@ -52,24 +51,12 @@ module CSVPlusPlus
52
51
  private
53
52
 
54
53
  sig { void }
55
- # rubocop:disable Metrics/MethodLength
56
54
  def build_workbook!
57
- @runtime.map_all_cells(@rows) do |cell|
58
- value = cell.evaluate(@runtime)
59
- if value&.start_with?('=')
60
- @worksheet.add_cell(@runtime.row_index, @runtime.cell_index, '', value.gsub(/^=/, ''))
61
- else
62
- @worksheet.add_cell(@runtime.row_index, @runtime.cell_index, value)
63
- end
64
-
65
- format_cell!(
66
- @runtime.row_index,
67
- @runtime.cell_index,
68
- ::T.cast(cell.modifier, ::CSVPlusPlus::Modifier::RubyXLModifier)
69
- )
55
+ @position.map_all_cells(@rows) do |cell|
56
+ build_cell(cell)
57
+ format_cell!(cell)
70
58
  end
71
59
  end
72
- # rubocop:enable Metrics/MethodLength
73
60
 
74
61
  sig do
75
62
  params(
@@ -160,30 +147,68 @@ module CSVPlusPlus
160
147
  cell.change_contents(cell.value)
161
148
  end
162
149
 
150
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLValue)) }
151
+ def value_to_rubyxl(cell)
152
+ return cell.value unless (ast = cell.ast)
153
+
154
+ case ast
155
+ when ::CSVPlusPlus::Entities::Number, ::CSVPlusPlus::Entities::Date
156
+ ast.value
157
+ else
158
+ ast.evaluate(@position)
159
+ end
160
+ end
161
+
163
162
  sig do
164
- params(row_index: ::Integer, cell_index: ::Integer, modifier: ::CSVPlusPlus::Modifier::RubyXLModifier).void
165
- end
166
- def format_cell!(row_index, cell_index, modifier)
167
- @worksheet.sheet_data[row_index][cell_index].tap do |cell|
168
- do_alignments!(cell, modifier)
169
- do_borders!(cell, modifier)
170
- do_fill!(cell, modifier)
171
- do_fonts!(cell, modifier)
172
- do_formats!(cell, modifier)
173
- do_number_formats!(cell, modifier)
163
+ params(
164
+ cell: ::CSVPlusPlus::Cell,
165
+ existing_value: ::T.nilable(::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLValue)
166
+ ).returns(::T.nilable(::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLValue))
167
+ end
168
+ def merge_value(cell, existing_value)
169
+ merge_cell_value(existing_value:, new_value: value_to_rubyxl(cell), options: @options)
170
+ end
171
+
172
+ sig { params(cell: ::CSVPlusPlus::Cell).void }
173
+ def format_cell!(cell)
174
+ modifier = ::T.cast(cell.modifier, ::CSVPlusPlus::Modifier::RubyXLModifier)
175
+ @reader.value_at(cell).tap do |rubyxl_cell|
176
+ do_alignments!(rubyxl_cell, modifier)
177
+ do_borders!(rubyxl_cell, modifier)
178
+ do_fill!(rubyxl_cell, modifier)
179
+ do_fonts!(rubyxl_cell, modifier)
180
+ do_formats!(rubyxl_cell, modifier)
181
+ do_number_formats!(rubyxl_cell, modifier)
174
182
  end
175
183
  end
176
184
 
177
185
  sig { returns(::RubyXL::Worksheet) }
178
186
  def open_worksheet
179
- if @input_filename && ::File.exist?(@input_filename)
180
- workbook = ::RubyXL::Parser.parse(@input_filename)
181
- workbook[@sheet_name] || workbook.add_worksheet(@sheet_name)
187
+ if ::File.exist?(@options.output_filename)
188
+ workbook = ::RubyXL::Parser.parse(@options.output_filename)
189
+ workbook[@options.sheet_name] || workbook.add_worksheet(@options.sheet_name)
182
190
  else
183
191
  workbook = ::RubyXL::Workbook.new
184
- workbook.worksheets[0].tap { |w| w.sheet_name = @sheet_name }
192
+ workbook.worksheets[0].tap { |w| w.sheet_name = @options.sheet_name }
185
193
  end
186
194
  end
195
+
196
+ sig { params(cell: ::CSVPlusPlus::Cell).void }
197
+ # rubocop:disable Metrics/MethodLength
198
+ def build_cell(cell)
199
+ if (existing_cell = @reader.value_at(cell))
200
+ merged_value = merge_value(cell, existing_cell.value)
201
+ existing_cell.change_contents(
202
+ cell.ast ? '' : merged_value,
203
+ cell.ast ? merged_value : nil
204
+ )
205
+ elsif (ast = cell.ast)
206
+ @worksheet.add_cell(cell.row_index, cell.index, '', ast.evaluate(@position))
207
+ else
208
+ @worksheet.add_cell(cell.row_index, cell.index, cell.value)
209
+ end
210
+ end
211
+ # rubocop:enable Metrics/MethodLength
187
212
  end
188
213
  # rubocop:enable Metrics/ClassLength
189
214
  end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Writer
6
+ # Some shared functionality that all Writers should build on
7
+ #
8
+ # @attr_reader position [Position] The current position - needed to resolve variables and display useful error
9
+ # messages
10
+ class Writer
11
+ extend ::T::Sig
12
+ extend ::T::Helpers
13
+
14
+ abstract!
15
+
16
+ sig { returns(::CSVPlusPlus::Runtime::Position) }
17
+ attr_reader :position
18
+
19
+ protected
20
+
21
+ sig do
22
+ params(position: ::CSVPlusPlus::Runtime::Position).void
23
+ end
24
+ # Open a CSV outputter to the +output_filename+ specified by the +Options+
25
+ #
26
+ # @param position [Position] The current position.
27
+ def initialize(position)
28
+ @position = position
29
+ end
30
+
31
+ sig { abstract.params(template: ::CSVPlusPlus::Template).void }
32
+ # Write the given +template+.
33
+ #
34
+ # @param template [Template]
35
+ def write(template); end
36
+
37
+ sig { abstract.void }
38
+ # Write a backup of the current spreadsheet.
39
+ def write_backup; end
40
+ end
41
+ end
42
+ end