rjq 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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +38 -0
- data/CHANGELOG.md +7 -0
- data/COMPATIBILITY.md +48 -0
- data/CONTRIBUTING.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/SECURITY.md +10 -0
- data/bin/rjq +9 -0
- data/lib/rjq/ast.rb +1475 -0
- data/lib/rjq/builtins/array.rb +3 -0
- data/lib/rjq/builtins/core.rb +3 -0
- data/lib/rjq/builtins/date.rb +3 -0
- data/lib/rjq/builtins/format.rb +3 -0
- data/lib/rjq/builtins/io.rb +3 -0
- data/lib/rjq/builtins/math.rb +3 -0
- data/lib/rjq/builtins/regex.rb +3 -0
- data/lib/rjq/builtins/sql.rb +3 -0
- data/lib/rjq/builtins/stream.rb +3 -0
- data/lib/rjq/builtins/string.rb +3 -0
- data/lib/rjq/builtins.rb +2103 -0
- data/lib/rjq/cli.rb +459 -0
- data/lib/rjq/color.rb +36 -0
- data/lib/rjq/compiler.rb +392 -0
- data/lib/rjq/errors.rb +76 -0
- data/lib/rjq/json/dumper.rb +191 -0
- data/lib/rjq/json/input_buffer.rb +99 -0
- data/lib/rjq/json/parser.rb +405 -0
- data/lib/rjq/json/stream_parser.rb +526 -0
- data/lib/rjq/json.rb +4 -0
- data/lib/rjq/lexer.rb +344 -0
- data/lib/rjq/math_functions.rb +168 -0
- data/lib/rjq/module_loader.rb +178 -0
- data/lib/rjq/modules.rb +88 -0
- data/lib/rjq/number.rb +189 -0
- data/lib/rjq/opcodes.rb +130 -0
- data/lib/rjq/parser.rb +755 -0
- data/lib/rjq/path.rb +250 -0
- data/lib/rjq/runtime.rb +377 -0
- data/lib/rjq/semantic_analyzer.rb +209 -0
- data/lib/rjq/value.rb +287 -0
- data/lib/rjq/version.rb +5 -0
- data/lib/rjq/vm.rb +1359 -0
- data/lib/rjq.rb +41 -0
- metadata +106 -0
data/lib/rjq/builtins.rb
ADDED
|
@@ -0,0 +1,2103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module Rjq
|
|
9
|
+
module Builtins
|
|
10
|
+
ZERO_ARITY_BUILTINS = %w[
|
|
11
|
+
empty length utf8bytelength type keys keys_unsorted values arrays objects iterables scalars booleans nulls
|
|
12
|
+
numbers strings not error halt halt_error input inputs debug stderr input_filename input_line_number null true
|
|
13
|
+
false infinite nan isinfinite isnan isnormal add any all flatten floor ceil round sqrt log log2 log10 exp exp2 exp10
|
|
14
|
+
pow10 atan abs cos sin tan acos asin cosh sinh tanh acosh asinh atanh cbrt significand logb gamma tgamma
|
|
15
|
+
lgamma lgamma_r frexp modf fabs nearbyint trunc rint j0 j1 y0 y1 erf erfc expm1 log1p isfinite finites normals
|
|
16
|
+
get_jq_origin get_prog_origin get_search_list to_entries from_entries to_number tonumber
|
|
17
|
+
tostring tojson fromjson ascii explode implode ascii_downcase ascii_upcase recurse recurse_down paths leaf_paths
|
|
18
|
+
tostream min max sort unique reverse combinations transpose first last env now gmtime localtime mktime fromdate
|
|
19
|
+
todate fromdateiso8601 todateiso8601 date builtins modulemeta
|
|
20
|
+
].freeze
|
|
21
|
+
ONE_ARITY_BUILTINS = %w[
|
|
22
|
+
has in IN INDEX error halt_error debug flatten range any all with_entries select map map_values split join
|
|
23
|
+
ltrimstr rtrimstr startswith endswith index rindex indices recurse recurse_down path paths leaf_paths getpath
|
|
24
|
+
delpaths del pick walk fromstream truncate_stream min_by max_by sort_by group_by GROUP_BY unique_by UNIQUE_BY
|
|
25
|
+
contains inside combinations bsearch first last nth repeat isempty strftime strflocaltime strptime dateadd datesub
|
|
26
|
+
test match capture scan splits format
|
|
27
|
+
].freeze
|
|
28
|
+
TWO_ARITY_BUILTINS = %w[
|
|
29
|
+
IN INDEX JOIN any all range recurse recurse_down pow atan2 ldexp scalb scalbln drem setpath nth limit until while split test match
|
|
30
|
+
scan splits sub gsub capture copysign fdim fmax fmin fmod hypot jn nextafter nexttoward remainder yn
|
|
31
|
+
].freeze
|
|
32
|
+
THREE_ARITY_BUILTINS = %w[JOIN range fma sub gsub].freeze
|
|
33
|
+
FOUR_ARITY_BUILTINS = %w[JOIN].freeze
|
|
34
|
+
|
|
35
|
+
BUILTIN_ARITIES = [
|
|
36
|
+
[0, ZERO_ARITY_BUILTINS],
|
|
37
|
+
[1, ONE_ARITY_BUILTINS],
|
|
38
|
+
[2, TWO_ARITY_BUILTINS],
|
|
39
|
+
[3, THREE_ARITY_BUILTINS],
|
|
40
|
+
[4, FOUR_ARITY_BUILTINS]
|
|
41
|
+
].each_with_object({}) do |(arity, names), registry|
|
|
42
|
+
names.each { |name| (registry[name] ||= []) << arity }
|
|
43
|
+
end.transform_values(&:freeze).freeze
|
|
44
|
+
BUILTIN_NAMES = BUILTIN_ARITIES.keys.freeze
|
|
45
|
+
EXTENSION_NAMES = %w[
|
|
46
|
+
GROUP_BY UNIQUE_BY ascii date dateadd datesub false leaf_paths null recurse_down to_number true
|
|
47
|
+
].freeze
|
|
48
|
+
JQ_BUILTIN_NAMES = (BUILTIN_NAMES - EXTENSION_NAMES).freeze
|
|
49
|
+
EXTENSION_ARITIES = EXTENSION_NAMES.to_h { |name| [name, BUILTIN_ARITIES.fetch(name)] }.freeze
|
|
50
|
+
FORMAT_NAMES = %w[@text @json @html @uri @csv @tsv @sh @base64 @base64d @base32 @base32d].freeze
|
|
51
|
+
REGISTRY = BUILTIN_NAMES.to_h { |name| [name, true] }.freeze
|
|
52
|
+
FILTER_ARGUMENT_POSITIONS = {
|
|
53
|
+
'IN' => [0, 1], 'INDEX' => [0, 1], 'JOIN' => [1, 2, 3],
|
|
54
|
+
'any' => [0, 1], 'all' => [0, 1], 'with_entries' => [0], 'select' => [0], 'map' => [0],
|
|
55
|
+
'map_values' => [0], 'recurse' => [0, 1], 'recurse_down' => [0, 1], 'path' => [0], 'paths' => [0],
|
|
56
|
+
'leaf_paths' => [0], 'del' => [0], 'pick' => [0], 'walk' => [0], 'fromstream' => [0],
|
|
57
|
+
'truncate_stream' => [0], 'min_by' => [0], 'max_by' => [0], 'sort_by' => [0], 'group_by' => [0],
|
|
58
|
+
'GROUP_BY' => [0], 'unique_by' => [0], 'UNIQUE_BY' => [0], 'first' => [0], 'last' => [0], 'nth' => [0, 1],
|
|
59
|
+
'limit' => [1], 'until' => [0, 1], 'while' => [0, 1], 'repeat' => [0], 'isempty' => [0],
|
|
60
|
+
'split' => [1], 'splits' => [1], 'sub' => [1], 'gsub' => [1]
|
|
61
|
+
}.transform_values(&:freeze).freeze
|
|
62
|
+
LEFT_OUTER_ARGUMENT_BUILTINS = %w[gsub range scan split splits sub].freeze
|
|
63
|
+
FLATTEN_UNBOUNDED = Object.new.freeze
|
|
64
|
+
|
|
65
|
+
module_function
|
|
66
|
+
|
|
67
|
+
def call(name, input, context, args)
|
|
68
|
+
call_stream(name, input, context, args).to_a
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def call_stream(name, input, context, args)
|
|
72
|
+
Enumerator.new do |yielder|
|
|
73
|
+
each_resolved_argument_set(name, args, input, context) do |resolved_args|
|
|
74
|
+
dispatch(name, input, context, resolved_args).each { |value| yielder << value }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def dispatch(name, input, context, args)
|
|
80
|
+
return call(name, args.fetch(0).eval(input, context).first, context, []) if name.start_with?('@') && !args.empty?
|
|
81
|
+
|
|
82
|
+
case name
|
|
83
|
+
when 'empty'
|
|
84
|
+
[]
|
|
85
|
+
when 'length'
|
|
86
|
+
[length(input)]
|
|
87
|
+
when 'utf8bytelength'
|
|
88
|
+
[utf8_byte_length(input)]
|
|
89
|
+
when 'type'
|
|
90
|
+
[Value.type_of(input)]
|
|
91
|
+
when 'keys'
|
|
92
|
+
[keys(input, sorted: true)]
|
|
93
|
+
when 'keys_unsorted'
|
|
94
|
+
[keys(input, sorted: false)]
|
|
95
|
+
when 'values'
|
|
96
|
+
input.nil? ? [] : [input]
|
|
97
|
+
when 'arrays'
|
|
98
|
+
input.is_a?(Array) ? [input] : []
|
|
99
|
+
when 'objects'
|
|
100
|
+
input.is_a?(Hash) ? [input] : []
|
|
101
|
+
when 'iterables'
|
|
102
|
+
input.is_a?(Array) || input.is_a?(Hash) ? [input] : []
|
|
103
|
+
when 'scalars'
|
|
104
|
+
input.is_a?(Array) || input.is_a?(Hash) ? [] : [input]
|
|
105
|
+
when 'booleans'
|
|
106
|
+
[true, false].include?(input) ? [input] : []
|
|
107
|
+
when 'nulls'
|
|
108
|
+
input.nil? ? [input] : []
|
|
109
|
+
when 'numbers'
|
|
110
|
+
input.is_a?(Numeric) ? [input] : []
|
|
111
|
+
when 'strings'
|
|
112
|
+
input.is_a?(String) ? [input] : []
|
|
113
|
+
when 'has'
|
|
114
|
+
[has?(input, eval_arg(args, 0, input, context))]
|
|
115
|
+
when 'in'
|
|
116
|
+
[has?(eval_arg(args, 0, input, context), input)]
|
|
117
|
+
when 'IN'
|
|
118
|
+
[in_sql?(input, context, args)]
|
|
119
|
+
when 'INDEX'
|
|
120
|
+
[index_sql(input, context, args)]
|
|
121
|
+
when 'JOIN'
|
|
122
|
+
[join_sql(input, context, args)]
|
|
123
|
+
when 'not'
|
|
124
|
+
[!Value.truthy?(input)]
|
|
125
|
+
when 'error'
|
|
126
|
+
raise ErrorValue, args.empty? ? input : eval_arg(args, 0, input, context)
|
|
127
|
+
when 'halt'
|
|
128
|
+
raise HaltError, nil
|
|
129
|
+
when 'halt_error'
|
|
130
|
+
raise HaltError.new(input, args.empty? ? 5 : eval_arg(args, 0, input, context).to_i)
|
|
131
|
+
when 'input'
|
|
132
|
+
input_builtin(context)
|
|
133
|
+
when 'inputs'
|
|
134
|
+
inputs_builtin(context)
|
|
135
|
+
when 'input_filename'
|
|
136
|
+
filename = current_input_record(context)&.filename || context.options.fetch(:current_filename, '<stdin>')
|
|
137
|
+
[filename || '<stdin>']
|
|
138
|
+
when 'input_line_number'
|
|
139
|
+
[current_input_record(context)&.line || context.options.fetch(:current_line, 1)]
|
|
140
|
+
when 'debug', 'stderr'
|
|
141
|
+
emit_diagnostic(name, input, context, args)
|
|
142
|
+
when 'null'
|
|
143
|
+
[nil]
|
|
144
|
+
when 'true'
|
|
145
|
+
[true]
|
|
146
|
+
when 'false'
|
|
147
|
+
[false]
|
|
148
|
+
when 'infinite'
|
|
149
|
+
[Float::INFINITY]
|
|
150
|
+
when 'nan'
|
|
151
|
+
[Float::NAN]
|
|
152
|
+
when 'isinfinite'
|
|
153
|
+
[input.is_a?(Float) && input.infinite? ? true : false]
|
|
154
|
+
when 'isnan'
|
|
155
|
+
[input.is_a?(Float) && input.nan?]
|
|
156
|
+
when 'isnormal'
|
|
157
|
+
[normal_number?(input)]
|
|
158
|
+
when 'isfinite'
|
|
159
|
+
[input.is_a?(Numeric) && input.to_f.finite?]
|
|
160
|
+
when 'finites'
|
|
161
|
+
input.is_a?(Numeric) && input.to_f.finite? ? [input] : []
|
|
162
|
+
when 'normals'
|
|
163
|
+
normal_number?(input) ? [input] : []
|
|
164
|
+
when 'add'
|
|
165
|
+
[add(input)]
|
|
166
|
+
when 'abs'
|
|
167
|
+
[absolute(input)]
|
|
168
|
+
when 'any'
|
|
169
|
+
[any?(input, context, args)]
|
|
170
|
+
when 'all'
|
|
171
|
+
[all?(input, context, args)]
|
|
172
|
+
when 'flatten'
|
|
173
|
+
args.empty? ? [flatten(input)] : args.fetch(0).eval(input, context).map { |depth| flatten(input, depth) }
|
|
174
|
+
when 'range'
|
|
175
|
+
range(input, context, args)
|
|
176
|
+
when 'floor', 'ceil', 'round', 'sqrt', 'log', 'log2', 'log10', 'exp', 'sin', 'cos', 'tan',
|
|
177
|
+
'asin', 'acos', 'atan', 'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh', 'cbrt',
|
|
178
|
+
'trunc', 'fabs', 'gamma', 'tgamma', 'lgamma', 'significand', 'logb', 'nearbyint',
|
|
179
|
+
'rint', 'frexp', 'modf', 'lgamma_r', 'j0', 'j1', 'y0', 'y1', 'erf', 'erfc', 'expm1', 'log1p'
|
|
180
|
+
[math_unary(name, input)]
|
|
181
|
+
when 'pow', 'atan2', 'ldexp', 'scalb', 'scalbln', 'fma', 'drem', 'copysign', 'fdim', 'fmax', 'fmin',
|
|
182
|
+
'fmod', 'hypot', 'jn', 'nextafter', 'nexttoward', 'remainder', 'yn'
|
|
183
|
+
[math_nary(name, input, context, args)]
|
|
184
|
+
when 'exp2'
|
|
185
|
+
[2**numeric(input)]
|
|
186
|
+
when 'exp10', 'pow10'
|
|
187
|
+
[10**numeric(input)]
|
|
188
|
+
when 'to_entries'
|
|
189
|
+
[to_entries(input)]
|
|
190
|
+
when 'from_entries'
|
|
191
|
+
[from_entries(assert_array(input))]
|
|
192
|
+
when 'with_entries'
|
|
193
|
+
[from_entries(map_entries(input, context, args.fetch(0)))]
|
|
194
|
+
when 'select'
|
|
195
|
+
select(input, context, args.fetch(0))
|
|
196
|
+
when 'map'
|
|
197
|
+
[map_filter(input, context, args.fetch(0))]
|
|
198
|
+
when 'map_values'
|
|
199
|
+
[map_values(input, context, args.fetch(0))]
|
|
200
|
+
when 'to_number', 'tonumber'
|
|
201
|
+
[to_number(input)]
|
|
202
|
+
when 'tostring'
|
|
203
|
+
[to_string(input)]
|
|
204
|
+
when 'tojson', '@json'
|
|
205
|
+
[JSON::Dumper.dump(input, indent: nil)]
|
|
206
|
+
when 'fromjson'
|
|
207
|
+
[JSON::Parser.parse_one(assert_string(input))]
|
|
208
|
+
when 'ascii'
|
|
209
|
+
[JSON::Dumper.dump(to_string(input), indent: nil, ascii: true)[1...-1]]
|
|
210
|
+
when 'explode'
|
|
211
|
+
[assert_string(input).each_codepoint.to_a]
|
|
212
|
+
when 'implode'
|
|
213
|
+
[implode(input)]
|
|
214
|
+
when 'split'
|
|
215
|
+
[split(input, context, args)]
|
|
216
|
+
when 'join'
|
|
217
|
+
args.empty? ? [join(input, '')] : args.fetch(0).eval(input, context).map { |separator| join(input, separator) }
|
|
218
|
+
when 'ltrimstr'
|
|
219
|
+
[assert_string(input).delete_prefix(assert_string(eval_arg(args, 0, input, context)))]
|
|
220
|
+
when 'rtrimstr'
|
|
221
|
+
[assert_string(input).delete_suffix(assert_string(eval_arg(args, 0, input, context)))]
|
|
222
|
+
when 'ascii_downcase'
|
|
223
|
+
[assert_string(input).tr('A-Z', 'a-z')]
|
|
224
|
+
when 'ascii_upcase'
|
|
225
|
+
[assert_string(input).tr('a-z', 'A-Z')]
|
|
226
|
+
when 'startswith'
|
|
227
|
+
[assert_string(input).start_with?(assert_string(eval_arg(args, 0, input, context)))]
|
|
228
|
+
when 'endswith'
|
|
229
|
+
[assert_string(input).end_with?(assert_string(eval_arg(args, 0, input, context)))]
|
|
230
|
+
when 'index'
|
|
231
|
+
args.fetch(0).eval(input, context).map { |needle| index_of(input, needle) }
|
|
232
|
+
when 'rindex'
|
|
233
|
+
args.fetch(0).eval(input, context).map { |needle| rindex_of(input, needle) }
|
|
234
|
+
when 'indices'
|
|
235
|
+
args.fetch(0).eval(input, context).map { |needle| indices_of(input, needle) }
|
|
236
|
+
when 'recurse', 'recurse_down'
|
|
237
|
+
recurse(input, context, args)
|
|
238
|
+
when 'path'
|
|
239
|
+
args.fetch(0).paths(input, context)
|
|
240
|
+
when 'paths'
|
|
241
|
+
paths_builtin(input, context, args, leaves_only: false)
|
|
242
|
+
when 'leaf_paths'
|
|
243
|
+
paths_builtin(input, context, args, leaves_only: true)
|
|
244
|
+
when 'getpath'
|
|
245
|
+
[Path.get(input, eval_arg(args, 0, input, context))]
|
|
246
|
+
when 'setpath'
|
|
247
|
+
[Path.set(Value.deep_copy(input), eval_arg(args, 0, input, context), eval_arg(args, 1, input, context))]
|
|
248
|
+
when 'delpaths'
|
|
249
|
+
[delpaths(input, eval_arg(args, 0, input, context))]
|
|
250
|
+
when 'del'
|
|
251
|
+
[delete_paths(input, context, args)]
|
|
252
|
+
when 'pick'
|
|
253
|
+
[pick(input, context, args)]
|
|
254
|
+
when 'walk'
|
|
255
|
+
walk(input, context, args.fetch(0))
|
|
256
|
+
when 'tostream'
|
|
257
|
+
to_stream(input)
|
|
258
|
+
when 'fromstream'
|
|
259
|
+
from_stream(filter_stream(args.fetch(0), input, context))
|
|
260
|
+
when 'truncate_stream'
|
|
261
|
+
truncate_stream(input, context, args)
|
|
262
|
+
when 'min'
|
|
263
|
+
[extreme(input, :min)]
|
|
264
|
+
when 'max'
|
|
265
|
+
[extreme(input, :max)]
|
|
266
|
+
when 'min_by'
|
|
267
|
+
[extreme_by(input, context, args.fetch(0), :min)]
|
|
268
|
+
when 'max_by'
|
|
269
|
+
[extreme_by(input, context, args.fetch(0), :max)]
|
|
270
|
+
when 'sort'
|
|
271
|
+
[assert_array(input).sort { |a, b| Value.compare(a, b) }]
|
|
272
|
+
when 'sort_by'
|
|
273
|
+
[sort_by_filter(input, context, args.fetch(0))]
|
|
274
|
+
when 'group_by', 'GROUP_BY'
|
|
275
|
+
[group_by_filter(input, context, args.fetch(0))]
|
|
276
|
+
when 'unique'
|
|
277
|
+
[unique_values(assert_array(input))]
|
|
278
|
+
when 'unique_by', 'UNIQUE_BY'
|
|
279
|
+
[unique_by_filter(input, context, args.fetch(0))]
|
|
280
|
+
when 'reverse'
|
|
281
|
+
[assert_array(input).reverse]
|
|
282
|
+
when 'contains'
|
|
283
|
+
[contains?(input, eval_arg(args, 0, input, context))]
|
|
284
|
+
when 'inside'
|
|
285
|
+
[contains?(eval_arg(args, 0, input, context), input)]
|
|
286
|
+
when 'combinations'
|
|
287
|
+
combinations(input, context, args)
|
|
288
|
+
when 'transpose'
|
|
289
|
+
[transpose(input)]
|
|
290
|
+
when 'bsearch'
|
|
291
|
+
bsearch(input, context, args)
|
|
292
|
+
when 'first'
|
|
293
|
+
first_builtin(input, context, args)
|
|
294
|
+
when 'last'
|
|
295
|
+
last_builtin(input, context, args)
|
|
296
|
+
when 'nth'
|
|
297
|
+
nth(input, context, args)
|
|
298
|
+
when 'limit'
|
|
299
|
+
limit(input, context, args)
|
|
300
|
+
when 'until'
|
|
301
|
+
until_filter(input, context, args)
|
|
302
|
+
when 'while'
|
|
303
|
+
while_filter(input, context, args)
|
|
304
|
+
when 'repeat'
|
|
305
|
+
repeat_filter(input, context, args)
|
|
306
|
+
when 'isempty'
|
|
307
|
+
[args.fetch(0).take(input, context, 1).empty?]
|
|
308
|
+
when 'builtins'
|
|
309
|
+
[JQ_BUILTIN_NAMES.flat_map { |builtin| builtin_arities(builtin) }.sort]
|
|
310
|
+
when 'modulemeta'
|
|
311
|
+
[modulemeta(input, context)]
|
|
312
|
+
when 'env'
|
|
313
|
+
[ENV.to_h]
|
|
314
|
+
when 'now'
|
|
315
|
+
[Time.now.to_f]
|
|
316
|
+
when 'gmtime'
|
|
317
|
+
[time_array(Time.at(numeric(input)).utc)]
|
|
318
|
+
when 'localtime'
|
|
319
|
+
[time_array(Time.at(numeric(input)).localtime)]
|
|
320
|
+
when 'mktime'
|
|
321
|
+
[mktime(input)]
|
|
322
|
+
when 'strftime'
|
|
323
|
+
[strftime_builtin(input, context, args)]
|
|
324
|
+
when 'strflocaltime'
|
|
325
|
+
[strftime_builtin(input, context, args, local: true)]
|
|
326
|
+
when 'strptime'
|
|
327
|
+
[strptime(input, context, args)]
|
|
328
|
+
when 'fromdate', 'fromdateiso8601'
|
|
329
|
+
[Time.iso8601(assert_string(input)).to_f]
|
|
330
|
+
when 'todate', 'todateiso8601'
|
|
331
|
+
[Time.at(numeric(input)).utc.iso8601]
|
|
332
|
+
when 'date'
|
|
333
|
+
[Time.now.utc.iso8601]
|
|
334
|
+
when 'dateadd'
|
|
335
|
+
[Time.at(numeric(input) + numeric(eval_arg(args, 0, input, context))).to_f]
|
|
336
|
+
when 'datesub'
|
|
337
|
+
[Time.at(numeric(input) - numeric(eval_arg(args, 0, input, context))).to_f]
|
|
338
|
+
when 'test'
|
|
339
|
+
regex, flags = regexp(input, context, args)
|
|
340
|
+
match = regex.match(assert_string(input))
|
|
341
|
+
[match ? !(flags.include?('n') && match[0].empty?) : false]
|
|
342
|
+
when 'match'
|
|
343
|
+
match_builtin(input, context, args)
|
|
344
|
+
when 'capture'
|
|
345
|
+
capture_builtin(input, context, args)
|
|
346
|
+
when 'format'
|
|
347
|
+
format_builtin(input, context, args)
|
|
348
|
+
when 'scan'
|
|
349
|
+
scan_builtin(input, context, args)
|
|
350
|
+
when 'splits'
|
|
351
|
+
splits_builtin(input, context, args)
|
|
352
|
+
when 'sub'
|
|
353
|
+
substitute(input, context, args, global: false)
|
|
354
|
+
when 'gsub'
|
|
355
|
+
substitute(input, context, args, global: true)
|
|
356
|
+
when '@text'
|
|
357
|
+
[to_string(input)]
|
|
358
|
+
when '@html'
|
|
359
|
+
[html_escape(to_string(input))]
|
|
360
|
+
when '@uri'
|
|
361
|
+
[uri_escape(to_string(input))]
|
|
362
|
+
when '@base64'
|
|
363
|
+
[[to_string(input)].pack('m0')]
|
|
364
|
+
when '@base64d'
|
|
365
|
+
[decode_base64(input)]
|
|
366
|
+
when '@base32'
|
|
367
|
+
[base32_encode(to_string(input))]
|
|
368
|
+
when '@base32d'
|
|
369
|
+
[base32_decode(assert_string(input))]
|
|
370
|
+
when '@csv'
|
|
371
|
+
[format_csv(input)]
|
|
372
|
+
when '@tsv'
|
|
373
|
+
[format_tsv(input)]
|
|
374
|
+
when '@sh'
|
|
375
|
+
[format_sh(input)]
|
|
376
|
+
when 'get_jq_origin'
|
|
377
|
+
[context.options.fetch(:jq_origin, File.expand_path('../..', __dir__))]
|
|
378
|
+
when 'get_prog_origin'
|
|
379
|
+
source_path = context.options[:source_path]
|
|
380
|
+
[source_path ? File.dirname(File.expand_path(source_path)) : Dir.pwd]
|
|
381
|
+
when 'get_search_list'
|
|
382
|
+
[search_list(context)]
|
|
383
|
+
else
|
|
384
|
+
raise CompileError, "#{name}/#{args.length} is not defined"
|
|
385
|
+
end
|
|
386
|
+
rescue RegexpError => e
|
|
387
|
+
raise unless defined?(Regexp::TimeoutError) && e.is_a?(Regexp::TimeoutError)
|
|
388
|
+
|
|
389
|
+
raise Rjq::RuntimeError, 'regular expression match timeout'
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def length(value)
|
|
393
|
+
case value
|
|
394
|
+
when NilClass
|
|
395
|
+
0
|
|
396
|
+
when String
|
|
397
|
+
value.each_char.count
|
|
398
|
+
when Array, Hash
|
|
399
|
+
value.length
|
|
400
|
+
when Numeric
|
|
401
|
+
value.abs
|
|
402
|
+
else
|
|
403
|
+
raise TypeError, "cannot get length of #{Value.type_of(value)}"
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def utf8_byte_length(value)
|
|
408
|
+
return value.bytesize if value.is_a?(String)
|
|
409
|
+
|
|
410
|
+
raise TypeError,
|
|
411
|
+
"#{Value.type_of(value)} (#{JSON::Dumper.dump(value, indent: nil)}) only strings have UTF-8 byte length"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def keys(value, sorted:)
|
|
415
|
+
case value
|
|
416
|
+
when Array
|
|
417
|
+
(0...value.length).to_a
|
|
418
|
+
when Hash
|
|
419
|
+
sorted ? value.keys.sort : value.keys
|
|
420
|
+
else
|
|
421
|
+
raise TypeError, "cannot get keys of #{Value.type_of(value)}"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def has?(container, key)
|
|
426
|
+
case container
|
|
427
|
+
when Array
|
|
428
|
+
unless key.is_a?(Numeric)
|
|
429
|
+
raise TypeError, "Cannot check whether array has a #{Value.type_of(key)} key"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
return false unless key.finite?
|
|
433
|
+
|
|
434
|
+
index = key.to_i
|
|
435
|
+
index >= 0 && index < container.length
|
|
436
|
+
when Hash
|
|
437
|
+
unless key.is_a?(String)
|
|
438
|
+
raise TypeError, "Cannot check whether object has a #{Value.type_of(key)} key"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
container.key?(key)
|
|
442
|
+
when NilClass
|
|
443
|
+
false
|
|
444
|
+
else
|
|
445
|
+
raise TypeError,
|
|
446
|
+
"Cannot check whether #{Value.type_of(container)} has a #{Value.type_of(key)} key"
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def input_builtin(context)
|
|
451
|
+
queue = context.options[:input_queue]
|
|
452
|
+
return [queue.shift] if queue && !queue.empty?
|
|
453
|
+
|
|
454
|
+
remaining = context.options.fetch(:remaining_inputs, [])
|
|
455
|
+
raise RuntimeError, 'break' if remaining.empty?
|
|
456
|
+
|
|
457
|
+
[remaining.first]
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def inputs_builtin(context)
|
|
461
|
+
queue = context.options[:input_queue]
|
|
462
|
+
return queue.each_remaining if queue
|
|
463
|
+
|
|
464
|
+
context.options.fetch(:remaining_inputs, [])
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def current_input_record(context)
|
|
468
|
+
context.options[:input_queue]&.current_record
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def emit_diagnostic(name, input, context, args)
|
|
472
|
+
io = context.options[:stderr] || $stderr
|
|
473
|
+
diagnostic = args.empty? ? input : eval_arg(args, 0, input, context)
|
|
474
|
+
if name == 'debug'
|
|
475
|
+
io.puts(JSON::Dumper.dump(['DEBUG:', diagnostic], indent: nil))
|
|
476
|
+
else
|
|
477
|
+
io.puts(to_string(diagnostic))
|
|
478
|
+
end
|
|
479
|
+
[input]
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def add(value)
|
|
483
|
+
assert_array(value).reduce(nil) do |sum, item|
|
|
484
|
+
sum.nil? ? item : AST::BinaryOp.new(AST::Literal.new(sum), '+', AST::Literal.new(item)).eval(nil, AST::Context.new).first
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def any?(input, context, args)
|
|
489
|
+
if args.length == 2
|
|
490
|
+
return source_any?(args[0], input, context) do |value|
|
|
491
|
+
source_any?(args[1], value, context) { |result| Value.truthy?(result) }
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
values = args.empty? ? iterable_values(input) : input_values(input, context, args.first)
|
|
496
|
+
values.any? { |value| Value.truthy?(value) }
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def all?(input, context, args)
|
|
500
|
+
if args.length == 2
|
|
501
|
+
return source_all?(args[0], input, context) do |value|
|
|
502
|
+
source_all?(args[1], value, context) { |result| Value.truthy?(result) }
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
values = args.empty? ? iterable_values(input) : input_values(input, context, args.first)
|
|
507
|
+
values.all? { |value| Value.truthy?(value) }
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def flatten(value, depth = FLATTEN_UNBOUNDED)
|
|
511
|
+
if !depth.equal?(FLATTEN_UNBOUNDED) && Value.compare(depth, 0).negative?
|
|
512
|
+
raise RuntimeError, 'flatten depth must not be negative'
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
flatten_items(iterable_values(value), depth)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def flatten_items(items, depth)
|
|
519
|
+
output = []
|
|
520
|
+
stack = items.to_a.reverse_each.map { |item| [item, depth] }
|
|
521
|
+
until stack.empty?
|
|
522
|
+
item, item_depth = stack.pop
|
|
523
|
+
unbounded = item_depth.equal?(FLATTEN_UNBOUNDED)
|
|
524
|
+
unless item.is_a?(Array) && (unbounded || !Value.equal?(item_depth, 0))
|
|
525
|
+
output << item
|
|
526
|
+
next
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
next_depth = unbounded ? FLATTEN_UNBOUNDED : subtract_flatten_depth(item_depth)
|
|
530
|
+
item.reverse_each { |child| stack << [child, next_depth] }
|
|
531
|
+
end
|
|
532
|
+
output
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def subtract_flatten_depth(depth)
|
|
536
|
+
AST::BinaryOp.new(AST::Literal.new(depth), '-', AST::Literal.new(1))
|
|
537
|
+
.eval(nil, AST::Context.new).first
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def range(input, context, args)
|
|
541
|
+
numbers = args.map { |arg| numeric(arg.eval(input, context).first) }
|
|
542
|
+
from, to, step =
|
|
543
|
+
case numbers.length
|
|
544
|
+
when 1
|
|
545
|
+
[0, numbers[0], 1]
|
|
546
|
+
when 2
|
|
547
|
+
[numbers[0], numbers[1], 1]
|
|
548
|
+
when 3
|
|
549
|
+
numbers
|
|
550
|
+
else
|
|
551
|
+
raise RuntimeError, 'range expects 1 to 3 arguments'
|
|
552
|
+
end
|
|
553
|
+
raise RuntimeError, 'range step cannot be zero' if step.zero?
|
|
554
|
+
|
|
555
|
+
range_values(from, to, step)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def range_values(from, to, step)
|
|
559
|
+
Enumerator.new do |yielder|
|
|
560
|
+
current = from
|
|
561
|
+
comparison = step.positive? ? -> { current < to } : -> { current > to }
|
|
562
|
+
while comparison.call
|
|
563
|
+
yielder << current
|
|
564
|
+
current += step
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def cartesian(sets)
|
|
570
|
+
return [[]] if sets.empty?
|
|
571
|
+
|
|
572
|
+
sets.reduce([[]]) do |acc, values|
|
|
573
|
+
acc.flat_map { |prefix| values.map { |value| prefix + [value] } }
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def math_unary(name, input)
|
|
578
|
+
value = numeric(input)
|
|
579
|
+
case name
|
|
580
|
+
when 'floor' then value.floor
|
|
581
|
+
when 'ceil' then value.ceil
|
|
582
|
+
when 'round' then value.round
|
|
583
|
+
when 'sqrt' then Math.sqrt(value)
|
|
584
|
+
when 'log' then Math.log(value)
|
|
585
|
+
when 'log2' then Math.log2(value)
|
|
586
|
+
when 'log10' then Math.log10(value)
|
|
587
|
+
when 'exp' then Math.exp(value)
|
|
588
|
+
when 'sin' then Math.sin(value)
|
|
589
|
+
when 'cos' then Math.cos(value)
|
|
590
|
+
when 'tan' then Math.tan(value)
|
|
591
|
+
when 'asin' then Math.asin(value)
|
|
592
|
+
when 'acos' then Math.acos(value)
|
|
593
|
+
when 'atan' then Math.atan(value)
|
|
594
|
+
when 'sinh' then Math.sinh(value)
|
|
595
|
+
when 'cosh' then Math.cosh(value)
|
|
596
|
+
when 'tanh' then Math.tanh(value)
|
|
597
|
+
when 'asinh' then Math.asinh(value)
|
|
598
|
+
when 'acosh' then Math.acosh(value)
|
|
599
|
+
when 'atanh' then Math.atanh(value)
|
|
600
|
+
when 'cbrt' then Math.cbrt(value)
|
|
601
|
+
when 'trunc' then value.truncate
|
|
602
|
+
when 'fabs' then value.abs
|
|
603
|
+
when 'gamma', 'tgamma' then Math.gamma(value)
|
|
604
|
+
when 'lgamma' then Math.lgamma(value).first
|
|
605
|
+
when 'lgamma_r' then Math.lgamma(value)
|
|
606
|
+
when 'frexp' then Math.frexp(value)
|
|
607
|
+
when 'modf'
|
|
608
|
+
integral = value.truncate
|
|
609
|
+
[value - integral, integral]
|
|
610
|
+
when 'significand'
|
|
611
|
+
return 0 if value.zero?
|
|
612
|
+
|
|
613
|
+
fraction, = Math.frexp(value)
|
|
614
|
+
fraction * 2
|
|
615
|
+
when 'logb'
|
|
616
|
+
return -Float::INFINITY if value.zero?
|
|
617
|
+
|
|
618
|
+
Math.log2(value.abs).floor
|
|
619
|
+
when 'nearbyint', 'rint' then round_to_even(value)
|
|
620
|
+
when 'j0', 'j1', 'y0', 'y1' then MathFunctions.bessel(name, value)
|
|
621
|
+
when 'erf' then Math.erf(value)
|
|
622
|
+
when 'erfc' then Math.erfc(value)
|
|
623
|
+
when 'expm1' then Math.expm1(value)
|
|
624
|
+
when 'log1p' then Math.log1p(value)
|
|
625
|
+
end
|
|
626
|
+
rescue Math::DomainError
|
|
627
|
+
Float::NAN
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def absolute(value)
|
|
631
|
+
if value.nil? || value == true || value == false
|
|
632
|
+
raise TypeError, "#{Value.type_of(value)} (#{JSON::Dumper.dump(value, indent: nil)}) cannot be negated"
|
|
633
|
+
end
|
|
634
|
+
return value unless value.is_a?(Numeric)
|
|
635
|
+
if value.is_a?(Number)
|
|
636
|
+
return value unless value.literal.start_with?('-')
|
|
637
|
+
return value if value.literal.match?(/\A-0+(?:\.0+)?(?:[eE][+-]?\d+)?\z/)
|
|
638
|
+
|
|
639
|
+
return value.to_f.abs
|
|
640
|
+
end
|
|
641
|
+
return value if value.zero? || !value.negative?
|
|
642
|
+
|
|
643
|
+
value.abs
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def math_nary(name, input, context, args)
|
|
647
|
+
values = args.empty? ? [numeric(input)] : args.map { |arg| numeric(arg.eval(input, context).first) }
|
|
648
|
+
case name
|
|
649
|
+
when 'pow' then values[0]**values[1]
|
|
650
|
+
when 'atan2' then Math.atan2(values[0], values[1])
|
|
651
|
+
when 'ldexp' then Math.ldexp(values[0], values[1].to_i)
|
|
652
|
+
when 'scalb' then MathFunctions.scalb(values[0], values[1])
|
|
653
|
+
when 'scalbln' then MathFunctions.scalbln(values[0], values[1])
|
|
654
|
+
when 'fma' then MathFunctions.fma(values[0], values[1], values[2])
|
|
655
|
+
when 'drem' then MathFunctions.remainder(values[0], values[1])
|
|
656
|
+
when 'copysign' then copy_sign(values[0], values[1])
|
|
657
|
+
when 'fdim' then values.any? { |value| value.to_f.nan? } ? Float::NAN : [values[0] - values[1], 0].max
|
|
658
|
+
when 'fmax' then float_extreme(values[0], values[1], :max)
|
|
659
|
+
when 'fmin' then float_extreme(values[0], values[1], :min)
|
|
660
|
+
when 'fmod' then values[0].remainder(values[1])
|
|
661
|
+
when 'hypot' then Math.hypot(values[0], values[1])
|
|
662
|
+
when 'jn', 'yn' then MathFunctions.bessel(name, values[0].to_i, values[1])
|
|
663
|
+
when 'nextafter', 'nexttoward' then next_float_toward(values[0], values[1])
|
|
664
|
+
when 'remainder' then MathFunctions.remainder(values[0], values[1])
|
|
665
|
+
end
|
|
666
|
+
rescue Math::DomainError, FloatDomainError, ZeroDivisionError
|
|
667
|
+
Float::NAN
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def normal_number?(value)
|
|
671
|
+
return false unless value.is_a?(Numeric)
|
|
672
|
+
|
|
673
|
+
float = value.to_f
|
|
674
|
+
float.finite? && float.abs >= Float::MIN
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def round_to_even(value)
|
|
678
|
+
rounded = value.to_f.round(half: :even)
|
|
679
|
+
rounded.zero? && value.to_f.negative? ? -0.0 : rounded
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def copy_sign(magnitude, sign)
|
|
683
|
+
negative = sign.to_f.negative? || (sign.to_f.zero? && (1.0 / sign.to_f).negative?)
|
|
684
|
+
negative ? -magnitude.to_f.abs : magnitude.to_f.abs
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def float_extreme(left, right, mode)
|
|
688
|
+
return right if left.to_f.nan?
|
|
689
|
+
return left if right.to_f.nan?
|
|
690
|
+
|
|
691
|
+
[left, right].public_send(mode)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def next_float_toward(value, target)
|
|
695
|
+
value = value.to_f
|
|
696
|
+
target = target.to_f
|
|
697
|
+
return target if value == target
|
|
698
|
+
return Float::NAN if value.nan? || target.nan?
|
|
699
|
+
|
|
700
|
+
value < target ? value.next_float : value.prev_float
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def ieee_remainder(left, right)
|
|
704
|
+
quotient = (left.to_f / right.to_f).round(half: :even)
|
|
705
|
+
left.to_f - (right.to_f * quotient)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def to_entries(value)
|
|
709
|
+
case value
|
|
710
|
+
when Hash
|
|
711
|
+
value.map { |key, item| { 'key' => key, 'value' => item } }
|
|
712
|
+
when Array
|
|
713
|
+
value.each_with_index.map { |item, index| { 'key' => index, 'value' => item } }
|
|
714
|
+
else
|
|
715
|
+
raise TypeError, "cannot convert #{Value.type_of(value)} to entries"
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def from_entries(entries)
|
|
720
|
+
entries.each_with_object({}) do |entry, object|
|
|
721
|
+
key = %w[key Key name Name].lazy.map { |name| Path.read_index(entry, name) }
|
|
722
|
+
.find { |candidate| Value.truthy?(candidate) }
|
|
723
|
+
unless key.is_a?(String)
|
|
724
|
+
raise TypeError,
|
|
725
|
+
"Cannot use #{Value.type_of(key)} (#{JSON::Dumper.dump(key, indent: nil)}) as object key"
|
|
726
|
+
end
|
|
727
|
+
value = entry.key?('value') ? entry['value'] : entry['Value']
|
|
728
|
+
object[key] = value
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def map_entries(input, context, filter)
|
|
733
|
+
to_entries(input).flat_map do |entry|
|
|
734
|
+
collect_filter(filter, entry, context)
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def select(input, context, filter)
|
|
739
|
+
Enumerator.new do |yielder|
|
|
740
|
+
filter_stream(filter, input, context).each do |value|
|
|
741
|
+
yielder << input if Value.truthy?(value)
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def map_filter(value, context, filter)
|
|
747
|
+
items = value.is_a?(Hash) ? value.values : assert_array(value)
|
|
748
|
+
items.flat_map { |item| collect_filter(filter, item, context) }
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def map_values(value, context, filter)
|
|
752
|
+
case value
|
|
753
|
+
when Array
|
|
754
|
+
value.each_with_object([]) do |item, out|
|
|
755
|
+
outputs = filter_stream(filter, item, context).take(1)
|
|
756
|
+
out << outputs.first unless outputs.empty?
|
|
757
|
+
end
|
|
758
|
+
when Hash
|
|
759
|
+
value.each_with_object({}) do |(key, item), out|
|
|
760
|
+
outputs = filter_stream(filter, item, context).take(1)
|
|
761
|
+
out[key] = outputs.first unless outputs.empty?
|
|
762
|
+
end
|
|
763
|
+
else
|
|
764
|
+
raise TypeError,
|
|
765
|
+
"Cannot iterate over #{Value.type_of(value)} (#{JSON::Dumper.dump(value, indent: nil)})"
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def to_number(value)
|
|
770
|
+
return value if value.is_a?(Numeric)
|
|
771
|
+
raise TypeError, "cannot convert #{Value.type_of(value)} to number" unless value.is_a?(String)
|
|
772
|
+
|
|
773
|
+
value.match?(/[.eE]/) ? Float(value) : Integer(value, 10)
|
|
774
|
+
rescue ArgumentError
|
|
775
|
+
raise TypeError, 'invalid numeric string'
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def to_string(value)
|
|
779
|
+
case value
|
|
780
|
+
when String
|
|
781
|
+
value
|
|
782
|
+
else
|
|
783
|
+
JSON::Dumper.dump(value, indent: nil)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def split(input, context, args)
|
|
788
|
+
string = assert_string(input)
|
|
789
|
+
if args.length > 1
|
|
790
|
+
return split_at_matches(string, regexp_matches(input, context, args, string))
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
separator = assert_string(eval_arg(args, 0, input, context))
|
|
794
|
+
return string.each_char.to_a if separator.empty?
|
|
795
|
+
|
|
796
|
+
string.split(separator, -1)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def split_at_matches(string, matches)
|
|
800
|
+
return [string] if matches.empty?
|
|
801
|
+
|
|
802
|
+
offset = 0
|
|
803
|
+
matches.map do |match|
|
|
804
|
+
part = string[offset...match.begin(0)].to_s
|
|
805
|
+
offset = match.end(0)
|
|
806
|
+
part
|
|
807
|
+
end << string[offset..].to_s
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def implode(input)
|
|
811
|
+
raise TypeError, 'implode input must be an array' unless input.is_a?(Array)
|
|
812
|
+
|
|
813
|
+
input.map do |item|
|
|
814
|
+
unless item.is_a?(Numeric) && !item.to_f.nan?
|
|
815
|
+
raise TypeError,
|
|
816
|
+
"#{Value.type_of(item)} (#{JSON::Dumper.dump(item,
|
|
817
|
+
indent: nil)}) can't be imploded, unicode codepoint needs to be numeric"
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
codepoint = item.to_f.finite? ? item.to_i : -1
|
|
821
|
+
codepoint = 0xFFFD if codepoint.negative? || codepoint > 0x10FFFF || codepoint.between?(0xD800, 0xDFFF)
|
|
822
|
+
[codepoint].pack('U')
|
|
823
|
+
end.join
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def join(input, separator)
|
|
827
|
+
out = +''
|
|
828
|
+
assert_array(input).each_with_index do |item, index|
|
|
829
|
+
out << separator.to_s if index.positive?
|
|
830
|
+
case item
|
|
831
|
+
when nil
|
|
832
|
+
nil
|
|
833
|
+
when String, Numeric, TrueClass, FalseClass
|
|
834
|
+
out << to_string(item)
|
|
835
|
+
else
|
|
836
|
+
raise TypeError,
|
|
837
|
+
"string (#{short_dump(out)}) and #{Value.type_of(item)} (#{short_dump(item)}) cannot be added"
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
out
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def short_dump(value)
|
|
844
|
+
dumped = JSON::Dumper.dump(value, indent: nil)
|
|
845
|
+
return "#{dumped[0, 11]}..." if value.is_a?(Hash) && dumped.length > 14
|
|
846
|
+
|
|
847
|
+
dumped.length > 18 ? "#{dumped[0, 15]}..." : dumped
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def recurse(input, context, args)
|
|
851
|
+
Enumerator.new do |yielder|
|
|
852
|
+
stack = [[input].each]
|
|
853
|
+
until stack.empty?
|
|
854
|
+
begin
|
|
855
|
+
value = stack.last.next
|
|
856
|
+
rescue StopIteration
|
|
857
|
+
stack.pop
|
|
858
|
+
next
|
|
859
|
+
end
|
|
860
|
+
yielder << value
|
|
861
|
+
children = if args.empty?
|
|
862
|
+
case value
|
|
863
|
+
when Array then value.each
|
|
864
|
+
when Hash then value.each_value
|
|
865
|
+
else [].each
|
|
866
|
+
end
|
|
867
|
+
else
|
|
868
|
+
filter_stream(args.first, value, context)
|
|
869
|
+
end
|
|
870
|
+
if args.length > 1
|
|
871
|
+
condition = args[1]
|
|
872
|
+
source_children = children
|
|
873
|
+
children = Enumerator.new do |child_yielder|
|
|
874
|
+
source_children.each do |child|
|
|
875
|
+
filter_stream(condition, child, context).each do |result|
|
|
876
|
+
child_yielder << child if Value.truthy?(result)
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
stack << children.each
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
def delpaths(input, paths)
|
|
887
|
+
raise TypeError, 'Paths must be specified as an array' unless paths.is_a?(Array)
|
|
888
|
+
|
|
889
|
+
copy = Value.deep_copy(input)
|
|
890
|
+
return nil if paths.any?(&:empty?)
|
|
891
|
+
|
|
892
|
+
ordered_delete_paths(paths).each { |path| Path.delete(copy, path) }
|
|
893
|
+
copy
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def delete_paths(input, context, args)
|
|
897
|
+
copy = Value.deep_copy(input)
|
|
898
|
+
paths = args.flat_map { |arg| collect_paths(arg, input, context) }
|
|
899
|
+
return nil if paths.any?(&:empty?)
|
|
900
|
+
|
|
901
|
+
ordered_delete_paths(paths).each { |path| Path.delete(copy, path) }
|
|
902
|
+
copy
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def pick(input, context, args)
|
|
906
|
+
paths = args.flat_map { |arg| collect_paths(arg, input, context) }
|
|
907
|
+
return Value.deep_copy(input) if paths.any?(&:empty?)
|
|
908
|
+
|
|
909
|
+
paths.each { |path| validate_pick_path(path) }
|
|
910
|
+
root = nil
|
|
911
|
+
paths.each do |path|
|
|
912
|
+
root ||= container_for_path(path)
|
|
913
|
+
Path.set(root, path, Value.deep_copy(Path.get(input, path)))
|
|
914
|
+
end
|
|
915
|
+
root
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def validate_pick_path(path)
|
|
919
|
+
return unless path.any? { |part| part.is_a?(Integer) && part.negative? }
|
|
920
|
+
|
|
921
|
+
raise RuntimeError, 'Out of bounds negative array index'
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def container_for_path(path)
|
|
925
|
+
path.first.is_a?(Numeric) ? [] : {}
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def paths_builtin(input, context, args, leaves_only:)
|
|
929
|
+
paths = Path.paths(input, leaves_only: leaves_only)
|
|
930
|
+
return paths.reject(&:empty?) if args.empty?
|
|
931
|
+
|
|
932
|
+
filter = args.fetch(0)
|
|
933
|
+
Enumerator.new do |yielder|
|
|
934
|
+
paths.each do |path|
|
|
935
|
+
filter_stream(filter, Path.get(input, path), context).each do |value|
|
|
936
|
+
yielder << path if !path.empty? && Value.truthy?(value)
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
end
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def walk(input, context, filter)
|
|
943
|
+
Enumerator.new do |yielder|
|
|
944
|
+
transformed =
|
|
945
|
+
case input
|
|
946
|
+
when Array
|
|
947
|
+
input.flat_map { |item| walk(item, context, filter).to_a }
|
|
948
|
+
when Hash
|
|
949
|
+
input.each_with_object({}) do |(key, value), out|
|
|
950
|
+
values = walk(value, context, filter).take(1)
|
|
951
|
+
out[key] = values.first unless values.empty?
|
|
952
|
+
end
|
|
953
|
+
else
|
|
954
|
+
input
|
|
955
|
+
end
|
|
956
|
+
filter_stream(filter, transformed, context).each { |value| yielder << value }
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def to_stream(value)
|
|
961
|
+
Enumerator.new do |yielder|
|
|
962
|
+
stack = [[:visit, value, []]]
|
|
963
|
+
until stack.empty?
|
|
964
|
+
type, current, path = stack.pop
|
|
965
|
+
if type == :emit
|
|
966
|
+
yielder << current
|
|
967
|
+
next
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
children = if current.is_a?(Array)
|
|
971
|
+
current.each_with_index.map { |item, index| [item, path + [index]] }
|
|
972
|
+
elsif current.is_a?(Hash)
|
|
973
|
+
current.map { |key, item| [item, path + [key]] }
|
|
974
|
+
end
|
|
975
|
+
if children.nil?
|
|
976
|
+
yielder << [path, current]
|
|
977
|
+
elsif children.empty?
|
|
978
|
+
yielder << [path, current.class.new]
|
|
979
|
+
else
|
|
980
|
+
last_component = current.is_a?(Array) ? current.length - 1 : current.keys.last
|
|
981
|
+
stack << [:emit, [path + [last_component]], nil]
|
|
982
|
+
children.reverse_each { |child, child_path| stack << [:visit, child, child_path] }
|
|
983
|
+
end
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
def from_stream(stream)
|
|
989
|
+
Enumerator.new do |yielder|
|
|
990
|
+
root = nil
|
|
991
|
+
stream.each do |event|
|
|
992
|
+
event = assert_array(event)
|
|
993
|
+
path = assert_array(event.first)
|
|
994
|
+
if event.length == 1
|
|
995
|
+
if path.length == 1 && !root.nil?
|
|
996
|
+
yielder << root
|
|
997
|
+
root = nil
|
|
998
|
+
end
|
|
999
|
+
next
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
value = event[1]
|
|
1003
|
+
if path.empty?
|
|
1004
|
+
yielder << value
|
|
1005
|
+
root = nil
|
|
1006
|
+
else
|
|
1007
|
+
root ||= container_for_path(path)
|
|
1008
|
+
Path.set(root, path, value)
|
|
1009
|
+
end
|
|
1010
|
+
end
|
|
1011
|
+
yielder << root unless root.nil?
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def truncate_stream(input, context, args)
|
|
1016
|
+
depth = input
|
|
1017
|
+
Enumerator.new do |yielder|
|
|
1018
|
+
filter_stream(args.fetch(0), nil, context).each do |event|
|
|
1019
|
+
path = Path.read_index(event, 0)
|
|
1020
|
+
next unless Value.compare(length(path), depth).positive?
|
|
1021
|
+
|
|
1022
|
+
updated = event.nil? ? [] : event.dup
|
|
1023
|
+
updated[0] = truncate_path(path, depth)
|
|
1024
|
+
yielder << updated
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def truncate_path(path, depth)
|
|
1030
|
+
return nil if path.nil?
|
|
1031
|
+
unless path.is_a?(Array) || path.is_a?(String)
|
|
1032
|
+
raise TypeError, "Cannot index #{Value.type_of(path)} with object"
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
start = truncate_boundary(depth, path.is_a?(String) ? path.each_char.count : path.length)
|
|
1036
|
+
return path.each_char.drop(start).join if path.is_a?(String)
|
|
1037
|
+
|
|
1038
|
+
path.drop(start)
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def truncate_boundary(depth, length)
|
|
1042
|
+
return 0 if depth.nil? || (depth.respond_to?(:nan?) && depth.nan?)
|
|
1043
|
+
raise TypeError, 'Array/string slice indices must be integers' unless depth.is_a?(Numeric)
|
|
1044
|
+
return depth.negative? ? 0 : length if depth.respond_to?(:infinite?) && depth.infinite?
|
|
1045
|
+
|
|
1046
|
+
index = depth.floor
|
|
1047
|
+
index += length if index.negative?
|
|
1048
|
+
[[index, 0].max, length].min
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
def extreme(input, mode)
|
|
1052
|
+
array = assert_array(input)
|
|
1053
|
+
return nil if array.empty?
|
|
1054
|
+
|
|
1055
|
+
array.public_send(mode) { |a, b| Value.compare(a, b) }
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
def extreme_by(input, context, filter, mode)
|
|
1059
|
+
array = assert_array(input)
|
|
1060
|
+
return nil if array.empty?
|
|
1061
|
+
|
|
1062
|
+
best = array.first
|
|
1063
|
+
best_key = filter_key(best, context, filter)
|
|
1064
|
+
array.drop(1).each do |item|
|
|
1065
|
+
key = filter_key(item, context, filter)
|
|
1066
|
+
comparison = Value.compare(key, best_key)
|
|
1067
|
+
if (mode == :min && comparison.negative?) || (mode == :max && comparison >= 0)
|
|
1068
|
+
best = item
|
|
1069
|
+
best_key = key
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
best
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
def sort_by_filter(input, context, filter)
|
|
1076
|
+
decorated_sort(input, context, filter).map(&:first)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
def group_by_filter(input, context, filter)
|
|
1080
|
+
decorated_sort(input, context, filter).each_with_object([]) do |(item, key), groups|
|
|
1081
|
+
if groups.empty? || !Value.equal?(groups.last.fetch(:key), key)
|
|
1082
|
+
groups << { key: key, values: [item] }
|
|
1083
|
+
else
|
|
1084
|
+
groups.last.fetch(:values) << item
|
|
1085
|
+
end
|
|
1086
|
+
end.map { |group| group.fetch(:values) }
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def unique_values(array)
|
|
1090
|
+
array = array.sort { |a, b| Value.compare(a, b) }
|
|
1091
|
+
array.each_with_object([]) do |item, out|
|
|
1092
|
+
out << item if out.empty? || !Value.equal?(out.last, item)
|
|
1093
|
+
end
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def unique_by_filter(input, context, filter)
|
|
1097
|
+
unique = decorated_sort(input, context, filter).each_with_object([]) do |(item, key), out|
|
|
1098
|
+
out << [item, key] if out.empty? || !Value.equal?(out.last.last, key)
|
|
1099
|
+
end
|
|
1100
|
+
unique.map(&:first)
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
def decorated_sort(input, context, filter)
|
|
1104
|
+
assert_array(input).each_with_index.map do |item, index|
|
|
1105
|
+
[item, filter_key(item, context, filter), index]
|
|
1106
|
+
end.sort do |left, right|
|
|
1107
|
+
comparison = Value.compare(left[1], right[1])
|
|
1108
|
+
comparison.zero? ? left[2] <=> right[2] : comparison
|
|
1109
|
+
end
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
def index_sql(input, context, args)
|
|
1113
|
+
source =
|
|
1114
|
+
if args.length == 2
|
|
1115
|
+
collect_filter(args[0], input, context)
|
|
1116
|
+
else
|
|
1117
|
+
assert_array(input)
|
|
1118
|
+
end
|
|
1119
|
+
filter = args.length == 2 ? args[1] : args.fetch(0)
|
|
1120
|
+
source.to_h do |item|
|
|
1121
|
+
[to_string(filter_key(item, context, filter)), item]
|
|
1122
|
+
end
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def join_sql(input, context, args)
|
|
1126
|
+
index = eval_arg(args, 0, input, context)
|
|
1127
|
+
filter = args.fetch(1)
|
|
1128
|
+
assert_array(input).map do |item|
|
|
1129
|
+
key = to_string(filter_key(item, context, filter))
|
|
1130
|
+
[item, index[key]]
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def in_sql?(input, context, args)
|
|
1135
|
+
if args.length == 1
|
|
1136
|
+
values = collect_filter(args.fetch(0), input, context)
|
|
1137
|
+
values = values.first if values.length == 1 && values.first.is_a?(Array)
|
|
1138
|
+
return values.any? { |item| Value.equal?(item, input) }
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
source_any?(args.fetch(0), input, context) do |item|
|
|
1142
|
+
source_any?(args.fetch(1), input, context) { |needle| Value.equal?(item, needle) }
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
def filter_key(value, context, filter)
|
|
1147
|
+
result = collect_filter(filter, value, context)
|
|
1148
|
+
result.length == 1 ? result.first : result
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
def contains?(container, contained)
|
|
1152
|
+
tasks = [[:evaluate, container, contained]]
|
|
1153
|
+
results = []
|
|
1154
|
+
until tasks.empty?
|
|
1155
|
+
action, *values = tasks.pop
|
|
1156
|
+
case action
|
|
1157
|
+
when :evaluate
|
|
1158
|
+
candidate, needle = values
|
|
1159
|
+
if candidate.is_a?(Hash) && needle.is_a?(Hash)
|
|
1160
|
+
entries = needle.to_a
|
|
1161
|
+
unless entries.all? { |key, _value| candidate.key?(key) }
|
|
1162
|
+
results << false
|
|
1163
|
+
next
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
tasks << [:hash_all, candidate, entries, 0]
|
|
1167
|
+
elsif candidate.is_a?(Array) && needle.is_a?(Array)
|
|
1168
|
+
tasks << [:array_all, candidate, needle, 0]
|
|
1169
|
+
elsif candidate.is_a?(String) && needle.is_a?(String)
|
|
1170
|
+
results << candidate.include?(needle)
|
|
1171
|
+
else
|
|
1172
|
+
results << Value.equal?(candidate, needle)
|
|
1173
|
+
end
|
|
1174
|
+
when :hash_all
|
|
1175
|
+
candidate, entries, index = values
|
|
1176
|
+
if index >= entries.length
|
|
1177
|
+
results << true
|
|
1178
|
+
else
|
|
1179
|
+
key, needle = entries[index]
|
|
1180
|
+
tasks << [:hash_after, candidate, entries, index]
|
|
1181
|
+
tasks << [:evaluate, candidate.fetch(key), needle]
|
|
1182
|
+
end
|
|
1183
|
+
when :hash_after
|
|
1184
|
+
candidate, entries, index = values
|
|
1185
|
+
if results.pop
|
|
1186
|
+
tasks << [:hash_all, candidate, entries, index + 1]
|
|
1187
|
+
else
|
|
1188
|
+
results << false
|
|
1189
|
+
end
|
|
1190
|
+
when :array_all
|
|
1191
|
+
candidate, needles, needle_index = values
|
|
1192
|
+
if needle_index >= needles.length
|
|
1193
|
+
results << true
|
|
1194
|
+
elsif candidate.empty?
|
|
1195
|
+
results << false
|
|
1196
|
+
else
|
|
1197
|
+
tasks << [:array_all_after, candidate, needles, needle_index]
|
|
1198
|
+
tasks << [:array_any, candidate, needles.fetch(needle_index), 0]
|
|
1199
|
+
end
|
|
1200
|
+
when :array_all_after
|
|
1201
|
+
candidate, needles, needle_index = values
|
|
1202
|
+
if results.pop
|
|
1203
|
+
tasks << [:array_all, candidate, needles, needle_index + 1]
|
|
1204
|
+
else
|
|
1205
|
+
results << false
|
|
1206
|
+
end
|
|
1207
|
+
when :array_any
|
|
1208
|
+
candidate, needle, candidate_index = values
|
|
1209
|
+
if candidate_index >= candidate.length
|
|
1210
|
+
results << false
|
|
1211
|
+
else
|
|
1212
|
+
tasks << [:array_any_after, candidate, needle, candidate_index]
|
|
1213
|
+
tasks << [:evaluate, candidate.fetch(candidate_index), needle]
|
|
1214
|
+
end
|
|
1215
|
+
when :array_any_after
|
|
1216
|
+
candidate, needle, candidate_index = values
|
|
1217
|
+
if results.pop
|
|
1218
|
+
results << true
|
|
1219
|
+
else
|
|
1220
|
+
tasks << [:array_any, candidate, needle, candidate_index + 1]
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
end
|
|
1224
|
+
results.fetch(0)
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
def index_of(input, needle)
|
|
1228
|
+
if input.is_a?(String)
|
|
1229
|
+
validate_string_search_needle(input, needle)
|
|
1230
|
+
return nil if needle.empty?
|
|
1231
|
+
|
|
1232
|
+
return input.index(needle)
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
return unsupported_search_result(input, needle) unless input.is_a?(Array)
|
|
1236
|
+
|
|
1237
|
+
needle = [needle] unless needle.is_a?(Array)
|
|
1238
|
+
max = input.length - assert_array(needle).length
|
|
1239
|
+
return nil if needle.empty?
|
|
1240
|
+
|
|
1241
|
+
(0..max).find { |index| array_slice_equal?(input, needle, index) }
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
def rindex_of(input, needle)
|
|
1245
|
+
if input.is_a?(String)
|
|
1246
|
+
validate_string_search_needle(input, needle)
|
|
1247
|
+
return nil if needle.empty?
|
|
1248
|
+
|
|
1249
|
+
return input.rindex(needle)
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
return unsupported_search_result(input, needle) unless input.is_a?(Array)
|
|
1253
|
+
|
|
1254
|
+
needle = [needle] unless needle.is_a?(Array)
|
|
1255
|
+
max = input.length - assert_array(needle).length
|
|
1256
|
+
return nil if needle.empty?
|
|
1257
|
+
|
|
1258
|
+
max.downto(0).find { |index| array_slice_equal?(input, needle, index) }
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
def indices_of(input, needle)
|
|
1262
|
+
if input.is_a?(String)
|
|
1263
|
+
positions = []
|
|
1264
|
+
offset = 0
|
|
1265
|
+
validate_string_search_needle(input, needle)
|
|
1266
|
+
return [] if needle.empty?
|
|
1267
|
+
|
|
1268
|
+
while (found = input.index(needle, offset))
|
|
1269
|
+
positions << found
|
|
1270
|
+
offset = found + 1
|
|
1271
|
+
end
|
|
1272
|
+
return positions
|
|
1273
|
+
end
|
|
1274
|
+
return unsupported_search_result(input, needle) unless input.is_a?(Array)
|
|
1275
|
+
|
|
1276
|
+
needle = [needle] unless needle.is_a?(Array)
|
|
1277
|
+
return [] if needle.empty?
|
|
1278
|
+
|
|
1279
|
+
max = input.length - needle.length
|
|
1280
|
+
(0..max).select { |index| array_slice_equal?(input, needle, index) }
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
def validate_string_search_needle(input, needle)
|
|
1284
|
+
return if needle.is_a?(String)
|
|
1285
|
+
raise TypeError, 'Array/string slice indices must be integers' if needle.is_a?(Hash)
|
|
1286
|
+
|
|
1287
|
+
Path.read_index(input, needle)
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
def unsupported_search_result(input, needle)
|
|
1291
|
+
if input.is_a?(Hash)
|
|
1292
|
+
Path.read_index(input, needle) unless needle.is_a?(String)
|
|
1293
|
+
return nil
|
|
1294
|
+
end
|
|
1295
|
+
if input.nil?
|
|
1296
|
+
return nil if needle.is_a?(String) || needle.is_a?(Numeric) || needle.is_a?(Hash)
|
|
1297
|
+
|
|
1298
|
+
Path.read_index(input, needle)
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
Path.read_index(input, needle)
|
|
1302
|
+
nil
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
def array_slice_equal?(input, needle, index)
|
|
1306
|
+
needle.each_with_index.all? { |item, offset| Value.equal?(input[index + offset], item) }
|
|
1307
|
+
end
|
|
1308
|
+
|
|
1309
|
+
def combinations(input, context, args)
|
|
1310
|
+
if args.length == 1
|
|
1311
|
+
raw_count = eval_arg(args, 0, input, context)
|
|
1312
|
+
raise RuntimeError, 'Range bounds must be numeric' unless raw_count.is_a?(Numeric)
|
|
1313
|
+
|
|
1314
|
+
count = raw_count.ceil
|
|
1315
|
+
return [[]] if count.negative?
|
|
1316
|
+
|
|
1317
|
+
arrays = Array.new(count) { combination_array(input) }
|
|
1318
|
+
else
|
|
1319
|
+
source = args.empty? ? assert_array(input) : eval_arg(args, 0, input, context)
|
|
1320
|
+
arrays = assert_array(source)
|
|
1321
|
+
end
|
|
1322
|
+
arrays.reduce([[]]) do |acc, array|
|
|
1323
|
+
assert_array(array)
|
|
1324
|
+
acc.flat_map { |prefix| array.map { |item| prefix + [item] } }
|
|
1325
|
+
end
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1328
|
+
def transpose(input)
|
|
1329
|
+
rows = assert_array(input).map { |row| assert_array(row) }
|
|
1330
|
+
max = rows.map(&:length).max || 0
|
|
1331
|
+
(0...max).map { |index| rows.map { |row| row[index] } }
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
def combination_array(value)
|
|
1335
|
+
return value if value.is_a?(Array)
|
|
1336
|
+
|
|
1337
|
+
raise TypeError,
|
|
1338
|
+
"Cannot iterate over #{Value.type_of(value)} (#{JSON::Dumper.dump(value, indent: nil)})"
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1341
|
+
def bsearch(input, context, args)
|
|
1342
|
+
array = assert_array(input)
|
|
1343
|
+
args.flat_map do |arg|
|
|
1344
|
+
arg.eval(input, context).map do |needle|
|
|
1345
|
+
found = array.bsearch_index { |item| Value.compare(item, needle) >= 0 }
|
|
1346
|
+
found && Value.equal?(array[found], needle) ? found : -((found || array.length) + 1)
|
|
1347
|
+
end
|
|
1348
|
+
end
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def first_builtin(input, context, args)
|
|
1352
|
+
values = args.empty? ? assert_array(input) : args.fetch(0).take(input, context, 1)
|
|
1353
|
+
values.empty? ? [] : [values.first]
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def last_builtin(input, context, args)
|
|
1357
|
+
values = args.empty? ? assert_array(input) : collect_filter(args.fetch(0), input, context)
|
|
1358
|
+
values.empty? ? [] : [values.last]
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
def nth(input, context, args)
|
|
1362
|
+
Enumerator.new do |yielder|
|
|
1363
|
+
filter_stream(args.fetch(0), input, context).each do |raw_index|
|
|
1364
|
+
if args.length == 1
|
|
1365
|
+
yielder << nth_index_value(input, raw_index)
|
|
1366
|
+
next
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
index = nth_filter_index(raw_index)
|
|
1370
|
+
if index.respond_to?(:infinite?) && index.infinite?
|
|
1371
|
+
filter_stream(args[1], input, context).each { |_value| nil }
|
|
1372
|
+
next
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
values = args[1].take(input, context, index + 1)
|
|
1376
|
+
yielder << values[index] if index < values.length
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
def nth_index_value(input, raw_index)
|
|
1382
|
+
index = if raw_index.is_a?(Numeric) && (!raw_index.respond_to?(:finite?) || raw_index.finite?)
|
|
1383
|
+
raw_index.to_i
|
|
1384
|
+
else
|
|
1385
|
+
raw_index
|
|
1386
|
+
end
|
|
1387
|
+
Path.read_index(input, index)
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
def nth_filter_index(value)
|
|
1391
|
+
if value.is_a?(Numeric)
|
|
1392
|
+
raise RuntimeError, "nth doesn't support negative indices" if value.respond_to?(:nan?) && value.nan?
|
|
1393
|
+
raise RuntimeError, "nth doesn't support negative indices" if value < 0
|
|
1394
|
+
return value if value.respond_to?(:infinite?) && value.infinite?
|
|
1395
|
+
|
|
1396
|
+
return value.ceil
|
|
1397
|
+
end
|
|
1398
|
+
if value.nil? || value == true || value == false
|
|
1399
|
+
raise RuntimeError, "nth doesn't support negative indices"
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
raise TypeError,
|
|
1403
|
+
"#{Value.type_of(value)} (#{short_dump(value)}) and number (1) cannot be added"
|
|
1404
|
+
end
|
|
1405
|
+
|
|
1406
|
+
def limit(input, context, args)
|
|
1407
|
+
Enumerator.new do |yielder|
|
|
1408
|
+
filter_stream(args.fetch(0), input, context).each do |raw_count|
|
|
1409
|
+
count = numeric(raw_count).ceil
|
|
1410
|
+
next if count <= 0
|
|
1411
|
+
|
|
1412
|
+
emitted = 0
|
|
1413
|
+
filter_stream(args.fetch(1), input, context).each do |value|
|
|
1414
|
+
yielder << value
|
|
1415
|
+
emitted += 1
|
|
1416
|
+
break if emitted >= count
|
|
1417
|
+
end
|
|
1418
|
+
end
|
|
1419
|
+
end
|
|
1420
|
+
end
|
|
1421
|
+
|
|
1422
|
+
def until_filter(input, context, args)
|
|
1423
|
+
condition, update = args
|
|
1424
|
+
Enumerator.new do |yielder|
|
|
1425
|
+
tasks = [[:visit, input]]
|
|
1426
|
+
until tasks.empty?
|
|
1427
|
+
type, value, state = tasks.pop
|
|
1428
|
+
if type == :emit
|
|
1429
|
+
yielder << value
|
|
1430
|
+
next
|
|
1431
|
+
end
|
|
1432
|
+
if type == :visit
|
|
1433
|
+
tasks << [:condition, value, filter_stream(condition, value, context)]
|
|
1434
|
+
next
|
|
1435
|
+
end
|
|
1436
|
+
|
|
1437
|
+
if type == :condition
|
|
1438
|
+
begin
|
|
1439
|
+
result = state.next
|
|
1440
|
+
tasks << [:condition, value, state]
|
|
1441
|
+
if Value.truthy?(result)
|
|
1442
|
+
tasks << [:emit, value]
|
|
1443
|
+
else
|
|
1444
|
+
tasks << [:updates, value, filter_stream(update, value, context)]
|
|
1445
|
+
end
|
|
1446
|
+
rescue StopIteration
|
|
1447
|
+
nil
|
|
1448
|
+
end
|
|
1449
|
+
elsif type == :updates
|
|
1450
|
+
begin
|
|
1451
|
+
next_value = state.next
|
|
1452
|
+
tasks << [:updates, value, state]
|
|
1453
|
+
tasks << [:visit, next_value]
|
|
1454
|
+
rescue StopIteration
|
|
1455
|
+
nil
|
|
1456
|
+
end
|
|
1457
|
+
end
|
|
1458
|
+
end
|
|
1459
|
+
end
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
def while_filter(input, context, args)
|
|
1463
|
+
condition, update = args
|
|
1464
|
+
Enumerator.new do |yielder|
|
|
1465
|
+
tasks = [[:visit, input]]
|
|
1466
|
+
until tasks.empty?
|
|
1467
|
+
type, value, state = tasks.pop
|
|
1468
|
+
if type == :emit
|
|
1469
|
+
yielder << value
|
|
1470
|
+
next
|
|
1471
|
+
end
|
|
1472
|
+
if type == :visit
|
|
1473
|
+
tasks << [:condition, value, filter_stream(condition, value, context)]
|
|
1474
|
+
next
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
if type == :condition
|
|
1478
|
+
begin
|
|
1479
|
+
result = state.next
|
|
1480
|
+
tasks << [:condition, value, state]
|
|
1481
|
+
if Value.truthy?(result)
|
|
1482
|
+
tasks << [:updates, value, filter_stream(update, value, context)]
|
|
1483
|
+
tasks << [:emit, value]
|
|
1484
|
+
end
|
|
1485
|
+
rescue StopIteration
|
|
1486
|
+
nil
|
|
1487
|
+
end
|
|
1488
|
+
elsif type == :updates
|
|
1489
|
+
begin
|
|
1490
|
+
next_value = state.next
|
|
1491
|
+
tasks << [:updates, value, state]
|
|
1492
|
+
tasks << [:visit, next_value]
|
|
1493
|
+
rescue StopIteration
|
|
1494
|
+
nil
|
|
1495
|
+
end
|
|
1496
|
+
end
|
|
1497
|
+
end
|
|
1498
|
+
end
|
|
1499
|
+
end
|
|
1500
|
+
|
|
1501
|
+
def repeat_filter(input, context, args)
|
|
1502
|
+
Enumerator.new do |yielder|
|
|
1503
|
+
loop do
|
|
1504
|
+
filter_stream(args.fetch(0), input, context).each { |value| yielder << value }
|
|
1505
|
+
end
|
|
1506
|
+
end
|
|
1507
|
+
end
|
|
1508
|
+
|
|
1509
|
+
def time_array(time)
|
|
1510
|
+
[time.year, time.month - 1, time.day, time.hour, time.min, time.sec, time.wday, time.yday - 1]
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
def mktime(input)
|
|
1514
|
+
values = assert_array(input)
|
|
1515
|
+
raise TypeError, 'mktime requires parsed datetime inputs' unless values.first(6).all?(Numeric)
|
|
1516
|
+
|
|
1517
|
+
Time.utc(values[0], values[1] + 1, values[2], values[3], values[4], values[5]).to_f
|
|
1518
|
+
rescue ArgumentError
|
|
1519
|
+
raise TypeError, 'mktime requires parsed datetime inputs'
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
def strftime_builtin(input, context, args, local: false)
|
|
1523
|
+
format = assert_string(eval_arg(args, 0, input, context))
|
|
1524
|
+
time =
|
|
1525
|
+
if input.is_a?(Array)
|
|
1526
|
+
values = input
|
|
1527
|
+
unless values.first(6).all?(Numeric)
|
|
1528
|
+
raise TypeError,
|
|
1529
|
+
"#{local ? 'strflocaltime' : 'strftime'}/1 requires parsed datetime inputs"
|
|
1530
|
+
end
|
|
1531
|
+
|
|
1532
|
+
if local
|
|
1533
|
+
Time.local(values[0], values[1] + 1, values[2], values[3], values[4],
|
|
1534
|
+
values[5])
|
|
1535
|
+
else
|
|
1536
|
+
Time.utc(values[0], values[1] + 1, values[2], values[3], values[4], values[5])
|
|
1537
|
+
end
|
|
1538
|
+
else
|
|
1539
|
+
raise TypeError, 'strflocaltime/1 requires parsed datetime inputs' if local
|
|
1540
|
+
|
|
1541
|
+
Time.at(numeric(input)).utc
|
|
1542
|
+
end
|
|
1543
|
+
time.strftime(format)
|
|
1544
|
+
rescue ArgumentError
|
|
1545
|
+
raise TypeError, "#{local ? 'strflocaltime' : 'strftime'}/1 requires parsed datetime inputs"
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
def strptime(input, context, args)
|
|
1549
|
+
parsed = DateTime.strptime(assert_string(input), assert_string(eval_arg(args, 0, input, context)))
|
|
1550
|
+
[parsed.year, parsed.month - 1, parsed.day, parsed.hour, parsed.min, parsed.sec, parsed.wday, parsed.yday - 1]
|
|
1551
|
+
rescue Date::Error
|
|
1552
|
+
raise RuntimeError, 'date does not match format'
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
def regexp(input, context, args)
|
|
1556
|
+
pattern, flags = regexp_parts(input, context, args)
|
|
1557
|
+
unknown_flags = flags.each_char.uniq - %w[g i m n p s l x]
|
|
1558
|
+
raise RuntimeError, "unsupported regular expression flag: #{unknown_flags.first}" unless unknown_flags.empty?
|
|
1559
|
+
|
|
1560
|
+
dot_matches_newline = flags.include?('m') || flags.include?('p')
|
|
1561
|
+
pattern = jq_regexp_pattern(pattern, dot_matches_newline: dot_matches_newline)
|
|
1562
|
+
options = 0
|
|
1563
|
+
options |= Regexp::IGNORECASE if flags.include?('i')
|
|
1564
|
+
options |= Regexp::MULTILINE if flags.include?('m') || flags.include?('p')
|
|
1565
|
+
options |= Regexp::EXTENDED if flags.include?('x')
|
|
1566
|
+
timeout = context.options[:regexp_timeout]
|
|
1567
|
+
regex = if timeout.nil?
|
|
1568
|
+
Regexp.new(pattern, options)
|
|
1569
|
+
elsif Regexp.respond_to?(:timeout)
|
|
1570
|
+
Regexp.new(pattern, options, timeout: timeout)
|
|
1571
|
+
else
|
|
1572
|
+
raise RuntimeError, 'regular expression timeout is not supported by this Ruby'
|
|
1573
|
+
end
|
|
1574
|
+
[regex, flags]
|
|
1575
|
+
rescue RegexpError, ArgumentError => e
|
|
1576
|
+
raise RuntimeError, e.message.to_s
|
|
1577
|
+
end
|
|
1578
|
+
|
|
1579
|
+
def jq_regexp_pattern(pattern, dot_matches_newline:)
|
|
1580
|
+
chars = pattern.each_char.to_a
|
|
1581
|
+
transformed, = transform_regexp_segment(chars, 0, line_anchors: false,
|
|
1582
|
+
dot_matches_newline: dot_matches_newline)
|
|
1583
|
+
transformed
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
def transform_regexp_segment(chars, index, line_anchors:, dot_matches_newline:, stop_at_group_end: false)
|
|
1587
|
+
output = +''
|
|
1588
|
+
while index < chars.length
|
|
1589
|
+
char = chars[index]
|
|
1590
|
+
if char == '\\'
|
|
1591
|
+
output << char
|
|
1592
|
+
index += 1
|
|
1593
|
+
output << chars[index] if index < chars.length
|
|
1594
|
+
elsif char == '['
|
|
1595
|
+
character_class, index = consume_regexp_character_class(chars, index)
|
|
1596
|
+
output << character_class
|
|
1597
|
+
next
|
|
1598
|
+
elsif char == '(' && (inline = scoped_regexp_options(chars, index))
|
|
1599
|
+
enabled, disabled, body_index = inline
|
|
1600
|
+
child_line_anchors = option_state(line_anchors, enabled, disabled, 'm')
|
|
1601
|
+
child_dot_matches_newline = option_state(dot_matches_newline, enabled, disabled, 's')
|
|
1602
|
+
body, next_index, closed = transform_regexp_segment(
|
|
1603
|
+
chars, body_index, line_anchors: child_line_anchors,
|
|
1604
|
+
dot_matches_newline: child_dot_matches_newline, stop_at_group_end: true
|
|
1605
|
+
)
|
|
1606
|
+
output << if closed
|
|
1607
|
+
ruby_regexp_group(enabled, disabled, dot_matches_newline, child_dot_matches_newline, body)
|
|
1608
|
+
else
|
|
1609
|
+
chars[index...body_index].join + body
|
|
1610
|
+
end
|
|
1611
|
+
index = next_index
|
|
1612
|
+
next
|
|
1613
|
+
elsif char == '('
|
|
1614
|
+
body, next_index, closed = transform_regexp_segment(
|
|
1615
|
+
chars, index + 1, line_anchors: line_anchors,
|
|
1616
|
+
dot_matches_newline: dot_matches_newline, stop_at_group_end: true
|
|
1617
|
+
)
|
|
1618
|
+
output << "(#{body}"
|
|
1619
|
+
output << ')' if closed
|
|
1620
|
+
index = next_index
|
|
1621
|
+
next
|
|
1622
|
+
elsif char == ')' && stop_at_group_end
|
|
1623
|
+
return [output, index + 1, true]
|
|
1624
|
+
elsif char == '^'
|
|
1625
|
+
output << (line_anchors ? '^' : '\\A')
|
|
1626
|
+
elsif char == '$'
|
|
1627
|
+
output << (line_anchors ? '$' : '\\Z')
|
|
1628
|
+
else
|
|
1629
|
+
output << char
|
|
1630
|
+
end
|
|
1631
|
+
index += 1
|
|
1632
|
+
end
|
|
1633
|
+
[output, index, !stop_at_group_end]
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
def consume_regexp_character_class(chars, index)
|
|
1637
|
+
output = +'['
|
|
1638
|
+
index += 1
|
|
1639
|
+
if chars[index] == '^'
|
|
1640
|
+
output << '^'
|
|
1641
|
+
index += 1
|
|
1642
|
+
end
|
|
1643
|
+
if chars[index] == ']'
|
|
1644
|
+
output << '\\]'
|
|
1645
|
+
index += 1
|
|
1646
|
+
end
|
|
1647
|
+
while index < chars.length
|
|
1648
|
+
char = chars[index]
|
|
1649
|
+
if char == '\\'
|
|
1650
|
+
output << char
|
|
1651
|
+
index += 1
|
|
1652
|
+
output << chars[index] if index < chars.length
|
|
1653
|
+
elsif char == '[' && %w[: . =].include?(chars[index + 1])
|
|
1654
|
+
marker = chars[index + 1]
|
|
1655
|
+
closing = "#{marker}]"
|
|
1656
|
+
while index < chars.length
|
|
1657
|
+
output << chars[index]
|
|
1658
|
+
index += 1
|
|
1659
|
+
next unless output.end_with?(closing)
|
|
1660
|
+
|
|
1661
|
+
break
|
|
1662
|
+
end
|
|
1663
|
+
next
|
|
1664
|
+
elsif char == ']'
|
|
1665
|
+
output << char
|
|
1666
|
+
return [output, index + 1]
|
|
1667
|
+
else
|
|
1668
|
+
output << char
|
|
1669
|
+
end
|
|
1670
|
+
index += 1
|
|
1671
|
+
end
|
|
1672
|
+
[output, index]
|
|
1673
|
+
end
|
|
1674
|
+
|
|
1675
|
+
def scoped_regexp_options(chars, index)
|
|
1676
|
+
return unless chars[index, 2] == ['(', '?']
|
|
1677
|
+
|
|
1678
|
+
cursor = index + 2
|
|
1679
|
+
enabled = +''
|
|
1680
|
+
while cursor < chars.length && %w[i m s x].include?(chars[cursor])
|
|
1681
|
+
enabled << chars[cursor]
|
|
1682
|
+
cursor += 1
|
|
1683
|
+
end
|
|
1684
|
+
disabled = +''
|
|
1685
|
+
if chars[cursor] == '-'
|
|
1686
|
+
cursor += 1
|
|
1687
|
+
while cursor < chars.length && %w[i m s x].include?(chars[cursor])
|
|
1688
|
+
disabled << chars[cursor]
|
|
1689
|
+
cursor += 1
|
|
1690
|
+
end
|
|
1691
|
+
end
|
|
1692
|
+
return unless chars[cursor] == ':' && !(enabled.empty? && disabled.empty?)
|
|
1693
|
+
|
|
1694
|
+
[enabled, disabled, cursor + 1]
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1697
|
+
def option_state(current, enabled, disabled, option)
|
|
1698
|
+
return true if enabled.include?(option)
|
|
1699
|
+
return false if disabled.include?(option)
|
|
1700
|
+
|
|
1701
|
+
current
|
|
1702
|
+
end
|
|
1703
|
+
|
|
1704
|
+
def ruby_regexp_group(enabled, disabled, parent_dotall, child_dotall, body)
|
|
1705
|
+
ruby_enabled = enabled.each_char.select { |option| %w[i x].include?(option) }
|
|
1706
|
+
ruby_disabled = disabled.each_char.select { |option| %w[i x].include?(option) }
|
|
1707
|
+
ruby_enabled << 'm' if child_dotall && !parent_dotall
|
|
1708
|
+
ruby_disabled << 'm' if parent_dotall && !child_dotall
|
|
1709
|
+
options = ruby_enabled.join
|
|
1710
|
+
options += "-#{ruby_disabled.join}" unless ruby_disabled.empty?
|
|
1711
|
+
options.empty? ? "(?:#{body})" : "(?#{options}:#{body})"
|
|
1712
|
+
end
|
|
1713
|
+
|
|
1714
|
+
def format_builtin(input, context, args)
|
|
1715
|
+
format_name = assert_string(eval_arg(args, 0, input, context))
|
|
1716
|
+
supported = %w[text json html uri csv tsv sh base64 base64d]
|
|
1717
|
+
raise RuntimeError, "format #{format_name.inspect} is not supported" unless supported.include?(format_name)
|
|
1718
|
+
|
|
1719
|
+
dispatch("@#{format_name}", input, context, [])
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
def search_list(context)
|
|
1723
|
+
configured = context.options.fetch(:library_path, [])
|
|
1724
|
+
return configured.map { |path| File.expand_path(path) } unless configured.empty?
|
|
1725
|
+
|
|
1726
|
+
environment = ENV.fetch('JQ_LIBRARY_PATH', '').split(File::PATH_SEPARATOR).reject(&:empty?)
|
|
1727
|
+
(environment + [File.expand_path('~/.jq'), File.expand_path('~/.rjq')]).uniq
|
|
1728
|
+
end
|
|
1729
|
+
|
|
1730
|
+
def regexp_parts(input, context, args)
|
|
1731
|
+
raw = eval_arg(args, 0, input, context)
|
|
1732
|
+
if raw.is_a?(Array)
|
|
1733
|
+
[assert_string(raw[0]), raw[1] ? assert_string(raw[1]) : '']
|
|
1734
|
+
else
|
|
1735
|
+
[assert_string(raw), args.length > 1 ? assert_string(eval_arg(args, 1, input, context)) : '']
|
|
1736
|
+
end
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
def regexp_matches(input, context, args, string)
|
|
1740
|
+
return matches_for_regexp(input, context, args, string) unless args.length > 1
|
|
1741
|
+
|
|
1742
|
+
matches = []
|
|
1743
|
+
filter_stream(args.fetch(1), input, context).each do |flags|
|
|
1744
|
+
flag_args = [args.fetch(0), AST::Literal.new(flags)]
|
|
1745
|
+
matches.concat(matches_for_regexp(input, context, flag_args, string))
|
|
1746
|
+
end
|
|
1747
|
+
matches
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
def matches_for_regexp(input, context, args, string)
|
|
1751
|
+
regex, flags = regexp(input, context, args)
|
|
1752
|
+
matches = string.to_enum(:scan, regex).map { Regexp.last_match }
|
|
1753
|
+
matches.reject { |match| flags.include?('n') && match[0].empty? }
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
def match_builtin(input, context, args)
|
|
1757
|
+
string = assert_string(input)
|
|
1758
|
+
regex, flags = regexp(input, context, args)
|
|
1759
|
+
global = flags.include?('g')
|
|
1760
|
+
matches = global ? string.to_enum(:scan, regex).map { Regexp.last_match } : [regex.match(string)].compact
|
|
1761
|
+
matches = matches.reject { |match| match[0].empty? } if flags.include?('n')
|
|
1762
|
+
matches.map { |match| match_object(match) }
|
|
1763
|
+
end
|
|
1764
|
+
|
|
1765
|
+
def match_object(match)
|
|
1766
|
+
{
|
|
1767
|
+
'offset' => match.begin(0),
|
|
1768
|
+
'length' => match[0].length,
|
|
1769
|
+
'string' => match[0],
|
|
1770
|
+
'captures' => (1...match.length).map do |index|
|
|
1771
|
+
value = match[index]
|
|
1772
|
+
if value.nil? && match[0].empty?
|
|
1773
|
+
value = ''
|
|
1774
|
+
offset = match.begin(0)
|
|
1775
|
+
else
|
|
1776
|
+
offset = value ? match.begin(index) : -1
|
|
1777
|
+
end
|
|
1778
|
+
{ 'offset' => offset, 'length' => value ? value.length : 0, 'string' => value,
|
|
1779
|
+
'name' => capture_name(match, index) }
|
|
1780
|
+
end
|
|
1781
|
+
}
|
|
1782
|
+
end
|
|
1783
|
+
|
|
1784
|
+
def capture_name(match, index)
|
|
1785
|
+
match.names.find { |name| match.regexp.named_captures.fetch(name).include?(index) }
|
|
1786
|
+
end
|
|
1787
|
+
|
|
1788
|
+
def capture_builtin(input, context, args)
|
|
1789
|
+
regex, = regexp(input, context, args)
|
|
1790
|
+
match = regex.match(assert_string(input))
|
|
1791
|
+
return [] unless match
|
|
1792
|
+
|
|
1793
|
+
[match.names.to_h { |name| [name, match[name]] }]
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def scan_builtin(input, context, args)
|
|
1797
|
+
regex, = regexp(input, context, args)
|
|
1798
|
+
assert_string(input).scan(regex).map { |item| item.is_a?(Array) && item.length == 1 ? item.first : item }
|
|
1799
|
+
end
|
|
1800
|
+
|
|
1801
|
+
def substitute(input, context, args, global:)
|
|
1802
|
+
string = assert_string(input)
|
|
1803
|
+
regex, = regexp(input, context, [args.fetch(0)] + args[2..].to_a)
|
|
1804
|
+
replacement_filters(args.fetch(1)).flat_map do |replacement_filter|
|
|
1805
|
+
if global
|
|
1806
|
+
gsub_with_filter(string, regex, replacement_filter,
|
|
1807
|
+
context)
|
|
1808
|
+
else
|
|
1809
|
+
sub_with_filter(string, regex, replacement_filter, context)
|
|
1810
|
+
end
|
|
1811
|
+
end
|
|
1812
|
+
end
|
|
1813
|
+
|
|
1814
|
+
def replacement_filters(node)
|
|
1815
|
+
return node.replacement_filters if node.respond_to?(:replacement_filters)
|
|
1816
|
+
return replacement_filters(node.left) + replacement_filters(node.right) if node.is_a?(AST::Comma)
|
|
1817
|
+
|
|
1818
|
+
[node]
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
def sub_with_filter(string, regex, replacement_filter, context)
|
|
1822
|
+
match = regex.match(string)
|
|
1823
|
+
return [string] unless match
|
|
1824
|
+
|
|
1825
|
+
replacements_for(replacement_filter, match, context).map do |replacement|
|
|
1826
|
+
string[0...match.begin(0)] + replacement + string[match.end(0)..].to_s
|
|
1827
|
+
end
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
def gsub_with_filter(string, regex, replacement_filter, context)
|
|
1831
|
+
matches = string.to_enum(:scan, regex).map { Regexp.last_match }
|
|
1832
|
+
return [string] if matches.empty?
|
|
1833
|
+
|
|
1834
|
+
first_replacements = replacements_for(replacement_filter, matches.first, context)
|
|
1835
|
+
first_replacements.each_index.filter_map do |branch|
|
|
1836
|
+
out = +''
|
|
1837
|
+
offset = 0
|
|
1838
|
+
complete = matches.each_with_index.all? do |match, index|
|
|
1839
|
+
replacements = index.zero? ? first_replacements : replacements_for(replacement_filter, match, context)
|
|
1840
|
+
replacement = replacements[branch]
|
|
1841
|
+
next false unless replacement
|
|
1842
|
+
|
|
1843
|
+
out << string[offset...match.begin(0)].to_s
|
|
1844
|
+
out << replacement
|
|
1845
|
+
offset = match.end(0)
|
|
1846
|
+
true
|
|
1847
|
+
end
|
|
1848
|
+
next unless complete
|
|
1849
|
+
|
|
1850
|
+
out << string[offset..].to_s
|
|
1851
|
+
end
|
|
1852
|
+
end
|
|
1853
|
+
|
|
1854
|
+
def replacements_for(replacement_filter, match, context)
|
|
1855
|
+
replacement_filter.eval(capture_values(match), context).map { |value| assert_string(value) }
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
def capture_values(match)
|
|
1859
|
+
match.names.to_h { |name| [name, match[name]] }
|
|
1860
|
+
end
|
|
1861
|
+
|
|
1862
|
+
def splits_builtin(input, context, args)
|
|
1863
|
+
string = assert_string(input)
|
|
1864
|
+
split_at_matches(string, regexp_matches(input, context, args, string))
|
|
1865
|
+
end
|
|
1866
|
+
|
|
1867
|
+
def format_csv(input)
|
|
1868
|
+
assert_array(input).map { |item| csv_field(item) }.join(',')
|
|
1869
|
+
end
|
|
1870
|
+
|
|
1871
|
+
def html_escape(input)
|
|
1872
|
+
CGI.escapeHTML(input).gsub(''', ''')
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
def csv_field(item)
|
|
1876
|
+
return '' if item.nil?
|
|
1877
|
+
return to_string(item) if item.is_a?(Numeric) || item == true || item == false
|
|
1878
|
+
unless item.is_a?(String)
|
|
1879
|
+
raise TypeError, "#{Value.type_of(item)} (#{short_dump(item)}) is not valid in a csv row"
|
|
1880
|
+
end
|
|
1881
|
+
|
|
1882
|
+
"\"#{item.gsub('"', '""')}\""
|
|
1883
|
+
end
|
|
1884
|
+
|
|
1885
|
+
def format_tsv(input)
|
|
1886
|
+
assert_array(input).map do |item|
|
|
1887
|
+
next '' if item.nil?
|
|
1888
|
+
next to_string(item) if item.is_a?(Numeric) || item == true || item == false
|
|
1889
|
+
unless item.is_a?(String)
|
|
1890
|
+
raise TypeError, "#{Value.type_of(item)} (#{short_dump(item)}) is not valid in a tsv row"
|
|
1891
|
+
end
|
|
1892
|
+
|
|
1893
|
+
item.gsub("\t", '\\t').gsub("\n", '\\n').gsub("\r", '\\r')
|
|
1894
|
+
end.join("\t")
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
def format_sh(input)
|
|
1898
|
+
values = input.is_a?(Array) ? input : [input]
|
|
1899
|
+
values.map do |item|
|
|
1900
|
+
case item
|
|
1901
|
+
when String then sh_quote(item)
|
|
1902
|
+
when Numeric, TrueClass, FalseClass, NilClass then to_string(item)
|
|
1903
|
+
else
|
|
1904
|
+
raise TypeError, "#{Value.type_of(item)} (#{short_dump(item)}) can not be escaped for shell"
|
|
1905
|
+
end
|
|
1906
|
+
end.join(' ')
|
|
1907
|
+
end
|
|
1908
|
+
|
|
1909
|
+
def sh_quote(input)
|
|
1910
|
+
"'#{input.gsub("'", "'\\\\''")}'"
|
|
1911
|
+
end
|
|
1912
|
+
|
|
1913
|
+
def uri_escape(input)
|
|
1914
|
+
input.bytes.map do |byte|
|
|
1915
|
+
char = byte.chr
|
|
1916
|
+
char.match?(/[A-Za-z0-9_.~-]/) ? char : '%%%02X' % byte
|
|
1917
|
+
end.join
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
def base32_encode(input)
|
|
1921
|
+
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
1922
|
+
encoded = +''
|
|
1923
|
+
buffer = 0
|
|
1924
|
+
bits = 0
|
|
1925
|
+
input.each_byte do |byte|
|
|
1926
|
+
buffer = (buffer << 8) | byte
|
|
1927
|
+
bits += 8
|
|
1928
|
+
while bits >= 5
|
|
1929
|
+
bits -= 5
|
|
1930
|
+
encoded << alphabet[(buffer >> bits) & 31]
|
|
1931
|
+
end
|
|
1932
|
+
buffer &= (1 << bits) - 1
|
|
1933
|
+
end
|
|
1934
|
+
encoded << alphabet[(buffer << (5 - bits)) & 31] if bits.positive?
|
|
1935
|
+
encoded + ('=' * ((8 - (encoded.length % 8)) % 8))
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
def decode_base64(input)
|
|
1939
|
+
string = assert_string(input)
|
|
1940
|
+
string.unpack1('m0').force_encoding(Encoding::UTF_8)
|
|
1941
|
+
rescue ArgumentError
|
|
1942
|
+
raise RuntimeError, "string (#{JSON::Dumper.dump(input, indent: nil)}) is not valid base64 data"
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1945
|
+
def base32_decode(input)
|
|
1946
|
+
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
1947
|
+
clean = input.upcase.delete('=')
|
|
1948
|
+
output = +''.b
|
|
1949
|
+
buffer = 0
|
|
1950
|
+
bits = 0
|
|
1951
|
+
clean.each_char do |char|
|
|
1952
|
+
index = alphabet.index(char)
|
|
1953
|
+
raise RuntimeError, "invalid base32 character #{char.inspect}" unless index
|
|
1954
|
+
|
|
1955
|
+
buffer = (buffer << 5) | index
|
|
1956
|
+
bits += 5
|
|
1957
|
+
if bits >= 8
|
|
1958
|
+
bits -= 8
|
|
1959
|
+
output << ((buffer >> bits) & 0xFF)
|
|
1960
|
+
buffer &= (1 << bits) - 1
|
|
1961
|
+
end
|
|
1962
|
+
end
|
|
1963
|
+
output
|
|
1964
|
+
end
|
|
1965
|
+
|
|
1966
|
+
def input_values(input, context, filter)
|
|
1967
|
+
Enumerator.new do |yielder|
|
|
1968
|
+
iterable_values(input).each do |item|
|
|
1969
|
+
filter_stream(filter, item, context).each { |value| yielder << value }
|
|
1970
|
+
end
|
|
1971
|
+
end
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1974
|
+
def iterable_values(input)
|
|
1975
|
+
return input.each if input.is_a?(Array)
|
|
1976
|
+
return input.each_value if input.is_a?(Hash)
|
|
1977
|
+
|
|
1978
|
+
raise TypeError,
|
|
1979
|
+
"Cannot iterate over #{Value.type_of(input)} (#{JSON::Dumper.dump(input, indent: nil)})"
|
|
1980
|
+
end
|
|
1981
|
+
|
|
1982
|
+
def source_any?(node, input, context, &)
|
|
1983
|
+
return node.source_any?(input, context, &) if node.respond_to?(:source_any?)
|
|
1984
|
+
|
|
1985
|
+
if node.is_a?(AST::Comma)
|
|
1986
|
+
return true if source_any?(node.left, input, context, &)
|
|
1987
|
+
|
|
1988
|
+
return source_any?(node.right, input, context, &)
|
|
1989
|
+
end
|
|
1990
|
+
node.eval(input, context).any?(&)
|
|
1991
|
+
end
|
|
1992
|
+
|
|
1993
|
+
def source_all?(node, input, context, &)
|
|
1994
|
+
return node.source_all?(input, context, &) if node.respond_to?(:source_all?)
|
|
1995
|
+
|
|
1996
|
+
if node.is_a?(AST::Comma)
|
|
1997
|
+
return false unless source_all?(node.left, input, context, &)
|
|
1998
|
+
|
|
1999
|
+
return source_all?(node.right, input, context, &)
|
|
2000
|
+
end
|
|
2001
|
+
node.eval(input, context).all?(&)
|
|
2002
|
+
end
|
|
2003
|
+
|
|
2004
|
+
def eval_arg(args, index, input, context)
|
|
2005
|
+
raise RuntimeError, "missing argument #{index}" unless args[index]
|
|
2006
|
+
|
|
2007
|
+
args[index].eval(input, context).first
|
|
2008
|
+
end
|
|
2009
|
+
|
|
2010
|
+
def filter_stream(filter, input, context)
|
|
2011
|
+
return filter.stream(input, context) if filter.respond_to?(:stream)
|
|
2012
|
+
|
|
2013
|
+
filter.eval(input, context).each
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2016
|
+
def collect_paths(filter, input, context)
|
|
2017
|
+
filter.paths(input, context)
|
|
2018
|
+
rescue Rjq::RuntimeError => e
|
|
2019
|
+
e.take_outputs
|
|
2020
|
+
raise
|
|
2021
|
+
end
|
|
2022
|
+
|
|
2023
|
+
def each_resolved_argument_set(name, args, input, context, &block)
|
|
2024
|
+
indices = (0...args.length).to_a
|
|
2025
|
+
indices.reverse! unless LEFT_OUTER_ARGUMENT_BUILTINS.include?(name)
|
|
2026
|
+
resolve_argument_indices(name, args, indices, input, context, [], &block)
|
|
2027
|
+
end
|
|
2028
|
+
|
|
2029
|
+
def resolve_argument_indices(name, args, indices, input, context, resolved, &block)
|
|
2030
|
+
return yield(resolved) if indices.empty?
|
|
2031
|
+
|
|
2032
|
+
index = indices.first
|
|
2033
|
+
remaining = indices.drop(1)
|
|
2034
|
+
argument = args.fetch(index)
|
|
2035
|
+
if FILTER_ARGUMENT_POSITIONS.fetch(name, []).include?(index)
|
|
2036
|
+
copy = resolved.dup
|
|
2037
|
+
copy[index] = argument
|
|
2038
|
+
return resolve_argument_indices(name, args, remaining, input, context, copy, &block)
|
|
2039
|
+
end
|
|
2040
|
+
|
|
2041
|
+
filter_stream(argument, input, context).each do |value|
|
|
2042
|
+
copy = resolved.dup
|
|
2043
|
+
copy[index] = AST::Literal.new(value)
|
|
2044
|
+
resolve_argument_indices(name, args, remaining, input, context, copy, &block)
|
|
2045
|
+
end
|
|
2046
|
+
end
|
|
2047
|
+
|
|
2048
|
+
def collect_filter(filter, input, context)
|
|
2049
|
+
filter_stream(filter, input, context).to_a
|
|
2050
|
+
rescue Rjq::RuntimeError => e
|
|
2051
|
+
e.take_outputs
|
|
2052
|
+
raise
|
|
2053
|
+
end
|
|
2054
|
+
|
|
2055
|
+
def ordered_delete_paths(paths)
|
|
2056
|
+
paths.sort do |left, right|
|
|
2057
|
+
parent_cmp = Value.compare(left[0...-1], right[0...-1])
|
|
2058
|
+
next parent_cmp unless parent_cmp.zero?
|
|
2059
|
+
|
|
2060
|
+
left_key = left.last
|
|
2061
|
+
right_key = right.last
|
|
2062
|
+
if left_key.is_a?(Integer) && right_key.is_a?(Integer)
|
|
2063
|
+
right_key <=> left_key
|
|
2064
|
+
else
|
|
2065
|
+
Value.compare(right, left)
|
|
2066
|
+
end
|
|
2067
|
+
end
|
|
2068
|
+
end
|
|
2069
|
+
|
|
2070
|
+
def builtin_arities(name)
|
|
2071
|
+
BUILTIN_ARITIES.fetch(name, []).map { |arity| "#{name}/#{arity}" }
|
|
2072
|
+
end
|
|
2073
|
+
|
|
2074
|
+
def valid_arity?(name, arity)
|
|
2075
|
+
BUILTIN_ARITIES.fetch(name, []).include?(arity)
|
|
2076
|
+
end
|
|
2077
|
+
|
|
2078
|
+
def modulemeta(input, context)
|
|
2079
|
+
metadata = context.options.fetch(:module_metadata, {})
|
|
2080
|
+
raise RuntimeError, "module not found: #{input}" unless metadata.key?(input)
|
|
2081
|
+
|
|
2082
|
+
metadata.fetch(input)
|
|
2083
|
+
end
|
|
2084
|
+
|
|
2085
|
+
def assert_array(value)
|
|
2086
|
+
raise TypeError, "expected array, got #{Value.type_of(value)}" unless value.is_a?(Array)
|
|
2087
|
+
|
|
2088
|
+
value
|
|
2089
|
+
end
|
|
2090
|
+
|
|
2091
|
+
def assert_string(value)
|
|
2092
|
+
raise TypeError, "expected string, got #{Value.type_of(value)}" unless value.is_a?(String)
|
|
2093
|
+
|
|
2094
|
+
value
|
|
2095
|
+
end
|
|
2096
|
+
|
|
2097
|
+
def numeric(value)
|
|
2098
|
+
raise TypeError, "expected number, got #{Value.type_of(value)}" unless value.is_a?(Numeric)
|
|
2099
|
+
|
|
2100
|
+
value
|
|
2101
|
+
end
|
|
2102
|
+
end
|
|
2103
|
+
end
|