csv_plus_plus 0.1.2 → 0.1.3
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 +1 -2
- data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
- data/lib/csv_plus_plus/cell.rb +46 -24
- data/lib/csv_plus_plus/cli.rb +23 -13
- data/lib/csv_plus_plus/cli_flag.rb +1 -2
- data/lib/csv_plus_plus/color.rb +32 -7
- data/lib/csv_plus_plus/compiler.rb +82 -60
- data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
- data/lib/csv_plus_plus/entities/boolean.rb +18 -9
- data/lib/csv_plus_plus/entities/builtins.rb +23 -9
- data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
- data/lib/csv_plus_plus/entities/date.rb +38 -5
- data/lib/csv_plus_plus/entities/entity.rb +27 -61
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
- data/lib/csv_plus_plus/entities/function.rb +23 -11
- data/lib/csv_plus_plus/entities/function_call.rb +24 -9
- data/lib/csv_plus_plus/entities/number.rb +24 -10
- data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
- data/lib/csv_plus_plus/entities/string.rb +19 -6
- data/lib/csv_plus_plus/entities/variable.rb +16 -4
- data/lib/csv_plus_plus/entities.rb +20 -13
- data/lib/csv_plus_plus/error/error.rb +11 -1
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
- data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
- data/lib/csv_plus_plus/error/writer_error.rb +8 -0
- data/lib/csv_plus_plus/error.rb +1 -0
- data/lib/csv_plus_plus/google_api_client.rb +7 -2
- data/lib/csv_plus_plus/google_options.rb +23 -18
- data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
- data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
- data/lib/csv_plus_plus/lexer.rb +24 -0
- 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 +61 -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 +82 -158
- data/lib/csv_plus_plus/options.rb +64 -19
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
- data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
- data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
- data/lib/csv_plus_plus/row.rb +53 -12
- data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
- data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
- data/lib/csv_plus_plus/runtime/references.rb +110 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
- data/lib/csv_plus_plus/runtime.rb +34 -191
- data/lib/csv_plus_plus/source_code.rb +66 -0
- data/lib/csv_plus_plus/template.rb +62 -35
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
- data/lib/csv_plus_plus/writer/csv.rb +11 -9
- data/lib/csv_plus_plus/writer/excel.rb +9 -2
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
- data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
- data/lib/csv_plus_plus/writer/open_document.rb +6 -1
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
- data/lib/csv_plus_plus/writer.rb +39 -9
- data/lib/csv_plus_plus.rb +29 -12
- metadata +18 -14
- 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/expand.rb +0 -20
- data/lib/csv_plus_plus/graph.rb +0 -62
- 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/google_sheet_modifier.rb +0 -77
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,209 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Runtime
|
6
|
+
# Methods for resolving functions and variables. These should be included onto a class that has +@variables+ and
|
7
|
+
# +@functions+ instance variables.
|
8
|
+
module CanResolveReferences
|
9
|
+
# Resolve all values in the ast of the current cell being processed
|
10
|
+
#
|
11
|
+
# @return [Entity]
|
12
|
+
def resolve_cell_value
|
13
|
+
return unless (ast = @cell&.ast)
|
14
|
+
|
15
|
+
last_round = nil
|
16
|
+
loop do
|
17
|
+
refs = ::CSVPlusPlus::Runtime::References.extract(ast, self)
|
18
|
+
return ast if refs.empty?
|
19
|
+
|
20
|
+
# TODO: throw an error here instead I think - basically we did a round and didn't make progress
|
21
|
+
return ast if last_round == refs
|
22
|
+
|
23
|
+
ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Bind +var_id+ to the current cell
|
28
|
+
#
|
29
|
+
# @param var_id [Symbol] The name of the variable to bind the cell reference to
|
30
|
+
#
|
31
|
+
# @return [CellReference]
|
32
|
+
def bind_variable_to_cell(var_id)
|
33
|
+
def_variable(
|
34
|
+
var_id,
|
35
|
+
::CSVPlusPlus::Entities::CellReference.new(
|
36
|
+
cell_index: @cell_index,
|
37
|
+
row_index: @row_index
|
38
|
+
)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Bind +var_id+ relative to an ![[expand]] modifier.
|
43
|
+
#
|
44
|
+
# @param var_id [Symbol] The name of the variable to bind the cell reference to
|
45
|
+
# @param expand [Expand] The expand where the variable is accessible (where it will be bound relative to)
|
46
|
+
#
|
47
|
+
# @return [CellReference]
|
48
|
+
def bind_variable_in_expand(var_id, expand)
|
49
|
+
def_variable(
|
50
|
+
var_id,
|
51
|
+
::CSVPlusPlus::Entities::CellReference.new(
|
52
|
+
scoped_to_expand: expand,
|
53
|
+
cell_index: @cell_index
|
54
|
+
)
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Variables outside of an ![[expand=...] are always in scope. If it's defined within an expand then things
|
59
|
+
# get trickier because the variable is only in scope while we're processing cells within that expand.
|
60
|
+
#
|
61
|
+
# @param var_id [Symbol] The variable's identifier that we are checking if it's in scope
|
62
|
+
#
|
63
|
+
# @return [boolean]
|
64
|
+
def in_scope?(var_id)
|
65
|
+
value = @variables[var_id]
|
66
|
+
|
67
|
+
raise_modifier_syntax_error('Undefined variable reference', var_id.to_s) if value.nil?
|
68
|
+
|
69
|
+
expand = value.type == ::CSVPlusPlus::Entities::Type::CellReference && value.scoped_to_expand
|
70
|
+
return true unless expand
|
71
|
+
|
72
|
+
unless expand.starts_at
|
73
|
+
raise(::CSVPlusPlus::Error::Error, 'Must call Template.expand_rows! before checking the scope of expands.')
|
74
|
+
end
|
75
|
+
|
76
|
+
@row_index >= expand.starts_at && (expand.ends_at.nil? || row_index <= expand.ends_at)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Resolve all variable references defined statically in the code section
|
82
|
+
# def resolve_static_variables!
|
83
|
+
# last_var_dependencies = {}
|
84
|
+
# loop do
|
85
|
+
# var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
|
86
|
+
# return if var_dependencies == last_var_dependencies
|
87
|
+
#
|
88
|
+
# # TODO: make the contract better here
|
89
|
+
# @variables = resolve_dependencies(var_dependencies, resolution_order, variables)
|
90
|
+
# last_var_dependencies = var_dependencies.clone
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# def only_static_vars(var_dependencies)
|
95
|
+
# var_dependencies.reject { |k| @runtime.builtin_variable?(k) }
|
96
|
+
# end
|
97
|
+
|
98
|
+
def resolve_functions(ast, refs)
|
99
|
+
refs.reduce(ast.dup) do |acc, elem|
|
100
|
+
function_replace(acc, elem.id, resolve_function(elem.id))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def resolve_variables(ast, refs)
|
105
|
+
refs.reduce(ast.dup) do |acc, elem|
|
106
|
+
variable_replace(acc, elem.id, resolve_variable(elem.id))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
|
111
|
+
# rubocop:disable Metrics/MethodLength
|
112
|
+
def function_replace(node, fn_id, replacement)
|
113
|
+
if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall && node.id == fn_id
|
114
|
+
call_function_or_builtin(replacement, node)
|
115
|
+
elsif node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
|
116
|
+
# not our function, but continue our depth first search on it
|
117
|
+
::CSVPlusPlus::Entities::FunctionCall.new(
|
118
|
+
node.id,
|
119
|
+
node.arguments.map { |n| function_replace(n, fn_id, replacement) },
|
120
|
+
infix: node.infix
|
121
|
+
)
|
122
|
+
else
|
123
|
+
node
|
124
|
+
end
|
125
|
+
end
|
126
|
+
# rubocop:enable Metrics/MethodLength
|
127
|
+
|
128
|
+
def resolve_function(fn_id)
|
129
|
+
id = fn_id.to_sym
|
130
|
+
return @functions[id] if defined_function?(id)
|
131
|
+
|
132
|
+
::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
|
133
|
+
end
|
134
|
+
|
135
|
+
def call_function_or_builtin(function_or_builtin, function_call)
|
136
|
+
if function_or_builtin.type == ::CSVPlusPlus::Entities::Type::Function
|
137
|
+
call_function(function_or_builtin, function_call)
|
138
|
+
else
|
139
|
+
function_or_builtin.resolve_fn.call(self, function_call.arguments)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def call_function(function, function_call)
|
144
|
+
i = 0
|
145
|
+
function.arguments.reduce(function.body.dup) do |ast, argument|
|
146
|
+
variable_replace(ast, argument, function_call.arguments[i]).tap do
|
147
|
+
i += 1
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
|
153
|
+
def variable_replace(node, var_id, replacement)
|
154
|
+
if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
|
155
|
+
arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
|
156
|
+
# TODO: refactor these places where we copy functions... it's brittle with the kwargs
|
157
|
+
::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
|
158
|
+
elsif node.type == ::CSVPlusPlus::Entities::Type::Variable && node.id == var_id
|
159
|
+
replacement
|
160
|
+
else
|
161
|
+
node
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def resolve_variable(var_id)
|
166
|
+
id = var_id.to_sym
|
167
|
+
return @variables[id] if defined_variable?(id)
|
168
|
+
|
169
|
+
raise_formula_syntax_error('Undefined variable', var_id) unless builtin_variable?(var_id)
|
170
|
+
|
171
|
+
::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
|
172
|
+
end
|
173
|
+
|
174
|
+
# def check_unbound_vars(dependencies, variables)
|
175
|
+
# unbound_vars = dependencies.values.flatten - variables.keys
|
176
|
+
# return if unbound_vars.empty?
|
177
|
+
#
|
178
|
+
# raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
|
179
|
+
# end
|
180
|
+
|
181
|
+
# def variable_resolution_order(variables)
|
182
|
+
# # we have a hash of variables => ASTs but they might have references to each other, so
|
183
|
+
# # we need to interpolate them first (before interpolating the cell values)
|
184
|
+
# var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
|
185
|
+
# # are there any references that we don't have variables for? (undefined variable)
|
186
|
+
# check_unbound_vars(var_dependencies, variables)
|
187
|
+
#
|
188
|
+
# # a topological sort will give us the order of dependencies
|
189
|
+
# [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
|
190
|
+
# # TODO: don't expose this exception directly to the caller
|
191
|
+
# rescue ::TSort::Cyclic
|
192
|
+
# @runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
|
193
|
+
# end
|
194
|
+
|
195
|
+
# def resolve_dependencies(var_dependencies, resolution_order, variables)
|
196
|
+
# {}.tap do |resolved_vars|
|
197
|
+
# # for each var and each dependency it has, build up and mutate resolved_vars
|
198
|
+
# resolution_order.each do |var|
|
199
|
+
# resolved_vars[var] = variables[var].dup
|
200
|
+
#
|
201
|
+
# var_dependencies[var].each do |dependency|
|
202
|
+
# resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
|
203
|
+
# end
|
204
|
+
# end
|
205
|
+
# end
|
206
|
+
# end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'tsort'
|
5
|
+
|
6
|
+
module CSVPlusPlus
|
7
|
+
module Runtime
|
8
|
+
# Graph ordering and searching functions
|
9
|
+
module Graph
|
10
|
+
# Get a list of all variables references in a given +ast+
|
11
|
+
# TODO: this is only used in one place - refactor it
|
12
|
+
def self.variable_references(ast, runtime, include_runtime_variables: false)
|
13
|
+
depth_first_search(ast) do |node|
|
14
|
+
next unless node.type == ::CSVPlusPlus::Entities::Type::Variable
|
15
|
+
|
16
|
+
node.id if !runtime.builtin_variable?(node.id) || include_runtime_variables
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create a dependency graph of +variables+
|
21
|
+
def self.dependency_graph(variables, runtime)
|
22
|
+
::CSVPlusPlus::Runtime::Graph::DependencyGraph[
|
23
|
+
variables.map { |var_id, ast| [var_id, variable_references(ast, runtime)] }
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
# TODO: I don't think we use this anymore - it was useful when I wanted to resolve variables in their dependency
|
28
|
+
# order
|
29
|
+
#
|
30
|
+
# Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
|
31
|
+
# because it will give us the order in which we need to resolve our variable dependencies.
|
32
|
+
#
|
33
|
+
# Given this dependency graph:
|
34
|
+
#
|
35
|
+
# { a: [b c], b: [c], c: [d], d: [] }
|
36
|
+
#
|
37
|
+
# it will return:
|
38
|
+
#
|
39
|
+
# [d, c, b, a]
|
40
|
+
#
|
41
|
+
def self.topological_sort(dependencies)
|
42
|
+
dependencies.tsort
|
43
|
+
end
|
44
|
+
|
45
|
+
# Do a DFS on an AST starting at +node+
|
46
|
+
def self.depth_first_search(node, accum = [], &)
|
47
|
+
ret = yield(node)
|
48
|
+
accum << ret unless ret.nil?
|
49
|
+
|
50
|
+
return accum unless node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
|
51
|
+
|
52
|
+
node.arguments.each { |n| depth_first_search(n, accum, &) }
|
53
|
+
accum
|
54
|
+
end
|
55
|
+
|
56
|
+
# A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
|
57
|
+
class DependencyGraph < Hash
|
58
|
+
include ::TSort
|
59
|
+
alias tsort_each_node each_key
|
60
|
+
|
61
|
+
# sort each child
|
62
|
+
def tsort_each_child(node, &)
|
63
|
+
fetch(node).each(&)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Runtime
|
6
|
+
# Functions needed to track all of the runtime pointers: current line number, current row number, current cell, etc.
|
7
|
+
# rubocop:disable Metrics/ModuleLength
|
8
|
+
module PositionTracker
|
9
|
+
extend ::T::Sig
|
10
|
+
|
11
|
+
sig { params(cell: ::T.nilable(::CSVPlusPlus::Cell)).returns(::T.nilable(::CSVPlusPlus::Cell)) }
|
12
|
+
attr_writer :cell
|
13
|
+
|
14
|
+
sig { params(cell_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
|
15
|
+
attr_writer :cell_index
|
16
|
+
|
17
|
+
sig { params(line_number: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
|
18
|
+
attr_writer :line_number
|
19
|
+
|
20
|
+
sig { params(row_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
|
21
|
+
attr_writer :row_index
|
22
|
+
|
23
|
+
sig { returns(::CSVPlusPlus::Cell) }
|
24
|
+
# The current cell index. This will only be set when processing the CSV section
|
25
|
+
#
|
26
|
+
# @return [Cell]
|
27
|
+
def cell
|
28
|
+
@cell ||= ::T.let(nil, ::T.nilable(::CSVPlusPlus::Cell))
|
29
|
+
assert_initted!(@cell)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(::Integer) }
|
33
|
+
# The current CSV cell index.
|
34
|
+
#
|
35
|
+
# This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
|
36
|
+
# caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
|
37
|
+
# +#map_row+/+#map_rows+/+#map_lines+
|
38
|
+
#
|
39
|
+
# @return [Integer]
|
40
|
+
def cell_index
|
41
|
+
@cell_index ||= ::T.let(nil, ::T.nilable(::Integer))
|
42
|
+
assert_initted!(@cell_index)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { returns(::Integer) }
|
46
|
+
# The current CSV row index. This will only be set when processing the CSV section
|
47
|
+
#
|
48
|
+
# This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
|
49
|
+
# caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
|
50
|
+
# +#map_row+/+#map_rows+/+#map_lines+
|
51
|
+
#
|
52
|
+
# @return [Integer]
|
53
|
+
def row_index
|
54
|
+
@row_index ||= ::T.let(nil, ::T.nilable(::Integer))
|
55
|
+
assert_initted!(@row_index)
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { returns(::Integer) }
|
59
|
+
# The current line number being processed. The line number is based on the entire file, irregardless of if it's
|
60
|
+
# parsing the code section or the CSV section
|
61
|
+
#
|
62
|
+
# This will only be set when processing the csvpp file and will throw an exception otherwise. It is up to the
|
63
|
+
# caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
|
64
|
+
# +#map_row+/+#map_rows+/+#map_lines+
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
def line_number
|
68
|
+
@line_number ||= ::T.let(nil, ::T.nilable(::Integer))
|
69
|
+
assert_initted!(@line_number)
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { void }
|
73
|
+
# Clean up the Tempfile we're using for parsing
|
74
|
+
def cleanup!
|
75
|
+
input&.close
|
76
|
+
input&.unlink
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { returns(::T.nilable(::Tempfile)) }
|
80
|
+
# The currently available input for parsing. The tmp state will be re-written
|
81
|
+
# between parsing the code section and the CSV section
|
82
|
+
#
|
83
|
+
# @return [::Tempfile]
|
84
|
+
def input
|
85
|
+
@input ||= ::T.let(::Tempfile.new, ::T.nilable(::Tempfile))
|
86
|
+
end
|
87
|
+
|
88
|
+
sig do
|
89
|
+
type_parameters(:I, :O).params(
|
90
|
+
lines: ::T::Enumerable[::T.type_parameter(:I)],
|
91
|
+
block: ::T.proc.params(args0: ::T.type_parameter(:I)).returns(::T.type_parameter(:O))
|
92
|
+
).returns(::T::Array[::T.type_parameter(:O)])
|
93
|
+
end
|
94
|
+
# Map over a csvpp file and keep track of line_number and row_index
|
95
|
+
#
|
96
|
+
# @param lines [Array]
|
97
|
+
#
|
98
|
+
# @return [Array]
|
99
|
+
def map_lines(lines, &block)
|
100
|
+
self.line_number = 1
|
101
|
+
lines.map do |line|
|
102
|
+
ret = block.call(line)
|
103
|
+
next_line!
|
104
|
+
ret
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
sig do
|
109
|
+
type_parameters(:I, :O)
|
110
|
+
.params(
|
111
|
+
row: ::T::Enumerable[::T.all(::T.type_parameter(:I), ::Object)],
|
112
|
+
block: ::T.proc.params(
|
113
|
+
cell: ::T.all(::T.type_parameter(:I), ::Object),
|
114
|
+
index: ::Integer
|
115
|
+
).returns(::T.type_parameter(:O))
|
116
|
+
)
|
117
|
+
.returns(::T::Array[::T.type_parameter(:O)])
|
118
|
+
end
|
119
|
+
# Map over a single row and keep track of the cell and it's index
|
120
|
+
#
|
121
|
+
# @param row [Array<Cell>] The row to map each cell over
|
122
|
+
#
|
123
|
+
# @return [Array]
|
124
|
+
def map_row(row, &block)
|
125
|
+
row.map.with_index do |cell, index|
|
126
|
+
self.cell_index = index
|
127
|
+
self.cell = cell if cell.is_a?(::CSVPlusPlus::Cell)
|
128
|
+
block.call(cell, index)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
sig do
|
133
|
+
type_parameters(:O).params(
|
134
|
+
rows: ::T::Enumerable[::CSVPlusPlus::Row],
|
135
|
+
block: ::T.proc.params(row: ::CSVPlusPlus::Row).returns(::T.type_parameter(:O))
|
136
|
+
).returns(::T::Array[::T.type_parameter(:O)])
|
137
|
+
end
|
138
|
+
# Map over all rows and keep track of row and line numbers
|
139
|
+
#
|
140
|
+
# @param rows [Array<Row>] The rows to map over (and keep track of indexes)
|
141
|
+
#
|
142
|
+
# @return [Array]
|
143
|
+
def map_rows(rows, &block)
|
144
|
+
self.row_index = 0
|
145
|
+
map_lines(rows) do |row|
|
146
|
+
block.call(row)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
sig do
|
151
|
+
type_parameters(:R)
|
152
|
+
.params(rows: ::T::Enumerable[::CSVPlusPlus::Row],
|
153
|
+
block: ::T.proc.params(cell: ::CSVPlusPlus::Cell, index: ::Integer).returns(::T.type_parameter(:R)))
|
154
|
+
.returns(::T::Array[::T::Array[::T.type_parameter(:R)]])
|
155
|
+
end
|
156
|
+
# Map over all +rows+ and over all of their +cells+, calling the +&block+ with each +Cell+
|
157
|
+
#
|
158
|
+
# @param rows [Array<Row>]
|
159
|
+
#
|
160
|
+
# @return [Array<Array>]
|
161
|
+
# rubocop:disable Naming/BlockForwarding
|
162
|
+
def map_all_cells(rows, &block)
|
163
|
+
self.row_index = 0
|
164
|
+
map_lines(rows) { |row| map_row(row.cells, &block) }
|
165
|
+
end
|
166
|
+
# rubocop:enable Naming/BlockForwarding
|
167
|
+
|
168
|
+
sig { returns(::Integer) }
|
169
|
+
# Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
|
170
|
+
#
|
171
|
+
# @return [Integer, nil]
|
172
|
+
def rownum
|
173
|
+
row_index + 1
|
174
|
+
end
|
175
|
+
|
176
|
+
sig do
|
177
|
+
type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
178
|
+
end
|
179
|
+
# Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
|
180
|
+
def start!(&block)
|
181
|
+
@row_index = @cell_index = 0
|
182
|
+
self.line_number = 1
|
183
|
+
|
184
|
+
ret = block.call
|
185
|
+
finish!
|
186
|
+
ret
|
187
|
+
end
|
188
|
+
|
189
|
+
sig { params(data: ::String).void }
|
190
|
+
# We mutate the input over and over. It's ok because it's just a Tempfile
|
191
|
+
#
|
192
|
+
# @param data [::String] The data to rewrite our input file to
|
193
|
+
def rewrite_input!(data)
|
194
|
+
input&.truncate(0)
|
195
|
+
input&.write(data)
|
196
|
+
input&.rewind
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
sig do
|
202
|
+
type_parameters(:R).params(runtime_value: ::T.nilable(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
203
|
+
end
|
204
|
+
def assert_initted!(runtime_value)
|
205
|
+
::T.must_because(runtime_value) do
|
206
|
+
'Runtime value accessed without an initialized runtime. Make sure you call Runtime#start! or ' \
|
207
|
+
'Runtime#start_at_csv! first.'
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
sig { void }
|
212
|
+
# Called to mark the trackers dirty. It should be an error to use them outside of an initialized context.
|
213
|
+
def finish!
|
214
|
+
@line_number = nil
|
215
|
+
@row_index = nil
|
216
|
+
@cell_index = nil
|
217
|
+
@cell = nil
|
218
|
+
end
|
219
|
+
|
220
|
+
sig { returns(::Integer) }
|
221
|
+
# Increment state to the next line
|
222
|
+
#
|
223
|
+
# @return [Integer]
|
224
|
+
def next_line!
|
225
|
+
self.row_index += 1
|
226
|
+
self.line_number += 1
|
227
|
+
end
|
228
|
+
end
|
229
|
+
# rubocop:enable Metrics/ModuleLength
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Runtime
|
6
|
+
# References in an AST that need to be resolved
|
7
|
+
#
|
8
|
+
# @attr functions [Array<Entities::Function>] Functions references
|
9
|
+
# @attr variables [Array<Entities::Variable>] Variable references
|
10
|
+
# TODO: turn this into a CanExtractReferences?
|
11
|
+
class References
|
12
|
+
extend ::T::Sig
|
13
|
+
|
14
|
+
sig { returns(::T::Array[::CSVPlusPlus::Entities::FunctionCall]) }
|
15
|
+
attr_accessor :functions
|
16
|
+
|
17
|
+
sig { returns(::T::Array[::CSVPlusPlus::Entities::Variable]) }
|
18
|
+
attr_accessor :variables
|
19
|
+
|
20
|
+
sig do
|
21
|
+
params(
|
22
|
+
ast: ::CSVPlusPlus::Entities::Entity,
|
23
|
+
runtime: ::CSVPlusPlus::Runtime::Runtime
|
24
|
+
).returns(::CSVPlusPlus::Runtime::References)
|
25
|
+
end
|
26
|
+
# Extract references from an AST and return them in a new +References+ object
|
27
|
+
#
|
28
|
+
# @param ast [Entity] An +Entity+ to do a depth first search on for references. Entities can be
|
29
|
+
# infinitely deep because they can contain other function calls as params to a function call
|
30
|
+
# @param runtime [Runtime] The current runtime
|
31
|
+
#
|
32
|
+
# @return [References]
|
33
|
+
def self.extract(ast, runtime)
|
34
|
+
new.tap do |refs|
|
35
|
+
::CSVPlusPlus::Runtime::Graph.depth_first_search(ast) do |node|
|
36
|
+
unless node.type == ::CSVPlusPlus::Entities::Type::FunctionCall \
|
37
|
+
|| node.type == ::CSVPlusPlus::Entities::Type::Variable
|
38
|
+
|
39
|
+
next
|
40
|
+
end
|
41
|
+
|
42
|
+
refs.functions << node if function_reference?(node, runtime)
|
43
|
+
refs.variables << node if variable_reference?(node, runtime)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
sig do
|
49
|
+
params(node: ::CSVPlusPlus::Entities::Entity, runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Boolean)
|
50
|
+
end
|
51
|
+
# Is the node a resolvable variable reference?
|
52
|
+
#
|
53
|
+
# @param node [Entity] The node to check if it's resolvable
|
54
|
+
# @param runtime [Runtime] The current runtime
|
55
|
+
#
|
56
|
+
# @return [boolean]
|
57
|
+
def self.variable_reference?(node, runtime)
|
58
|
+
return false unless node.type == ::CSVPlusPlus::Entities::Type::Variable
|
59
|
+
|
60
|
+
if runtime.in_scope?(node.id)
|
61
|
+
true
|
62
|
+
else
|
63
|
+
runtime.raise_modifier_syntax_error(
|
64
|
+
"#{node.id} can only be referenced within the ![[expand]] where it was defined.",
|
65
|
+
node.id.to_s
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
private_class_method :variable_reference?
|
70
|
+
|
71
|
+
sig do
|
72
|
+
params(node: ::CSVPlusPlus::Entities::Entity, runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Boolean)
|
73
|
+
end
|
74
|
+
# Is the node a resolvable function reference?
|
75
|
+
#
|
76
|
+
# @param node [Entity] The node to check if it's resolvable
|
77
|
+
# @param runtime [Runtime] The current runtime
|
78
|
+
#
|
79
|
+
# @return [boolean]
|
80
|
+
def self.function_reference?(node, runtime)
|
81
|
+
node.type == ::CSVPlusPlus::Entities::Type::FunctionCall \
|
82
|
+
&& (runtime.defined_function?(node.id) || runtime.builtin_function?(::T.must(node.id)))
|
83
|
+
end
|
84
|
+
private_class_method :function_reference?
|
85
|
+
|
86
|
+
sig { void }
|
87
|
+
# Create an object with empty references. The caller will build them up as it depth-first-searches
|
88
|
+
def initialize
|
89
|
+
@functions = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::FunctionCall])
|
90
|
+
@variables = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::Variable])
|
91
|
+
end
|
92
|
+
|
93
|
+
sig { params(other: ::CSVPlusPlus::Runtime::References).returns(::T::Boolean) }
|
94
|
+
# @param other [References]
|
95
|
+
#
|
96
|
+
# @return [boolean]
|
97
|
+
def ==(other)
|
98
|
+
@functions == other.functions && @variables == other.variables
|
99
|
+
end
|
100
|
+
|
101
|
+
sig { returns(::T::Boolean) }
|
102
|
+
# Are there any references to be resolved?
|
103
|
+
#
|
104
|
+
# @return [::T::Boolean]
|
105
|
+
def empty?
|
106
|
+
@functions.empty? && @variables.empty?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|