liquid2 0.1.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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rubocop.yml +46 -0
  4. data/.ruby-version +1 -0
  5. data/.vscode/settings.json +32 -0
  6. data/CHANGELOG.md +5 -0
  7. data/LICENSE.txt +21 -0
  8. data/LICENSE_SHOPIFY.txt +20 -0
  9. data/README.md +219 -0
  10. data/Rakefile +23 -0
  11. data/Steepfile +26 -0
  12. data/lib/liquid2/context.rb +297 -0
  13. data/lib/liquid2/environment.rb +287 -0
  14. data/lib/liquid2/errors.rb +79 -0
  15. data/lib/liquid2/expression.rb +20 -0
  16. data/lib/liquid2/expressions/arguments.rb +25 -0
  17. data/lib/liquid2/expressions/array.rb +20 -0
  18. data/lib/liquid2/expressions/blank.rb +41 -0
  19. data/lib/liquid2/expressions/boolean.rb +20 -0
  20. data/lib/liquid2/expressions/filtered.rb +136 -0
  21. data/lib/liquid2/expressions/identifier.rb +43 -0
  22. data/lib/liquid2/expressions/lambda.rb +53 -0
  23. data/lib/liquid2/expressions/logical.rb +71 -0
  24. data/lib/liquid2/expressions/loop.rb +79 -0
  25. data/lib/liquid2/expressions/path.rb +33 -0
  26. data/lib/liquid2/expressions/range.rb +28 -0
  27. data/lib/liquid2/expressions/relational.rb +119 -0
  28. data/lib/liquid2/expressions/template_string.rb +20 -0
  29. data/lib/liquid2/filter.rb +95 -0
  30. data/lib/liquid2/filters/array.rb +202 -0
  31. data/lib/liquid2/filters/date.rb +20 -0
  32. data/lib/liquid2/filters/default.rb +16 -0
  33. data/lib/liquid2/filters/json.rb +15 -0
  34. data/lib/liquid2/filters/math.rb +87 -0
  35. data/lib/liquid2/filters/size.rb +11 -0
  36. data/lib/liquid2/filters/slice.rb +17 -0
  37. data/lib/liquid2/filters/sort.rb +96 -0
  38. data/lib/liquid2/filters/string.rb +204 -0
  39. data/lib/liquid2/loader.rb +59 -0
  40. data/lib/liquid2/loaders/file_system_loader.rb +76 -0
  41. data/lib/liquid2/loaders/mixins.rb +52 -0
  42. data/lib/liquid2/node.rb +113 -0
  43. data/lib/liquid2/nodes/comment.rb +18 -0
  44. data/lib/liquid2/nodes/output.rb +24 -0
  45. data/lib/liquid2/nodes/tags/assign.rb +35 -0
  46. data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
  47. data/lib/liquid2/nodes/tags/capture.rb +40 -0
  48. data/lib/liquid2/nodes/tags/case.rb +111 -0
  49. data/lib/liquid2/nodes/tags/cycle.rb +63 -0
  50. data/lib/liquid2/nodes/tags/decrement.rb +29 -0
  51. data/lib/liquid2/nodes/tags/doc.rb +24 -0
  52. data/lib/liquid2/nodes/tags/echo.rb +31 -0
  53. data/lib/liquid2/nodes/tags/extends.rb +3 -0
  54. data/lib/liquid2/nodes/tags/for.rb +155 -0
  55. data/lib/liquid2/nodes/tags/if.rb +84 -0
  56. data/lib/liquid2/nodes/tags/include.rb +123 -0
  57. data/lib/liquid2/nodes/tags/increment.rb +29 -0
  58. data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
  59. data/lib/liquid2/nodes/tags/liquid.rb +29 -0
  60. data/lib/liquid2/nodes/tags/macro.rb +3 -0
  61. data/lib/liquid2/nodes/tags/raw.rb +30 -0
  62. data/lib/liquid2/nodes/tags/render.rb +137 -0
  63. data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
  64. data/lib/liquid2/nodes/tags/translate.rb +3 -0
  65. data/lib/liquid2/nodes/tags/unless.rb +23 -0
  66. data/lib/liquid2/nodes/tags/with.rb +3 -0
  67. data/lib/liquid2/parser.rb +917 -0
  68. data/lib/liquid2/scanner.rb +595 -0
  69. data/lib/liquid2/static_analysis.rb +301 -0
  70. data/lib/liquid2/tag.rb +22 -0
  71. data/lib/liquid2/template.rb +182 -0
  72. data/lib/liquid2/undefined.rb +131 -0
  73. data/lib/liquid2/utils/cache.rb +80 -0
  74. data/lib/liquid2/utils/chain_hash.rb +40 -0
  75. data/lib/liquid2/utils/unescape.rb +119 -0
  76. data/lib/liquid2/version.rb +5 -0
  77. data/lib/liquid2.rb +90 -0
  78. data/performance/benchmark.rb +73 -0
  79. data/performance/memory_profile.rb +62 -0
  80. data/performance/profile.rb +71 -0
  81. data/sig/liquid2.rbs +2348 -0
  82. data.tar.gz.sig +0 -0
  83. metadata +164 -0
  84. metadata.gz.sig +0 -0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "time"
5
+
6
+ module Liquid2
7
+ # Liquid filters and helper methods.
8
+ module Filters
9
+ # Cast _obj_ to an enumerable for use in a Liquid filter.
10
+ # @param obj [Object]
11
+ # @return [Enumerable]
12
+ def self.to_enumerable(obj)
13
+ case obj
14
+ when Array
15
+ obj.flatten
16
+ when Hash, String
17
+ [obj]
18
+ # when String
19
+ # obj.each_char
20
+ when Enumerable
21
+ obj
22
+ else
23
+ obj.respond_to?(:each) ? obj.each : [obj]
24
+ end
25
+ end
26
+
27
+ # Cast _obj_ to a number.
28
+ def self.to_number(obj, default: 0)
29
+ case obj
30
+ when String
31
+ # Cast to float before integer as `to_f` will parse exponents, `to_i` will not.
32
+ # Use `Float(obj)` instead of `obj.to_f` because `to_f` ignores trailing non-digit chars.
33
+ obj.match?(/\A-?\d+(?:[eE]\+?\d+)?\Z/) ? obj.to_f.to_i : Float(obj)
34
+ when Float, Integer, BigDecimal, Numeric
35
+ # Numeric is the base class for heap allocated numbers.
36
+ obj
37
+ else
38
+ default
39
+ end
40
+ rescue ArgumentError
41
+ default
42
+ end
43
+
44
+ # Case _obj_ to an Integer.
45
+ def self.to_integer(obj)
46
+ obj.is_a?(Integer) ? obj : Integer(obj)
47
+ end
48
+
49
+ # Cast _obj_ to a number, favouring BigDecimal over Float.
50
+ def self.to_decimal(obj, default: 0)
51
+ case obj
52
+ when String
53
+ obj.match?(/\A-?\d+(?:[eE]\+?\d+)?\Z/) ? obj.to_f.to_i : BigDecimal(obj)
54
+ when Float
55
+ BigDecimal(obj.to_s)
56
+ when Integer, BigDecimal, Numeric
57
+ obj
58
+ else
59
+ default
60
+ end
61
+ rescue ArgumentError
62
+ default
63
+ end
64
+
65
+ # Cast _obj_ to a date and time. Return `nil` if casting fails.
66
+ #
67
+ # TODO: This was copied from Shopify/liquid. Include their license and copyright.
68
+ def self.to_date(obj)
69
+ return obj if obj.respond_to?(:strftime)
70
+
71
+ if obj.is_a?(String)
72
+ return nil if obj.empty?
73
+
74
+ obj = obj.downcase
75
+ end
76
+
77
+ case obj
78
+ when "now", "today"
79
+ Time.now
80
+ when /\A\d+\z/, Integer
81
+ Time.at(obj.to_i)
82
+ when String
83
+ Time.parse(obj)
84
+ end
85
+ rescue ::ArgumentError
86
+ nil
87
+ end
88
+
89
+ def self.fetch(obj, key, default = nil)
90
+ obj[key]
91
+ rescue ArgumentError, TypeError, NoMethodError
92
+ default
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Return the concatenation of items in _left_ separated by _sep_.
7
+ # Coerce items in _left_ to strings if they aren't strings already.
8
+ def self.join(left, sep = " ")
9
+ sep = Liquid2.to_s(sep)
10
+ to_enumerable(left).map { |item| Liquid2.to_s(item) }.join(Liquid2.to_s(sep))
11
+ end
12
+
13
+ # Return a copy of _left_ with nil items removed.
14
+ # Coerce _left_ to an array-like object if it is not one already.
15
+ #
16
+ # If _key_ is given, assume items in _left_ are hash-like and remove items from _left_
17
+ # where `item.fetch(key, nil)` is nil.
18
+ #
19
+ # If key is not `:undefined`, coerce it to a string before calling `fetch` on items in
20
+ # _left_.
21
+ def self.compact(left, key = :undefined, context:)
22
+ left = Liquid2::Filters.to_enumerable(left)
23
+
24
+ case key
25
+ when Liquid2::Lambda
26
+ key.map(context, left).zip(left).reject do |r, _i|
27
+ r.nil? || r.is_a?(Liquid2::Undefined)
28
+ end.map(&:last)
29
+ when :undefined
30
+ left.compact
31
+ else
32
+ # TODO: stringify key?
33
+ left.reject do |item|
34
+ item.respond_to?(:fetch) ? item.fetch(key, nil).nil? : true
35
+ end
36
+ end
37
+ end
38
+
39
+ # Return _left_ concatenated with _right_, or nil if _right_ is not an array.
40
+ # Coerce _left_ to an array if it isn't an array already.
41
+ def self.concat(left, right)
42
+ unless right.respond_to?(:to_ary)
43
+ raise Liquid2::LiquidArgumentError.new("expected an array", nil)
44
+ end
45
+
46
+ Filters.to_enumerable(left).to_a.concat(right)
47
+ end
48
+
49
+ def self.find(left, key, value = nil, context:)
50
+ left = Liquid2::Filters.to_enumerable(left)
51
+
52
+ if key.is_a?(Liquid2::Lambda)
53
+ key.map(context, left).zip(left).reject do |r, i|
54
+ return i unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
55
+ end
56
+ elsif !value.nil? && !Liquid2.undefined?(value)
57
+ left.each do |item|
58
+ return item if fetch(item, key) == value
59
+ end
60
+ else
61
+ left.each do |item|
62
+ return item if fetch(item, key)
63
+ end
64
+ end
65
+
66
+ nil
67
+ end
68
+
69
+ def self.find_index(left, key, value = nil, context:)
70
+ left = Liquid2::Filters.to_enumerable(left)
71
+
72
+ if key.is_a?(Liquid2::Lambda)
73
+ key.map(context, left).reject.with_index do |r, index|
74
+ return index unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
75
+ end
76
+ elsif !value.nil? && !Liquid2.undefined?(value)
77
+ left.each_with_index do |item, index|
78
+ return index if fetch(item, key) == value
79
+ end
80
+ else
81
+ left.each_with_index do |item, index|
82
+ return index if fetch(item, key)
83
+ end
84
+ end
85
+
86
+ nil
87
+ end
88
+
89
+ def self.has(left, key, value = nil, context:)
90
+ left = Liquid2::Filters.to_enumerable(left)
91
+
92
+ if key.is_a?(Liquid2::Lambda)
93
+ key.map(context, left).reject do |r|
94
+ return true unless r.is_a?(Liquid2::Undefined) || !Liquid2.truthy?(context, r)
95
+ end
96
+ elsif !value.nil? && !Liquid2.undefined?(value)
97
+ left.each do |item|
98
+ return true if fetch(item, key) == value
99
+ end
100
+ else
101
+ left.each do |item|
102
+ return true if fetch(item, key)
103
+ end
104
+ end
105
+
106
+ false
107
+ end
108
+
109
+ # Return the first item in _left_, or `nil` if _left_ does not have a first item.
110
+ def self.first(left)
111
+ case left
112
+ when String
113
+ left[0]
114
+ else
115
+ left.first if left.respond_to?(:first)
116
+ end
117
+ end
118
+
119
+ # Return the last item in _left_, or `nil` if _left_ does not have a last item.
120
+ def self.last(left)
121
+ case left
122
+ when String
123
+ left[-1]
124
+ else
125
+ left.last if left.respond_to?(:last)
126
+ end
127
+ end
128
+
129
+ def self.map(left, key, context:)
130
+ left = Liquid2::Filters.to_enumerable(left)
131
+
132
+ if key.is_a?(Liquid2::Lambda)
133
+ key.map(context, left).map do |item|
134
+ item.is_a?(Liquid2::Undefined) ? nil : item
135
+ end
136
+ else
137
+ key = Liquid2.to_s(key)
138
+ left.map { |item| item[key] }
139
+ end
140
+ end
141
+
142
+ # Return _left_ with all items in reverse order.
143
+ # Coerce _left_ to an array if it isn't an array already.
144
+ def self.reverse(left)
145
+ to_enumerable(left).to_a.reverse
146
+ end
147
+
148
+ def self.reject(left, key, value = nil, context:)
149
+ left = Liquid2::Filters.to_enumerable(left)
150
+
151
+ if key.is_a?(Liquid2::Lambda)
152
+ key.map(context, left).zip(left).reject do |r, _item|
153
+ r.is_a?(Liquid2::Undefined) || Liquid2.truthy?(context, r)
154
+ end.map(&:last)
155
+ elsif !value.nil? && !Liquid2.undefined?(value)
156
+ key = Liquid2.to_s(key)
157
+ left.reject do |item|
158
+ fetch(item, key) == value
159
+ end
160
+ else
161
+ key = Liquid2.to_s(key)
162
+ left.reject do |item|
163
+ Liquid2.truthy?(context, fetch(item, key))
164
+ end
165
+ end
166
+ end
167
+
168
+ def self.where(left, key, value = nil, context:)
169
+ left = Liquid2::Filters.to_enumerable(left)
170
+
171
+ if key.is_a?(Liquid2::Lambda)
172
+ key.map(context, left).zip(left).filter do |r, _item|
173
+ r.is_a?(Liquid2::Undefined) || Liquid2.truthy?(context, r)
174
+ end.map(&:last)
175
+ elsif !value.nil? && !Liquid2.undefined?(value)
176
+ key = Liquid2.to_s(key)
177
+ left.filter do |item|
178
+ fetch(item, key) == value
179
+ end
180
+ else
181
+ key = Liquid2.to_s(key)
182
+ left.filter do |item|
183
+ Liquid2.truthy?(context, fetch(item, key))
184
+ end
185
+ end
186
+ end
187
+
188
+ # Deduplicate items in _left_.
189
+ # Coerce _left_ to an array if it isn't an array already.
190
+ def self.uniq(left, key = nil, context:)
191
+ left = Liquid2::Filters.to_enumerable(left)
192
+
193
+ if key.nil?
194
+ left.to_a.uniq
195
+ elsif key.is_a?(Liquid2::Lambda)
196
+ key.map(context, left).zip(left).uniq { |r, _item| r }.map(&:last)
197
+ else
198
+ left.to_a.uniq { |item| fetch(item, key) }
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Format date and time object _left_ with _format_.
7
+ # Coerce _left_ to a `Time` if it is not a time-like object already.
8
+ # Coerce _format_ to a string if it is not a string already.
9
+ def self.date(left, format)
10
+ format = Liquid2.to_s(format)
11
+ return left if format.empty?
12
+
13
+ if (date = Filters.to_date(left))
14
+ date.strftime(format)
15
+ else
16
+ left
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Return _left_, or _default_ if _obj_ is `nil`, `false` or empty.
7
+ # If _allow_false_ is `true`, _left_ is returned if _left_ is `false`.
8
+ def self.default(left, default = "", context:, allow_false: false)
9
+ return default if left.respond_to?(:force_default) && left.force_default
10
+
11
+ obj = left.respond_to?(:to_liquid) ? left.to_liquid(context) : left
12
+ falsey = allow_false ? left.nil? : !obj
13
+ falsey || (left.respond_to?(:empty?) && left.empty?) ? default : left
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Return _left_ serialized in JSON format.
7
+ def self.json(left, pretty: false)
8
+ if pretty
9
+ JSON.pretty_generate(left)
10
+ else
11
+ JSON.generate(left)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Return the absolute value of _left_.
7
+ def self.abs(left)
8
+ to_number(left).abs
9
+ end
10
+
11
+ # Return the maximum of _left_ and _right_.
12
+ def self.at_least(left, right)
13
+ [to_number(left), to_number(right)].max
14
+ end
15
+
16
+ # Return the minimum of _left_ and _right_.
17
+ def self.at_most(left, right)
18
+ [to_number(left), to_number(right)].min
19
+ end
20
+
21
+ # Return _left_ rounded up to the next whole number.
22
+ def self.ceil(left)
23
+ to_number(left).ceil
24
+ end
25
+
26
+ # Return the result of dividing _left_ by _right_.
27
+ # If both _left_ and _right_ are integers, integer division is performed.
28
+ def self.divided_by(left, right)
29
+ to_decimal(left) / to_decimal(right) # steep:ignore
30
+ rescue ZeroDivisionError => e
31
+ raise LiquidTypeError.new(e.message, nil)
32
+ end
33
+
34
+ # Return the result of multiplying _left_ by _right_.
35
+ def self.times(left, right)
36
+ to_decimal(left) * to_decimal(right) # steep:ignore
37
+ end
38
+
39
+ # Return _left_ rounded down to the next whole number.
40
+ def self.floor(left)
41
+ to_number(left).floor
42
+ end
43
+
44
+ # Return _right_ subtracted from _left_.
45
+ def self.minus(left, right)
46
+ to_decimal(left) - to_decimal(right) # steep:ignore
47
+ end
48
+
49
+ # Return the remainder of dividing _left_ by _right_.
50
+ def self.modulo(left, right)
51
+ to_decimal(left) % to_decimal(right) # steep:ignore
52
+ rescue ZeroDivisionError => e
53
+ raise LiquidTypeError.new(e.message, nil)
54
+ end
55
+
56
+ # Return _right_ added to _left_.
57
+ def self.plus(left, right)
58
+ to_decimal(left) + to_decimal(right) # steep:ignore
59
+ end
60
+
61
+ # Return _left_ rounded to _ndigits_ decimal digits.
62
+ def self.round(left, ndigits = 0)
63
+ left = to_decimal(left)
64
+ return left.round if ndigits == 0 # rubocop:disable Style/NumericPredicate
65
+
66
+ left.round(to_decimal(ndigits)) # steep:ignore
67
+ end
68
+
69
+ def self.sum(left, key = nil, context:)
70
+ left = Liquid2::Filters.to_enumerable(left)
71
+
72
+ case key
73
+ when Liquid2::Lambda
74
+ items = key.map(context, left).reject do |item|
75
+ Liquid2.undefined?(item)
76
+ end
77
+
78
+ items.sum { |item| Liquid2::Filters.to_decimal(item) }
79
+ when nil, Liquid2::Undefined
80
+ left.sum { |item| Liquid2::Filters.to_decimal(item) } # steep:ignore
81
+ else
82
+ k = Liquid2.to_s(key)
83
+ left.sum { |item| Liquid2::Filters.to_decimal(fetch(item, k)) } # steep:ignore
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Return the size of _left_, or zero if _left_ has no size.
7
+ def self.size(left)
8
+ left.respond_to?(:size) ? left.size : 0
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ # Return the subsequence of _left_ starting at _start_ up to _length_.
7
+ def self.slice(left, start, length = 1)
8
+ length = 1 if Liquid2.undefined?(length)
9
+ case left
10
+ when Array
11
+ left.slice(to_integer(start), to_integer(length)) || []
12
+ else
13
+ Liquid2.to_s(left).slice(to_integer(start), to_integer(length)) || ""
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # Liquid filters and helper methods.
5
+ module Filters
6
+ def self.sort(left, key = nil, context:)
7
+ left = Liquid2::Filters.to_enumerable(left)
8
+
9
+ case key
10
+ when Liquid2::Lambda
11
+ key.map(context, left).zip(left).sort do |a, b|
12
+ nil_safe_compare(a.first, b.first)
13
+ end.map(&:last)
14
+ when nil, Liquid2::Undefined
15
+ left.sort { |a, b| nil_safe_compare(a, b) }
16
+ else
17
+ key = Liquid2.to_s(key)
18
+ left.sort { |a, b| nil_safe_compare(fetch(a, key), fetch(b, key)) }
19
+ end
20
+ end
21
+
22
+ def self.sort_natural(left, key = nil, context:)
23
+ left = Liquid2::Filters.to_enumerable(left)
24
+
25
+ case key
26
+ when Liquid2::Lambda
27
+ key.map(context, left).zip(left).sort do |a, b|
28
+ nil_safe_casecmp(a.first, b.first)
29
+ end.map(&:last)
30
+ when nil, Liquid2::Undefined
31
+ left.sort { |a, b| nil_safe_casecmp(a, b) }
32
+ else
33
+ key = Liquid2.to_s(key)
34
+ left.sort { |a, b| nil_safe_casecmp(fetch(a, key), fetch(b, key)) }
35
+ end
36
+ end
37
+
38
+ def self.sort_numeric(left, key = nil, context:)
39
+ left = Liquid2::Filters.to_enumerable(left)
40
+
41
+ case key
42
+ when Liquid2::Lambda
43
+ key.map(context, left).zip(left).sort do |a, b|
44
+ numeric_compare(a.first, b.first)
45
+ end.map(&:last)
46
+ when nil, Liquid2::Undefined
47
+ left.sort { |a, b| numeric_compare(a, b) }
48
+ else
49
+ key = Liquid2.to_s(key)
50
+ left.sort { |a, b| numeric_compare(fetch(a, key), fetch(b, key)) }
51
+ end
52
+ end
53
+
54
+ def self.nil_safe_compare(left, right)
55
+ result = left <=> right
56
+
57
+ if result
58
+ result
59
+ elsif left.nil?
60
+ 1
61
+ elsif right.nil?
62
+ -1
63
+ else
64
+ raise Liquid2::LiquidArgumentError.new("can't sort incomparable type", nil)
65
+ end
66
+ end
67
+
68
+ def self.nil_safe_casecmp(left, right)
69
+ if !left.nil? && !right.nil?
70
+ left.to_s.casecmp(right.to_s)
71
+ elsif left.nil? && right.nil?
72
+ 0
73
+ else
74
+ left.nil? ? 1 : -1
75
+ end
76
+ end
77
+
78
+ def self.numeric_compare(left, right)
79
+ # @type var res: untyped
80
+ res = ints(left) <=> ints(right)
81
+ res || -1
82
+ end
83
+
84
+ def self.ints(obj)
85
+ case obj
86
+ when Integer, Float, BigDecimal
87
+ [obj]
88
+ else
89
+ numeric = obj.to_s.scan(/-?\d+/)
90
+ return [Float::INFINITY] if numeric.empty?
91
+
92
+ numeric.map(&:to_i)
93
+ end
94
+ end
95
+ end
96
+ end