csv_plus_plus 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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