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.
- checksums.yaml +4 -4
- data/README.md +9 -5
- data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
- data/lib/csv_plus_plus/cell.rb +29 -41
- data/lib/csv_plus_plus/cli.rb +53 -80
- data/lib/csv_plus_plus/cli_flag.rb +71 -71
- data/lib/csv_plus_plus/color.rb +32 -7
- data/lib/csv_plus_plus/compiler.rb +98 -66
- data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
- data/lib/csv_plus_plus/entities/boolean.rb +26 -10
- data/lib/csv_plus_plus/entities/builtins.rb +66 -24
- data/lib/csv_plus_plus/entities/date.rb +42 -6
- data/lib/csv_plus_plus/entities/entity.rb +17 -69
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
- data/lib/csv_plus_plus/entities/function.rb +34 -11
- data/lib/csv_plus_plus/entities/function_call.rb +49 -10
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +30 -11
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
- data/lib/csv_plus_plus/entities/string.rb +23 -7
- data/lib/csv_plus_plus/entities.rb +7 -16
- data/lib/csv_plus_plus/error/cli_error.rb +17 -0
- data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
- data/lib/csv_plus_plus/error/error.rb +25 -2
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +8 -0
- data/lib/csv_plus_plus/error.rb +5 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +25 -10
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
- data/lib/csv_plus_plus/lexer.rb +64 -1
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +78 -0
- data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
- data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
- data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
- data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
- data/lib/csv_plus_plus/modifier.rb +89 -160
- data/lib/csv_plus_plus/options/file_options.rb +49 -0
- data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
- data/lib/csv_plus_plus/options/options.rb +97 -0
- data/lib/csv_plus_plus/options.rb +34 -77
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
- data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
- data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
- data/lib/csv_plus_plus/reader/csv.rb +50 -0
- data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
- data/lib/csv_plus_plus/reader/reader.rb +27 -0
- data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
- data/lib/csv_plus_plus/reader.rb +14 -0
- data/lib/csv_plus_plus/row.rb +53 -12
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position.rb +242 -0
- data/lib/csv_plus_plus/runtime/references.rb +115 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +34 -191
- data/lib/csv_plus_plus/source_code.rb +71 -0
- data/lib/csv_plus_plus/template.rb +71 -39
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/csv.rb +37 -8
- data/lib/csv_plus_plus/writer/excel.rb +25 -5
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
- data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
- data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
- data/lib/csv_plus_plus/writer/merger.rb +31 -0
- data/lib/csv_plus_plus/writer/open_document.rb +21 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +79 -10
- data/lib/csv_plus_plus.rb +47 -18
- metadata +50 -21
- data/lib/csv_plus_plus/can_define_references.rb +0 -88
- data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
- data/lib/csv_plus_plus/data_validation.rb +0 -138
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
- data/lib/csv_plus_plus/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
- data/lib/csv_plus_plus/expand.rb +0 -20
- data/lib/csv_plus_plus/google_options.rb +0 -27
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
- data/lib/csv_plus_plus/references.rb +0 -68
- data/lib/csv_plus_plus/scope.rb +0 -196
- data/lib/csv_plus_plus/validated_modifier.rb +0 -164
- data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
|
@@ -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,199 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
4
|
+
require_relative './runtime/graph'
|
|
5
|
+
require_relative './runtime/position'
|
|
6
|
+
require_relative './runtime/references'
|
|
7
|
+
require_relative './runtime/runtime'
|
|
8
|
+
require_relative './runtime/scope'
|
|
9
|
+
|
|
3
10
|
module CSVPlusPlus
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# central place for these things to be managed.
|
|
11
|
+
# All functionality needed to keep track of the runtime AKA execution context. This module has a lot of
|
|
12
|
+
# reponsibilities:
|
|
7
13
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# @attr_reader length_of_csv_section [Integer] The length (count of lines) of the CSV part of the original csvpp
|
|
13
|
-
# input.
|
|
14
|
-
# @attr_reader length_of_original_file [Integer] The length (count of lines) of the original csvpp input.
|
|
14
|
+
# - variables and function resolution and scoping
|
|
15
|
+
# - variable & function definitions
|
|
16
|
+
# - keeping track of the runtime state (the current cell being processed)
|
|
17
|
+
# - rewriting the input file that's being parsed
|
|
15
18
|
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# @param lines [Array]
|
|
38
|
-
#
|
|
39
|
-
# @return [Array]
|
|
40
|
-
def map_lines(lines, &block)
|
|
41
|
-
@line_number = 1
|
|
42
|
-
lines.map do |line|
|
|
43
|
-
block.call(line).tap { next_line! }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Map over a single row and keep track of the cell and it's index
|
|
48
|
-
#
|
|
49
|
-
# @param row [Array<Cell>] The row to map each cell over
|
|
50
|
-
#
|
|
51
|
-
# @return [Array]
|
|
52
|
-
def map_row(row, &block)
|
|
53
|
-
@cell_index = 0
|
|
54
|
-
row.map.with_index do |cell, index|
|
|
55
|
-
set_cell!(cell, index)
|
|
56
|
-
block.call(cell, index)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Map over all rows and keep track of row and line numbers
|
|
61
|
-
#
|
|
62
|
-
# @param rows [Array<Row>] The rows to map over (and keep track of indexes)
|
|
63
|
-
# @param cells_too [boolean] If the cells of each +row+ should be iterated over also.
|
|
64
|
-
#
|
|
65
|
-
# @return [Array]
|
|
66
|
-
def map_rows(rows, cells_too: false, &block)
|
|
67
|
-
@row_index = 0
|
|
68
|
-
map_lines(rows) do |row|
|
|
69
|
-
if cells_too
|
|
70
|
-
# it's either CSV or a Row object
|
|
71
|
-
map_row((row.is_a?(::CSVPlusPlus::Row) ? row.cells : row), &block)
|
|
72
|
-
else
|
|
73
|
-
block.call(row)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Increment state to the next line
|
|
79
|
-
#
|
|
80
|
-
# @return [Integer]
|
|
81
|
-
def next_line!
|
|
82
|
-
@row_index += 1 unless @row_index.nil?
|
|
83
|
-
@line_number += 1
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
|
|
87
|
-
#
|
|
88
|
-
# @return [Integer, nil]
|
|
89
|
-
def rownum
|
|
90
|
-
return if @row_index.nil?
|
|
91
|
-
|
|
92
|
-
@row_index + 1
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Set the current cell and index
|
|
96
|
-
#
|
|
97
|
-
# @param cell [Cell] The current cell
|
|
98
|
-
# @param cell_index [Integer] The index of the cell
|
|
99
|
-
def set_cell!(cell, cell_index)
|
|
100
|
-
@cell = cell
|
|
101
|
-
@cell_index = cell_index
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
|
|
105
|
-
def start!
|
|
106
|
-
@row_index = @cell_index = nil
|
|
107
|
-
@line_number = 1
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Reset the runtime state starting at the CSV section
|
|
111
|
-
def start_at_csv!
|
|
112
|
-
# TODO: isn't the input re-written anyway without the code section? why do we need this?
|
|
113
|
-
start!
|
|
114
|
-
@line_number = @length_of_code_section || 1
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# @return [String]
|
|
118
|
-
def to_s
|
|
119
|
-
"Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Get the current (entity) value of a runtime value
|
|
123
|
-
#
|
|
124
|
-
# @param var_id [String, Symbol] The Variable#id of the variable being resolved.
|
|
125
|
-
#
|
|
126
|
-
# @return [Entity]
|
|
127
|
-
def runtime_value(var_id)
|
|
128
|
-
if runtime_variable?(var_id)
|
|
129
|
-
::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
|
|
130
|
-
else
|
|
131
|
-
raise_formula_syntax_error('Undefined variable', var_id)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Is +var_id+ a runtime variable? (it's a static variable otherwise)
|
|
136
|
-
#
|
|
137
|
-
# @param var_id [String, Symbol] The Variable#id to check if it's a runtime variable
|
|
138
|
-
#
|
|
139
|
-
# @return [boolean]
|
|
140
|
-
def runtime_variable?(var_id)
|
|
141
|
-
::CSVPlusPlus::Entities::Builtins::VARIABLES.key?(var_id.to_sym)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Called when an error is encoutered during parsing. It will construct a useful
|
|
145
|
-
# error with the current +@row/@cell_index+, +@line_number+ and +@filename+
|
|
146
|
-
#
|
|
147
|
-
# @param message [String] A message relevant to why this error is being raised.
|
|
148
|
-
# @param bad_input [String] The offending input that caused this error to be thrown.
|
|
149
|
-
# @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
|
|
150
|
-
def raise_formula_syntax_error(message, bad_input, wrapped_error: nil)
|
|
151
|
-
raise(::CSVPlusPlus::Error::FormulaSyntaxError.new(message, bad_input, self, wrapped_error:))
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# The currently available input for parsing. The tmp state will be re-written
|
|
155
|
-
# between parsing the code section and the CSV section
|
|
156
|
-
#
|
|
157
|
-
# @return [String]
|
|
158
|
-
def input
|
|
159
|
-
@tmp
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# We mutate the input over and over. It's ok because it's just a Tempfile
|
|
163
|
-
#
|
|
164
|
-
# @param data [String] The data to rewrite our input file to
|
|
165
|
-
def rewrite_input!(data)
|
|
166
|
-
@tmp.truncate(0)
|
|
167
|
-
@tmp.write(data)
|
|
168
|
-
@tmp.rewind
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Clean up the Tempfile we're using for parsing
|
|
172
|
-
def cleanup!
|
|
173
|
-
return unless @tmp
|
|
174
|
-
|
|
175
|
-
@tmp.close
|
|
176
|
-
@tmp.unlink
|
|
177
|
-
@tmp = nil
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
private
|
|
181
|
-
|
|
182
|
-
def count_code_section_lines(lines)
|
|
183
|
-
eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
|
|
184
|
-
lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def init_input!(input)
|
|
188
|
-
lines = (input || '').split(/\s*\n\s*/)
|
|
189
|
-
@length_of_original_file = lines.length
|
|
190
|
-
@length_of_code_section = count_code_section_lines(lines)
|
|
191
|
-
@length_of_csv_section = @length_of_original_file - @length_of_code_section
|
|
192
|
-
|
|
193
|
-
# we're gonna take our input file, write it to a tmp file then each
|
|
194
|
-
# step is gonna mutate that tmp file
|
|
195
|
-
@tmp = ::Tempfile.new
|
|
196
|
-
rewrite_input!(input)
|
|
19
|
+
module Runtime
|
|
20
|
+
extend ::T::Sig
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(
|
|
24
|
+
source_code: ::CSVPlusPlus::SourceCode,
|
|
25
|
+
position: ::T.nilable(::CSVPlusPlus::Runtime::Position),
|
|
26
|
+
scope: ::T.nilable(::CSVPlusPlus::Runtime::Scope)
|
|
27
|
+
).returns(::CSVPlusPlus::Runtime::Runtime)
|
|
28
|
+
end
|
|
29
|
+
# Initialize a runtime instance with all the functionality we need. A runtime is one-to-one with a file being
|
|
30
|
+
# compiled.
|
|
31
|
+
#
|
|
32
|
+
# @param source_code [SourceCode] The csv++ source code to be compiled
|
|
33
|
+
# @param position [Position, nil]
|
|
34
|
+
# @param scope [Scope, nil]
|
|
35
|
+
#
|
|
36
|
+
# @return [Runtime::Runtime]
|
|
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:)
|
|
197
40
|
end
|
|
198
41
|
end
|
|
199
42
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
# Information about the unparsed source code
|
|
6
|
+
class SourceCode
|
|
7
|
+
extend ::T::Sig
|
|
8
|
+
|
|
9
|
+
sig { returns(::String) }
|
|
10
|
+
attr_reader :input
|
|
11
|
+
|
|
12
|
+
sig { returns(::Pathname) }
|
|
13
|
+
attr_reader :filename
|
|
14
|
+
|
|
15
|
+
sig { returns(::Integer) }
|
|
16
|
+
attr_reader :length_of_csv_section
|
|
17
|
+
|
|
18
|
+
sig { returns(::Integer) }
|
|
19
|
+
attr_reader :length_of_code_section
|
|
20
|
+
|
|
21
|
+
sig { returns(::Integer) }
|
|
22
|
+
attr_reader :length_of_file
|
|
23
|
+
|
|
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)
|
|
29
|
+
|
|
30
|
+
lines = @input.split(/[\r\n]/)
|
|
31
|
+
@length_of_file = ::T.let(lines.length, ::Integer)
|
|
32
|
+
@length_of_code_section = ::T.let(count_code_section_lines(lines), ::Integer)
|
|
33
|
+
@length_of_csv_section = ::T.let(@length_of_file - @length_of_code_section, ::Integer)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(line_number: ::Integer).returns(::T::Boolean) }
|
|
37
|
+
# Does the given +line_number+ land in the code section of the file? (which includes the --- separator)
|
|
38
|
+
#
|
|
39
|
+
# @param line_number [Integer]
|
|
40
|
+
#
|
|
41
|
+
# @return [T::Boolean]
|
|
42
|
+
def in_code_section?(line_number)
|
|
43
|
+
line_number <= @length_of_code_section
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(line_number: ::Integer).returns(::T::Boolean) }
|
|
47
|
+
# Does the given +line_number+ land in the CSV section of the file?
|
|
48
|
+
#
|
|
49
|
+
# @param line_number [Integer]
|
|
50
|
+
#
|
|
51
|
+
# @return [T::Boolean]
|
|
52
|
+
def in_csv_section?(line_number)
|
|
53
|
+
line_number > @length_of_code_section
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
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
|
+
|
|
65
|
+
sig { params(lines: ::T::Array[::String]).returns(::Integer) }
|
|
66
|
+
def count_code_section_lines(lines)
|
|
67
|
+
eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
|
|
68
|
+
lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|