csv_plus_plus 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/docs/CHANGELOG.md +16 -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 +71 -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 +97 -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 +31 -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 +37 -12
  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,31 @@
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
+ # Our strategy for resolving differences between new changes and existing
20
+ def merge_cell_value(existing_value:, new_value:, options:)
21
+ # TODO: make an option that specifies if we override (take new data over old)
22
+ merged_value = new_value || existing_value
23
+
24
+ return merged_value if !options.verbose || merged_value == existing_value
25
+
26
+ warn("Overwriting existing value: \"#{existing_value}\" with \"#{new_value}\"")
27
+ merged_value
28
+ end
29
+ end
30
+ end
31
+ 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