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,178 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
# Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+). Variable resolution is delegated
|
|
6
|
+
# to the +Scope+
|
|
7
|
+
#
|
|
8
|
+
# @attr_reader options [Options] The +Options+ to compile with
|
|
9
|
+
# @attr_reader runtime [Runtime] The runtime execution
|
|
10
|
+
# rubocop:disable Metrics/ClassLength
|
|
11
|
+
class Compiler
|
|
12
|
+
extend ::T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(::CSVPlusPlus::Options) }
|
|
15
|
+
attr_reader :options
|
|
16
|
+
|
|
17
|
+
sig { returns(::CSVPlusPlus::Runtime::Runtime) }
|
|
18
|
+
attr_reader :runtime
|
|
19
|
+
|
|
20
|
+
sig do
|
|
21
|
+
params(
|
|
22
|
+
options: ::CSVPlusPlus::Options,
|
|
23
|
+
runtime: ::CSVPlusPlus::Runtime::Runtime,
|
|
24
|
+
block: ::T.proc.params(arg0: ::CSVPlusPlus::Compiler).void
|
|
25
|
+
).void
|
|
26
|
+
end
|
|
27
|
+
# Create a compiler and make sure it gets cleaned up
|
|
28
|
+
#
|
|
29
|
+
# @param options [Options]
|
|
30
|
+
# @param runtime [Runtime] The initial +Runtime+ for the compiler
|
|
31
|
+
def self.with_compiler(options:, runtime:, &block)
|
|
32
|
+
if options.verbose
|
|
33
|
+
::CSVPlusPlus::BenchmarkedCompiler.with_benchmarks(options:, runtime:) do |c|
|
|
34
|
+
block.call(c)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
block.call(new(options:, runtime:))
|
|
38
|
+
end
|
|
39
|
+
ensure
|
|
40
|
+
runtime.cleanup!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(options: ::CSVPlusPlus::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
|
44
|
+
# @param options [Options]
|
|
45
|
+
# @param runtime [Runtime]
|
|
46
|
+
def initialize(options:, runtime:)
|
|
47
|
+
@options = options
|
|
48
|
+
@runtime = runtime
|
|
49
|
+
|
|
50
|
+
# TODO: infer a type
|
|
51
|
+
# allow user-supplied key/values to override anything global or from the code section
|
|
52
|
+
@runtime.def_variables(
|
|
53
|
+
options.key_values.transform_values { |v| ::CSVPlusPlus::Entities::String.new(v.to_s) }
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { params(benchmark: ::Benchmark::Report).void }
|
|
58
|
+
# Attach a +Benchmark+ and a place to store timings to the compiler class.
|
|
59
|
+
#
|
|
60
|
+
# @param benchmark [Benchmark] A +Benchmark+ instance
|
|
61
|
+
def benchmark=(benchmark)
|
|
62
|
+
@benchmark = ::T.let(benchmark, ::T.nilable(::Benchmark::Report))
|
|
63
|
+
@timings = ::T.let([], ::T.nilable(::T::Array[::Benchmark::Tms]))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { returns(::CSVPlusPlus::Template) }
|
|
67
|
+
# Compile a template and return a +::CSVPlusPlus::Template+ instance ready to be written with a +Writer+
|
|
68
|
+
#
|
|
69
|
+
# @return [Template]
|
|
70
|
+
def compile_template
|
|
71
|
+
parse_code_section!
|
|
72
|
+
rows = parse_csv_section!
|
|
73
|
+
|
|
74
|
+
::CSVPlusPlus::Template.new(rows:, runtime: @runtime).tap do |t|
|
|
75
|
+
t.validate_infinite_expands(@runtime)
|
|
76
|
+
expanding! { t.expand_rows! }
|
|
77
|
+
bind_all_vars! { t.bind_all_vars!(@runtime) }
|
|
78
|
+
resolve_all_cells!(t)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(block: ::T.proc.params(runtime: ::CSVPlusPlus::Runtime::Runtime).void).void }
|
|
83
|
+
# Write the compiled results
|
|
84
|
+
def outputting!(&block)
|
|
85
|
+
@runtime.start_at_csv! { block.call(@runtime) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
protected
|
|
89
|
+
|
|
90
|
+
sig { void }
|
|
91
|
+
# Parses the input file and sets variables on +@runtime+ as necessary
|
|
92
|
+
def parse_code_section!
|
|
93
|
+
@runtime.start! do
|
|
94
|
+
# TODO: this flow can probably be refactored, it used to have more needs back when we had to
|
|
95
|
+
# parse and save the code_section
|
|
96
|
+
parsing_code_section do |input|
|
|
97
|
+
csv_section = ::CSVPlusPlus::Parser::CodeSection.new.parse(input, @runtime)
|
|
98
|
+
|
|
99
|
+
# return the csv_section to the caller because they're gonna re-write input with it
|
|
100
|
+
next csv_section
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sig { returns(::T::Array[::CSVPlusPlus::Row]) }
|
|
106
|
+
# Parse the CSV section and return an array of +Row+s
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<Row>]
|
|
109
|
+
def parse_csv_section!
|
|
110
|
+
@runtime.start_at_csv! do
|
|
111
|
+
@runtime.map_lines(::CSV.new(::T.unsafe(@runtime.input))) do |csv_row|
|
|
112
|
+
parse_row(::T.cast(csv_row, ::T::Array[::String]))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
ensure
|
|
116
|
+
# we're done with the file and everything is in memory
|
|
117
|
+
@runtime.cleanup!
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(template: ::CSVPlusPlus::Template).returns(::T::Array[::T::Array[::CSVPlusPlus::Entities::Entity]]) }
|
|
121
|
+
# Iterates through each cell of each row and resolves it's variable and function references.
|
|
122
|
+
#
|
|
123
|
+
# @param template [Template]
|
|
124
|
+
#
|
|
125
|
+
# @return [Array<Entity>]
|
|
126
|
+
def resolve_all_cells!(template)
|
|
127
|
+
@runtime.start_at_csv! do
|
|
128
|
+
@runtime.map_all_cells(template.rows) do |cell|
|
|
129
|
+
cell.ast = @runtime.resolve_cell_value if cell.ast
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig { params(block: ::T.proc.void).void }
|
|
135
|
+
# Expanding rows
|
|
136
|
+
def expanding!(&block)
|
|
137
|
+
@runtime.start_at_csv! { block.call }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sig { params(block: ::T.proc.void).void }
|
|
141
|
+
# Binding all [[var=]] directives
|
|
142
|
+
def bind_all_vars!(&block)
|
|
143
|
+
@runtime.start_at_csv! { block.call }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
sig { params(block: ::T.proc.params(arg0: ::String).returns(::String)).void }
|
|
149
|
+
def parsing_code_section(&block)
|
|
150
|
+
csv_section = block.call(::T.must(::T.must(@runtime.input).read))
|
|
151
|
+
@runtime.rewrite_input!(csv_section)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sig { params(csv_row: ::T::Array[::T.nilable(::String)]).returns(::CSVPlusPlus::Row) }
|
|
155
|
+
# Using the current +@runtime+ and the given +csv_row+ parse it into a +Row+ of +Cell+s
|
|
156
|
+
# +csv_row+ should have already been run through a CSV parser and is an array of strings
|
|
157
|
+
#
|
|
158
|
+
# @param csv_row [Array<Array<String>>]
|
|
159
|
+
#
|
|
160
|
+
# @return [Row]
|
|
161
|
+
def parse_row(csv_row)
|
|
162
|
+
row_modifier = ::CSVPlusPlus::Modifier.new(@options, row_level: true)
|
|
163
|
+
|
|
164
|
+
cells = @runtime.map_row(csv_row) { |value, _cell_index| parse_cell(value || '', row_modifier) }
|
|
165
|
+
|
|
166
|
+
::CSVPlusPlus::Row.new(cells:, index: @runtime.row_index, modifier: row_modifier)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
sig { params(value: ::String, row_modifier: ::CSVPlusPlus::Modifier::Modifier).returns(::CSVPlusPlus::Cell) }
|
|
170
|
+
def parse_cell(value, row_modifier)
|
|
171
|
+
cell_modifier = ::CSVPlusPlus::Modifier.new(@options)
|
|
172
|
+
parsed_value = ::CSVPlusPlus::Parser::Modifier.new(cell_modifier:, row_modifier:).parse(value, @runtime)
|
|
173
|
+
|
|
174
|
+
::CSVPlusPlus::Cell.parse(parsed_value, runtime:, modifier: cell_modifier)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
# rubocop:enable Metrics/ClassLength
|
|
178
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Entities
|
|
6
|
+
# Some helpful functions that can be mixed into a class to help building ASTs
|
|
7
|
+
module ASTBuilder
|
|
8
|
+
extend ::T::Sig
|
|
9
|
+
|
|
10
|
+
sig do
|
|
11
|
+
params(
|
|
12
|
+
method_name: ::Symbol,
|
|
13
|
+
args: ::T.untyped,
|
|
14
|
+
kwargs: ::T.untyped,
|
|
15
|
+
block: ::T.untyped
|
|
16
|
+
).returns(::CSVPlusPlus::Entities::Entity)
|
|
17
|
+
end
|
|
18
|
+
# Let the current class have functions which can build a given entity by calling it's type. For example
|
|
19
|
+
# +number(1)+, +variable(:foo)+
|
|
20
|
+
#
|
|
21
|
+
# @param method_name [Symbol] The +method_name+ to respond to
|
|
22
|
+
# @param args [Array] The arguments to create the entity with
|
|
23
|
+
# @param kwargs [Hash] The arguments to create the entity with
|
|
24
|
+
#
|
|
25
|
+
# @return [Entity, #super]
|
|
26
|
+
# rubocop:disable Naming/BlockForwarding
|
|
27
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
28
|
+
entity_class_name = method_name.to_s.split('_').map(&:capitalize).join.to_sym
|
|
29
|
+
::CSVPlusPlus::Entities.const_get(entity_class_name).new(*args, **kwargs, &block)
|
|
30
|
+
rescue ::NameError
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
# rubocop:enable Naming/BlockForwarding
|
|
34
|
+
|
|
35
|
+
sig { params(method_name: ::Symbol, _args: ::T.untyped).returns(::T::Boolean) }
|
|
36
|
+
# Let the current class have functions which can build a given entity by calling it's type. For example
|
|
37
|
+
# +number(1)+, +variable(:foo)+
|
|
38
|
+
#
|
|
39
|
+
# @param method_name [Symbol] The +method_name+ to respond to
|
|
40
|
+
# @param _args [::T.Untyped] The arguments to create the entity with
|
|
41
|
+
#
|
|
42
|
+
# @return [::T::Boolean, #super]
|
|
43
|
+
def respond_to_missing?(method_name, *_args)
|
|
44
|
+
!::CSVPlusPlus::Entities::Type.deserialize(method_name.to_s.gsub('_', '')).nil?
|
|
45
|
+
rescue ::KeyError
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Entities
|
|
6
|
+
# A boolean value
|
|
7
|
+
#
|
|
8
|
+
# @attr_reader value [true, false]
|
|
9
|
+
class Boolean < Entity
|
|
10
|
+
sig { returns(::T::Boolean) }
|
|
11
|
+
attr_reader :value
|
|
12
|
+
|
|
13
|
+
sig { params(value: ::T.any(::String, ::T::Boolean)).void }
|
|
14
|
+
# @param value [::String, boolean]
|
|
15
|
+
def initialize(value)
|
|
16
|
+
super(::CSVPlusPlus::Entities::Type::Boolean)
|
|
17
|
+
# TODO: probably can do a lot better in general on type validation
|
|
18
|
+
@value = ::T.let(value.is_a?(::String) ? (value.downcase == 'true') : value, ::T::Boolean)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { override.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
|
|
22
|
+
# @param _runtime [Runtime]
|
|
23
|
+
#
|
|
24
|
+
# @return [::String]
|
|
25
|
+
def evaluate(_runtime)
|
|
26
|
+
@value.to_s.upcase
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
|
|
30
|
+
# @param other [Entity]
|
|
31
|
+
#
|
|
32
|
+
# @return [::T::Boolean]
|
|
33
|
+
def ==(other)
|
|
34
|
+
return false unless super
|
|
35
|
+
|
|
36
|
+
other.is_a?(self.class) && value == other.value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Entities
|
|
6
|
+
# Provides ASTs for builtin functions and variables
|
|
7
|
+
module Builtins
|
|
8
|
+
extend ::CSVPlusPlus::Entities::ASTBuilder
|
|
9
|
+
|
|
10
|
+
VARIABLES = {
|
|
11
|
+
# The number (integer) of the current cell. Starts at 1
|
|
12
|
+
cellnum: runtime_value(->(r) { number(r.cell_index + 1) }),
|
|
13
|
+
|
|
14
|
+
# A reference to the current cell
|
|
15
|
+
cellref: runtime_value(->(r) { cell_reference(row_index: r.row_index, cell_index: r.cell_index) }),
|
|
16
|
+
|
|
17
|
+
# A reference to the row above
|
|
18
|
+
rowabove: runtime_value(->(r) { cell_reference(row_index: [0, (r.row_index - 1)].max) }),
|
|
19
|
+
|
|
20
|
+
# A reference to the row below
|
|
21
|
+
rowbelow: runtime_value(->(r) { cell_reference(row_index: r.row_index + 1) }),
|
|
22
|
+
|
|
23
|
+
# The number (integer) of the current row. Starts at 1
|
|
24
|
+
rownum: runtime_value(->(r) { number(r.rownum) }),
|
|
25
|
+
|
|
26
|
+
# A reference to the current row
|
|
27
|
+
rowref: runtime_value(->(r) { cell_reference(row_index: r.row_index) })
|
|
28
|
+
}.freeze
|
|
29
|
+
public_constant :VARIABLES
|
|
30
|
+
|
|
31
|
+
FUNCTIONS = {
|
|
32
|
+
# TODO: A reference to a cell in a given row?
|
|
33
|
+
# A reference to a cell above the current row
|
|
34
|
+
# cellabove: runtime_value(->(r, args) { cell_reference(ref: [args[0], [1, (r.rownum - 1)].max].join) }),
|
|
35
|
+
cellabove: runtime_value(
|
|
36
|
+
lambda { |r, args|
|
|
37
|
+
cell_reference(cell_index: args[0].cell_index, row_index: [0, (r.row_index - 1)].max)
|
|
38
|
+
}
|
|
39
|
+
),
|
|
40
|
+
|
|
41
|
+
# A reference to a cell in the current row
|
|
42
|
+
celladjacent: runtime_value(
|
|
43
|
+
lambda { |r, args|
|
|
44
|
+
cell_reference(cell_index: args[0].cell_index, row_index: r.row_index)
|
|
45
|
+
}
|
|
46
|
+
),
|
|
47
|
+
|
|
48
|
+
# A reference to a cell below the current row
|
|
49
|
+
cellbelow: runtime_value(
|
|
50
|
+
lambda { |r, args|
|
|
51
|
+
cell_reference(cell_index: args[0].cell_index, row_index: r.row_index + 1)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}.freeze
|
|
55
|
+
public_constant :FUNCTIONS
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Entities
|
|
6
|
+
# A reference to a cell. Internally it is represented by a simple +cell_index+ and +row_index+ but there are
|
|
7
|
+
# functions for converting to and from A1-style formats. Supported formats are:
|
|
8
|
+
#
|
|
9
|
+
# * `1` - A reference to the entire first row
|
|
10
|
+
# * `A` - A reference to the entire first column
|
|
11
|
+
# * `A1` - A reference to the first cell (top left)
|
|
12
|
+
# * `A1:D10` - The range defined between A1 and D10
|
|
13
|
+
# * `Sheet1!B2` - Cell B2 on the sheet "Sheet1"
|
|
14
|
+
#
|
|
15
|
+
# @attr sheet_name [String, nil] The name of the sheet reference
|
|
16
|
+
# @attr_reader cell_index [Integer, nil] The cell index of the cell being referenced
|
|
17
|
+
# @attr_reader row_index [Integer, nil] The row index of the cell being referenced
|
|
18
|
+
# @attr_reader scoped_to_expand [Expand, nil] If set, the expand in which this variable is scoped to. It cannot be
|
|
19
|
+
# resolved outside of the given expand.
|
|
20
|
+
# @attr_reader upper_cell_index [Integer, nil] If set, the cell reference is a range and this is the upper cell
|
|
21
|
+
# index of it
|
|
22
|
+
# @attr_reader upper_row_index [Integer, nil] If set, the cell reference is a range and this is the upper row index
|
|
23
|
+
# of it
|
|
24
|
+
# rubocop:disable Metrics/ClassLength
|
|
25
|
+
class CellReference < Entity
|
|
26
|
+
extend ::T::Sig
|
|
27
|
+
|
|
28
|
+
sig { returns(::T.nilable(::String)) }
|
|
29
|
+
attr_accessor :sheet_name
|
|
30
|
+
|
|
31
|
+
sig { returns(::T.nilable(::Integer)) }
|
|
32
|
+
attr_reader :cell_index
|
|
33
|
+
|
|
34
|
+
sig { returns(::T.nilable(::Integer)) }
|
|
35
|
+
attr_reader :row_index
|
|
36
|
+
|
|
37
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
|
|
38
|
+
attr_reader :scoped_to_expand
|
|
39
|
+
|
|
40
|
+
sig { returns(::T.nilable(::Integer)) }
|
|
41
|
+
attr_reader :upper_cell_index
|
|
42
|
+
|
|
43
|
+
sig { returns(::T.nilable(::Integer)) }
|
|
44
|
+
attr_reader :upper_row_index
|
|
45
|
+
|
|
46
|
+
# TODO: this is getting gross, maybe define an actual parser
|
|
47
|
+
A1_NOTATION_REGEXP = /
|
|
48
|
+
^
|
|
49
|
+
(?:
|
|
50
|
+
(?:
|
|
51
|
+
(?:'([^'\\]|\\.)*') # allow for a single-quoted sheet name
|
|
52
|
+
|
|
|
53
|
+
(\w+) # or if it's not quoted, just allow \w+
|
|
54
|
+
)
|
|
55
|
+
! # if a sheet name is specified, it's always followed by a !
|
|
56
|
+
)?
|
|
57
|
+
([a-zA-Z0-9]+) # the only part required - something alphanumeric
|
|
58
|
+
(?: :([a-zA-Z0-9]+))? # and they might make it a range
|
|
59
|
+
$
|
|
60
|
+
/x
|
|
61
|
+
public_constant :A1_NOTATION_REGEXP
|
|
62
|
+
|
|
63
|
+
ALPHA = ::T.let(('A'..'Z').to_a.freeze, ::T::Array[::String])
|
|
64
|
+
private_constant :ALPHA
|
|
65
|
+
|
|
66
|
+
sig { params(cell_reference_string: ::String).returns(::T::Boolean) }
|
|
67
|
+
# Does the given +cell_reference_string+ conform to a valid cell reference?
|
|
68
|
+
#
|
|
69
|
+
# {https://developers.google.com/sheets/api/guides/concepts}
|
|
70
|
+
#
|
|
71
|
+
# @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
|
|
72
|
+
# A1 notation but maybe can support R1C1)
|
|
73
|
+
#
|
|
74
|
+
# @return [::T::Boolean]
|
|
75
|
+
def self.valid_cell_reference?(cell_reference_string)
|
|
76
|
+
!(cell_reference_string =~ ::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP).nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig do
|
|
80
|
+
params(
|
|
81
|
+
cell_index: ::T.nilable(::Integer),
|
|
82
|
+
ref: ::T.nilable(::String),
|
|
83
|
+
row_index: ::T.nilable(::Integer),
|
|
84
|
+
scoped_to_expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)
|
|
85
|
+
).void
|
|
86
|
+
end
|
|
87
|
+
# Either +ref+, +cell_index+ or +row_index+ must be specified.
|
|
88
|
+
#
|
|
89
|
+
# @param cell_index [Integer, nil] The index of the cell being referenced.
|
|
90
|
+
# @param ref [Integer, nil] An A1-style cell reference (that will be parsed into it's row/cell indexes).
|
|
91
|
+
# @param row_index [Integer, nil] The index of the row being referenced.
|
|
92
|
+
# @param scoped_to_expand [Expand] The [[expand]] that this cell reference will be scoped to. In other words, it
|
|
93
|
+
# will only be able to be resolved if the runtime is within the bounds of the expand (it can't be referenced
|
|
94
|
+
# outside of the expand.)
|
|
95
|
+
# rubocop:disable Metrics/MethodLength
|
|
96
|
+
def initialize(cell_index: nil, ref: nil, row_index: nil, scoped_to_expand: nil)
|
|
97
|
+
raise(::ArgumentError, 'Must specify :ref, :cell_index or :row_index') unless ref || cell_index || row_index
|
|
98
|
+
|
|
99
|
+
super(::CSVPlusPlus::Entities::Type::CellReference)
|
|
100
|
+
|
|
101
|
+
if ref
|
|
102
|
+
from_a1_ref!(ref)
|
|
103
|
+
else
|
|
104
|
+
@cell_index = ::T.let(cell_index, ::T.nilable(::Integer))
|
|
105
|
+
@row_index = ::T.let(row_index, ::T.nilable(::Integer))
|
|
106
|
+
|
|
107
|
+
@upper_cell_index = ::T.let(nil, ::T.nilable(::Integer))
|
|
108
|
+
@upper_row_index = ::T.let(nil, ::T.nilable(::Integer))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@scoped_to_expand = scoped_to_expand
|
|
112
|
+
end
|
|
113
|
+
# rubocop:enable Metrics/MethodLength
|
|
114
|
+
|
|
115
|
+
sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
|
|
116
|
+
# @param other [Entity]
|
|
117
|
+
#
|
|
118
|
+
# @return [boolean]
|
|
119
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
120
|
+
def ==(other)
|
|
121
|
+
return false unless super
|
|
122
|
+
|
|
123
|
+
other.is_a?(self.class) && @cell_index == other.cell_index && @row_index == other.row_index \
|
|
124
|
+
&& @sheet_name == other.sheet_name && @scoped_to_expand == other.scoped_to_expand \
|
|
125
|
+
&& @upper_cell_index == other.upper_cell_index && @upper_row_index == other.upper_row_index
|
|
126
|
+
end
|
|
127
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
128
|
+
|
|
129
|
+
sig { override.params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
|
|
130
|
+
# Get the A1-style cell reference
|
|
131
|
+
#
|
|
132
|
+
# @param runtime [Runtime] The current runtime
|
|
133
|
+
#
|
|
134
|
+
# @return [::String] An A1-style reference
|
|
135
|
+
def evaluate(runtime)
|
|
136
|
+
# unless in_scope?(runtime)
|
|
137
|
+
# runtime.raise_modifier_syntax_error(message: 'Reference is out of scope', bad_input: runtime.cell.value)
|
|
138
|
+
# end
|
|
139
|
+
|
|
140
|
+
to_a1_ref(runtime) || ''
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { returns(::T::Boolean) }
|
|
144
|
+
# Is the cell_reference a range? - something like A1:D10
|
|
145
|
+
#
|
|
146
|
+
# @return [boolean]
|
|
147
|
+
def range?
|
|
148
|
+
!upper_row_index.nil? || !upper_cell_index.nil?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::String)) }
|
|
154
|
+
# Turns index-based/X,Y coordinates into a A1 format
|
|
155
|
+
#
|
|
156
|
+
# @param runtime [Runtime]
|
|
157
|
+
#
|
|
158
|
+
# @return [::String, nil]
|
|
159
|
+
def to_a1_ref(runtime)
|
|
160
|
+
row_index = runtime_row_index(runtime)
|
|
161
|
+
return unless row_index || @cell_index
|
|
162
|
+
|
|
163
|
+
rowref = row_index ? (row_index + 1).to_s : ''
|
|
164
|
+
cellref = @cell_index ? to_a1_cell_ref : ''
|
|
165
|
+
[cellref, rowref].join
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::Integer)) }
|
|
169
|
+
def runtime_row_index(runtime)
|
|
170
|
+
@scoped_to_expand ? runtime.row_index : @row_index
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
sig { returns(::String) }
|
|
174
|
+
# Turns a cell index into an A1 reference (just the "A" part - for example 0 == 'A', 1 == 'B', 2 == 'C', etc.)
|
|
175
|
+
#
|
|
176
|
+
# @return [::String]
|
|
177
|
+
def to_a1_cell_ref
|
|
178
|
+
c = @cell_index.dup
|
|
179
|
+
ref = ''
|
|
180
|
+
|
|
181
|
+
while c >= 0
|
|
182
|
+
# rubocop:disable Lint/ConstantResolution
|
|
183
|
+
ref += ::T.must(ALPHA[c % 26])
|
|
184
|
+
# rubocop:enable Lint/ConstantResolution
|
|
185
|
+
c = (c / 26).floor - 1
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
ref.reverse
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
sig { params(ref: ::String).void }
|
|
192
|
+
def from_a1_ref!(ref)
|
|
193
|
+
quoted_sheet_name, unquoted_sheet_name, lower_range, upper_range = ::T.must(
|
|
194
|
+
ref.strip.match(
|
|
195
|
+
::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP
|
|
196
|
+
)
|
|
197
|
+
).captures
|
|
198
|
+
|
|
199
|
+
@sheet_name = quoted_sheet_name || unquoted_sheet_name
|
|
200
|
+
|
|
201
|
+
parse_lower_range!(lower_range) if lower_range
|
|
202
|
+
parse_upper_range!(upper_range) if upper_range
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
sig { params(lower_range: ::String).void }
|
|
206
|
+
def parse_lower_range!(lower_range)
|
|
207
|
+
cell_ref, row_ref = ::T.must(lower_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
|
|
208
|
+
@cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
|
|
209
|
+
@row_index = Integer(row_ref, 10) - 1 if row_ref
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sig { params(upper_range: ::String).void }
|
|
213
|
+
# TODO: make this less redundant with the above function
|
|
214
|
+
def parse_upper_range!(upper_range)
|
|
215
|
+
cell_ref, row_ref = ::T.must(upper_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
|
|
216
|
+
@upper_cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
|
|
217
|
+
@upper_row_index = Integer(row_ref, 10) - 1 if row_ref
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
sig { params(cell_ref: ::String).returns(::Integer) }
|
|
221
|
+
def from_a1_cell_ref!(cell_ref)
|
|
222
|
+
(cell_ref.upcase.chars.reduce(0) do |cell_index, letter|
|
|
223
|
+
# rubocop:disable Lint/ConstantResolution
|
|
224
|
+
(cell_index * 26) + ::T.must(ALPHA.find_index(letter)) + 1
|
|
225
|
+
# rubocop:enable Lint/ConstantResolution
|
|
226
|
+
end) - 1
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
# rubocop:enable Metrics/ClassLength
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Entities
|
|
6
|
+
# A date value
|
|
7
|
+
#
|
|
8
|
+
# @attr_reader value [Date] The parsed date
|
|
9
|
+
class Date < Entity
|
|
10
|
+
extend ::T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(::Date) }
|
|
13
|
+
attr_reader :value
|
|
14
|
+
|
|
15
|
+
# TODO: support time granularity?
|
|
16
|
+
DATE_STRING_REGEXP = %r{^\d{1,2}[/-]\d{1,2}[/-]\d{1,4}?$}
|
|
17
|
+
public_constant :DATE_STRING_REGEXP
|
|
18
|
+
|
|
19
|
+
sig { params(date_string: ::String).returns(::T::Boolean) }
|
|
20
|
+
# Is the given string a valid date?
|
|
21
|
+
#
|
|
22
|
+
# @param date_string [::String]
|
|
23
|
+
def self.valid_date?(date_string)
|
|
24
|
+
new(date_string)
|
|
25
|
+
true
|
|
26
|
+
rescue ::Date::Error
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(value: ::String).void }
|
|
31
|
+
# @param value [::String] The user-inputted date value
|
|
32
|
+
def initialize(value)
|
|
33
|
+
super(::CSVPlusPlus::Entities::Type::Date)
|
|
34
|
+
|
|
35
|
+
parsed =
|
|
36
|
+
begin
|
|
37
|
+
::Date.parse(value)
|
|
38
|
+
rescue ::Date::Error
|
|
39
|
+
::Date.strptime(value, '%d/%m/%yyyy')
|
|
40
|
+
end
|
|
41
|
+
@value = ::T.let(parsed, ::Date)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { override.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
|
|
45
|
+
# @param _runtime [Runtime]
|
|
46
|
+
#
|
|
47
|
+
# @return [::String]
|
|
48
|
+
def evaluate(_runtime)
|
|
49
|
+
@value.strftime('%m/%d/%y')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
|
|
53
|
+
# @param other [Entity]
|
|
54
|
+
#
|
|
55
|
+
# @return [T::Boolean]
|
|
56
|
+
def ==(other)
|
|
57
|
+
return false unless super
|
|
58
|
+
|
|
59
|
+
other.is_a?(self.class) && other.value == @value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Entities
|
|
6
|
+
# A basic building block of the abstract syntax tree (AST)
|
|
7
|
+
#
|
|
8
|
+
# @attr_reader id [Symbol] The identifier of the entity. For functions this is the function name,
|
|
9
|
+
# for variables it's the variable name
|
|
10
|
+
# @attr_reader type [Entities::Type] The type of the entity. Each type should have a corresponding class definition
|
|
11
|
+
# in CSVPlusPlus::Entities
|
|
12
|
+
class Entity
|
|
13
|
+
extend ::T::Sig
|
|
14
|
+
extend ::T::Helpers
|
|
15
|
+
|
|
16
|
+
abstract!
|
|
17
|
+
|
|
18
|
+
sig { returns(::T.nilable(::Symbol)) }
|
|
19
|
+
attr_reader :id
|
|
20
|
+
|
|
21
|
+
sig { returns(::CSVPlusPlus::Entities::Type) }
|
|
22
|
+
attr_reader :type
|
|
23
|
+
|
|
24
|
+
sig { params(type: ::CSVPlusPlus::Entities::Type, id: ::T.nilable(::Symbol)).void }
|
|
25
|
+
# @param type [Entities::Type]
|
|
26
|
+
# @param id [Symbol, nil]
|
|
27
|
+
def initialize(type, id: nil)
|
|
28
|
+
@type = type
|
|
29
|
+
@id = ::T.let(id&.downcase&.to_sym || nil, ::T.nilable(::Symbol))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { overridable.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
|
|
33
|
+
# Each class should define it's own version of #==
|
|
34
|
+
# @param other [Entity]
|
|
35
|
+
#
|
|
36
|
+
# @return [boolean]
|
|
37
|
+
def ==(other)
|
|
38
|
+
self.class == other.class && @type == other.type && @id == other.id
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { abstract.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
|
|
42
|
+
# Uses the given +runtime+ to evaluate itself in the current context
|
|
43
|
+
#
|
|
44
|
+
# @param _runtime [Runtime] The current runtime
|
|
45
|
+
#
|
|
46
|
+
# @return [::String]
|
|
47
|
+
def evaluate(_runtime); end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|