csv_plus_plus 0.1.1 → 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 +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
|