csv_plus_plus 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +18 -62
- data/lib/csv_plus_plus/benchmarked_compiler.rb +62 -0
- data/lib/csv_plus_plus/{code_section.rb → can_define_references.rb} +22 -35
- data/lib/csv_plus_plus/can_resolve_references.rb +8 -0
- data/lib/csv_plus_plus/cell.rb +3 -3
- data/lib/csv_plus_plus/cli.rb +24 -7
- data/lib/csv_plus_plus/color.rb +12 -6
- data/lib/csv_plus_plus/compiler.rb +156 -0
- data/lib/csv_plus_plus/data_validation.rb +138 -0
- data/lib/csv_plus_plus/{language → entities}/ast_builder.rb +3 -5
- data/lib/csv_plus_plus/entities/boolean.rb +31 -0
- data/lib/csv_plus_plus/{language → entities}/builtins.rb +2 -4
- data/lib/csv_plus_plus/entities/cell_reference.rb +60 -0
- data/lib/csv_plus_plus/entities/date.rb +30 -0
- data/lib/csv_plus_plus/entities/entity.rb +84 -0
- data/lib/csv_plus_plus/entities/function.rb +33 -0
- data/lib/csv_plus_plus/entities/function_call.rb +35 -0
- data/lib/csv_plus_plus/entities/number.rb +34 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +26 -0
- data/lib/csv_plus_plus/entities/string.rb +29 -0
- data/lib/csv_plus_plus/entities/variable.rb +25 -0
- data/lib/csv_plus_plus/entities.rb +33 -0
- data/lib/csv_plus_plus/error/error.rb +10 -0
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +36 -0
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +27 -0
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +49 -0
- data/lib/csv_plus_plus/{language → error}/syntax_error.rb +6 -14
- data/lib/csv_plus_plus/error/writer_error.rb +9 -0
- data/lib/csv_plus_plus/error.rb +9 -2
- data/lib/csv_plus_plus/expand.rb +3 -1
- data/lib/csv_plus_plus/google_api_client.rb +4 -0
- data/lib/csv_plus_plus/lexer/lexer.rb +13 -6
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +17 -0
- data/lib/csv_plus_plus/modifier.rb +73 -65
- data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +20 -20
- data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +83 -87
- data/lib/csv_plus_plus/parser/modifier.tab.rb +484 -0
- data/lib/csv_plus_plus/references.rb +68 -0
- data/lib/csv_plus_plus/row.rb +0 -3
- data/lib/csv_plus_plus/runtime.rb +199 -0
- data/lib/csv_plus_plus/scope.rb +196 -0
- data/lib/csv_plus_plus/template.rb +10 -10
- data/lib/csv_plus_plus/validated_modifier.rb +164 -0
- data/lib/csv_plus_plus/version.rb +1 -1
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +6 -4
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +24 -29
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +33 -12
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +3 -6
- data/lib/csv_plus_plus.rb +19 -10
- metadata +34 -24
- data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
- 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/modifier.tab.rb +0 -907
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
# A validation on a cell value. Used to support the `validate=` modifier directive. This is mostly based on the
|
5
|
+
# Google Sheets API spec which can be seen here:
|
6
|
+
#
|
7
|
+
# {https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType}
|
8
|
+
#
|
9
|
+
# @attr_reader arguments [Array<::String>] The parsed arguments as required by the condition.
|
10
|
+
# @attr_reader condition [Symbol] The condition (:blank, :text_eq, :date_before, etc.)
|
11
|
+
# @attr_reader invalid_reason [::String, nil] If set, the reason why this modifier is not valid.
|
12
|
+
class DataValidation
|
13
|
+
attr_reader :arguments, :condition, :invalid_reason
|
14
|
+
|
15
|
+
# @param value [::String] The value to parse as a data validation
|
16
|
+
def initialize(value)
|
17
|
+
condition, args = unquote(value).split(/\s*:\s*/)
|
18
|
+
@arguments = unquote(args || '').split(/\s+/)
|
19
|
+
@condition = condition.to_sym
|
20
|
+
|
21
|
+
validate!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Each data validation represented by (+condition+) has their own require
|
25
|
+
# @return [boolean]
|
26
|
+
def valid?
|
27
|
+
@invalid_reason.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def unquote(str)
|
33
|
+
# TODO: I'm pretty sure this isn't sufficient and we need to deal with the backslashes
|
34
|
+
str.gsub(/^['\s]*|['\s]*$/, '')
|
35
|
+
end
|
36
|
+
|
37
|
+
def invalid!(reason)
|
38
|
+
@invalid_reason = reason
|
39
|
+
end
|
40
|
+
|
41
|
+
def a_number(arg)
|
42
|
+
Float(arg)
|
43
|
+
rescue ::ArgumentError
|
44
|
+
invalid!("Requires a number but given: #{arg}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def a1_notation(arg)
|
48
|
+
return arg if ::CSVPlusPlus::Entities::CellReference.valid_cell_reference?(arg)
|
49
|
+
end
|
50
|
+
|
51
|
+
def a_date(arg, allow_relative_date: false)
|
52
|
+
return arg if ::CSVPlusPlus::Entities::Date.valid_date?(arg)
|
53
|
+
|
54
|
+
if allow_relative_date
|
55
|
+
a_relative_date(arg)
|
56
|
+
else
|
57
|
+
invalid!("Requires a date but given: #{arg}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def a_relative_date(arg)
|
62
|
+
return arg if %w[past_month past_week past_year yesterday today tomorrow].include?(arg.downcase)
|
63
|
+
|
64
|
+
invalid!('Requires a relative date: past_month, past_week, past_year, yesterday, today or tomorrow')
|
65
|
+
end
|
66
|
+
|
67
|
+
def no_args
|
68
|
+
return if @arguments.empty?
|
69
|
+
|
70
|
+
invalid!("Requires no arguments but #{@arguments.length} given: #{@arguments}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def one_arg
|
74
|
+
return @arguments[0] if @arguments.length == 1
|
75
|
+
|
76
|
+
invalid!("Requires only one argument but #{@arguments.length} given: #{@arguments}")
|
77
|
+
end
|
78
|
+
|
79
|
+
def one_arg_or_more
|
80
|
+
return @arguments if @arguments.length.positive?
|
81
|
+
|
82
|
+
invalid!("Requires at least one argument but #{@arguments.length} given: #{@arguments}")
|
83
|
+
end
|
84
|
+
|
85
|
+
def two_dates
|
86
|
+
return @arguments if @arguments.length == 2 && a_date(@arguments[0]) && a_date(@arguments[1])
|
87
|
+
|
88
|
+
invalid!("Requires exactly two dates but given: #{@arguments}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def two_numbers
|
92
|
+
return @arguments if @arguments.length == 2 && a_number(@arguments[0]) && a_number(@arguments[1])
|
93
|
+
|
94
|
+
invalid!("Requires exactly two numbers but given: #{@arguments}")
|
95
|
+
end
|
96
|
+
|
97
|
+
# validate_boolean is a weird one because it can have 0, 1 or 2 @arguments - all of them must be (true | false)
|
98
|
+
def validate_boolean
|
99
|
+
return @arguments if @arguments.empty?
|
100
|
+
|
101
|
+
converted_args = @arguments.map(&:strip).map(&:downcase)
|
102
|
+
return @arguments if [1, 2].include?(@arguments.length) && converted_args.all? do |arg|
|
103
|
+
%w[true false].include?(arg)
|
104
|
+
end
|
105
|
+
|
106
|
+
invalid!("Requires 0, 1 or 2 arguments and they all must be either 'true' or 'false'. Received: #{arguments}")
|
107
|
+
end
|
108
|
+
|
109
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
110
|
+
def validate!
|
111
|
+
case condition.to_sym
|
112
|
+
when :blank, :date_is_valid, :not_blank, :text_is_email, :text_is_url
|
113
|
+
no_args
|
114
|
+
when :text_contains, :text_ends_with, :text_eq, :text_not_contains, :text_starts_with
|
115
|
+
one_arg
|
116
|
+
when :date_after, :date_before, :date_on_or_after, :date_on_or_before
|
117
|
+
a_date(one_arg, allow_relative_date: true)
|
118
|
+
when :date_eq, :date_not_eq
|
119
|
+
a_date(one_arg)
|
120
|
+
when :date_between, :date_not_between
|
121
|
+
two_dates
|
122
|
+
when :one_of_range
|
123
|
+
a1_notation(one_arg)
|
124
|
+
when :custom_formula, :one_of_list, :text_not_eq
|
125
|
+
one_arg_or_more
|
126
|
+
when :number_eq, :number_greater, :number_greater_than_eq, :number_less, :number_less_than_eq, :number_not_eq
|
127
|
+
a_number(one_arg)
|
128
|
+
when :number_between, :number_not_between
|
129
|
+
two_numbers
|
130
|
+
when :boolean
|
131
|
+
validate_boolean
|
132
|
+
else
|
133
|
+
invalid!('Not a recognized data validation directive')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
137
|
+
end
|
138
|
+
end
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative './entities'
|
4
|
-
|
5
3
|
module CSVPlusPlus
|
6
|
-
module
|
4
|
+
module Entities
|
7
5
|
# Some helpful functions that can be mixed into a class to help building ASTs
|
8
6
|
module ASTBuilder
|
9
7
|
# Let the current class have functions which can build a given entity by calling it's type. For example
|
@@ -14,7 +12,7 @@ module CSVPlusPlus
|
|
14
12
|
#
|
15
13
|
# @return [Entity, #super]
|
16
14
|
def method_missing(method_name, *args, **kwargs, &)
|
17
|
-
entity_class = ::CSVPlusPlus::
|
15
|
+
entity_class = ::CSVPlusPlus::Entities::TYPES[method_name.to_sym]
|
18
16
|
return super unless entity_class
|
19
17
|
|
20
18
|
entity_class.new(*args, **kwargs, &)
|
@@ -28,7 +26,7 @@ module CSVPlusPlus
|
|
28
26
|
#
|
29
27
|
# @return [Boolean, #super]
|
30
28
|
def respond_to_missing?(method_name, *_arguments)
|
31
|
-
::CSVPlusPlus::
|
29
|
+
::CSVPlusPlus::Entities::TYPES.include?(method_name.to_sym) || super
|
32
30
|
end
|
33
31
|
|
34
32
|
# Turns index-based/X,Y coordinates into a A1 format
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './entity'
|
4
|
+
|
5
|
+
module CSVPlusPlus
|
6
|
+
module Entities
|
7
|
+
# A boolean value
|
8
|
+
#
|
9
|
+
# @attr_reader value [true, false]
|
10
|
+
class Boolean < Entity
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
# @param value [String, Boolean]
|
14
|
+
def initialize(value)
|
15
|
+
super(:boolean)
|
16
|
+
# TODO: probably can do a lot better in general on type validation
|
17
|
+
@value = value.is_a?(::String) ? (value.downcase == 'true') : value
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def to_s
|
22
|
+
@value.to_s.upcase
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [boolean]
|
26
|
+
def ==(other)
|
27
|
+
super && value == other.value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,12 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative './ast_builder'
|
4
|
-
|
5
3
|
module CSVPlusPlus
|
6
|
-
module
|
4
|
+
module Entities
|
7
5
|
# Provides ASTs for builtin functions and variables
|
8
6
|
module Builtins
|
9
|
-
extend ::CSVPlusPlus::
|
7
|
+
extend ::CSVPlusPlus::Entities::ASTBuilder
|
10
8
|
|
11
9
|
VARIABLES = {
|
12
10
|
# The number (integer) of the current cell. Starts at 1
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './ast_builder'
|
4
|
+
require_relative './entity'
|
5
|
+
|
6
|
+
module CSVPlusPlus
|
7
|
+
module Entities
|
8
|
+
# A reference to a cell
|
9
|
+
#
|
10
|
+
# @attr_reader cell_reference [String] The cell reference in A1 format
|
11
|
+
class CellReference < Entity
|
12
|
+
attr_reader :cell_reference
|
13
|
+
|
14
|
+
A1_NOTATION_REGEXP = /(['\w]+!)?\w+:\w+/
|
15
|
+
public_constant :A1_NOTATION_REGEXP
|
16
|
+
|
17
|
+
# Create a +CellReference+ to the given indexes
|
18
|
+
#
|
19
|
+
# @param cell_index [Integer] The current cell index
|
20
|
+
# @param row_index [Integer] The current row index
|
21
|
+
#
|
22
|
+
# @return [CellReference]
|
23
|
+
def self.from_index(cell_index:, row_index:)
|
24
|
+
return unless row_index || cell_index
|
25
|
+
|
26
|
+
# I can't just extend this class due to circular references :(
|
27
|
+
::Class.new.extend(::CSVPlusPlus::Entities::ASTBuilder).ref(cell_index:, row_index:)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Does the given +cell_reference_string+ conform to a valid cell reference?
|
31
|
+
#
|
32
|
+
# {https://developers.google.com/sheets/api/guides/concepts}
|
33
|
+
#
|
34
|
+
# @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
|
35
|
+
# A1 notation but maybe can support R1C1)
|
36
|
+
#
|
37
|
+
# @return [boolean]
|
38
|
+
def self.valid_cell_reference?(cell_reference_string)
|
39
|
+
!(cell_reference_string =~ ::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP).nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param cell_reference [String] The cell reference in A1 format
|
43
|
+
def initialize(cell_reference)
|
44
|
+
super(:cell_reference)
|
45
|
+
|
46
|
+
@cell_reference = cell_reference
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [::String]
|
50
|
+
def to_s
|
51
|
+
@cell_reference
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Boolean]
|
55
|
+
def ==(other)
|
56
|
+
super && @cell_reference == other.cell_reference
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Entities
|
5
|
+
# A date value
|
6
|
+
#
|
7
|
+
# @attr_reader value [Date] The parsed date
|
8
|
+
class Date < Entity
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
# TODO: support time?
|
12
|
+
DATE_STRING_REGEXP = %r{^\d{1,2}[/-]\d{1,2}[/-]\d{1,4}?$}
|
13
|
+
public_constant :DATE_STRING_REGEXP
|
14
|
+
|
15
|
+
# Is the given string a valid date?
|
16
|
+
#
|
17
|
+
# @param date_string [::String]
|
18
|
+
def self.valid_date?(date_string)
|
19
|
+
!(date_string.strip =~ ::CSVPlusPlus::Entities::Date::DATE_STRING_REGEXP).nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param value [String] The user-inputted date value
|
23
|
+
def initialize(value)
|
24
|
+
super(:date)
|
25
|
+
|
26
|
+
@value = ::Date.parse(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../entities'
|
4
|
+
|
5
|
+
module CSVPlusPlus
|
6
|
+
module Entities
|
7
|
+
# A basic building block of the abstract syntax tree (AST)
|
8
|
+
#
|
9
|
+
# @attr_reader id [Symbol] The identifier of the entity. For functions this is the function name,
|
10
|
+
# for variables it's the variable name
|
11
|
+
# @attr_reader type [Symbol] The type of the entity. Valid values are defined in +::CSVPlusPlus::Entities::TYPES+
|
12
|
+
class Entity
|
13
|
+
attr_reader :id, :type
|
14
|
+
|
15
|
+
# @param type [::String, Symbol]
|
16
|
+
# @param id [::String, nil]
|
17
|
+
def initialize(type, id: nil)
|
18
|
+
@type = type.to_sym
|
19
|
+
@id = id.downcase.to_sym if id
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [boolean]
|
23
|
+
def ==(other)
|
24
|
+
self.class == other.class && @type == other.type && @id == other.id
|
25
|
+
end
|
26
|
+
|
27
|
+
# Respond to predicates that correspond to types like #boolean?, #string?, etc
|
28
|
+
#
|
29
|
+
# @param method_name [Symbol] The +method_name+ to respond to
|
30
|
+
def method_missing(method_name, *_arguments)
|
31
|
+
if method_name =~ /^(\w+)\?$/
|
32
|
+
t = ::Regexp.last_match(1)
|
33
|
+
a_type?(t) && @type == t.to_sym
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Respond to predicates by type (entity.boolean?, entity.string?, etc)
|
40
|
+
#
|
41
|
+
# @param method_name [Symbol] The +method_name+ to respond to
|
42
|
+
#
|
43
|
+
# @return [boolean]
|
44
|
+
def respond_to_missing?(method_name, *_arguments)
|
45
|
+
(method_name =~ /^(\w+)\?$/ && a_type?(::Regexp.last_match(1))) || super
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def a_type?(str)
|
51
|
+
::CSVPlusPlus::Entities::TYPES.include?(str.to_sym)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# An entity that can take other entities as arguments. Current use cases for this
|
56
|
+
# are function calls and function definitions
|
57
|
+
#
|
58
|
+
# @attr_reader arguments [Array<Entity>] The arguments supplied to this entity.
|
59
|
+
class EntityWithArguments < Entity
|
60
|
+
attr_reader :arguments
|
61
|
+
|
62
|
+
# @param type [::String, Symbol]
|
63
|
+
# @param id [::String]
|
64
|
+
# @param arguments [Array<Entity>]
|
65
|
+
def initialize(type, id: nil, arguments: [])
|
66
|
+
super(type, id:)
|
67
|
+
@arguments = arguments
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [boolean]
|
71
|
+
def ==(other)
|
72
|
+
super && @arguments == other.arguments
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
attr_writer :arguments
|
78
|
+
|
79
|
+
def arguments_to_s
|
80
|
+
@arguments.join(', ')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './entity'
|
4
|
+
|
5
|
+
module CSVPlusPlus
|
6
|
+
module Entities
|
7
|
+
# A function definition
|
8
|
+
#
|
9
|
+
# @attr_reader body [Entity] The body of the function. +body+ can contain variable references
|
10
|
+
# from +@arguments+
|
11
|
+
class Function < EntityWithArguments
|
12
|
+
attr_reader :body
|
13
|
+
|
14
|
+
# @param id [Symbool, String] the name of the function - what it will be callable by
|
15
|
+
# @param arguments [Array<Symbol>]
|
16
|
+
# @param body [Entity]
|
17
|
+
def initialize(id, arguments, body)
|
18
|
+
super(:function, id:, arguments: arguments.map(&:to_sym))
|
19
|
+
@body = body
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
def to_s
|
24
|
+
"def #{@id.to_s.upcase}(#{arguments_to_s}) #{@body}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [boolean]
|
28
|
+
def ==(other)
|
29
|
+
super && @body == other.body
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Entities
|
5
|
+
# A function call
|
6
|
+
#
|
7
|
+
# @attr_reader infix [boolean] Whether or not this function call is infix (X * Y, A + B, etc)
|
8
|
+
class FunctionCall < EntityWithArguments
|
9
|
+
attr_reader :infix
|
10
|
+
|
11
|
+
# @param id [String] The name of the function
|
12
|
+
# @param arguments [Array<Entity>] The arguments to the function
|
13
|
+
# @param infix [boolean] Whether the function is infix
|
14
|
+
def initialize(id, arguments, infix: false)
|
15
|
+
super(:function_call, id:, arguments:)
|
16
|
+
|
17
|
+
@infix = infix
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def to_s
|
22
|
+
if @infix
|
23
|
+
"(#{arguments.join(" #{@id} ")})"
|
24
|
+
else
|
25
|
+
"#{@id.to_s.upcase}(#{arguments_to_s})"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [boolean]
|
30
|
+
def ==(other)
|
31
|
+
super && @id == other.id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Entities
|
5
|
+
# A number value
|
6
|
+
#
|
7
|
+
# @attr_reader value [Numeric] The parsed number value
|
8
|
+
class Number < Entity
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
# @param value [String, Numeric] Either a +String+ that looks like a number, or an already parsed Numeric
|
12
|
+
def initialize(value)
|
13
|
+
super(:number)
|
14
|
+
|
15
|
+
@value =
|
16
|
+
if value.instance_of?(::String)
|
17
|
+
value.include?('.') ? Float(value) : Integer(value, 10)
|
18
|
+
else
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [String]
|
24
|
+
def to_s
|
25
|
+
@value.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [boolean]
|
29
|
+
def ==(other)
|
30
|
+
super && value == other.value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Entities
|
5
|
+
# A runtime value. These are values which can be materialized at any point via the +resolve_fn+
|
6
|
+
# which takes an ExecutionContext as a param
|
7
|
+
#
|
8
|
+
# @attr_reader resolve_fn [lambda] A lambda that is called when the runtime value is resolved
|
9
|
+
class RuntimeValue < Entity
|
10
|
+
attr_reader :arguments, :resolve_fn
|
11
|
+
|
12
|
+
# @param resolve_fn [lambda] A lambda that is called when the runtime value is resolved
|
13
|
+
def initialize(resolve_fn, arguments: nil)
|
14
|
+
super(:runtime_value)
|
15
|
+
|
16
|
+
@arguments = arguments
|
17
|
+
@resolve_fn = resolve_fn
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def to_s
|
22
|
+
'(runtime_value)'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Entities
|
5
|
+
# A string value
|
6
|
+
#
|
7
|
+
# @attr_reader value [String]
|
8
|
+
class String < Entity
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
# @param value [String] The string that has been parsed out of the template
|
12
|
+
def initialize(value)
|
13
|
+
super(:string)
|
14
|
+
|
15
|
+
@value = value.gsub(/^"|"$/, '')
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
def to_s
|
20
|
+
"\"#{@value}\""
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [boolean]
|
24
|
+
def ==(other)
|
25
|
+
super && value == other.value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Entities
|
5
|
+
# TODO: get rid of this I think - everything will just be References
|
6
|
+
#
|
7
|
+
# A reference to a variable
|
8
|
+
class Variable < Entity
|
9
|
+
# @param id [Symbol] The identifier of the variable
|
10
|
+
def initialize(id)
|
11
|
+
super(:variable, id:)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
def to_s
|
16
|
+
"$$#{@id}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [boolean]
|
20
|
+
def ==(other)
|
21
|
+
super && id == other.id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'entities/boolean'
|
4
|
+
require_relative 'entities/cell_reference'
|
5
|
+
require_relative 'entities/date'
|
6
|
+
require_relative 'entities/entity'
|
7
|
+
require_relative 'entities/function'
|
8
|
+
require_relative 'entities/function_call'
|
9
|
+
require_relative 'entities/number'
|
10
|
+
require_relative 'entities/runtime_value'
|
11
|
+
require_relative 'entities/string'
|
12
|
+
require_relative 'entities/variable'
|
13
|
+
|
14
|
+
module CSVPlusPlus
|
15
|
+
module Entities
|
16
|
+
TYPES = {
|
17
|
+
boolean: ::CSVPlusPlus::Entities::Boolean,
|
18
|
+
cell_reference: ::CSVPlusPlus::Entities::CellReference,
|
19
|
+
date: ::CSVPlusPlus::Entities::Date,
|
20
|
+
function: ::CSVPlusPlus::Entities::Function,
|
21
|
+
function_call: ::CSVPlusPlus::Entities::FunctionCall,
|
22
|
+
number: ::CSVPlusPlus::Entities::Number,
|
23
|
+
runtime_value: ::CSVPlusPlus::Entities::RuntimeValue,
|
24
|
+
string: ::CSVPlusPlus::Entities::String,
|
25
|
+
variable: ::CSVPlusPlus::Entities::Variable
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
public_constant :TYPES
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
require_relative 'entities/ast_builder'
|
33
|
+
require_relative 'entities/builtins'
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Error
|
5
|
+
# An error thrown by our code (generally to be handled at the top level bin/ command)
|
6
|
+
class Error < StandardError
|
7
|
+
# TODO: perhaps give this a better name? something more descriptive than just Error
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './syntax_error'
|
4
|
+
|
5
|
+
module CSVPlusPlus
|
6
|
+
module Error
|
7
|
+
# An error that can be thrown when there is an error parsing a modifier
|
8
|
+
#
|
9
|
+
# @attr_reader message [::String] A helpful error message
|
10
|
+
# @attr_reader bad_input [String] The offending input that caused the error to be thrown
|
11
|
+
class FormulaSyntaxError < ::CSVPlusPlus::Error::SyntaxError
|
12
|
+
attr_reader :message, :bad_input
|
13
|
+
|
14
|
+
# You must supply either a +choices+ or +message+
|
15
|
+
#
|
16
|
+
# @param message [String] A relevant message to show
|
17
|
+
# @param bad_input [String] The offending input that caused the error to be thrown
|
18
|
+
# @param runtime [Runtime] The current runtime
|
19
|
+
# @param wrapped_error [StandardError] The underlying error that caused the syntax error. For example a
|
20
|
+
# Racc::ParseError that was thrown
|
21
|
+
def initialize(message, bad_input, runtime, wrapped_error: nil)
|
22
|
+
@bad_input = bad_input
|
23
|
+
@message = message
|
24
|
+
|
25
|
+
super(runtime, wrapped_error:)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a relevant error message given +@bad_input+ and +@message+.
|
29
|
+
#
|
30
|
+
# @return [::String]
|
31
|
+
def error_message
|
32
|
+
"#{@message}: \"#{@bad_input}\""
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './syntax_error'
|
4
|
+
|
5
|
+
module CSVPlusPlus
|
6
|
+
module Error
|
7
|
+
# An Error that wraps a +ModifierValidationError+ with a +Runtime+.
|
8
|
+
class ModifierSyntaxError < ::CSVPlusPlus::Error::SyntaxError
|
9
|
+
# You must supply either a +choices+ or +message+
|
10
|
+
#
|
11
|
+
# @param runtime [Runtime] The current runtime
|
12
|
+
# @param wrapped_error [ModifierValidationError] The validtion error that this is wrapping
|
13
|
+
def initialize(runtime, wrapped_error:)
|
14
|
+
@wrapped_error = wrapped_error
|
15
|
+
|
16
|
+
super(runtime, wrapped_error:)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calls +wrapped_error.error_message+.
|
20
|
+
#
|
21
|
+
# @return [::String]
|
22
|
+
def error_message
|
23
|
+
@wrapped_error.error_message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|