csv_plus_plus 0.1.2 → 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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +23 -13
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +32 -7
  9. data/lib/csv_plus_plus/compiler.rb +82 -60
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
  11. data/lib/csv_plus_plus/entities/boolean.rb +18 -9
  12. data/lib/csv_plus_plus/entities/builtins.rb +23 -9
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
  14. data/lib/csv_plus_plus/entities/date.rb +38 -5
  15. data/lib/csv_plus_plus/entities/entity.rb +27 -61
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +23 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +24 -9
  19. data/lib/csv_plus_plus/entities/number.rb +24 -10
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
  21. data/lib/csv_plus_plus/entities/string.rb +19 -6
  22. data/lib/csv_plus_plus/entities/variable.rb +16 -4
  23. data/lib/csv_plus_plus/entities.rb +20 -13
  24. data/lib/csv_plus_plus/error/error.rb +11 -1
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
  28. data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
  29. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  30. data/lib/csv_plus_plus/error.rb +1 -0
  31. data/lib/csv_plus_plus/google_api_client.rb +7 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -158
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
  46. data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
  48. data/lib/csv_plus_plus/row.rb +53 -12
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +34 -191
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +62 -35
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +29 -12
  69. metadata +18 -14
  70. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  71. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  72. data/lib/csv_plus_plus/data_validation.rb +0 -138
  73. data/lib/csv_plus_plus/expand.rb +0 -20
  74. data/lib/csv_plus_plus/graph.rb +0 -62
  75. data/lib/csv_plus_plus/references.rb +0 -68
  76. data/lib/csv_plus_plus/scope.rb +0 -196
  77. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  78. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  79. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,32 +1,69 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './ast_builder'
4
- require_relative './entity'
5
-
6
4
  module CSVPlusPlus
7
5
  module Entities
8
- # A reference to a cell
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"
9
14
  #
10
- # @attr_reader cell_reference [String] The cell reference in A1 format
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
11
25
  class CellReference < Entity
12
- attr_reader :cell_reference
26
+ extend ::T::Sig
13
27
 
14
- A1_NOTATION_REGEXP = /(['\w]+!)?\w+:\w+/
15
- public_constant :A1_NOTATION_REGEXP
28
+ sig { returns(::T.nilable(::String)) }
29
+ attr_accessor :sheet_name
16
30
 
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
31
+ sig { returns(::T.nilable(::Integer)) }
32
+ attr_reader :cell_index
25
33
 
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
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
29
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) }
30
67
  # Does the given +cell_reference_string+ conform to a valid cell reference?
31
68
  #
32
69
  # {https://developers.google.com/sheets/api/guides/concepts}
@@ -34,27 +71,161 @@ module CSVPlusPlus
34
71
  # @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
35
72
  # A1 notation but maybe can support R1C1)
36
73
  #
37
- # @return [boolean]
74
+ # @return [::T::Boolean]
38
75
  def self.valid_cell_reference?(cell_reference_string)
39
76
  !(cell_reference_string =~ ::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP).nil?
40
77
  end
41
78
 
42
- # @param cell_reference [String] The cell reference in A1 format
43
- def initialize(cell_reference)
44
- super(:cell_reference)
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
45
110
 
46
- @cell_reference = cell_reference
111
+ @scoped_to_expand = scoped_to_expand
47
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
48
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
+ #
49
176
  # @return [::String]
50
- def to_s
51
- @cell_reference
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
52
189
  end
53
190
 
54
- # @return [Boolean]
55
- def ==(other)
56
- super && @cell_reference == other.cell_reference
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
57
227
  end
58
228
  end
229
+ # rubocop:enable Metrics/ClassLength
59
230
  end
60
231
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -6,24 +7,56 @@ module CSVPlusPlus
6
7
  #
7
8
  # @attr_reader value [Date] The parsed date
8
9
  class Date < Entity
10
+ extend ::T::Sig
11
+
12
+ sig { returns(::Date) }
9
13
  attr_reader :value
10
14
 
11
- # TODO: support time?
15
+ # TODO: support time granularity?
12
16
  DATE_STRING_REGEXP = %r{^\d{1,2}[/-]\d{1,2}[/-]\d{1,4}?$}
13
17
  public_constant :DATE_STRING_REGEXP
14
18
 
19
+ sig { params(date_string: ::String).returns(::T::Boolean) }
15
20
  # Is the given string a valid date?
16
21
  #
17
22
  # @param date_string [::String]
18
23
  def self.valid_date?(date_string)
19
- !(date_string.strip =~ ::CSVPlusPlus::Entities::Date::DATE_STRING_REGEXP).nil?
24
+ new(date_string)
25
+ true
26
+ rescue ::Date::Error
27
+ false
20
28
  end
21
29
 
22
- # @param value [String] The user-inputted date value
30
+ sig { params(value: ::String).void }
31
+ # @param value [::String] The user-inputted date value
23
32
  def initialize(value)
24
- super(:date)
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
25
58
 
26
- @value = ::Date.parse(value)
59
+ other.is_a?(self.class) && other.value == @value
27
60
  end
28
61
  end
29
62
  end
@@ -1,84 +1,50 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative '../entities'
4
-
5
4
  module CSVPlusPlus
6
5
  module Entities
7
6
  # A basic building block of the abstract syntax tree (AST)
8
7
  #
9
8
  # @attr_reader id [Symbol] The identifier of the entity. For functions this is the function name,
10
9
  # 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+
10
+ # @attr_reader type [Entities::Type] The type of the entity. Each type should have a corresponding class definition
11
+ # in CSVPlusPlus::Entities
12
12
  class Entity
13
- attr_reader :id, :type
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
14
23
 
15
- # @param type [::String, Symbol]
16
- # @param id [::String, nil]
24
+ sig { params(type: ::CSVPlusPlus::Entities::Type, id: ::T.nilable(::Symbol)).void }
25
+ # @param type [Entities::Type]
26
+ # @param id [Symbol, nil]
17
27
  def initialize(type, id: nil)
18
- @type = type.to_sym
19
- @id = id.downcase.to_sym if id
28
+ @type = type
29
+ @id = ::T.let(id&.downcase&.to_sym || nil, ::T.nilable(::Symbol))
20
30
  end
21
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
+ #
22
36
  # @return [boolean]
23
37
  def ==(other)
24
38
  self.class == other.class && @type == other.type && @id == other.id
25
39
  end
26
40
 
27
- # Respond to predicates that correspond to types like #boolean?, #string?, etc
41
+ sig { abstract.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
42
+ # Uses the given +runtime+ to evaluate itself in the current context
28
43
  #
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)
44
+ # @param _runtime [Runtime] The current runtime
40
45
  #
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
46
+ # @return [::String]
47
+ def evaluate(_runtime); end
82
48
  end
83
49
  end
84
50
  end
@@ -0,0 +1,57 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # An entity that can take other entities as arguments. Current use cases for this
7
+ # are function calls and function definitions
8
+ #
9
+ # @attr_reader arguments [Array<Entity>] The arguments supplied to this entity.
10
+ class EntityWithArguments < Entity
11
+ extend ::T::Sig
12
+
13
+ abstract!
14
+
15
+ sig { returns(::T::Array[::CSVPlusPlus::Entities::Entity]) }
16
+ attr_reader :arguments
17
+
18
+ sig do
19
+ params(
20
+ type: ::CSVPlusPlus::Entities::Type,
21
+ id: ::T.nilable(::Symbol),
22
+ arguments: ::T::Array[::CSVPlusPlus::Entities::Entity]
23
+ ).void
24
+ end
25
+ # @param type [Entities::Type]
26
+ # @param id [::String]
27
+ # @param arguments [Array<Entity>]
28
+ def initialize(type, id: nil, arguments: [])
29
+ super(type, id:)
30
+ @arguments = arguments
31
+ end
32
+
33
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
34
+ # @param other [Entity]
35
+ #
36
+ # @return [boolean]
37
+ def ==(other)
38
+ return false unless other.is_a?(self.class)
39
+
40
+ @arguments == other.arguments && super
41
+ end
42
+
43
+ protected
44
+
45
+ sig do
46
+ params(arguments: ::T::Array[::CSVPlusPlus::Entities::Entity])
47
+ .returns(::T::Array[::CSVPlusPlus::Entities::Entity])
48
+ end
49
+ attr_writer :arguments
50
+
51
+ sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Array[::String]) }
52
+ def evaluate_arguments(runtime)
53
+ @arguments.map { |arg| arg.evaluate(runtime) }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,32 +1,44 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './entity'
4
-
5
4
  module CSVPlusPlus
6
5
  module Entities
7
6
  # A function definition
8
7
  #
9
8
  # @attr_reader body [Entity] The body of the function. +body+ can contain variable references
10
- # from +@arguments+
9
+ # from +@arguments+
11
10
  class Function < EntityWithArguments
11
+ extend ::T::Sig
12
+
13
+ sig { returns(::CSVPlusPlus::Entities::Entity) }
12
14
  attr_reader :body
13
15
 
14
- # @param id [Symbool, String] the name of the function - what it will be callable by
16
+ sig { params(id: ::Symbol, arguments: ::T::Array[::Symbol], body: ::CSVPlusPlus::Entities::Entity).void }
17
+ # @param id [Symbol] the name of the function - what it will be callable by
15
18
  # @param arguments [Array<Symbol>]
16
19
  # @param body [Entity]
17
20
  def initialize(id, arguments, body)
18
- super(:function, id:, arguments: arguments.map(&:to_sym))
19
- @body = body
21
+ super(::CSVPlusPlus::Entities::Type::Function, id:, arguments: arguments.map(&:to_sym))
22
+
23
+ @body = ::T.let(body, ::CSVPlusPlus::Entities::Entity)
20
24
  end
21
25
 
22
- # @return [String]
23
- def to_s
24
- "def #{@id.to_s.upcase}(#{arguments_to_s}) #{@body}"
26
+ sig { override.params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
27
+ # @param runtime [Runtime]
28
+ #
29
+ # @return [::String]
30
+ def evaluate(runtime)
31
+ "def #{@id.to_s.upcase}(#{arguments.map(&:to_s).join(', ')}) #{@body.evaluate(runtime)}"
25
32
  end
26
33
 
27
- # @return [boolean]
34
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
35
+ # @param other [Entity]
36
+ #
37
+ # @return [::T::Boolean]
28
38
  def ==(other)
29
- super && @body == other.body
39
+ return false unless super
40
+
41
+ other.is_a?(self.class) && @body == other.body
30
42
  end
31
43
  end
32
44
  end
@@ -1,34 +1,49 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
5
  module Entities
5
- # A function call
6
+ # A function call that can be either infix (A + B) or prefix (ADD(A, B))
6
7
  #
7
8
  # @attr_reader infix [boolean] Whether or not this function call is infix (X * Y, A + B, etc)
8
9
  class FunctionCall < EntityWithArguments
10
+ extend ::T::Sig
11
+
12
+ sig { returns(::T::Boolean) }
9
13
  attr_reader :infix
10
14
 
11
- # @param id [String] The name of the function
15
+ sig { params(id: ::Symbol, arguments: ::T::Array[::CSVPlusPlus::Entities::Entity], infix: ::T::Boolean).void }
16
+ # @param id [::String] The name of the function
12
17
  # @param arguments [Array<Entity>] The arguments to the function
13
- # @param infix [boolean] Whether the function is infix
18
+ # @param infix [T::Boolean] Whether the function is infix
14
19
  def initialize(id, arguments, infix: false)
15
- super(:function_call, id:, arguments:)
20
+ super(::CSVPlusPlus::Entities::Type::FunctionCall, id:, arguments:)
16
21
 
17
22
  @infix = infix
18
23
  end
19
24
 
20
- # @return [String]
21
- def to_s
25
+ sig { override.params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
26
+ # @param runtime [Runtime]
27
+ #
28
+ # @return [::String]
29
+ def evaluate(runtime)
30
+ evaluated_arguments = evaluate_arguments(runtime)
31
+
22
32
  if @infix
23
- "(#{arguments.join(" #{@id} ")})"
33
+ "(#{evaluated_arguments.join(" #{@id} ")})"
24
34
  else
25
- "#{@id.to_s.upcase}(#{arguments_to_s})"
35
+ "#{@id.to_s.upcase}(#{evaluated_arguments.join(', ')})"
26
36
  end
27
37
  end
28
38
 
39
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
40
+ # @param other [Entity]
41
+ #
29
42
  # @return [boolean]
30
43
  def ==(other)
31
- super && @id == other.id
44
+ return false unless super
45
+
46
+ other.is_a?(self.class) && @id == other.id && @infix == other.infix
32
47
  end
33
48
  end
34
49
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -6,28 +7,41 @@ module CSVPlusPlus
6
7
  #
7
8
  # @attr_reader value [Numeric] The parsed number value
8
9
  class Number < Entity
10
+ sig { returns(::Numeric) }
9
11
  attr_reader :value
10
12
 
13
+ sig { params(value: ::T.any(::String, ::Numeric)).void }
11
14
  # @param value [String, Numeric] Either a +String+ that looks like a number, or an already parsed Numeric
12
15
  def initialize(value)
13
- super(:number)
16
+ super(::CSVPlusPlus::Entities::Type::Number)
14
17
 
15
18
  @value =
16
- if value.instance_of?(::String)
17
- value.include?('.') ? Float(value) : Integer(value, 10)
18
- else
19
- value
20
- end
19
+ ::T.let(
20
+ (if value.is_a?(::String)
21
+ value.include?('.') ? Float(value) : Integer(value, 10)
22
+ else
23
+ value
24
+ end),
25
+ ::Numeric
26
+ )
21
27
  end
22
28
 
23
- # @return [String]
24
- def to_s
29
+ sig { override.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
30
+ # @param _runtime [Runtime]
31
+ #
32
+ # @return [::String]
33
+ def evaluate(_runtime)
25
34
  @value.to_s
26
35
  end
27
36
 
28
- # @return [boolean]
37
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
38
+ # @param other [Entity]
39
+ #
40
+ # @return [::T::Boolean]
29
41
  def ==(other)
30
- super && value == other.value
42
+ return false unless super
43
+
44
+ other.is_a?(self.class) && @value == other.value
31
45
  end
32
46
  end
33
47
  end