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.
- checksums.yaml +4 -4
- data/README.md +13 -3
- data/docs/CHANGELOG.md +18 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
- data/lib/csv_plus_plus/cell.rb +1 -35
- data/lib/csv_plus_plus/cli.rb +43 -80
- data/lib/csv_plus_plus/cli_flag.rb +77 -70
- data/lib/csv_plus_plus/color.rb +1 -1
- data/lib/csv_plus_plus/compiler.rb +31 -21
- data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
- data/lib/csv_plus_plus/entities/boolean.rb +16 -9
- data/lib/csv_plus_plus/entities/builtins.rb +68 -40
- data/lib/csv_plus_plus/entities/date.rb +14 -11
- data/lib/csv_plus_plus/entities/entity.rb +11 -29
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
- data/lib/csv_plus_plus/entities/function.rb +22 -11
- data/lib/csv_plus_plus/entities/function_call.rb +35 -11
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +15 -10
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
- data/lib/csv_plus_plus/entities/string.rb +13 -10
- data/lib/csv_plus_plus/entities.rb +2 -18
- 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 +18 -5
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +1 -1
- data/lib/csv_plus_plus/error.rb +4 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +18 -8
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
- data/lib/csv_plus_plus/lexer.rb +40 -1
- data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
- data/lib/csv_plus_plus/modifier/expand.rb +17 -0
- data/lib/csv_plus_plus/modifier.rb +6 -1
- 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 +102 -0
- data/lib/csv_plus_plus/options.rb +22 -110
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
- data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
- data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
- 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/runtime/graph.rb +6 -6
- data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
- data/lib/csv_plus_plus/runtime/references.rb +32 -27
- data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +9 -9
- data/lib/csv_plus_plus/source_code.rb +14 -9
- data/lib/csv_plus_plus/template.rb +17 -12
- data/lib/csv_plus_plus/version.rb +1 -1
- data/lib/csv_plus_plus/writer/csv.rb +32 -5
- data/lib/csv_plus_plus/writer/excel.rb +19 -6
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
- data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
- data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
- data/lib/csv_plus_plus/writer/merger.rb +56 -0
- data/lib/csv_plus_plus/writer/open_document.rb +16 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +58 -19
- data/lib/csv_plus_plus.rb +26 -14
- metadata +43 -18
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
- data/lib/csv_plus_plus/entities/variable.rb +0 -37
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
- data/lib/csv_plus_plus/google_options.rb +0 -32
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
- data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
- data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
- 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/
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
35
|
-
# @param
|
|
33
|
+
# @param position [Position, nil]
|
|
34
|
+
# @param scope [Scope, nil]
|
|
36
35
|
#
|
|
37
36
|
# @return [Runtime::Runtime]
|
|
38
|
-
def self.new(source_code:,
|
|
39
|
-
::CSVPlusPlus::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:)
|
|
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(::
|
|
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(
|
|
25
|
-
# @param
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
@@ -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::
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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::
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
16
|
-
return unless ::File.exist?(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
64
|
+
::FileUtils.cp(output_filename, filename)
|
|
52
65
|
filename
|
|
53
66
|
end
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|