csv_plus_plus 0.1.1 → 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 +18 -63
- data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
- data/lib/csv_plus_plus/cell.rb +46 -24
- data/lib/csv_plus_plus/cli.rb +44 -17
- data/lib/csv_plus_plus/cli_flag.rb +1 -2
- data/lib/csv_plus_plus/color.rb +42 -11
- data/lib/csv_plus_plus/compiler.rb +178 -0
- data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
- data/lib/csv_plus_plus/entities/boolean.rb +40 -0
- data/lib/csv_plus_plus/entities/builtins.rb +58 -0
- data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
- data/lib/csv_plus_plus/entities/date.rb +63 -0
- data/lib/csv_plus_plus/entities/entity.rb +50 -0
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
- data/lib/csv_plus_plus/entities/function.rb +45 -0
- data/lib/csv_plus_plus/entities/function_call.rb +50 -0
- data/lib/csv_plus_plus/entities/number.rb +48 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
- data/lib/csv_plus_plus/entities/string.rb +42 -0
- data/lib/csv_plus_plus/entities/variable.rb +37 -0
- data/lib/csv_plus_plus/entities.rb +40 -0
- data/lib/csv_plus_plus/error/error.rb +20 -0
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
- data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
- data/lib/csv_plus_plus/error/writer_error.rb +17 -0
- data/lib/csv_plus_plus/error.rb +10 -2
- data/lib/csv_plus_plus/google_api_client.rb +11 -2
- data/lib/csv_plus_plus/google_options.rb +23 -18
- data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
- 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 +18 -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 -150
- data/lib/csv_plus_plus/options.rb +64 -19
- data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
- data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
- data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
- data/lib/csv_plus_plus/row.rb +53 -15
- 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 +42 -0
- data/lib/csv_plus_plus/source_code.rb +66 -0
- data/lib/csv_plus_plus/template.rb +63 -36
- 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 +7 -4
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
- 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 -33
- data/lib/csv_plus_plus/writer.rb +39 -9
- data/lib/csv_plus_plus.rb +41 -15
- metadata +44 -30
- data/lib/csv_plus_plus/code_section.rb +0 -101
- data/lib/csv_plus_plus/expand.rb +0 -18
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
- data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
- data/lib/csv_plus_plus/language/builtins.rb +0 -46
- data/lib/csv_plus_plus/language/compiler.rb +0 -152
- data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
- data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
- data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
- data/lib/csv_plus_plus/language/entities/function.rb +0 -35
- data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
- data/lib/csv_plus_plus/language/entities/number.rb +0 -36
- data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
- data/lib/csv_plus_plus/language/entities/string.rb +0 -31
- data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/language/entities.rb +0 -28
- data/lib/csv_plus_plus/language/references.rb +0 -70
- data/lib/csv_plus_plus/language/runtime.rb +0 -205
- data/lib/csv_plus_plus/language/scope.rb +0 -192
- data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
- data/lib/csv_plus_plus/modifier.tab.rb +0 -907
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
- 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
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
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
|
+
|
|
11
|
+
module CSVPlusPlus
|
|
12
|
+
# All functionality needed to keep track of the runtime AKA execution context. This module has a lot of
|
|
13
|
+
# reponsibilities:
|
|
14
|
+
#
|
|
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
|
|
19
|
+
#
|
|
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:)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
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
|
-
# Contains the
|
|
5
|
+
# Contains the data from a parsed csvpp template.
|
|
5
6
|
#
|
|
6
|
-
# @attr_reader code_section [CodeSection] The +CodeSection+ containing the functions and variables defined herein
|
|
7
7
|
# @attr_reader rows [Array<Row>] The +Row+s that comprise this +Template+
|
|
8
|
+
# @attr_reader runtime [Runtime] The +Runtime+ containing all function and variable references
|
|
8
9
|
class Template
|
|
9
|
-
|
|
10
|
+
extend ::T::Sig
|
|
10
11
|
|
|
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 }
|
|
12
19
|
# @param rows [Array<Row>] The +Row+s that comprise this +Template+
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative './file_backer_upper'
|
|
@@ -7,13 +8,19 @@ module CSVPlusPlus
|
|
|
7
8
|
module Writer
|
|
8
9
|
# A class that can output a +Template+ to an Excel file
|
|
9
10
|
class Excel < ::CSVPlusPlus::Writer::BaseWriter
|
|
11
|
+
extend ::T::Sig
|
|
12
|
+
|
|
10
13
|
include ::CSVPlusPlus::Writer::FileBackerUpper
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
sig { override.params(template: ::CSVPlusPlus::Template).void }
|
|
16
|
+
# Write the +template+ to an Excel file
|
|
17
|
+
#
|
|
18
|
+
# @param template [Template] The template to write
|
|
13
19
|
def write(template)
|
|
14
20
|
::CSVPlusPlus::Writer::RubyXLBuilder.new(
|
|
15
|
-
input_filename: @options.output_filename,
|
|
21
|
+
input_filename: ::T.must(@options.output_filename),
|
|
16
22
|
rows: template.rows,
|
|
23
|
+
runtime: @runtime,
|
|
17
24
|
sheet_name: @options.sheet_name
|
|
18
25
|
).build_workbook.write(@options.output_filename)
|
|
19
26
|
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
# typed: false
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
require 'pathname'
|
|
5
|
-
|
|
6
4
|
module CSVPlusPlus
|
|
7
5
|
module Writer
|
|
8
6
|
# A module that can be mixed into any Writer that needs to back up it's @output_filename (all of them except Google
|
|
@@ -26,6 +24,7 @@ module CSVPlusPlus
|
|
|
26
24
|
|
|
27
25
|
private
|
|
28
26
|
|
|
27
|
+
# rubocop:disable Metrics/MethodLength
|
|
29
28
|
def attempt_backups
|
|
30
29
|
attempted =
|
|
31
30
|
# rubocop:disable Lint/ConstantResolution
|
|
@@ -39,8 +38,12 @@ module CSVPlusPlus
|
|
|
39
38
|
return backed_up_to
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
raise(
|
|
41
|
+
raise(
|
|
42
|
+
::CSVPlusPlus::Error::WriterError,
|
|
43
|
+
"Unable to write backup file despite trying these: #{attempted.join(', ')}"
|
|
44
|
+
)
|
|
43
45
|
end
|
|
46
|
+
# rubocop:enable Metrics/MethodLength
|
|
44
47
|
|
|
45
48
|
def backup(filename)
|
|
46
49
|
return if ::File.exist?(filename)
|