csv_plus_plus 0.1.1 → 0.1.3

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -63
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +44 -17
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +42 -11
  9. data/lib/csv_plus_plus/compiler.rb +178 -0
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
  11. data/lib/csv_plus_plus/entities/boolean.rb +40 -0
  12. data/lib/csv_plus_plus/entities/builtins.rb +58 -0
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
  14. data/lib/csv_plus_plus/entities/date.rb +63 -0
  15. data/lib/csv_plus_plus/entities/entity.rb +50 -0
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +45 -0
  18. data/lib/csv_plus_plus/entities/function_call.rb +50 -0
  19. data/lib/csv_plus_plus/entities/number.rb +48 -0
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
  21. data/lib/csv_plus_plus/entities/string.rb +42 -0
  22. data/lib/csv_plus_plus/entities/variable.rb +37 -0
  23. data/lib/csv_plus_plus/entities.rb +40 -0
  24. data/lib/csv_plus_plus/error/error.rb +20 -0
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
  28. data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
  29. data/lib/csv_plus_plus/error/writer_error.rb +17 -0
  30. data/lib/csv_plus_plus/error.rb +10 -2
  31. data/lib/csv_plus_plus/google_api_client.rb +11 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +18 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -150
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
  46. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
  48. data/lib/csv_plus_plus/row.rb +53 -15
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +42 -0
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +63 -36
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +7 -4
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -33
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +41 -15
  69. metadata +44 -30
  70. data/lib/csv_plus_plus/code_section.rb +0 -101
  71. data/lib/csv_plus_plus/expand.rb +0 -18
  72. data/lib/csv_plus_plus/graph.rb +0 -62
  73. data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
  74. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  75. data/lib/csv_plus_plus/language/builtins.rb +0 -46
  76. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  77. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  78. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  79. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  80. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  81. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  82. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  83. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  84. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  85. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/language/entities.rb +0 -28
  87. data/lib/csv_plus_plus/language/references.rb +0 -70
  88. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  89. data/lib/csv_plus_plus/language/scope.rb +0 -192
  90. data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
  91. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
  92. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
  93. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,21 +1,42 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './google_sheet_modifier'
4
-
5
4
  module CSVPlusPlus
6
5
  module Writer
7
6
  # Given +rows+ from a +Template+, build requests compatible with Google Sheets Ruby API
8
7
  # rubocop:disable Metrics/ClassLength
9
8
  class GoogleSheetBuilder
10
- # @param current_sheet_values
11
- def initialize(current_sheet_values:, sheet_id:, rows:, column_index: 0, row_index: 0)
9
+ extend ::T::Sig
10
+
11
+ sig do
12
+ 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)
19
+ ).void
20
+ 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+
25
+ # @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
12
31
  @current_sheet_values = current_sheet_values
13
32
  @sheet_id = sheet_id
14
33
  @rows = rows
15
34
  @column_index = column_index
16
35
  @row_index = row_index
36
+ @runtime = runtime
17
37
  end
18
38
 
39
+ sig { returns(::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest) }
19
40
  # Build a Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest
20
41
  #
21
42
  # @return [Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest]
@@ -25,12 +46,7 @@ module CSVPlusPlus
25
46
 
26
47
  private
27
48
 
28
- def sheets_ns
29
- ::Google::Apis::SheetsV4
30
- end
31
-
32
- def sheets_color(color); end
33
-
49
+ sig { params(extended_value: ::Google::Apis::SheetsV4::ExtendedValue, value: ::T.nilable(::String)).void }
34
50
  def set_extended_value_type!(extended_value, value)
35
51
  v = value || ''
36
52
  if v.start_with?('=')
@@ -38,25 +54,27 @@ module CSVPlusPlus
38
54
  elsif v.match(/^-?[\d.]+$/)
39
55
  extended_value.number_value = value
40
56
  elsif v.downcase == 'true' || v.downcase == 'false'
41
- extended_value.boolean_value = value
57
+ extended_value.bool_value = value
42
58
  else
43
59
  extended_value.string_value = value
44
60
  end
45
61
  end
46
62
 
63
+ sig { params(mod: ::CSVPlusPlus::Modifier::GoogleSheetModifier).returns(::Google::Apis::SheetsV4::CellFormat) }
47
64
  def build_cell_format(mod)
48
- sheets_ns::CellFormat.new.tap do |cf|
65
+ ::Google::Apis::SheetsV4::CellFormat.new.tap do |cf|
49
66
  cf.text_format = mod.text_format
50
67
 
51
- cf.horizontal_alignment = mod.halign
52
- cf.vertical_alignment = mod.valign
53
- cf.background_color = mod.color
54
- cf.number_format = mod.numberformat
68
+ cf.horizontal_alignment = mod.horizontal_alignment
69
+ cf.vertical_alignment = mod.vertical_alignment
70
+ cf.background_color = mod.background_color
71
+ cf.number_format = mod.number_format
55
72
  end
56
73
  end
57
74
 
75
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::GridRange) }
58
76
  def grid_range_for_cell(cell)
59
- sheets_ns::GridRange.new(
77
+ ::Google::Apis::SheetsV4::GridRange.new(
60
78
  sheet_id: @sheet_id,
61
79
  start_column_index: cell.index,
62
80
  end_column_index: cell.index + 1,
@@ -65,79 +83,105 @@ module CSVPlusPlus
65
83
  )
66
84
  end
67
85
 
86
+ sig { params(row_index: ::Integer, cell_index: ::Integer).returns(::T.nilable(::String)) }
68
87
  def current_value(row_index, cell_index)
69
- @current_sheet_values[row_index][cell_index]
88
+ ::T.must(@current_sheet_values[row_index])[cell_index]
70
89
  rescue ::StandardError
71
90
  nil
72
91
  end
73
92
 
93
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::ExtendedValue) }
74
94
  def build_cell_value(cell)
75
- sheets_ns::ExtendedValue.new.tap do |xv|
95
+ ::Google::Apis::SheetsV4::ExtendedValue.new.tap do |xv|
76
96
  value =
77
97
  if cell.value.nil?
78
98
  current_value(cell.row_index, cell.index)
79
99
  else
80
- cell.to_csv
100
+ cell.evaluate(@runtime)
81
101
  end
82
102
 
83
103
  set_extended_value_type!(xv, value)
84
104
  end
85
105
  end
86
106
 
107
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::CellData) }
87
108
  def build_cell_data(cell)
88
- mod = ::CSVPlusPlus::Writer::GoogleSheetModifier.new(cell.modifier)
109
+ mod = cell.modifier
89
110
 
90
- sheets_ns::CellData.new.tap do |cd|
91
- cd.user_entered_format = build_cell_format(mod)
111
+ ::Google::Apis::SheetsV4::CellData.new.tap do |cd|
112
+ cd.user_entered_format = build_cell_format(::T.cast(mod, ::CSVPlusPlus::Modifier::GoogleSheetModifier))
92
113
  cd.note = mod.note if mod.note
93
114
 
94
- # XXX apply data validation
115
+ # TODO: apply data validation
95
116
  cd.user_entered_value = build_cell_value(cell)
96
117
  end
97
118
  end
98
119
 
120
+ sig { params(row: ::CSVPlusPlus::Row).returns(::Google::Apis::SheetsV4::RowData) }
99
121
  def build_row_data(row)
100
- sheets_ns::RowData.new(values: row.cells.map { |cell| build_cell_data(cell) })
122
+ ::Google::Apis::SheetsV4::RowData.new(values: row.cells.map { |cell| build_cell_data(cell) })
101
123
  end
102
124
 
125
+ sig { params(rows: ::T::Array[::CSVPlusPlus::Row]).returns(::Google::Apis::SheetsV4::UpdateCellsRequest) }
103
126
  def build_update_cells_request(rows)
104
- sheets_ns::UpdateCellsRequest.new(
127
+ ::Google::Apis::SheetsV4::UpdateCellsRequest.new(
105
128
  fields: '*',
106
- start: sheets_ns::GridCoordinate.new(
129
+ start: ::Google::Apis::SheetsV4::GridCoordinate.new(
107
130
  sheet_id: @sheet_id,
108
131
  column_index: @column_index,
109
132
  row_index: @row_index
110
133
  ),
111
- rows: rows.map { |row| build_row_data(row) }
134
+ rows:
112
135
  )
113
136
  end
114
137
 
138
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::UpdateBordersRequest) }
115
139
  def build_border(cell)
116
- mod = cell.modifier
117
- # TODO: allow different border styles per side
118
- border = sheets_ns::Border.new(color: mod.bordercolor || '#000000', style: mod.borderstyle || 'solid')
119
- sheets_ns::UpdateBordersRequest.new(
120
- top: mod.border_along?('top') ? border : nil,
121
- right: mod.border_along?('right') ? border : nil,
122
- left: mod.border_along?('left') ? border : nil,
123
- bottom: mod.border_along?('bottom') ? border : nil,
140
+ mod = ::T.cast(cell.modifier, ::CSVPlusPlus::Modifier::GoogleSheetModifier)
141
+ border = mod.border
142
+
143
+ ::Google::Apis::SheetsV4::UpdateBordersRequest.new(
144
+ top: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Top) ? border : nil,
145
+ right: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Right) ? border : nil,
146
+ left: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Left) ? border : nil,
147
+ bottom: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Bottom) ? border : nil,
124
148
  range: grid_range_for_cell(cell)
125
149
  )
126
150
  end
127
151
 
152
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::Request) }
128
153
  def build_update_borders_request(cell)
129
- sheets_ns::Request.new(update_borders: build_border(cell))
154
+ ::Google::Apis::SheetsV4::Request.new(update_borders: build_border(cell))
130
155
  end
131
156
 
132
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
157
+ sig { params(rows: ::T::Array[::CSVPlusPlus::Row]).returns(::T::Array[::Google::Apis::SheetsV4::Request]) }
158
+ # rubocop:disable Metrics/MethodLength
159
+ def chunked_requests(rows)
160
+ accum = []
161
+ [].tap do |chunked|
162
+ @runtime.map_rows(rows) do |row|
163
+ accum << build_row_data(row)
164
+ next unless accum.length == 1000
165
+
166
+ chunked << ::Google::Apis::SheetsV4::Request.new(update_cells: build_update_cells_request(accum))
167
+ accum = []
168
+ end
169
+
170
+ unless accum.empty?
171
+ chunked << ::Google::Apis::SheetsV4::Request.new(update_cells: build_update_cells_request(accum))
172
+ end
173
+ end
174
+ end
175
+ # rubocop:enable Metrics/MethodLength
176
+
177
+ sig do
178
+ params(rows: ::T::Array[::CSVPlusPlus::Row]).returns(::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest)
179
+ end
133
180
  def build_batch_request(rows)
134
- sheets_ns::BatchUpdateSpreadsheetRequest.new.tap do |bu|
135
- bu.requests =
136
- rows.each_slice(1000).to_a.map do |chunked_rows|
137
- sheets_ns::Request.new(update_cells: build_update_cells_request(chunked_rows))
138
- end
181
+ ::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new.tap do |bu|
182
+ bu.requests = chunked_requests(rows)
139
183
 
140
- rows.each do |row|
184
+ @runtime.map_rows(rows) do |row|
141
185
  row.cells.filter { |c| c.modifier.any_border? }
142
186
  .each do |cell|
143
187
  bu.requests << build_update_borders_request(cell)
@@ -145,7 +189,6 @@ module CSVPlusPlus
145
189
  end
146
190
  end
147
191
  end
148
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
149
192
  end
150
193
  # rubocop:enable Metrics/ClassLength
151
194
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative '../google_api_client'
@@ -7,25 +8,37 @@ require_relative 'google_sheet_builder'
7
8
  module CSVPlusPlus
8
9
  module Writer
9
10
  # A class that can write a +Template+ to Google Sheets (via their API)
11
+ # rubocop:disable Metrics/ClassLength
10
12
  class GoogleSheets < ::CSVPlusPlus::Writer::BaseWriter
13
+ extend ::T::Sig
14
+
15
+ sig { returns(::String) }
16
+ attr_reader :sheet_id
17
+
18
+ sig { returns(::T.nilable(::String)) }
19
+ attr_reader :sheet_name
20
+
11
21
  # TODO: it would be nice to raise this but we shouldn't expand out more than necessary for our data
12
22
  SPREADSHEET_INFINITY = 1000
13
23
  public_constant :SPREADSHEET_INFINITY
14
24
 
25
+ sig { params(options: ::CSVPlusPlus::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void }
15
26
  # @param options [Options]
16
- def initialize(options)
17
- super(options)
18
-
19
- @sheet_id = options.google.sheet_id
20
- @sheet_name = options.sheet_name
27
+ # @param runtime [Runtime]
28
+ def initialize(options, runtime)
29
+ super(options, runtime)
30
+
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)
21
35
  end
22
36
 
37
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
23
38
  # write a +template+ to Google Sheets
24
39
  #
25
40
  # @param template [Template]
26
41
  def write(template)
27
- @sheets_client = ::CSVPlusPlus::GoogleApiClient.sheets_client
28
-
29
42
  fetch_spreadsheet!
30
43
  fetch_spreadsheet_values!
31
44
 
@@ -34,6 +47,7 @@ module CSVPlusPlus
34
47
  update_cells!(template)
35
48
  end
36
49
 
50
+ sig { override.void }
37
51
  # write a backup of the google sheet
38
52
  def write_backup
39
53
  drive_client = ::CSVPlusPlus::GoogleApiClient.drive_client
@@ -42,23 +56,12 @@ module CSVPlusPlus
42
56
 
43
57
  private
44
58
 
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)
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)]])
60
64
  end
61
-
62
65
  def extract_current_values(formatted_values, formula_values)
63
66
  formatted_values.values.map.each_with_index do |row, x|
64
67
  row.map.each_with_index do |_cell, y|
@@ -72,49 +75,96 @@ module CSVPlusPlus
72
75
  end
73
76
  end
74
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)) }
75
107
  def strip_to_nil(str)
76
108
  str.strip.empty? ? nil : str
77
109
  end
78
110
 
111
+ sig { params(render_option: ::String).returns(::Google::Apis::SheetsV4::ValueRange) }
79
112
  def get_all_spreadsheet_values(render_option)
80
113
  @sheets_client.get_spreadsheet_values(@sheet_id, full_range, value_render_option: render_option)
81
114
  end
82
115
 
116
+ sig { returns(::T.nilable(::Google::Apis::SheetsV4::Sheet)) }
83
117
  def sheet
84
118
  return unless @sheet_name
85
119
 
86
- @spreadsheet.sheets.find { |s| s.properties.title.strip == @sheet_name.strip }
120
+ spreadsheet.sheets.find { |s| s.properties.title.strip == @sheet_name.strip }
87
121
  end
88
122
 
89
- def fetch_spreadsheet!
90
- @spreadsheet = @sheets_client.get_spreadsheet(@sheet_id)
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
91
131
 
132
+ @spreadsheet
133
+ end
134
+
135
+ sig { void }
136
+ def fetch_spreadsheet!
92
137
  return unless @sheet_name.nil?
93
138
 
94
- @sheet_name = @spreadsheet.sheets&.first&.properties&.title
139
+ @sheet_name = spreadsheet.sheets&.first&.properties&.title
95
140
  end
96
141
 
142
+ sig { void }
97
143
  def create_sheet!
98
144
  return if sheet
99
145
 
100
146
  @sheets_client.create_spreadsheet(@sheet_name)
101
147
  fetch_spreadsheet!
102
- @sheet_name = @spreadsheet.sheets.last.properties.title
148
+ @sheet_name = spreadsheet.sheets.last.properties.title
103
149
  end
104
150
 
151
+ sig { params(template: ::CSVPlusPlus::Template).void }
105
152
  def update_cells!(template)
106
153
  @sheets_client.batch_update_spreadsheet(@sheet_id, builder(template).batch_update_spreadsheet_request)
107
154
  end
108
155
 
156
+ sig { params(template: ::CSVPlusPlus::Template).returns(::CSVPlusPlus::Writer::GoogleSheetBuilder) }
109
157
  def builder(template)
110
158
  ::CSVPlusPlus::Writer::GoogleSheetBuilder.new(
159
+ runtime: @runtime,
111
160
  rows: template.rows,
112
161
  sheet_id: sheet&.properties&.sheet_id,
113
162
  column_index: @options.offset[1],
114
163
  row_index: @options.offset[0],
115
- current_sheet_values: @current_values
164
+ current_sheet_values: ::T.must(@current_values)
116
165
  )
117
166
  end
118
167
  end
168
+ # rubocop:enable Metrics/ClassLength
119
169
  end
120
170
  end
@@ -1,10 +1,15 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
5
  module Writer
5
- ##
6
6
  # A class that can output a +Template+ to an Excel file
7
7
  class OpenDocument < ::CSVPlusPlus::Writer::BaseWriter
8
+ extend ::T::Sig
9
+
10
+ include ::CSVPlusPlus::Writer::FileBackerUpper
11
+
12
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
8
13
  # write a +template+ to an OpenDocument file
9
14
  def write(template)
10
15
  # TODO
@@ -1,53 +1,93 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './rubyxl_modifier'
4
-
5
4
  module CSVPlusPlus
6
5
  module Writer
7
6
  # Build a RubyXL workbook formatted according to the given +rows+
8
7
  #
9
8
  # @attr_reader input_filename [String] The filename being written to
10
9
  # @attr_reader rows [Array<Row>] The rows being written
10
+ # rubocop:disable Metrics/ClassLength
11
11
  class RubyXLBuilder
12
- attr_reader :input_filename, :rows
12
+ extend ::T::Sig
13
+
14
+ RubyXLCell = ::T.type_alias { ::T.all(::RubyXL::Cell, ::RubyXL::CellConvenienceMethods) }
15
+ public_constant :RubyXLCell
16
+
17
+ sig { returns(::T.nilable(::String)) }
18
+ attr_reader :input_filename
13
19
 
14
- # @param input_filename [String] The file to write to
20
+ sig { returns(::T::Array[::CSVPlusPlus::Row]) }
21
+ attr_reader :rows
22
+
23
+ sig do
24
+ params(
25
+ input_filename: ::T.nilable(::String),
26
+ rows: ::T::Array[::CSVPlusPlus::Row],
27
+ runtime: ::CSVPlusPlus::Runtime::Runtime,
28
+ sheet_name: ::T.nilable(::String)
29
+ ).void
30
+ end
31
+ # @param input_filename [::String] The file to write to
15
32
  # @param rows [Array<Row>] The rows to write
16
- # @param sheet_name [String] The name of the sheet within the workbook to write to
17
- def initialize(input_filename:, rows:, sheet_name:)
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)
18
36
  @rows = rows
19
37
  @input_filename = input_filename
38
+ @runtime = runtime
20
39
  @sheet_name = sheet_name
40
+ @worksheet = ::T.let(open_worksheet, ::RubyXL::Worksheet)
21
41
  end
22
42
 
43
+ sig { returns(::RubyXL::Workbook) }
23
44
  # Build a +RubyXL::Workbook+ with the given +@rows+ in +sheet_name+
24
45
  #
25
46
  # @return [RubyXL::Workbook]
26
47
  def build_workbook
27
- open_workbook.tap do |workbook|
28
- @worksheet = workbook[@sheet_name]
29
- build_workbook!
30
- end
48
+ build_workbook!
49
+ @worksheet.workbook
31
50
  end
32
51
 
33
52
  private
34
53
 
54
+ sig { void }
55
+ # rubocop:disable Metrics/MethodLength
35
56
  def build_workbook!
36
- @rows.each_with_index do |row, x|
37
- row.cells.each_with_index do |cell, y|
38
- modifier = ::CSVPlusPlus::Writer::RubyXLModifier.new(cell.modifier)
39
-
40
- @worksheet.add_cell(x, y, cell.to_csv)
41
- format_cell!(x, y, modifier)
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)
42
63
  end
64
+
65
+ format_cell!(
66
+ @runtime.row_index,
67
+ @runtime.cell_index,
68
+ ::T.cast(cell.modifier, ::CSVPlusPlus::Modifier::RubyXLModifier)
69
+ )
43
70
  end
44
71
  end
72
+ # rubocop:enable Metrics/MethodLength
45
73
 
74
+ sig do
75
+ params(
76
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
77
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
78
+ ).void
79
+ end
46
80
  def do_alignments!(cell, modifier)
47
- cell.change_horizontal_alignment(modifier.halign) if modifier.halign
48
- cell.change_vertical_alignment(modifier.valign) if modifier.valign
81
+ cell.change_horizontal_alignment(modifier.horizontal_alignment) if modifier.halign
82
+ cell.change_vertical_alignment(modifier.vertical_alignment) if modifier.valign
49
83
  end
50
84
 
85
+ sig do
86
+ params(
87
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
88
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
89
+ ).void
90
+ end
51
91
  # rubocop:disable Metrics/MethodLength
52
92
  def do_borders!(cell, modifier)
53
93
  return unless modifier.any_border?
@@ -62,29 +102,56 @@ module CSVPlusPlus
62
102
  end
63
103
  else
64
104
  modifier.borders.each do |direction|
65
- cell.change_border(direction, color || weight)
105
+ # TODO: move direction.serialize into the RubyXLModifier
106
+ cell.change_border(direction.serialize, color || weight)
66
107
  end
67
108
  end
68
109
  end
69
110
  # rubocop:enable Metrics/MethodLength
70
111
 
112
+ sig do
113
+ params(
114
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
115
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
116
+ ).void
117
+ end
71
118
  def do_fill!(cell, modifier)
72
- cell.change_fill(modifier.color.to_hex) if modifier.color
119
+ return unless modifier.color
120
+
121
+ cell.change_fill(modifier.color&.to_hex)
73
122
  end
74
123
 
124
+ sig do
125
+ params(
126
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
127
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
128
+ ).void
129
+ end
75
130
  def do_formats!(cell, modifier)
76
- cell.change_font_bold(true) if modifier.formatted?('bold')
77
- cell.change_font_italics(true) if modifier.formatted?('italic')
78
- cell.change_font_underline(true) if modifier.formatted?('underline')
79
- cell.change_font_strikethrough(true) if modifier.formatted?('strikethrough')
131
+ cell.change_font_bold(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Bold)
132
+ cell.change_font_italics(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Italic)
133
+ cell.change_font_underline(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Underline)
134
+ cell.change_font_strikethrough(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Strikethrough)
80
135
  end
81
136
 
137
+ sig do
138
+ params(
139
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
140
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
141
+ ).void
142
+ end
82
143
  def do_fonts!(cell, modifier)
83
- cell.change_font_color(modifier.fontcolor.to_hex) if modifier.fontcolor
144
+ cell.change_font_color(::T.must(modifier.fontcolor).to_hex) if modifier.fontcolor
84
145
  cell.change_font_name(modifier.fontfamily) if modifier.fontfamily
85
146
  cell.change_font_size(modifier.fontsize) if modifier.fontsize
86
147
  end
87
148
 
149
+ sig do
150
+ params(
151
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
152
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
153
+ ).void
154
+ end
88
155
  def do_number_formats!(cell, modifier)
89
156
  return unless modifier.numberformat
90
157
 
@@ -93,6 +160,9 @@ module CSVPlusPlus
93
160
  cell.change_contents(cell.value)
94
161
  end
95
162
 
163
+ sig do
164
+ params(row_index: ::Integer, cell_index: ::Integer, modifier: ::CSVPlusPlus::Modifier::RubyXLModifier).void
165
+ end
96
166
  def format_cell!(row_index, cell_index, modifier)
97
167
  @worksheet.sheet_data[row_index][cell_index].tap do |cell|
98
168
  do_alignments!(cell, modifier)
@@ -104,17 +174,17 @@ module CSVPlusPlus
104
174
  end
105
175
  end
106
176
 
107
- def open_workbook
108
- if ::File.exist?(@input_filename)
109
- ::RubyXL::Parser.parse(@input_filename).tap do |workbook|
110
- workbook.add_worksheet(@sheet_name) unless workbook[@sheet_name]
111
- end
177
+ sig { returns(::RubyXL::Worksheet) }
178
+ 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)
112
182
  else
113
- ::RubyXL::Workbook.new.tap do |workbook|
114
- workbook.worksheets[0].sheet_name = @sheet_name
115
- end
183
+ workbook = ::RubyXL::Workbook.new
184
+ workbook.worksheets[0].tap { |w| w.sheet_name = @sheet_name }
116
185
  end
117
186
  end
118
187
  end
188
+ # rubocop:enable Metrics/ClassLength
119
189
  end
120
190
  end