csv_plus_plus 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  6. data/lib/csv_plus_plus/cell.rb +29 -41
  7. data/lib/csv_plus_plus/cli.rb +53 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +71 -71
  9. data/lib/csv_plus_plus/color.rb +32 -7
  10. data/lib/csv_plus_plus/compiler.rb +98 -66
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
  12. data/lib/csv_plus_plus/entities/boolean.rb +26 -10
  13. data/lib/csv_plus_plus/entities/builtins.rb +66 -24
  14. data/lib/csv_plus_plus/entities/date.rb +42 -6
  15. data/lib/csv_plus_plus/entities/entity.rb +17 -69
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
  17. data/lib/csv_plus_plus/entities/function.rb +34 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +49 -10
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +30 -11
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
  23. data/lib/csv_plus_plus/entities/string.rb +23 -7
  24. data/lib/csv_plus_plus/entities.rb +7 -16
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +25 -2
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  33. data/lib/csv_plus_plus/error.rb +5 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +25 -10
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
  38. data/lib/csv_plus_plus/lexer.rb +64 -1
  39. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  40. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  41. data/lib/csv_plus_plus/modifier/expand.rb +78 -0
  42. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  43. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  44. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  45. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  46. data/lib/csv_plus_plus/modifier.rb +89 -160
  47. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  48. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  49. data/lib/csv_plus_plus/options/options.rb +97 -0
  50. data/lib/csv_plus_plus/options.rb +34 -77
  51. data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
  52. data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
  53. data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
  54. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  55. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  56. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  57. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  58. data/lib/csv_plus_plus/reader.rb +14 -0
  59. data/lib/csv_plus_plus/row.rb +53 -12
  60. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  61. data/lib/csv_plus_plus/runtime/position.rb +242 -0
  62. data/lib/csv_plus_plus/runtime/references.rb +115 -0
  63. data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
  64. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  65. data/lib/csv_plus_plus/runtime.rb +34 -191
  66. data/lib/csv_plus_plus/source_code.rb +71 -0
  67. data/lib/csv_plus_plus/template.rb +71 -39
  68. data/lib/csv_plus_plus/version.rb +2 -1
  69. data/lib/csv_plus_plus/writer/csv.rb +37 -8
  70. data/lib/csv_plus_plus/writer/excel.rb +25 -5
  71. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
  72. data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
  73. data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
  74. data/lib/csv_plus_plus/writer/merger.rb +31 -0
  75. data/lib/csv_plus_plus/writer/open_document.rb +21 -2
  76. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
  77. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  78. data/lib/csv_plus_plus/writer.rb +79 -10
  79. data/lib/csv_plus_plus.rb +47 -18
  80. metadata +50 -21
  81. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  82. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  83. data/lib/csv_plus_plus/data_validation.rb +0 -138
  84. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
  85. data/lib/csv_plus_plus/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
  87. data/lib/csv_plus_plus/expand.rb +0 -20
  88. data/lib/csv_plus_plus/google_options.rb +0 -27
  89. data/lib/csv_plus_plus/graph.rb +0 -62
  90. data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
  91. data/lib/csv_plus_plus/references.rb +0 -68
  92. data/lib/csv_plus_plus/scope.rb +0 -196
  93. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  94. data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
  95. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
  96. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  97. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,179 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Writer
6
+ # Given +rows+ from a +Template+, build requests compatible with Google Sheets Ruby API
7
+ # rubocop:disable Metrics/ClassLength
8
+ class GoogleSheetsBuilder
9
+ extend ::T::Sig
10
+ include ::CSVPlusPlus::Writer::Merger
11
+
12
+ sig do
13
+ params(
14
+ options: ::CSVPlusPlus::Options::GoogleSheetsOptions,
15
+ position: ::CSVPlusPlus::Runtime::Position,
16
+ reader: ::CSVPlusPlus::Reader::GoogleSheets,
17
+ rows: ::T::Array[::CSVPlusPlus::Row]
18
+ ).void
19
+ end
20
+ # @param options [Options]
21
+ # @param position [Position] The current position.
22
+ # @param reader [::CSVPlusPlus::Reader::GoogleSheets]
23
+ # @param rows [Array<Row>] The rows to render
24
+ def initialize(options:, position:, reader:, rows:)
25
+ @options = options
26
+ @position = position
27
+ @reader = reader
28
+ @rows = rows
29
+ end
30
+
31
+ sig { returns(::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest) }
32
+ # Build a Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest
33
+ #
34
+ # @return [Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest]
35
+ def batch_update_spreadsheet_request
36
+ build_batch_request(@rows)
37
+ end
38
+
39
+ private
40
+
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
55
+ end
56
+ end
57
+ # rubocop:enable Metrics/MethodLength
58
+
59
+ sig { params(mod: ::CSVPlusPlus::Modifier::GoogleSheetModifier).returns(::Google::Apis::SheetsV4::CellFormat) }
60
+ def build_cell_format(mod)
61
+ ::Google::Apis::SheetsV4::CellFormat.new.tap do |cf|
62
+ cf.text_format = mod.text_format
63
+
64
+ cf.horizontal_alignment = mod.horizontal_alignment
65
+ cf.vertical_alignment = mod.vertical_alignment
66
+ cf.background_color = mod.background_color
67
+ cf.number_format = mod.number_format
68
+ end
69
+ end
70
+
71
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::GridRange) }
72
+ def grid_range_for_cell(cell)
73
+ ::Google::Apis::SheetsV4::GridRange.new(
74
+ start_column_index: cell.index,
75
+ end_column_index: cell.index + 1,
76
+ start_row_index: cell.row_index,
77
+ end_row_index: cell.row_index + 1
78
+ )
79
+ end
80
+
81
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::ExtendedValue) }
82
+ def build_cell_value(cell)
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
+ )
90
+ end
91
+
92
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::CellData) }
93
+ def build_cell_data(cell)
94
+ mod = cell.modifier
95
+
96
+ ::Google::Apis::SheetsV4::CellData.new.tap do |cd|
97
+ cd.user_entered_format = build_cell_format(::T.cast(mod, ::CSVPlusPlus::Modifier::GoogleSheetModifier))
98
+ cd.note = mod.note if mod.note
99
+
100
+ # TODO: apply data validation
101
+ cd.user_entered_value = build_cell_value(cell)
102
+ end
103
+ end
104
+
105
+ sig { params(row: ::CSVPlusPlus::Row).returns(::Google::Apis::SheetsV4::RowData) }
106
+ def build_row_data(row)
107
+ ::Google::Apis::SheetsV4::RowData.new(values: row.cells.map { |cell| build_cell_data(cell) })
108
+ end
109
+
110
+ sig { params(rows: ::T::Array[::CSVPlusPlus::Row]).returns(::Google::Apis::SheetsV4::UpdateCellsRequest) }
111
+ def build_update_cells_request(rows)
112
+ ::Google::Apis::SheetsV4::UpdateCellsRequest.new(
113
+ fields: '*',
114
+ start: ::Google::Apis::SheetsV4::GridCoordinate.new(
115
+ column_index: @options.offset[1],
116
+ row_index: @options.offset[0]
117
+ ),
118
+ rows:
119
+ )
120
+ end
121
+
122
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::UpdateBordersRequest) }
123
+ def build_border(cell)
124
+ mod = ::T.cast(cell.modifier, ::CSVPlusPlus::Modifier::GoogleSheetModifier)
125
+ border = mod.border
126
+
127
+ ::Google::Apis::SheetsV4::UpdateBordersRequest.new(
128
+ top: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Top) ? border : nil,
129
+ right: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Right) ? border : nil,
130
+ left: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Left) ? border : nil,
131
+ bottom: mod.border_along?(::CSVPlusPlus::Modifier::BorderSide::Bottom) ? border : nil,
132
+ range: grid_range_for_cell(cell)
133
+ )
134
+ end
135
+
136
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::Google::Apis::SheetsV4::Request) }
137
+ def build_update_borders_request(cell)
138
+ ::Google::Apis::SheetsV4::Request.new(update_borders: build_border(cell))
139
+ end
140
+
141
+ sig { params(rows: ::T::Array[::CSVPlusPlus::Row]).returns(::T::Array[::Google::Apis::SheetsV4::Request]) }
142
+ # rubocop:disable Metrics/MethodLength
143
+ def chunked_requests(rows)
144
+ accum = []
145
+ [].tap do |chunked|
146
+ @position.map_rows(rows) do |row|
147
+ accum << build_row_data(row)
148
+ next unless accum.length == 1000
149
+
150
+ chunked << ::Google::Apis::SheetsV4::Request.new(update_cells: build_update_cells_request(accum))
151
+ accum = []
152
+ end
153
+
154
+ unless accum.empty?
155
+ chunked << ::Google::Apis::SheetsV4::Request.new(update_cells: build_update_cells_request(accum))
156
+ end
157
+ end
158
+ end
159
+ # rubocop:enable Metrics/MethodLength
160
+
161
+ sig do
162
+ params(rows: ::T::Array[::CSVPlusPlus::Row]).returns(::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest)
163
+ end
164
+ def build_batch_request(rows)
165
+ ::Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new.tap do |bu|
166
+ bu.requests = chunked_requests(rows)
167
+
168
+ @position.map_rows(rows) do |row|
169
+ row.cells.filter { |c| c.modifier.any_border? }
170
+ .each do |cell|
171
+ bu.requests << build_update_borders_request(cell)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ # rubocop:enable Metrics/ClassLength
178
+ end
179
+ end
@@ -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
@@ -1,14 +1,33 @@
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
- class OpenDocument < ::CSVPlusPlus::Writer::BaseWriter
7
+ class OpenDocument < ::CSVPlusPlus::Writer::Writer
8
+ extend ::T::Sig
9
+ include ::CSVPlusPlus::Writer::FileBackerUpper
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
+
20
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
8
21
  # write a +template+ to an OpenDocument file
9
22
  def write(template)
10
23
  # TODO
11
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
12
31
  end
13
32
  end
14
33
  end
@@ -1,50 +1,80 @@
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
- # @attr_reader input_filename [String] The filename being written to
8
+ # @attr_reader output_filename [Pathname, nil] 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
+ include ::CSVPlusPlus::Writer::Merger
14
+
15
+ RubyXLCell = ::T.type_alias { ::T.all(::RubyXL::Cell, ::RubyXL::CellConvenienceMethods) }
16
+ public_constant :RubyXLCell
17
+
18
+ RubyXLValue = ::T.type_alias { ::T.any(::String, ::Numeric, ::Date) }
19
+ public_constant :RubyXLValue
20
+
21
+ sig { returns(::T::Array[::CSVPlusPlus::Row]) }
22
+ attr_reader :rows
13
23
 
14
- # @param input_filename [String] The file to write to
24
+ sig do
25
+ params(
26
+ options: ::CSVPlusPlus::Options::FileOptions,
27
+ position: ::CSVPlusPlus::Runtime::Position,
28
+ rows: ::T::Array[::CSVPlusPlus::Row]
29
+ ).void
30
+ end
31
+ # @param options [Options::FileOptions]
32
+ # @param position [Position] The current position
15
33
  # @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:)
34
+ def initialize(options:, position:, rows:)
35
+ @options = options
36
+ @position = position
18
37
  @rows = rows
19
- @input_filename = input_filename
20
- @sheet_name = sheet_name
38
+ @worksheet = ::T.let(open_worksheet, ::RubyXL::Worksheet)
39
+ @reader = ::T.let(::CSVPlusPlus::Reader::RubyXL.new(@options, @worksheet), ::CSVPlusPlus::Reader::RubyXL)
21
40
  end
22
41
 
42
+ sig { returns(::RubyXL::Workbook) }
23
43
  # Build a +RubyXL::Workbook+ with the given +@rows+ in +sheet_name+
24
44
  #
25
45
  # @return [RubyXL::Workbook]
26
46
  def build_workbook
27
- open_workbook.tap { build_workbook! }
47
+ build_workbook!
48
+ @worksheet.workbook
28
49
  end
29
50
 
30
51
  private
31
52
 
53
+ sig { void }
32
54
  def build_workbook!
33
- @rows.each_with_index do |row, x|
34
- row.cells.each_with_index do |cell, y|
35
- modifier = ::CSVPlusPlus::Writer::RubyXLModifier.new(cell.modifier)
36
-
37
- @worksheet.add_cell(x, y, cell.to_csv)
38
- format_cell!(x, y, modifier)
39
- end
55
+ @position.map_all_cells(@rows) do |cell|
56
+ build_cell(cell)
57
+ format_cell!(cell)
40
58
  end
41
59
  end
42
60
 
61
+ sig do
62
+ params(
63
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
64
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
65
+ ).void
66
+ end
43
67
  def do_alignments!(cell, modifier)
44
- cell.change_horizontal_alignment(modifier.halign) if modifier.halign
45
- cell.change_vertical_alignment(modifier.valign) if modifier.valign
68
+ cell.change_horizontal_alignment(modifier.horizontal_alignment) if modifier.halign
69
+ cell.change_vertical_alignment(modifier.vertical_alignment) if modifier.valign
46
70
  end
47
71
 
72
+ sig do
73
+ params(
74
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
75
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
76
+ ).void
77
+ end
48
78
  # rubocop:disable Metrics/MethodLength
49
79
  def do_borders!(cell, modifier)
50
80
  return unless modifier.any_border?
@@ -59,29 +89,56 @@ module CSVPlusPlus
59
89
  end
60
90
  else
61
91
  modifier.borders.each do |direction|
62
- cell.change_border(direction, color || weight)
92
+ # TODO: move direction.serialize into the RubyXLModifier
93
+ cell.change_border(direction.serialize, color || weight)
63
94
  end
64
95
  end
65
96
  end
66
97
  # rubocop:enable Metrics/MethodLength
67
98
 
99
+ sig do
100
+ params(
101
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
102
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
103
+ ).void
104
+ end
68
105
  def do_fill!(cell, modifier)
69
- cell.change_fill(modifier.color.to_hex) if modifier.color
106
+ return unless modifier.color
107
+
108
+ cell.change_fill(modifier.color&.to_hex)
70
109
  end
71
110
 
111
+ sig do
112
+ params(
113
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
114
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
115
+ ).void
116
+ end
72
117
  def do_formats!(cell, modifier)
73
- cell.change_font_bold(true) if modifier.formatted?('bold')
74
- cell.change_font_italics(true) if modifier.formatted?('italic')
75
- cell.change_font_underline(true) if modifier.formatted?('underline')
76
- cell.change_font_strikethrough(true) if modifier.formatted?('strikethrough')
118
+ cell.change_font_bold(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Bold)
119
+ cell.change_font_italics(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Italic)
120
+ cell.change_font_underline(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Underline)
121
+ cell.change_font_strikethrough(true) if modifier.formatted?(::CSVPlusPlus::Modifier::TextFormat::Strikethrough)
77
122
  end
78
123
 
124
+ sig do
125
+ params(
126
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
127
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
128
+ ).void
129
+ end
79
130
  def do_fonts!(cell, modifier)
80
- cell.change_font_color(modifier.fontcolor.to_hex) if modifier.fontcolor
131
+ cell.change_font_color(::T.must(modifier.fontcolor).to_hex) if modifier.fontcolor
81
132
  cell.change_font_name(modifier.fontfamily) if modifier.fontfamily
82
133
  cell.change_font_size(modifier.fontsize) if modifier.fontsize
83
134
  end
84
135
 
136
+ sig do
137
+ params(
138
+ cell: ::CSVPlusPlus::Writer::RubyXLBuilder::RubyXLCell,
139
+ modifier: ::CSVPlusPlus::Modifier::RubyXLModifier
140
+ ).void
141
+ end
85
142
  def do_number_formats!(cell, modifier)
86
143
  return unless modifier.numberformat
87
144
 
@@ -90,28 +147,69 @@ module CSVPlusPlus
90
147
  cell.change_contents(cell.value)
91
148
  end
92
149
 
93
- def format_cell!(row_index, cell_index, modifier)
94
- @worksheet.sheet_data[row_index][cell_index].tap do |cell|
95
- do_alignments!(cell, modifier)
96
- do_borders!(cell, modifier)
97
- do_fill!(cell, modifier)
98
- do_fonts!(cell, modifier)
99
- do_formats!(cell, modifier)
100
- do_number_formats!(cell, modifier)
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)
101
159
  end
102
160
  end
103
161
 
104
- def open_workbook
105
- if ::File.exist?(@input_filename)
106
- ::RubyXL::Parser.parse(@input_filename).tap do |workbook|
107
- @worksheet = workbook[@sheet_name] || workbook.add_worksheet(@sheet_name)
108
- end
162
+ sig do
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)
182
+ end
183
+ end
184
+
185
+ sig { returns(::RubyXL::Worksheet) }
186
+ def open_worksheet
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)
109
190
  else
110
- ::RubyXL::Workbook.new.tap do |workbook|
111
- @worksheet = workbook.worksheets[0].tap { |w| w.sheet_name = @sheet_name }
112
- end
191
+ workbook = ::RubyXL::Workbook.new
192
+ workbook.worksheets[0].tap { |w| w.sheet_name = @options.sheet_name }
113
193
  end
114
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
115
212
  end
213
+ # rubocop:enable Metrics/ClassLength
116
214
  end
117
215
  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
@@ -1,24 +1,93 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './writer/base_writer'
4
+ require_relative './writer/merger'
5
+ require_relative './writer/writer'
6
+
4
7
  require_relative './writer/csv'
5
8
  require_relative './writer/excel'
6
9
  require_relative './writer/google_sheets'
10
+ require_relative './writer/google_sheets_builder'
7
11
  require_relative './writer/open_document'
12
+ require_relative './writer/rubyxl_builder'
8
13
 
9
14
  module CSVPlusPlus
10
- # Various strategies for writing to various formats (excel, google sheets, CSV, OpenDocument)
15
+ # Various strategies for writing to various formats (excel, google sheets, CSV & OpenDocument (not yet implemented))
11
16
  module Writer
17
+ extend ::T::Sig
18
+
19
+ sig do
20
+ params(
21
+ options: ::CSVPlusPlus::Options::Options,
22
+ position: ::CSVPlusPlus::Runtime::Position
23
+ ).returns(
24
+ ::T.any(
25
+ ::CSVPlusPlus::Writer::CSV,
26
+ ::CSVPlusPlus::Writer::Excel,
27
+ ::CSVPlusPlus::Writer::GoogleSheets,
28
+ ::CSVPlusPlus::Writer::OpenDocument
29
+ )
30
+ )
31
+ end
12
32
  # Return an instance of a writer depending on the given +options+
13
- def self.writer(options)
14
- return ::CSVPlusPlus::Writer::GoogleSheets.new(options) if options.google
15
-
16
- case options.output_filename
17
- when /\.csv$/ then ::CSVPlusPlus::Writer::CSV.new(options)
18
- when /\.ods$/ then ::CSVPlusPlus::Writer::OpenDocument.new(options)
19
- when /\.xl(sx|sm|tx|tm)$/ then ::CSVPlusPlus::Writer::Excel.new(options)
20
- else raise(::CSVPlusPlus::Error, "Unsupported file extension: #{options.output_filename}")
33
+ #
34
+ # @param options [Options] The supplied options.
35
+ # @param position [Position] The current position.
36
+ #
37
+ # @return [Writer::CSV | Writer::Excel | Writer::GoogleSheets | Writer::OpenDocument]
38
+ def self.writer(options, position)
39
+ output_format = options.output_format
40
+ case output_format
41
+ when ::CSVPlusPlus::Options::OutputFormat::CSV then csv(options, position)
42
+ when ::CSVPlusPlus::Options::OutputFormat::Excel then excel(options, position)
43
+ when ::CSVPlusPlus::Options::OutputFormat::GoogleSheets then google_sheets(options, position)
44
+ when ::CSVPlusPlus::Options::OutputFormat::OpenDocument then open_document(options, position)
45
+ else ::T.absurd(output_format)
21
46
  end
22
47
  end
48
+
49
+ sig do
50
+ params(
51
+ options: ::CSVPlusPlus::Options::Options,
52
+ position: ::CSVPlusPlus::Runtime::Position
53
+ ).returns(::CSVPlusPlus::Writer::CSV)
54
+ end
55
+ # Instantiate a +CSV+ writer
56
+ def self.csv(options, position)
57
+ ::CSVPlusPlus::Writer::CSV.new(::T.cast(options, ::CSVPlusPlus::Options::FileOptions), position)
58
+ end
59
+
60
+ sig do
61
+ params(
62
+ options: ::CSVPlusPlus::Options::Options,
63
+ position: ::CSVPlusPlus::Runtime::Position
64
+ ).returns(::CSVPlusPlus::Writer::Excel)
65
+ end
66
+ # Instantiate a +Excel+ writer
67
+ def self.excel(options, position)
68
+ ::CSVPlusPlus::Writer::Excel.new(::T.cast(options, ::CSVPlusPlus::Options::FileOptions), position)
69
+ end
70
+
71
+ sig do
72
+ params(
73
+ options: ::CSVPlusPlus::Options::Options,
74
+ position: ::CSVPlusPlus::Runtime::Position
75
+ ).returns(::CSVPlusPlus::Writer::GoogleSheets)
76
+ end
77
+ # Instantiate a +GoogleSheets+ writer
78
+ def self.google_sheets(options, position)
79
+ ::CSVPlusPlus::Writer::GoogleSheets.new(::T.cast(options, ::CSVPlusPlus::Options::GoogleSheetsOptions), position)
80
+ end
81
+
82
+ sig do
83
+ params(
84
+ options: ::CSVPlusPlus::Options::Options,
85
+ position: ::CSVPlusPlus::Runtime::Position
86
+ ).returns(::CSVPlusPlus::Writer::OpenDocument)
87
+ end
88
+ # Instantiate an +OpenDocument+ writer
89
+ def self.open_document(options, position)
90
+ ::CSVPlusPlus::Writer::OpenDocument.new(::T.cast(options, ::CSVPlusPlus::Options::FileOptions), position)
91
+ end
23
92
  end
24
93
  end