csv_plus_plus 0.1.3 → 0.2.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -3
  3. data/docs/CHANGELOG.md +18 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
  6. data/lib/csv_plus_plus/cell.rb +1 -35
  7. data/lib/csv_plus_plus/cli.rb +43 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +77 -70
  9. data/lib/csv_plus_plus/color.rb +1 -1
  10. data/lib/csv_plus_plus/compiler.rb +31 -21
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
  12. data/lib/csv_plus_plus/entities/boolean.rb +16 -9
  13. data/lib/csv_plus_plus/entities/builtins.rb +68 -40
  14. data/lib/csv_plus_plus/entities/date.rb +14 -11
  15. data/lib/csv_plus_plus/entities/entity.rb +11 -29
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
  17. data/lib/csv_plus_plus/entities/function.rb +22 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +35 -11
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +15 -10
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
  23. data/lib/csv_plus_plus/entities/string.rb +13 -10
  24. data/lib/csv_plus_plus/entities.rb +2 -18
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +18 -5
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +1 -1
  33. data/lib/csv_plus_plus/error.rb +4 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +18 -8
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
  38. data/lib/csv_plus_plus/lexer.rb +40 -1
  39. data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
  40. data/lib/csv_plus_plus/modifier/expand.rb +17 -0
  41. data/lib/csv_plus_plus/modifier.rb +6 -1
  42. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  43. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  44. data/lib/csv_plus_plus/options/options.rb +102 -0
  45. data/lib/csv_plus_plus/options.rb +22 -110
  46. data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
  47. data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
  48. data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
  49. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  50. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  51. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  52. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  53. data/lib/csv_plus_plus/reader.rb +14 -0
  54. data/lib/csv_plus_plus/runtime/graph.rb +6 -6
  55. data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
  56. data/lib/csv_plus_plus/runtime/references.rb +32 -27
  57. data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
  58. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  59. data/lib/csv_plus_plus/runtime.rb +9 -9
  60. data/lib/csv_plus_plus/source_code.rb +14 -9
  61. data/lib/csv_plus_plus/template.rb +17 -12
  62. data/lib/csv_plus_plus/version.rb +1 -1
  63. data/lib/csv_plus_plus/writer/csv.rb +32 -5
  64. data/lib/csv_plus_plus/writer/excel.rb +19 -6
  65. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
  66. data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
  67. data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
  68. data/lib/csv_plus_plus/writer/merger.rb +56 -0
  69. data/lib/csv_plus_plus/writer/open_document.rb +16 -2
  70. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
  71. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  72. data/lib/csv_plus_plus/writer.rb +58 -19
  73. data/lib/csv_plus_plus.rb +26 -14
  74. metadata +43 -18
  75. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
  76. data/lib/csv_plus_plus/entities/variable.rb +0 -37
  77. data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
  78. data/lib/csv_plus_plus/google_options.rb +0 -32
  79. data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
  80. data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
  81. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
  82. data/lib/csv_plus_plus/writer/base_writer.rb +0 -45
@@ -0,0 +1,280 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # Responsible for storing and resolving variables and function references
7
+ # rubocop:disable Metrics/ClassLength
8
+ class Scope
9
+ extend ::T::Sig
10
+
11
+ sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function]) }
12
+ attr_reader :functions
13
+
14
+ sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]) }
15
+ attr_reader :variables
16
+
17
+ sig do
18
+ params(
19
+ functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
20
+ variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
21
+ ).void
22
+ end
23
+ # @param functions [Hash<Symbol, Function>] Pre-defined functions
24
+ # @param variables [Hash<Symbol, Entity>] Pre-defined variables
25
+ def initialize(functions: {}, variables: {})
26
+ @functions = functions
27
+ @variables = variables
28
+ end
29
+
30
+ sig { params(id: ::Symbol, entity: ::CSVPlusPlus::Entities::Entity).returns(::CSVPlusPlus::Entities::Entity) }
31
+ # Define a (or re-define an existing) variable
32
+ #
33
+ # @param id [String, Symbol] The identifier for the variable
34
+ # @param entity [Entity] The value (entity) the variable holds
35
+ #
36
+ # @return [Entity] The value of the variable (+entity+)
37
+ def def_variable(id, entity)
38
+ @variables[id] = entity
39
+ end
40
+
41
+ sig { params(vars: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]).void }
42
+ # Define (or re-define existing) variables
43
+ #
44
+ # @param vars [Hash<Symbol, Variable>] Variables to define
45
+ def def_variables(vars)
46
+ vars.each { |id, entity| def_variable(id, entity) }
47
+ end
48
+
49
+ sig do
50
+ params(id: ::Symbol, function: ::CSVPlusPlus::Entities::Function).returns(::CSVPlusPlus::Entities::Function)
51
+ end
52
+ # Define a (or re-define an existing) function
53
+ #
54
+ # @param id [Symbol] The identifier for the function
55
+ # @param function [Entities::Function] The defined function
56
+ #
57
+ # @return [Entities::Function] The defined function
58
+ def def_function(id, function)
59
+ @functions[id.to_sym] = function
60
+ end
61
+
62
+ sig { params(var_id: ::Symbol, position: ::CSVPlusPlus::Runtime::Position).returns(::T::Boolean) }
63
+ # Variables outside of an ![[expand=...] are always in scope. If it's defined within an expand then things
64
+ # get trickier because the variable is only in scope while we're processing cells within that expand.
65
+ #
66
+ # @param var_id [Symbol] The variable's identifier that we are checking if it's in scope
67
+ # @param position [Position]
68
+ #
69
+ # @return [boolean]
70
+ def in_scope?(var_id, position)
71
+ value = @variables[var_id]
72
+
73
+ return false unless value
74
+
75
+ expand = value.is_a?(::CSVPlusPlus::Entities::Reference) && value.a1_ref.scoped_to_expand
76
+ !expand || expand.position_within?(position)
77
+ end
78
+
79
+ sig { returns(::String) }
80
+ # Provide a summary of the functions and variables compiled (to show in verbose mode)
81
+ #
82
+ # @return [::String]
83
+ def verbose_summary
84
+ <<~SUMMARY
85
+ # Code Section Summary
86
+
87
+ ## Resolved Variables
88
+
89
+ #{variable_summary}
90
+
91
+ ## Functions
92
+
93
+ #{function_summary}
94
+ SUMMARY
95
+ end
96
+
97
+ sig do
98
+ params(
99
+ position: ::CSVPlusPlus::Runtime::Position,
100
+ ast: ::CSVPlusPlus::Entities::Entity,
101
+ refs: ::T::Enumerable[::CSVPlusPlus::Entities::FunctionCall]
102
+ ).returns(::CSVPlusPlus::Entities::Entity)
103
+ end
104
+ # @param position [Position
105
+ # @param ast [Entity]
106
+ # @param refs [Array<FunctionCall>]
107
+ #
108
+ # @return [Entity]
109
+ def resolve_functions(position, ast, refs)
110
+ refs.reduce(ast.dup) do |acc, elem|
111
+ function_replace(position, acc, elem.id, resolve_function(elem.id))
112
+ end
113
+ end
114
+
115
+ sig do
116
+ params(
117
+ position: ::CSVPlusPlus::Runtime::Position,
118
+ ast: ::CSVPlusPlus::Entities::Entity,
119
+ refs: ::T::Enumerable[::CSVPlusPlus::Entities::Reference]
120
+ ).returns(::CSVPlusPlus::Entities::Entity)
121
+ end
122
+ # @param position [Position]
123
+ # @param ast [Entity]
124
+ # @param refs [Array<Variable>]
125
+ #
126
+ # @return [Entity]
127
+ def resolve_variables(position, ast, refs)
128
+ refs.reduce(ast.dup) do |acc, elem|
129
+ next acc unless (id = elem.id)
130
+
131
+ variable_replace(acc, id, resolve_variable(position, id))
132
+ end
133
+ end
134
+
135
+ sig do
136
+ params(
137
+ position: ::CSVPlusPlus::Runtime::Position,
138
+ node: ::CSVPlusPlus::Entities::Entity,
139
+ fn_id: ::Symbol,
140
+ replacement: ::T.any(::CSVPlusPlus::Entities::Function, ::CSVPlusPlus::Entities::RuntimeValue)
141
+ ).returns(::CSVPlusPlus::Entities::Entity)
142
+ end
143
+ # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
144
+ # rubocop:disable Metrics/MethodLength
145
+ def function_replace(position, node, fn_id, replacement)
146
+ if node.is_a?(::CSVPlusPlus::Entities::FunctionCall) && node.id == fn_id
147
+ call_function_or_builtin(position, replacement, node)
148
+ elsif node.is_a?(::CSVPlusPlus::Entities::FunctionCall)
149
+ # not our function, but continue our depth first search on it
150
+ ::CSVPlusPlus::Entities::FunctionCall.new(
151
+ node.id,
152
+ node.arguments.map { |n| function_replace(position, n, fn_id, replacement) },
153
+ infix: node.infix
154
+ )
155
+ else
156
+ node
157
+ end
158
+ end
159
+ # rubocop:enable Metrics/MethodLength
160
+
161
+ sig do
162
+ params(fn_id: ::Symbol)
163
+ .returns(::T.any(::CSVPlusPlus::Entities::Function, ::CSVPlusPlus::Entities::RuntimeValue))
164
+ end
165
+ # @param fn_id [Symbol]
166
+ #
167
+ # @return [Entities::Function]
168
+ def resolve_function(fn_id)
169
+ return ::T.must(@functions[fn_id]) if @functions.key?(fn_id)
170
+
171
+ builtin = ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[fn_id]
172
+ raise(::CSVPlusPlus::Error::FormulaSyntaxError.new('Undefined function', bad_input: fn_id.to_s)) unless builtin
173
+
174
+ builtin
175
+ end
176
+
177
+ sig do
178
+ params(
179
+ position: ::CSVPlusPlus::Runtime::Position,
180
+ function_or_builtin: ::T.any(::CSVPlusPlus::Entities::RuntimeValue, ::CSVPlusPlus::Entities::Function),
181
+ function_call: ::CSVPlusPlus::Entities::FunctionCall
182
+ ).returns(::CSVPlusPlus::Entities::Entity)
183
+ end
184
+ # @param position [Position]
185
+ # @param function_or_builtin [Entities::Function, Entities::RuntimeValue]
186
+ # @param function_call [Entities::FunctionCall]
187
+ #
188
+ # @return [Entities::Entity]
189
+ def call_function_or_builtin(position, function_or_builtin, function_call)
190
+ if function_or_builtin.is_a?(::CSVPlusPlus::Entities::RuntimeValue)
191
+ function_or_builtin.call(position, function_call.arguments)
192
+ else
193
+ call_function(function_or_builtin, function_call)
194
+ end
195
+ end
196
+
197
+ sig do
198
+ params(
199
+ function: ::CSVPlusPlus::Entities::Function,
200
+ function_call: ::CSVPlusPlus::Entities::FunctionCall
201
+ ).returns(::CSVPlusPlus::Entities::Entity)
202
+ end
203
+ # Since functions are just built up from existing functions, a "call" is effectively replacing the variable
204
+ # references in the +@body+ with the ones being passed as arguments
205
+ #
206
+ # @param function [Entities::Function] The function being called
207
+ # @param function_call [Entities::FunctionCall] The actual call of the function
208
+ #
209
+ # @return [Entities::Entity]
210
+ def call_function(function, function_call)
211
+ i = 0
212
+ function.arguments.reduce(function.body.dup) do |ast, argument|
213
+ variable_replace(ast, argument, ::T.must(function_call.arguments[i])).tap do
214
+ i += 1
215
+ end
216
+ end
217
+ end
218
+
219
+ sig do
220
+ params(
221
+ node: ::CSVPlusPlus::Entities::Entity,
222
+ var_id: ::Symbol,
223
+ replacement: ::CSVPlusPlus::Entities::Entity
224
+ ).returns(::CSVPlusPlus::Entities::Entity)
225
+ end
226
+ # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
227
+ def variable_replace(node, var_id, replacement)
228
+ if node.is_a?(::CSVPlusPlus::Entities::FunctionCall)
229
+ arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
230
+ # TODO: refactor these places where we copy functions... it's brittle with the kwargs
231
+ ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
232
+ elsif node.is_a?(::CSVPlusPlus::Entities::Reference) && node.id == var_id
233
+ replacement
234
+ else
235
+ node
236
+ end
237
+ end
238
+
239
+ sig do
240
+ params(position: ::CSVPlusPlus::Runtime::Position, var_id: ::Symbol).returns(::CSVPlusPlus::Entities::Entity)
241
+ end
242
+ # @param position [Position]
243
+ # @param var_id [Symbol]
244
+ #
245
+ # @return [Entities::Entity]
246
+ def resolve_variable(position, var_id)
247
+ return ::T.must(@variables[var_id]) if @variables.key?(var_id)
248
+
249
+ unless ::CSVPlusPlus::Entities::Builtins.builtin_variable?(var_id)
250
+ raise(::CSVPlusPlus::Error::FormulaSyntaxError.new('Undefined variable', bad_input: var_id.to_s))
251
+ end
252
+
253
+ ::T.must(::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id]).call(position, [])
254
+ end
255
+
256
+ sig { returns(::String) }
257
+ # Create a summary of all currently defined variables
258
+ #
259
+ # @return [String]
260
+ def variable_summary
261
+ return '(no variables defined)' if @variables.empty?
262
+
263
+ @variables.map { |k, v| "#{k} := #{v}" }
264
+ .join("\n")
265
+ end
266
+
267
+ sig { returns(::String) }
268
+ # Create a summary of all currently defined functions
269
+ #
270
+ # @return [String]
271
+ def function_summary
272
+ return '(no functions defined)' if @functions.empty?
273
+
274
+ @functions.map { |k, f| "#{k}: #{f}" }
275
+ .join("\n")
276
+ end
277
+ end
278
+ # rubocop:enable Metrics/ClassLength
279
+ end
280
+ end
@@ -1,12 +1,11 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative './runtime/can_define_references'
5
- require_relative './runtime/can_resolve_references'
6
4
  require_relative './runtime/graph'
7
- require_relative './runtime/position_tracker'
5
+ require_relative './runtime/position'
8
6
  require_relative './runtime/references'
9
7
  require_relative './runtime/runtime'
8
+ require_relative './runtime/scope'
10
9
 
11
10
  module CSVPlusPlus
12
11
  # All functionality needed to keep track of the runtime AKA execution context. This module has a lot of
@@ -23,20 +22,21 @@ module CSVPlusPlus
23
22
  sig do
24
23
  params(
25
24
  source_code: ::CSVPlusPlus::SourceCode,
26
- functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
27
- variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
25
+ position: ::T.nilable(::CSVPlusPlus::Runtime::Position),
26
+ scope: ::T.nilable(::CSVPlusPlus::Runtime::Scope)
28
27
  ).returns(::CSVPlusPlus::Runtime::Runtime)
29
28
  end
30
29
  # Initialize a runtime instance with all the functionality we need. A runtime is one-to-one with a file being
31
30
  # compiled.
32
31
  #
33
32
  # @param source_code [SourceCode] The csv++ source code to be compiled
34
- # @param functions [Hash<Symbol, Function>] Pre-defined functions
35
- # @param variables [Hash<Symbol, Entity>] Pre-defined variables
33
+ # @param position [Position, nil]
34
+ # @param scope [Scope, nil]
36
35
  #
37
36
  # @return [Runtime::Runtime]
38
- def self.new(source_code:, functions: {}, variables: {})
39
- ::CSVPlusPlus::Runtime::Runtime.new(source_code:, functions:, variables:)
37
+ def self.new(source_code:, position: nil, scope: nil)
38
+ position ||= ::CSVPlusPlus::Runtime::Position.new(source_code.input)
39
+ ::CSVPlusPlus::Runtime::Runtime.new(source_code:, position:, scope:)
40
40
  end
41
41
  end
42
42
  end
@@ -9,7 +9,7 @@ module CSVPlusPlus
9
9
  sig { returns(::String) }
10
10
  attr_reader :input
11
11
 
12
- sig { returns(::String) }
12
+ sig { returns(::Pathname) }
13
13
  attr_reader :filename
14
14
 
15
15
  sig { returns(::Integer) }
@@ -21,15 +21,13 @@ module CSVPlusPlus
21
21
  sig { returns(::Integer) }
22
22
  attr_reader :length_of_file
23
23
 
24
- sig { params(input: ::String, filename: ::T.nilable(::String)).void }
25
- # @param input [::String] The source code being parsed
26
- # @param filename [::String, nil] The name of the file the source came from. If not set we assume it came
27
- # from stdin
28
- def initialize(input:, filename: nil)
29
- @input = input
30
- @filename = ::T.let(filename || 'stdin', ::String)
24
+ sig { params(filename: ::String, input: ::T.nilable(::String)).void }
25
+ # @param filename [::String] The name of the file the source came from.
26
+ def initialize(filename, input: nil)
27
+ @filename = ::T.let(::Pathname.new(filename), ::Pathname)
28
+ @input = ::T.let(input || read_file, ::String)
31
29
 
32
- lines = input.split(/[\r\n]/)
30
+ lines = @input.split(/[\r\n]/)
33
31
  @length_of_file = ::T.let(lines.length, ::Integer)
34
32
  @length_of_code_section = ::T.let(count_code_section_lines(lines), ::Integer)
35
33
  @length_of_csv_section = ::T.let(@length_of_file - @length_of_code_section, ::Integer)
@@ -57,6 +55,13 @@ module CSVPlusPlus
57
55
 
58
56
  private
59
57
 
58
+ sig { returns(::String) }
59
+ def read_file
60
+ raise(::CSVPlusPlus::Error::CLIError, "Source file #{@filename} does not exist") unless ::File.exist?(@filename)
61
+
62
+ ::File.read(@filename)
63
+ end
64
+
60
65
  sig { params(lines: ::T::Array[::String]).returns(::Integer) }
61
66
  def count_code_section_lines(lines)
62
67
  eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
@@ -27,24 +27,29 @@ module CSVPlusPlus
27
27
  # Only run after expanding all rows, now we can bind all [[var=]] modifiers to a variable. There are two distinct
28
28
  # types of variable bindings here:
29
29
  #
30
- # * Binding to a cell: for this we just make a +CellReference+ to the cell itself (A1, B4, etc)
30
+ # * Binding to a cell: for this we just make an +A1Reference+ to the cell itself (A1, B4, etc)
31
31
  # * Binding to a cell within an expand: the variable can only be resolved within that expand and needs to be
32
32
  # relative to it's row (it can't be an absolute cell reference like above)
33
33
  #
34
34
  # @param runtime [Runtime] The current runtime
35
+ # rubocop:disable Metrics/MethodLength
35
36
  def bind_all_vars!(runtime)
36
- runtime.map_rows(@rows) do |row|
37
+ runtime.position.map_rows(@rows) do |row|
37
38
  # rubocop:disable Style/MissingElse
38
39
  if row.unexpanded?
39
40
  # rubocop:enable Style/MissingElse
40
- raise(::CSVPlusPlus::Error::Error, 'Template#expand_rows! must be called before Template#bind_all_vars!')
41
+ raise(
42
+ ::CSVPlusPlus::Error::CompilerError,
43
+ 'Template#expand_rows! must be called before Template#bind_all_vars!'
44
+ )
41
45
  end
42
46
 
43
- runtime.map_row(row.cells) do |cell|
47
+ runtime.position.map_row(row.cells) do |cell|
44
48
  bind_vars(cell, row.modifier.expand)
45
49
  end
46
50
  end
47
51
  end
52
+ # rubocop:enable Metrics/MethodLength
48
53
 
49
54
  sig { returns(::T::Array[::CSVPlusPlus::Row]) }
50
55
  # Apply expand= (adding rows to the results) modifiers to the parsed template. This happens in towards the end of
@@ -64,17 +69,17 @@ module CSVPlusPlus
64
69
  end
65
70
  end
66
71
 
67
- sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).void }
72
+ sig { void }
68
73
  # Make sure that the template has a valid amount of infinite expand modifiers
69
- #
70
- # @param runtime [Runtime] The compiler's current runtime
71
- def validate_infinite_expands(runtime)
74
+ def validate_infinite_expands
72
75
  infinite_expand_rows = @rows.filter { |r| r.modifier.expand&.infinite? }
73
76
  return unless infinite_expand_rows.length > 1
74
77
 
75
- runtime.raise_modifier_syntax_error(
76
- 'You can only have one infinite expand= (on all others you must specify an amount)',
77
- infinite_expand_rows[1].to_s
78
+ raise(
79
+ ::CSVPlusPlus::Error::ModifierSyntaxError.new(
80
+ 'You can only have one infinite expand= (on all others you must specify an amount)',
81
+ bad_input: infinite_expand_rows[1].to_s
82
+ )
78
83
  )
79
84
  end
80
85
 
@@ -85,7 +90,7 @@ module CSVPlusPlus
85
90
  def verbose_summary
86
91
  # TODO: we can probably include way more stats in here
87
92
  <<~SUMMARY
88
- #{@runtime.verbose_summary}
93
+ #{@runtime.scope.verbose_summary}
89
94
 
90
95
  > #{@rows.length} rows to be written
91
96
  SUMMARY
@@ -2,6 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module CSVPlusPlus
5
- VERSION = '0.1.3'
5
+ VERSION = '0.2.1'
6
6
  public_constant :VERSION
7
7
  end
@@ -6,10 +6,20 @@ require_relative './file_backer_upper'
6
6
  module CSVPlusPlus
7
7
  module Writer
8
8
  # A class that can output a +Template+ to CSV
9
- class CSV < ::CSVPlusPlus::Writer::BaseWriter
9
+ class CSV < ::CSVPlusPlus::Writer::Writer
10
10
  extend ::T::Sig
11
-
12
11
  include ::CSVPlusPlus::Writer::FileBackerUpper
12
+ include ::CSVPlusPlus::Writer::Merger
13
+
14
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
15
+ # @param options [Options::FileOptions]
16
+ # @param position [Runtime::Position]
17
+ def initialize(options, position)
18
+ super(position)
19
+
20
+ @reader = ::T.let(::CSVPlusPlus::Reader::CSV.new(options), ::CSVPlusPlus::Reader::CSV)
21
+ @options = options
22
+ end
13
23
 
14
24
  sig { override.params(template: ::CSVPlusPlus::Template).void }
15
25
  # Write a +template+ to CSV
@@ -17,19 +27,36 @@ module CSVPlusPlus
17
27
  # @param template [Template] The template to use as input to be written. It should have been compiled by calling
18
28
  # Compiler#compile_template
19
29
  def write(template)
20
- # TODO: also read it and merge the results
21
30
  ::CSV.open(@options.output_filename, 'wb') do |csv|
22
- @runtime.map_rows(template.rows) do |row|
31
+ @position.map_rows(template.rows) do |row|
23
32
  csv << build_row(row)
24
33
  end
25
34
  end
26
35
  end
27
36
 
37
+ sig { override.void }
38
+ # Write a backup of the current spreadsheet.
39
+ def write_backup
40
+ backup_file(@options)
41
+ end
42
+
43
+ sig { params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::String)) }
44
+ # Turn the cell into a CSV-
45
+ def evaluate_cell(cell)
46
+ if (ast = cell.ast)
47
+ "=#{ast.evaluate(@position)}"
48
+ else
49
+ cell.value
50
+ end
51
+ end
52
+
28
53
  private
29
54
 
30
55
  sig { params(row: ::CSVPlusPlus::Row).returns(::T::Array[::T.nilable(::String)]) }
31
56
  def build_row(row)
32
- @runtime.map_row(row.cells) { |cell, _i| cell.evaluate(@runtime) }
57
+ @position.map_row(row.cells) do |cell, _i|
58
+ merge_cell_value(existing_value: @reader.value_at(cell), new_value: evaluate_cell(cell), options: @options)
59
+ end
33
60
  end
34
61
  end
35
62
  end
@@ -7,23 +7,36 @@ require_relative './rubyxl_builder'
7
7
  module CSVPlusPlus
8
8
  module Writer
9
9
  # A class that can output a +Template+ to an Excel file
10
- class Excel < ::CSVPlusPlus::Writer::BaseWriter
10
+ class Excel < ::CSVPlusPlus::Writer::Writer
11
11
  extend ::T::Sig
12
-
13
12
  include ::CSVPlusPlus::Writer::FileBackerUpper
14
13
 
14
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
15
+ # @param options [Options::FileOptions]
16
+ # @param position [Runtime::Position]
17
+ def initialize(options, position)
18
+ super(position)
19
+
20
+ @options = options
21
+ end
22
+
15
23
  sig { override.params(template: ::CSVPlusPlus::Template).void }
16
24
  # Write the +template+ to an Excel file
17
25
  #
18
26
  # @param template [Template] The template to write
19
27
  def write(template)
20
28
  ::CSVPlusPlus::Writer::RubyXLBuilder.new(
21
- input_filename: ::T.must(@options.output_filename),
22
- rows: template.rows,
23
- runtime: @runtime,
24
- sheet_name: @options.sheet_name
29
+ options: @options,
30
+ position: @position,
31
+ rows: template.rows
25
32
  ).build_workbook.write(@options.output_filename)
26
33
  end
34
+
35
+ sig { override.void }
36
+ # Write a backup of the current spreadsheet.
37
+ def write_backup
38
+ backup_file(@options)
39
+ end
27
40
  end
28
41
  end
29
42
  end
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module CSVPlusPlus
@@ -6,32 +6,44 @@ module CSVPlusPlus
6
6
  # A module that can be mixed into any Writer that needs to back up it's @output_filename (all of them except Google
7
7
  # Sheets)
8
8
  module FileBackerUpper
9
+ include ::Kernel
10
+ extend ::T::Sig
11
+
9
12
  # I don't want to include a bunch of second/millisecond stuff in the filename unless we
10
13
  # really need to. so try a less specifically formatted filename then get more specific
11
- DESIRED_BACKUP_FORMATS = [%(%Y_%m_%d-%I_%M%p), %(%Y_%m_%d-%I_%M_%S%p), %(%Y_%m_%d-%I_%M_%S_%L%p)].freeze
14
+ DESIRED_BACKUP_FORMATS = ::T.let(
15
+ [
16
+ %(%Y_%m_%d-%I_%M%p),
17
+ %(%Y_%m_%d-%I_%M_%S%p),
18
+ %(%Y_%m_%d-%I_%M_%S_%L%p)
19
+ ].freeze,
20
+ ::T::Array[::String]
21
+ )
12
22
  private_constant :DESIRED_BACKUP_FORMATS
13
23
 
24
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions).returns(::T.nilable(::Pathname)) }
14
25
  # Assuming the underlying spreadsheet is file-based, create a backup of it
15
- def write_backup
16
- return unless ::File.exist?(@options.output_filename)
26
+ def backup_file(options)
27
+ return unless ::File.exist?(options.output_filename)
17
28
 
18
29
  # TODO: also don't do anything if the current backups contents haven't changed (do a md5sum or something)
19
30
 
20
- attempt_backups.tap do |backed_up_to|
21
- warn("Backed up #{@options.output_filename} to #{backed_up_to}") if @options.verbose
31
+ attempt_backups(options).tap do |backed_up_to|
32
+ puts("Backed up #{options.output_filename} to #{backed_up_to}") if options.verbose
22
33
  end
23
34
  end
24
35
 
25
36
  private
26
37
 
38
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions).returns(::Pathname) }
27
39
  # rubocop:disable Metrics/MethodLength
28
- def attempt_backups
40
+ def attempt_backups(options)
29
41
  attempted =
30
42
  # rubocop:disable Lint/ConstantResolution
31
43
  DESIRED_BACKUP_FORMATS.map do |file_format|
32
44
  # rubocop:enable Lint/ConstantResolution
33
- filename = format_backup_filename(file_format)
34
- backed_up_to = backup(filename)
45
+ filename = format_backup_filename(file_format, options.output_filename)
46
+ backed_up_to = backup(filename, options.output_filename)
35
47
 
36
48
  next filename unless backed_up_to
37
49
 
@@ -45,16 +57,17 @@ module CSVPlusPlus
45
57
  end
46
58
  # rubocop:enable Metrics/MethodLength
47
59
 
48
- def backup(filename)
60
+ sig { params(filename: ::Pathname, output_filename: ::Pathname).returns(::T.nilable(::Pathname)) }
61
+ def backup(filename, output_filename)
49
62
  return if ::File.exist?(filename)
50
63
 
51
- ::FileUtils.cp(@options.output_filename, filename)
64
+ ::FileUtils.cp(output_filename, filename)
52
65
  filename
53
66
  end
54
67
 
55
- def format_backup_filename(file_format)
56
- pn = ::Pathname.new(@options.output_filename)
57
- pn.sub_ext("-#{::Time.now.strftime(file_format)}" + pn.extname)
68
+ sig { params(file_format: ::String, output_filename: ::Pathname).returns(::Pathname) }
69
+ def format_backup_filename(file_format, output_filename)
70
+ output_filename.sub_ext("-#{::Time.now.strftime(file_format)}" + output_filename.extname)
58
71
  end
59
72
  end
60
73
  end