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,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
|