feel 0.3.1 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edefdae3c70a4358387f8f37f1a046f4b4f385547b7182bb2a36bbbf98b6f03e
4
- data.tar.gz: a042d4d5cb4215a0adef33de6fe3e9d67d13be9bd49d749bdd7425061c60edb5
3
+ metadata.gz: 5e3c6cadc65effe99f563dcd6ceca765770c00205f0b5236795da8a59e18d39b
4
+ data.tar.gz: a3989725d4ea3f5d13319afd0576b332d7372bf17621c874985746fdffc09027
5
5
  SHA512:
6
- metadata.gz: 23e7dad7a59cf974865c29dac18c97ab12167a49e5b90295e731fff08f7dc0b7b0deef44f29ced81f873781636bc8f6f7d4c1af613d489fb2389d505bef4546a
7
- data.tar.gz: 5e287d05c9f463350f6fdc34771d51cf05a90e4f76e0932dc79ce7704cc8c19c1887f3456ee33da5dfcb9698cc03ce8ed7a368ec5c44f3fcbca3bc95ed1e8839
6
+ metadata.gz: 8591d4a8b94eb173eb1b05e3d739dfccc23eabf40cabd9e98c0e5216cefd59e83deb6080d8c62288f2170254c21b7ea27a91cd858defe6a8ac2e7de4fea49b7e
7
+ data.tar.gz: 68c18813491cce3e569e864afe54b674b869c2c7fa9b616deb59ca1e39698c5c0cad1f813ad57eadf06df11787ead485bd261396d38bf0ac9c0aeaa65bff4018
@@ -51,15 +51,91 @@ grammar FEEL
51
51
  # 4.d arithmetic negation ;
52
52
  #
53
53
  rule arithmetic_expression
54
- addition /
55
- subtraction /
56
- multiplication /
57
- division /
54
+ additive_operation /
55
+ multiplicative_operation /
58
56
  exponentiation /
59
57
  arithmetic_negation /
60
58
  bracketed_arithmetic_expression
61
59
  end
62
60
 
61
+ # Precedence hierarchy used within operator rules (low to high):
62
+ # additive > multiplicative > exponential > atom
63
+
64
+ rule additive_expression
65
+ additive_operation /
66
+ multiplicative_expression
67
+ end
68
+
69
+ rule multiplicative_expression
70
+ multiplicative_operation /
71
+ exponential_expression
72
+ end
73
+
74
+ #
75
+ # 21. addition = expression , "+" , expression ;
76
+ # 22. subtraction = expression , "-" , expression ;
77
+ #
78
+ rule additive_operation
79
+ head:multiplicative_expression tail:(__ op:("+" / "-") __ multiplicative_expression)+ {
80
+ def eval(context={})
81
+ result = head.eval(context)
82
+ tail.elements.each do |e|
83
+ operand = e.multiplicative_expression.eval(context)
84
+ if e.op.text_value == "+"
85
+ return nil if result.nil? || operand.nil?
86
+ result = result + operand
87
+ else
88
+ return nil if result.nil? || operand.nil?
89
+ result = result - operand
90
+ end
91
+ end
92
+ result
93
+ end
94
+ }
95
+ end
96
+
97
+ #
98
+ # 23. multiplication = expression , "\*" , expression ;
99
+ # 24. division = expression , "/" , expression ;
100
+ #
101
+ rule multiplicative_operation
102
+ head:exponential_expression tail:(__ op:("*" !"*" / "/") __ exponential_expression)+ {
103
+ def eval(context={})
104
+ result = head.eval(context)
105
+ tail.elements.each do |e|
106
+ operand = e.exponential_expression.eval(context)
107
+ if e.op.text_value.start_with?("*")
108
+ return nil if result.nil? || operand.nil?
109
+ result = result * operand
110
+ else
111
+ return nil if result.nil? || operand.nil?
112
+ result = result / operand
113
+ end
114
+ end
115
+ result
116
+ end
117
+ }
118
+ end
119
+
120
+ rule exponential_expression
121
+ exponentiation /
122
+ arithmetic_atom
123
+ end
124
+
125
+ rule arithmetic_atom
126
+ arithmetic_negation /
127
+ bracketed_additive_expression /
128
+ simple_value
129
+ end
130
+
131
+ rule bracketed_additive_expression
132
+ "(" __ additive_expression __ ")" {
133
+ def eval(context={})
134
+ additive_expression.eval(context)
135
+ end
136
+ }
137
+ end
138
+
63
139
  rule bracketed_arithmetic_expression
64
140
  "(" __ arithmetic_expression __ ")" {
65
141
  def eval(context={})
@@ -219,49 +295,16 @@ grammar FEEL
219
295
  head:name tail:(__ "." __ name)* <QualifiedName>
220
296
  end
221
297
 
222
- #
223
- # 21. addition = expression , "+" , expression ;
224
- #
225
- rule addition
226
- head:non_recursive_simple_expression_for_arithmetic_expression __ "+" __ tail:expression <Addition>
227
- end
228
-
229
- rule non_recursive_simple_expression_for_arithmetic_expression
230
- bracketed_arithmetic_expression /
231
- simple_value
232
- end
233
-
234
298
  rule non_recursive_simple_expression_for_comparison
235
299
  arithmetic_expression /
236
300
  simple_value
237
301
  end
238
302
 
239
- #
240
- # 22. subtraction = expression , "-" , expression ;
241
- #
242
- rule subtraction
243
- head:non_recursive_simple_expression_for_arithmetic_expression __ "-" __ tail:expression <Subtraction>
244
- end
245
-
246
- #
247
- # 23. multiplication = expression , "\*" , expression ;
248
- #
249
- rule multiplication
250
- head:non_recursive_simple_expression_for_arithmetic_expression __ "*" __ tail:expression <Multiplication>
251
- end
252
-
253
- #
254
- # 24. division = expression , "/" , expression ;
255
- #
256
- rule division
257
- head:non_recursive_simple_expression_for_arithmetic_expression __ "/" __ tail:expression <Division>
258
- end
259
-
260
303
  #
261
304
  # 25. exponentiation = expression, "\*\*", expression ;
262
305
  #
263
306
  rule exponentiation
264
- head:non_recursive_simple_expression_for_arithmetic_expression __ "**" __ tail:expression <Exponentiation>
307
+ head:arithmetic_atom __ "**" __ tail:exponential_expression <Exponentiation>
265
308
  end
266
309
 
267
310
  #
@@ -270,7 +313,9 @@ grammar FEEL
270
313
  rule arithmetic_negation
271
314
  "-" __ "(" __ expr:expression __ ")" {
272
315
  def eval(context={})
273
- -expr.eval(context)
316
+ val = expr.eval(context)
317
+ return nil if val.nil?
318
+ -val
274
319
  end
275
320
  }
276
321
  end
@@ -370,9 +415,14 @@ grammar FEEL
370
415
  numeric_literal /
371
416
  string_literal /
372
417
  boolean_literal /
418
+ at_literal /
373
419
  date_time_literal
374
420
  end
375
421
 
422
+ rule at_literal
423
+ "@" string_literal <AtLiteral>
424
+ end
425
+
376
426
  #
377
427
  # 35. string literal = '"' , { character - ('"' | vertical space) }, '"' ;
378
428
  #
@@ -453,7 +503,7 @@ grammar FEEL
453
503
  # 40. function invocation = expression , parameters ;
454
504
  #
455
505
  rule function_invocation
456
- fn_name:(!reserved_word qualified_name) __ "(" __ params:(positional_parameters)? __ ")" <FunctionInvocation>
506
+ fn_name:(!reserved_word qualified_name) __ "(" __ params:(positional_parameters)? __ ")" property:(__ "." __ name)? <FunctionInvocation>
457
507
  end
458
508
 
459
509
  #
@@ -623,7 +673,7 @@ grammar FEEL
623
673
  # 62. date time literal = ( "date" | "time" | "date and time" | "duration" ) , "(" , string literal , ")" ;
624
674
  #
625
675
  rule date_time_literal
626
- keyword:date_time_keyword __ "(" __ head:expression __ tail:("," __ expression)* __ ")" <DateTimeLiteral>
676
+ keyword:date_time_keyword __ "(" __ head:expression __ tail:("," __ expression)* __ ")" property:(__ "." __ name)? <DateTimeLiteral>
627
677
  end
628
678
 
629
679
  rule date_time_keyword
data/lib/feel/nodes.rb CHANGED
@@ -19,6 +19,51 @@ module FEEL
19
19
  qualified_names.to_a
20
20
  end
21
21
 
22
+ def access_property(result, property_name)
23
+ case result
24
+ when DateTime
25
+ case property_name
26
+ when "year" then result.year
27
+ when "month" then result.month
28
+ when "day" then result.day
29
+ when "weekday" then result.cwday
30
+ when "hour" then result.hour
31
+ when "minute" then result.min
32
+ when "second" then result.sec
33
+ end
34
+ when Date
35
+ case property_name
36
+ when "year" then result.year
37
+ when "month" then result.month
38
+ when "day" then result.day
39
+ when "weekday" then result.cwday
40
+ end
41
+ when Time
42
+ case property_name
43
+ when "hour" then result.hour
44
+ when "minute" then result.min
45
+ when "second" then result.sec
46
+ when "time offset" then result.utc_offset
47
+ when "timezone" then result.zone
48
+ end
49
+ when ActiveSupport::Duration
50
+ case property_name
51
+ when "years" then result.parts[:years] || 0
52
+ when "months" then result.parts[:months] || 0
53
+ when "days" then result.parts[:days] || 0
54
+ when "hours" then result.parts[:hours] || 0
55
+ when "minutes" then result.parts[:minutes] || 0
56
+ when "seconds" then result.parts[:seconds] || 0
57
+ end
58
+ when Hash
59
+ if result.key?(property_name.to_sym)
60
+ result[property_name.to_sym]
61
+ else
62
+ result[property_name]
63
+ end
64
+ end
65
+ end
66
+
22
67
  def raise_evaluation_error(missing_name, ctx = {})
23
68
  names = qualified_names_in_context(ctx)
24
69
  checker = DidYouMean::SpellChecker.new(dictionary: names)
@@ -84,13 +129,13 @@ module FEEL
84
129
 
85
130
  case operator
86
131
  when "<"
87
- ->(input) { input < endpoint }
132
+ ->(input) { input.nil? || endpoint.nil? ? nil : input < endpoint }
88
133
  when "<="
89
- ->(input) { input <= endpoint }
134
+ ->(input) { input.nil? || endpoint.nil? ? nil : input <= endpoint }
90
135
  when ">"
91
- ->(input) { input > endpoint }
136
+ ->(input) { input.nil? || endpoint.nil? ? nil : input > endpoint }
92
137
  when ">="
93
- ->(input) { input >= endpoint }
138
+ ->(input) { input.nil? || endpoint.nil? ? nil : input >= endpoint }
94
139
  else
95
140
  ->(input) { input == endpoint }
96
141
  end
@@ -112,16 +157,17 @@ module FEEL
112
157
  finish = end_token.text_value
113
158
  first_val = first.eval(context)
114
159
  second_val = second.eval(context)
160
+ return ->(_input) { nil } if first_val.nil? || second_val.nil?
115
161
 
116
162
  case [start, finish]
117
163
  when ["(", ")"]
118
- ->(input) { first_val < input && input < second_val }
164
+ ->(input) { input.nil? ? nil : first_val < input && input < second_val }
119
165
  when ["[", "]"]
120
- ->(input) { first_val <= input && input <= second_val }
166
+ ->(input) { input.nil? ? nil : first_val <= input && input <= second_val }
121
167
  when ["(", "]"]
122
- ->(input) { first_val < input && input <= second_val }
168
+ ->(input) { input.nil? ? nil : first_val < input && input <= second_val }
123
169
  when ["[", ")"]
124
- ->(input) { first_val <= input && input < second_val }
170
+ ->(input) { input.nil? ? nil : first_val <= input && input < second_val }
125
171
  end
126
172
  end
127
173
  end
@@ -232,11 +278,15 @@ module FEEL
232
278
  initial_value = context_get(context, head_name)
233
279
 
234
280
  # Process each segment in the tail, evaluating names to handle backticks
235
- tail.elements.inject(initial_value) do |hash, element|
236
- return nil unless hash
281
+ tail.elements.inject(initial_value) do |value, element|
282
+ return nil unless value
237
283
 
238
284
  key = element.name.eval(context)
239
- context_get(hash, key, root: context)
285
+ if value.respond_to?(:key?)
286
+ context_get(value, key, root: context)
287
+ else
288
+ access_property(value, key)
289
+ end
240
290
  end
241
291
  end
242
292
  end
@@ -258,50 +308,14 @@ module FEEL
258
308
  end
259
309
 
260
310
  #
261
- # 21. addition = expression , "+" , expression ;
311
+ # 25. exponentiation = expression, "\*\*", expression ;
262
312
  #
263
- class Addition < Node
313
+ class Exponentiation < Node
264
314
  def eval(context = {})
265
315
  head_val = head.eval(context)
266
316
  tail_val = tail.eval(context)
267
317
  return nil if head_val.nil? || tail_val.nil?
268
- head.eval(context) + tail.eval(context)
269
- end
270
- end
271
-
272
- #
273
- # 22. subtraction = expression , "-" , expression ;
274
- #
275
- class Subtraction < Node
276
- def eval(context = {})
277
- head.eval(context) - tail.eval(context)
278
- end
279
- end
280
-
281
- #
282
- # 23. multiplication = expression , "\*" , expression ;
283
- #
284
- class Multiplication < Node
285
- def eval(context = {})
286
- head.eval(context) * tail.eval(context)
287
- end
288
- end
289
-
290
- #
291
- # 24. division = expression , "/" , expression ;
292
- #
293
- class Division < Node
294
- def eval(context = {})
295
- head.eval(context) / tail.eval(context)
296
- end
297
- end
298
-
299
- #
300
- # 25. exponentiation = expression, "\*\*", expression ;
301
- #
302
- class Exponentiation < Node
303
- def eval(context = {})
304
- head.eval(context) ** tail.eval(context)
318
+ head_val ** tail_val
305
319
  end
306
320
  end
307
321
 
@@ -427,6 +441,23 @@ module FEEL
427
441
  end
428
442
  end
429
443
 
444
+ class AtLiteral < Node
445
+ def eval(_context = {})
446
+ value = string_literal.eval
447
+ return nil if value.nil?
448
+ case value
449
+ when /\AP/
450
+ ActiveSupport::Duration.parse(value)
451
+ when /\A\d{4}-\d{2}-\d{2}T/
452
+ DateTime.parse(value)
453
+ when /\A\d{4}-\d{2}-\d{2}\z/
454
+ Date.parse(value)
455
+ when /\A\d{2}:\d{2}/
456
+ Time.parse(value)
457
+ end
458
+ end
459
+ end
460
+
430
461
  #
431
462
  # 38. digit = [0-9] ;
432
463
  #
@@ -449,7 +480,11 @@ module FEEL
449
480
 
450
481
  args = params.present? ? params.eval(context) : []
451
482
 
452
- fn.call(*args)
483
+ result = fn.call(*args)
484
+ if defined?(property) && property.present?
485
+ result = access_property(result, property.name.eval(context))
486
+ end
487
+ result
453
488
  end
454
489
  end
455
490
 
@@ -549,13 +584,14 @@ module FEEL
549
584
  #
550
585
  class Comparison < Node
551
586
  def eval(context = {})
587
+ left_val = left.eval(context)
588
+ right_val = right.eval(context)
552
589
  case operator.text_value
553
- when "<" then left.eval(context) < right.eval(context)
554
- when "<=" then left.eval(context) <= right.eval(context)
555
- when ">=" then left.eval(context) >= right.eval(context)
556
- when ">" then left.eval(context) > right.eval(context)
557
- when "!=" then left.eval(context) != right.eval(context)
558
- when "=" then left.eval(context) == right.eval(context)
590
+ when "<", "<=", ">=", ">"
591
+ return nil if left_val.nil? || right_val.nil?
592
+ left_val.send(operator.text_value, right_val)
593
+ when "!=" then left_val != right_val
594
+ when "=" then left_val == right_val
559
595
  end
560
596
  end
561
597
  end
@@ -686,7 +722,7 @@ module FEEL
686
722
  class ContextEntryList < Node
687
723
  def eval(context = {})
688
724
  context_entries.inject({}) do |hash, entry|
689
- hash.merge(entry.eval(context))
725
+ hash.merge(entry.eval(context.merge(hash)))
690
726
  end
691
727
  end
692
728
 
@@ -717,7 +753,7 @@ module FEEL
717
753
  return nil if head_val.nil?
718
754
  return head_val if head_val.is_a?(ActiveSupport::Duration) || head_val.is_a?(DateTime) || head_val.is_a?(Date) || head_val.is_a?(Time)
719
755
 
720
- case keyword.text_value
756
+ result = case keyword.text_value
721
757
  when "date and time"
722
758
  DateTime.parse(head_val)
723
759
  when "date"
@@ -731,6 +767,11 @@ module FEEL
731
767
  ActiveSupport::Duration.parse(head_val)
732
768
  end
733
769
  end
770
+
771
+ if defined?(property) && property.present?
772
+ result = access_property(result, property.name.eval(context))
773
+ end
774
+ result
734
775
  end
735
776
 
736
777
  def duration_range(start_date, end_date)
data/lib/feel/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FEEL
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Connected Bits
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-06 00:00:00.000000000 Z
10
+ date: 2026-03-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activemodel