csv_plus_plus 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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