csv_plus_plus 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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,126 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Runtime
|
6
|
+
# The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc) for parsing
|
7
|
+
# a given file. We take multiple runs through the input file for parsing so it's really convenient to have a
|
8
|
+
# central place for these things to be managed.
|
9
|
+
#
|
10
|
+
# @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
|
11
|
+
# +filename+ can be +nil+ if it's read from stdin.
|
12
|
+
#
|
13
|
+
# @attr cell [Cell] The current cell being processed
|
14
|
+
# @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
|
15
|
+
# @attr row_index [Integer] The index of the current row being processed (starts at 0)
|
16
|
+
# @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
|
17
|
+
class Runtime
|
18
|
+
extend ::T::Sig
|
19
|
+
|
20
|
+
include ::CSVPlusPlus::Runtime::CanDefineReferences
|
21
|
+
include ::CSVPlusPlus::Runtime::CanResolveReferences
|
22
|
+
include ::CSVPlusPlus::Runtime::PositionTracker
|
23
|
+
|
24
|
+
sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function]) }
|
25
|
+
attr_reader :functions
|
26
|
+
|
27
|
+
sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]) }
|
28
|
+
attr_reader :variables
|
29
|
+
|
30
|
+
sig { returns(::CSVPlusPlus::SourceCode) }
|
31
|
+
attr_reader :source_code
|
32
|
+
|
33
|
+
sig do
|
34
|
+
params(
|
35
|
+
source_code: ::CSVPlusPlus::SourceCode,
|
36
|
+
functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
|
37
|
+
variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
|
38
|
+
).void
|
39
|
+
end
|
40
|
+
# @param source_code [SourceCode] The source code being compiled
|
41
|
+
# @param functions [Hash<Symbol, Function>] Pre-defined functions
|
42
|
+
# @param variables [Hash<Symbol, Entity>] Pre-defined variables
|
43
|
+
def initialize(source_code:, functions: {}, variables: {})
|
44
|
+
@functions = functions
|
45
|
+
@variables = variables
|
46
|
+
@source_code = source_code
|
47
|
+
|
48
|
+
rewrite_input!(source_code.input)
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { params(fn_id: ::Symbol).returns(::T::Boolean) }
|
52
|
+
# Is +fn_id+ a builtin function?
|
53
|
+
#
|
54
|
+
# @param fn_id [Symbol] The Function#id to check if it's a runtime variable
|
55
|
+
#
|
56
|
+
# @return [T::Boolean]
|
57
|
+
def builtin_function?(fn_id)
|
58
|
+
::CSVPlusPlus::Entities::Builtins::FUNCTIONS.key?(fn_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { params(var_id: ::Symbol).returns(::T::Boolean) }
|
62
|
+
# Is +var_id+ a builtin variable?
|
63
|
+
#
|
64
|
+
# @param var_id [Symbol] The Variable#id to check if it's a runtime variable
|
65
|
+
#
|
66
|
+
# @return [T::Boolean]
|
67
|
+
def builtin_variable?(var_id)
|
68
|
+
::CSVPlusPlus::Entities::Builtins::VARIABLES.key?(var_id)
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { returns(::T::Boolean) }
|
72
|
+
# Is the parser currently inside of the code section? (includes the `---`)
|
73
|
+
#
|
74
|
+
# @return [T::Boolean]
|
75
|
+
def parsing_code_section?
|
76
|
+
source_code.in_code_section?(line_number)
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { returns(::T::Boolean) }
|
80
|
+
# Is the parser currently inside of the CSV section?
|
81
|
+
#
|
82
|
+
# @return [T::Boolean]
|
83
|
+
def parsing_csv_section?
|
84
|
+
source_code.in_csv_section?(line_number)
|
85
|
+
end
|
86
|
+
|
87
|
+
sig do
|
88
|
+
params(message: ::String, bad_input: ::String, wrapped_error: ::T.nilable(::StandardError))
|
89
|
+
.returns(::T.noreturn)
|
90
|
+
end
|
91
|
+
# Called when an error is encoutered during parsing formulas (whether in the code section or a cell). It will
|
92
|
+
# construct a useful error with the current +@row/@cell_index+, +@line_number+ and +@filename+
|
93
|
+
#
|
94
|
+
# @param message [::String] A message relevant to why this error is being raised.
|
95
|
+
# @param bad_input [::String] The offending input that caused this error to be thrown.
|
96
|
+
# @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
|
97
|
+
def raise_formula_syntax_error(message, bad_input, wrapped_error: nil)
|
98
|
+
raise(::CSVPlusPlus::Error::FormulaSyntaxError.new(message, bad_input, self, wrapped_error:))
|
99
|
+
end
|
100
|
+
|
101
|
+
sig do
|
102
|
+
params(message: ::String, bad_input: ::String, wrapped_error: ::T.nilable(::StandardError))
|
103
|
+
.returns(::T.noreturn)
|
104
|
+
end
|
105
|
+
# Called when an error is encountered while parsing a modifier.
|
106
|
+
#
|
107
|
+
# @param message [::String] A message relevant to why this error is being raised.
|
108
|
+
# @param bad_input [::String] The offending input that caused this error to be thrown.
|
109
|
+
# @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
|
110
|
+
def raise_modifier_syntax_error(message, bad_input, wrapped_error: nil)
|
111
|
+
raise(::CSVPlusPlus::Error::ModifierSyntaxError.new(self, bad_input:, message:, wrapped_error:))
|
112
|
+
end
|
113
|
+
|
114
|
+
sig do
|
115
|
+
type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
116
|
+
end
|
117
|
+
# Reset the runtime state starting at the CSV section
|
118
|
+
# rubocop:disable Naming/BlockForwarding
|
119
|
+
def start_at_csv!(&block)
|
120
|
+
self.line_number = source_code.length_of_code_section + 1
|
121
|
+
start!(&block)
|
122
|
+
end
|
123
|
+
# rubocop:enable Naming/BlockForwarding
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -1,199 +1,42 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
4
|
+
require_relative './runtime/can_define_references'
|
5
|
+
require_relative './runtime/can_resolve_references'
|
6
|
+
require_relative './runtime/graph'
|
7
|
+
require_relative './runtime/position_tracker'
|
8
|
+
require_relative './runtime/references'
|
9
|
+
require_relative './runtime/runtime'
|
10
|
+
|
3
11
|
module CSVPlusPlus
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# central place for these things to be managed.
|
12
|
+
# All functionality needed to keep track of the runtime AKA execution context. This module has a lot of
|
13
|
+
# reponsibilities:
|
7
14
|
#
|
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.
|
15
|
+
# - variables and function resolution and scoping
|
16
|
+
# - variable & function definitions
|
17
|
+
# - keeping track of the runtime state (the current cell being processed)
|
18
|
+
# - rewriting the input file that's being parsed
|
15
19
|
#
|
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)
|
20
|
+
module Runtime
|
21
|
+
extend ::T::Sig
|
22
|
+
|
23
|
+
sig do
|
24
|
+
params(
|
25
|
+
source_code: ::CSVPlusPlus::SourceCode,
|
26
|
+
functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
|
27
|
+
variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
|
28
|
+
).returns(::CSVPlusPlus::Runtime::Runtime)
|
29
|
+
end
|
30
|
+
# Initialize a runtime instance with all the functionality we need. A runtime is one-to-one with a file being
|
31
|
+
# compiled.
|
32
|
+
#
|
33
|
+
# @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
|
36
|
+
#
|
37
|
+
# @return [Runtime::Runtime]
|
38
|
+
def self.new(source_code:, functions: {}, variables: {})
|
39
|
+
::CSVPlusPlus::Runtime::Runtime.new(source_code:, functions:, variables:)
|
197
40
|
end
|
198
41
|
end
|
199
42
|
end
|
@@ -0,0 +1,66 @@
|
|
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(::String) }
|
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(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)
|
31
|
+
|
32
|
+
lines = input.split(/[\r\n]/)
|
33
|
+
@length_of_file = ::T.let(lines.length, ::Integer)
|
34
|
+
@length_of_code_section = ::T.let(count_code_section_lines(lines), ::Integer)
|
35
|
+
@length_of_csv_section = ::T.let(@length_of_file - @length_of_code_section, ::Integer)
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { params(line_number: ::Integer).returns(::T::Boolean) }
|
39
|
+
# Does the given +line_number+ land in the code section of the file? (which includes the --- separator)
|
40
|
+
#
|
41
|
+
# @param line_number [Integer]
|
42
|
+
#
|
43
|
+
# @return [T::Boolean]
|
44
|
+
def in_code_section?(line_number)
|
45
|
+
line_number <= @length_of_code_section
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(line_number: ::Integer).returns(::T::Boolean) }
|
49
|
+
# Does the given +line_number+ land in the CSV section of the file?
|
50
|
+
#
|
51
|
+
# @param line_number [Integer]
|
52
|
+
#
|
53
|
+
# @return [T::Boolean]
|
54
|
+
def in_csv_section?(line_number)
|
55
|
+
line_number > @length_of_code_section
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
sig { params(lines: ::T::Array[::String]).returns(::Integer) }
|
61
|
+
def count_code_section_lines(lines)
|
62
|
+
eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
|
63
|
+
lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,42 +1,70 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module CSVPlusPlus
|
4
5
|
# Contains the data from a parsed csvpp template.
|
5
6
|
#
|
6
7
|
# @attr_reader rows [Array<Row>] The +Row+s that comprise this +Template+
|
7
|
-
# @attr_reader
|
8
|
+
# @attr_reader runtime [Runtime] The +Runtime+ containing all function and variable references
|
8
9
|
class Template
|
9
|
-
|
10
|
+
extend ::T::Sig
|
10
11
|
|
12
|
+
sig { returns(::T::Array[::CSVPlusPlus::Row]) }
|
13
|
+
attr_reader :rows
|
14
|
+
|
15
|
+
sig { returns(::CSVPlusPlus::Runtime::Runtime) }
|
16
|
+
attr_reader :runtime
|
17
|
+
|
18
|
+
sig { params(rows: ::T::Array[::CSVPlusPlus::Row], runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
11
19
|
# @param rows [Array<Row>] The +Row+s that comprise this +Template+
|
12
|
-
# @param
|
13
|
-
def initialize(rows:,
|
14
|
-
@scope = scope
|
20
|
+
# @param runtime [Runtime] The +Runtime+ containing all function and variable references
|
21
|
+
def initialize(rows:, runtime:)
|
15
22
|
@rows = rows
|
23
|
+
@runtime = runtime
|
16
24
|
end
|
17
25
|
|
18
|
-
|
19
|
-
|
20
|
-
|
26
|
+
sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
27
|
+
# Only run after expanding all rows, now we can bind all [[var=]] modifiers to a variable. There are two distinct
|
28
|
+
# types of variable bindings here:
|
29
|
+
#
|
30
|
+
# * Binding to a cell: for this we just make a +CellReference+ to the cell itself (A1, B4, etc)
|
31
|
+
# * Binding to a cell within an expand: the variable can only be resolved within that expand and needs to be
|
32
|
+
# relative to it's row (it can't be an absolute cell reference like above)
|
33
|
+
#
|
34
|
+
# @param runtime [Runtime] The current runtime
|
35
|
+
def bind_all_vars!(runtime)
|
36
|
+
runtime.map_rows(@rows) do |row|
|
37
|
+
# rubocop:disable Style/MissingElse
|
38
|
+
if row.unexpanded?
|
39
|
+
# rubocop:enable Style/MissingElse
|
40
|
+
raise(::CSVPlusPlus::Error::Error, 'Template#expand_rows! must be called before Template#bind_all_vars!')
|
41
|
+
end
|
42
|
+
|
43
|
+
runtime.map_row(row.cells) do |cell|
|
44
|
+
bind_vars(cell, row.modifier.expand)
|
45
|
+
end
|
46
|
+
end
|
21
47
|
end
|
22
48
|
|
23
|
-
|
49
|
+
sig { returns(::T::Array[::CSVPlusPlus::Row]) }
|
50
|
+
# Apply expand= (adding rows to the results) modifiers to the parsed template. This happens in towards the end of
|
51
|
+
# compilation because expanding rows will change the relative rownums as rows are added, and variables can't be
|
52
|
+
# bound until the rows have been assigned their final rownums.
|
24
53
|
#
|
25
54
|
# @return [Array<Row>]
|
26
55
|
def expand_rows!
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
56
|
+
# TODO: make it so that an infinite expand will not overwrite the rows below it, but instead merge with them
|
57
|
+
@rows =
|
58
|
+
rows.reduce([]) do |expanded_rows, row|
|
59
|
+
if row.modifier.expand
|
60
|
+
row.expand_rows(starts_at: expanded_rows.length, into: expanded_rows)
|
61
|
+
else
|
62
|
+
expanded_rows << row.tap { |r| r.index = expanded_rows.length }
|
63
|
+
end
|
34
64
|
end
|
35
|
-
)
|
36
|
-
|
37
|
-
@rows = expanded_rows
|
38
65
|
end
|
39
66
|
|
67
|
+
sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
40
68
|
# Make sure that the template has a valid amount of infinite expand modifiers
|
41
69
|
#
|
42
70
|
# @param runtime [Runtime] The compiler's current runtime
|
@@ -44,19 +72,20 @@ module CSVPlusPlus
|
|
44
72
|
infinite_expand_rows = @rows.filter { |r| r.modifier.expand&.infinite? }
|
45
73
|
return unless infinite_expand_rows.length > 1
|
46
74
|
|
47
|
-
runtime.
|
75
|
+
runtime.raise_modifier_syntax_error(
|
48
76
|
'You can only have one infinite expand= (on all others you must specify an amount)',
|
49
|
-
infinite_expand_rows[1]
|
77
|
+
infinite_expand_rows[1].to_s
|
50
78
|
)
|
51
79
|
end
|
52
80
|
|
53
|
-
|
81
|
+
sig { returns(::String) }
|
82
|
+
# Provide a summary of the state of the template (and it's +@runtime+)
|
54
83
|
#
|
55
|
-
# @return [String]
|
84
|
+
# @return [::String]
|
56
85
|
def verbose_summary
|
57
86
|
# TODO: we can probably include way more stats in here
|
58
87
|
<<~SUMMARY
|
59
|
-
#{@
|
88
|
+
#{@runtime.verbose_summary}
|
60
89
|
|
61
90
|
> #{@rows.length} rows to be written
|
62
91
|
SUMMARY
|
@@ -64,17 +93,15 @@ module CSVPlusPlus
|
|
64
93
|
|
65
94
|
private
|
66
95
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
push_row_fn.call(row)
|
77
|
-
end
|
96
|
+
sig { params(cell: ::CSVPlusPlus::Cell, expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)).void }
|
97
|
+
def bind_vars(cell, expand)
|
98
|
+
var = cell.modifier.var
|
99
|
+
return unless var
|
100
|
+
|
101
|
+
if expand
|
102
|
+
@runtime.bind_variable_in_expand(var, expand)
|
103
|
+
else
|
104
|
+
@runtime.bind_variable_to_cell(var)
|
78
105
|
end
|
79
106
|
end
|
80
107
|
end
|
@@ -1,20 +1,45 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module CSVPlusPlus
|
4
5
|
module Writer
|
5
6
|
# Some shared functionality that all Writers should build on
|
7
|
+
#
|
8
|
+
# @attr_reader options [Options] The supplied options - some of which are relevant for our writer instance
|
9
|
+
# @attr_reader runtime [Runtime] The current runtime - needed to resolve variables and display useful error messages
|
6
10
|
class BaseWriter
|
7
|
-
|
11
|
+
extend ::T::Sig
|
12
|
+
extend ::T::Helpers
|
13
|
+
|
14
|
+
abstract!
|
15
|
+
|
16
|
+
sig { returns(::CSVPlusPlus::Options) }
|
17
|
+
attr_reader :options
|
18
|
+
|
19
|
+
sig { returns(::CSVPlusPlus::Runtime::Runtime) }
|
20
|
+
attr_reader :runtime
|
8
21
|
|
9
22
|
protected
|
10
23
|
|
11
|
-
|
12
|
-
|
24
|
+
sig { params(options: ::CSVPlusPlus::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
25
|
+
# Open a CSV outputter to the +output_filename+ specified by the +Options+
|
26
|
+
#
|
27
|
+
# @param options [Options] The supplied options.
|
28
|
+
# @param runtime [Runtime] The current runtime.
|
29
|
+
def initialize(options, runtime)
|
13
30
|
@options = options
|
14
|
-
|
31
|
+
@runtime = runtime
|
15
32
|
end
|
16
33
|
|
17
|
-
|
34
|
+
sig { abstract.params(template: ::CSVPlusPlus::Template).void }
|
35
|
+
# Write the given +template+.
|
36
|
+
#
|
37
|
+
# @param template [Template]
|
38
|
+
def write(template); end
|
39
|
+
|
40
|
+
sig { abstract.void }
|
41
|
+
# Write a backup of the current spreadsheet.
|
42
|
+
def write_backup; end
|
18
43
|
end
|
19
44
|
end
|
20
45
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require_relative './file_backer_upper'
|
@@ -6,28 +7,29 @@ module CSVPlusPlus
|
|
6
7
|
module Writer
|
7
8
|
# A class that can output a +Template+ to CSV
|
8
9
|
class CSV < ::CSVPlusPlus::Writer::BaseWriter
|
10
|
+
extend ::T::Sig
|
11
|
+
|
9
12
|
include ::CSVPlusPlus::Writer::FileBackerUpper
|
10
13
|
|
11
|
-
|
14
|
+
sig { override.params(template: ::CSVPlusPlus::Template).void }
|
15
|
+
# Write a +template+ to CSV
|
16
|
+
#
|
17
|
+
# @param template [Template] The template to use as input to be written. It should have been compiled by calling
|
18
|
+
# Compiler#compile_template
|
12
19
|
def write(template)
|
13
20
|
# TODO: also read it and merge the results
|
14
21
|
::CSV.open(@options.output_filename, 'wb') do |csv|
|
15
|
-
template.rows
|
22
|
+
@runtime.map_rows(template.rows) do |row|
|
16
23
|
csv << build_row(row)
|
17
24
|
end
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
21
|
-
protected
|
22
|
-
|
23
|
-
def load_requires
|
24
|
-
require('csv')
|
25
|
-
end
|
26
|
-
|
27
28
|
private
|
28
29
|
|
30
|
+
sig { params(row: ::CSVPlusPlus::Row).returns(::T::Array[::T.nilable(::String)]) }
|
29
31
|
def build_row(row)
|
30
|
-
row.cells.
|
32
|
+
@runtime.map_row(row.cells) { |cell, _i| cell.evaluate(@runtime) }
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|