csv_plus_plus 0.1.0 → 0.1.2
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/CHANGELOG.md +16 -1
- data/README.md +18 -62
- data/lib/csv_plus_plus/benchmarked_compiler.rb +62 -0
- data/lib/csv_plus_plus/can_define_references.rb +88 -0
- 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 +5 -7
- 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 +19 -11
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +17 -0
- data/lib/csv_plus_plus/modifier.rb +73 -70
- data/lib/csv_plus_plus/options.rb +3 -0
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +305 -0
- data/lib/csv_plus_plus/parser/code_section.tab.rb +410 -0
- 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 +21 -5
- 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 +41 -16
- metadata +34 -24
- data/lib/csv_plus_plus/code_section.rb +0 -68
- data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
- data/lib/csv_plus_plus/language/cell_value.tab.rb +0 -332
- data/lib/csv_plus_plus/language/code_section.tab.rb +0 -442
- data/lib/csv_plus_plus/language/compiler.rb +0 -157
- 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 -26
- 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 -188
- 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
|
@@ -13,11 +11,11 @@ module CSVPlusPlus
|
|
13
11
|
# @param arguments [] The arguments to create the entity with
|
14
12
|
#
|
15
13
|
# @return [Entity, #super]
|
16
|
-
def method_missing(method_name, *
|
17
|
-
entity_class = ::CSVPlusPlus::
|
14
|
+
def method_missing(method_name, *args, **kwargs, &)
|
15
|
+
entity_class = ::CSVPlusPlus::Entities::TYPES[method_name.to_sym]
|
18
16
|
return super unless entity_class
|
19
17
|
|
20
|
-
entity_class.new(*
|
18
|
+
entity_class.new(*args, **kwargs, &)
|
21
19
|
end
|
22
20
|
|
23
21
|
# Let the current class have functions which can build a given entity by calling it's type. For example
|
@@ -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
|