csv_plus_plus 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/docs/CHANGELOG.md +16 -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 +71 -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 +97 -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 +31 -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 +37 -12
  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.0'
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