csv_plus_plus 0.1.2 → 0.2.0

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 (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