File Coverage

blib/lib/JQ/Lite/Util/Transform.pm
Criterion Covered Total %
statement 1062 1193 89.0
branch 690 956 72.1
condition 142 239 59.4
subroutine 98 99 98.9
pod n/a
total 1992 2487 80.1


line stmt bran cond sub pod time code
1             package JQ::Lite::Util;
2              
3 176     176   1470 use strict;
  176         548  
  176         7536  
4 176     176   1310 use warnings;
  176         545  
  176         10674  
5              
6 176     176   1076 use JSON::PP ();
  176         348  
  176         4512  
7 176     176   899 use List::Util qw(sum min max);
  176         322  
  176         15157  
8 176     176   1154 use Scalar::Util qw(looks_like_number);
  176         331  
  176         8296  
9 176     176   961 use MIME::Base64 qw(encode_base64 decode_base64);
  176         323  
  176         9190  
10 176     176   930 use Encode qw(encode is_utf8);
  176         372  
  176         7228  
11 176     176   964 use B ();
  176         328  
  176         3220171  
12              
13             our ($JSON_DECODER, $FROMJSON_DECODER, $TOJSON_ENCODER);
14              
15             sub _apply_tostring {
16 7     7   18 my ($value) = @_;
17              
18 7 100       21 if (!defined $value) {
19 1         7 return 'null';
20             }
21              
22 6 100       27 if (JSON::PP::is_bool($value)) {
23 1 50       50 return $value ? 'true' : 'false';
24             }
25              
26 5 100       52 if (!ref $value) {
27 3         15 return "$value";
28             }
29              
30 2 50 66     15 if (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
31 2         11 return $TOJSON_ENCODER->encode($value);
32             }
33              
34 0         0 return $TOJSON_ENCODER->encode($value);
35             }
36              
37             sub _apply_tojson {
38 21     21   52 my ($value) = @_;
39              
40 21         98 return $TOJSON_ENCODER->encode($value);
41             }
42              
43             sub _apply_fromjson {
44 15     15   35 my ($value) = @_;
45              
46 15 100       41 return undef if !defined $value;
47              
48 14 100       38 if (ref $value eq 'ARRAY') {
49 3         10 return [ map { _apply_fromjson($_) } @$value ];
  10         29  
50             }
51              
52 11 50       21 return $value if ref $value;
53              
54 11         55 my $text = "$value";
55 11         19 my $decoded = eval { $FROMJSON_DECODER->decode($text) };
  11         44  
56              
57 11 100       2515 return $@ ? $value : $decoded;
58             }
59              
60             sub _apply_numeric_function {
61 51     51   146 my ($value, $callback) = @_;
62              
63 51 100       120 return undef if !defined $value;
64              
65 48 100       171 if (JSON::PP::is_bool($value)) {
66 9 100       172 my $numeric = $value ? 1 : 0;
67 9         96 return $callback->($numeric);
68             }
69              
70 39 100       348 if (!ref $value) {
71 27 100       160 return looks_like_number($value) ? $callback->($value) : $value;
72             }
73              
74 12 50       45 if (ref $value eq 'ARRAY') {
75 12         31 return [ map { _apply_numeric_function($_, $callback) } @$value ];
  36         80  
76             }
77              
78 0         0 return $value;
79             }
80              
81             sub _apply_clamp {
82 20     20   43 my ($value, $min, $max) = @_;
83              
84 20 100       45 return undef if !defined $value;
85              
86 19 100       41 if (JSON::PP::is_bool($value)) {
87 1 50       47 my $numeric = $value ? 1 : 0;
88 1         15 return _clamp_scalar($numeric, $min, $max);
89             }
90              
91 18 100       105 if (!ref $value) {
92 14         43 return _clamp_scalar($value, $min, $max);
93             }
94              
95 4 50       16 if (ref $value eq 'ARRAY') {
96 4         11 return [ map { _apply_clamp($_, $min, $max) } @$value ];
  11         28  
97             }
98              
99 0         0 return $value;
100             }
101              
102             sub _normalize_numeric_bound {
103 19     19   43 my ($value) = @_;
104              
105 19 100       49 return undef if !defined $value;
106              
107 18 50       53 if (JSON::PP::is_bool($value)) {
108 0 0       0 return $value ? 1 : 0;
109             }
110              
111 18 50       151 return looks_like_number($value) ? 0 + $value : undef;
112             }
113              
114             sub _clamp_scalar {
115 15     15   32 my ($value, $min, $max) = @_;
116              
117 15 100       45 return $value unless looks_like_number($value);
118              
119 14         22 my $numeric = 0 + $value;
120 14 100 100     61 $numeric = $min if defined $min && $numeric < $min;
121 14 100 100     54 $numeric = $max if defined $max && $numeric > $max;
122              
123 14         51 return $numeric;
124             }
125              
126             sub _apply_to_number {
127 16     16   23 my ($value) = @_;
128              
129 16 100       23 return undef if !defined $value;
130              
131 15 100       25 if (JSON::PP::is_bool($value)) {
132 2 100       41 return $value ? 1 : 0;
133             }
134              
135 13 100       53 if (!ref $value) {
136 7 100       33 return looks_like_number($value) ? 0 + $value : $value;
137             }
138              
139 6 100       12 if (ref $value eq 'ARRAY') {
140 4         5 return [ map { _apply_to_number($_) } @$value ];
  9         20  
141             }
142              
143 2         7 return $value;
144             }
145              
146             sub _extract_numeric_values {
147 34     34   78 my ($values) = @_;
148              
149 34 50       110 return () unless ref $values eq 'ARRAY';
150              
151             return map {
152 96 100       742 JSON::PP::is_bool($_) ? ($_ ? 1 : 0) : 0 + $_;
    100          
153             } grep {
154 34 100 66     72 defined $_ && (JSON::PP::is_bool($_) || (!ref $_ && looks_like_number($_)));
  123   100     1116  
155             } @$values;
156             }
157              
158             sub _normalize_percentile {
159 16     16   36 my ($value) = @_;
160              
161 16 50       36 return undef if !defined $value;
162              
163 16 50       67 if (JSON::PP::is_bool($value)) {
164 0 0       0 $value = $value ? 1 : 0;
165             }
166              
167 16 50       128 return undef if ref $value;
168 16 100       70 return undef unless looks_like_number($value);
169              
170 15         32 my $fraction = 0 + $value;
171              
172 15 100       37 return undef if $fraction != $fraction; # NaN
173 14 100       38 return undef if ($fraction * 0) != ($fraction * 0); # infinity
174              
175 12 100       30 if ($fraction > 1) {
176 9 100       29 $fraction /= 100 if $fraction <= 100;
177             }
178              
179 12 100       27 $fraction = 0 if $fraction < 0;
180 12 100       28 $fraction = 1 if $fraction > 1;
181              
182 12         30 return $fraction;
183             }
184              
185             sub _percentile_value {
186 12     12   29 my ($numbers, $fraction) = @_;
187              
188 12 50       34 return undef unless ref $numbers eq 'ARRAY';
189 12 50       46 return undef unless @$numbers;
190              
191 12 50       28 $fraction = 0 if $fraction < 0;
192 12 50       31 $fraction = 1 if $fraction > 1;
193              
194 12 100       30 return $numbers->[0] if @$numbers == 1;
195              
196 11         30 my $rank = $fraction * (@$numbers - 1);
197 11         23 my $lower_index = int($rank);
198 11 100       30 my $upper_index = $lower_index == @$numbers - 1 ? $lower_index : $lower_index + 1;
199 11         21 my $weight = $rank - $lower_index;
200              
201 11 100       36 return $numbers->[$lower_index] if $upper_index == $lower_index;
202              
203 9         19 my $lower = $numbers->[$lower_index];
204 9         15 my $upper = $numbers->[$upper_index];
205              
206 9         56 return $lower + ($upper - $lower) * $weight;
207             }
208              
209             sub _apply_merge_objects {
210 3     3   9 my ($value) = @_;
211              
212 3 100       12 if (ref $value eq 'ARRAY') {
213 2         4 my %merged;
214 2         4 my $saw_object = 0;
215              
216 2         7 for my $element (@$value) {
217 8 100       21 next unless ref $element eq 'HASH';
218 3         20 %merged = (%merged, %$element);
219 3         10 $saw_object = 1;
220             }
221              
222 2 100       34 return $saw_object ? \%merged : {};
223             }
224              
225 1 50       7 if (ref $value eq 'HASH') {
226 1         8 return { %$value };
227             }
228              
229 0         0 return $value;
230             }
231              
232             sub _to_entries {
233 4     4   9 my ($value) = @_;
234              
235 4 100       10 if (ref $value eq 'HASH') {
236 2         11 return [ map { { key => $_, value => $value->{$_} } } sort keys %$value ];
  4         20  
237             }
238              
239 2 50       7 if (ref $value eq 'ARRAY') {
240 2         6 return [ map { { key => $_, value => $value->[$_] } } 0 .. $#$value ];
  4         21  
241             }
242              
243 0         0 return $value;
244             }
245              
246             sub _is_string_scalar {
247 211     211   367 my ($value) = @_;
248              
249 211 50       422 return 0 if !defined $value;
250 211 50       424 return 0 if ref $value;
251              
252 211         886 my $sv = B::svref_2object(\$value);
253 211         712 my $flags = $sv->FLAGS;
254              
255 211 100       850 return $flags & B::SVp_POK() ? 1 : 0;
256             }
257              
258             sub _from_entries {
259 11     11   16 my ($value) = @_;
260              
261 11 100       34 die 'from_entries(): argument must be an array' unless ref $value eq 'ARRAY';
262              
263 10         11 my %result;
264             my @numeric_keys;
265 10         11 my $saw_non_numeric_key = 0;
266 10         17 for my $entry (@$value) {
267 15         17 my ($key, $val);
268              
269 15 100       45 if (ref $entry eq 'HASH') {
    100          
270 11 50       18 die 'from_entries(): entry is missing key' if !exists $entry->{key};
271 11 100       30 die 'from_entries(): entry is missing value' if !exists $entry->{value};
272 10         34 ($key, $val) = ($entry->{key}, $entry->{value});
273             }
274             elsif (ref $entry eq 'ARRAY') {
275 3 50       6 die 'from_entries(): entry must have a key and value' if @$entry < 2;
276 3         5 ($key, $val) = @{$entry}[0, 1];
  3         5  
277             }
278             else {
279 1         73 die 'from_entries(): entry must be an object or [key, value] tuple';
280             }
281              
282 13 100 66     37 if (!defined $key || ref $key) {
283 1         15 die 'from_entries(): key must be a string';
284             }
285              
286 12         14 $key = "$key";
287              
288 12 50       17 die 'from_entries(): key must be a string' if !_is_string_scalar($key);
289              
290 12         22 $result{$key} = $val;
291              
292 12 100       36 if ($key =~ /^(?:0|[1-9]\d*)$/) {
293 6         14 push @numeric_keys, 0 + $key;
294             }
295             else {
296 6         37 $saw_non_numeric_key = 1;
297             }
298             }
299              
300 7 100 100     20 if (@numeric_keys && !$saw_non_numeric_key) {
301 3         4 my %seen;
302 3         4 my $max_index = -1;
303 3         4 for my $index (@numeric_keys) {
304 5 50       27 next if $seen{$index}++;
305 5 50       9 $max_index = $index if $index > $max_index;
306             }
307              
308 3 100 66     11 if ($max_index + 1 == scalar(keys %result) && $max_index + 1 == scalar(@numeric_keys)) {
309 2         5 my @array = map { $result{$_} } 0 .. $max_index;
  4         9  
310 2         9 return \@array;
311             }
312             }
313              
314 5         18 return \%result;
315             }
316              
317             sub _apply_with_entries {
318 1     1   3 my ($self, $value, $filter) = @_;
319              
320 1 50 33     6 return $value unless ref $value eq 'HASH' || ref $value eq 'ARRAY';
321              
322 1         3 my $entries = _to_entries($value);
323 1 50       5 return $value unless ref $entries eq 'ARRAY';
324              
325 1         3 my @transformed;
326 1         3 for my $entry (@$entries) {
327 2         6 my @results = $self->run_query(_encode_json($entry), $filter);
328 2         3 for my $result (@results) {
329 1         3 my $normalized = _normalize_entry($result);
330 1 50       10 push @transformed, $normalized if $normalized;
331             }
332             }
333              
334 1         3 return _from_entries(\@transformed);
335             }
336              
337             sub _apply_map_values {
338 8     8   28 my ($self, $value, $filter) = @_;
339              
340 8 50       28 return $value if !defined $value;
341              
342 8 100       35 if (ref $value eq 'HASH') {
343 5         15 my %result;
344 5         53 for my $key (keys %$value) {
345 8         24 my $original = $value->{$key};
346 8         33 my @outputs = $self->run_query(_encode_json($original), $filter);
347 8 100       61 next unless @outputs;
348 5         24 $result{$key} = $outputs[0];
349             }
350 5         33 return \%result;
351             }
352              
353 3 50       17 if (ref $value eq 'ARRAY') {
354 3         8 my @result;
355 3         13 for my $original (@$value) {
356 8 100 66     49 if (ref $original eq 'HASH' || ref $original eq 'ARRAY') {
357 2         11 push @result, _apply_map_values($self, $original, $filter);
358 2         6 next;
359             }
360              
361 6         26 my @outputs = $self->run_query(_encode_json($original), $filter);
362 6 100       23 push @result, $outputs[0] if @outputs;
363             }
364 3         23 return \@result;
365             }
366              
367 0         0 return $value;
368             }
369              
370             sub _apply_walk {
371 13     13   29 my ($self, $value, $filter) = @_;
372              
373 13 100       34 if (ref $value eq 'HASH') {
    100          
374 2         3 my %copy;
375 2         6 for my $key (keys %$value) {
376 4         14 $copy{$key} = _apply_walk($self, $value->{$key}, $filter);
377             }
378 2         6 $value = \%copy;
379             }
380             elsif (ref $value eq 'ARRAY') {
381 3         6 my @copy = map { _apply_walk($self, $_, $filter) } @$value;
  6         14  
382 3         6 $value = \@copy;
383             }
384              
385 13         30 my @results = $self->run_query(_encode_json($value), $filter);
386 13 50       52 return @results ? $results[0] : undef;
387             }
388              
389             sub _apply_recurse {
390 2     2   5 my ($self, $value, $filter) = @_;
391              
392 2         4 my @stack = ($value);
393 2         3 my @outputs;
394              
395 2         5 while (@stack) {
396 9         10 my $current = pop @stack;
397 9         14 push @outputs, $current;
398              
399 9 50       12 next unless defined $current;
400              
401 9         10 my @children;
402 9 100       16 if (defined $filter) {
    100          
    100          
403 4         10 my $json = _encode_json($current);
404 4         601 @children = $self->run_query($json, $filter);
405             }
406             elsif (ref $current eq 'ARRAY') {
407 1         2 @children = @$current;
408             }
409             elsif (ref $current eq 'HASH') {
410 1         3 @children = map { $current->{$_} } sort keys %$current;
  2         5  
411             }
412              
413 9 100       16 next unless @children;
414              
415 4         8 for my $child (reverse @children) {
416 7         13 push @stack, $child;
417             }
418             }
419              
420 2         8 return @outputs;
421             }
422              
423             sub _apply_delpaths {
424 10     10   23 my ($self, $value, $filter) = @_;
425              
426 10 50       18 return $value if !defined $value;
427 10 50 33     55 return $value if !ref $value || JSON::PP::is_bool($value);
428              
429 10   50     76 $filter //= '';
430 10         34 $filter =~ s/^\s+|\s+$//g;
431 10 50       29 return $value if $filter eq '';
432              
433 10         12 my @paths;
434 10         11 my $decoded_paths = eval { _decode_json($filter) };
  10         24  
435 10 50 33     2509 if (!$@ && defined $decoded_paths) {
436 10 100       1072 if (ref $decoded_paths eq 'ARRAY') {
437 9 100       21 if (grep { ref($_) ne 'ARRAY' } @$decoded_paths) {
  12         40  
438 2         37 die 'delpaths(): paths must be an array of path arrays';
439             }
440              
441 7         11 push @paths, map { _validate_path_array($_, 'delpaths') } @$decoded_paths;
  10         494  
442             }
443             else {
444 1         16 die 'delpaths(): paths must be an array of path arrays';
445             }
446             }
447              
448 5 50       9 if (!@paths) {
449 0         0 my @outputs = $self->run_query(_encode_json($value), $filter);
450 0         0 for my $output (@outputs) {
451 0 0       0 next unless defined $output;
452              
453 0 0       0 if (ref $output eq 'ARRAY') {
454 0 0       0 if (grep { ref($_) ne 'ARRAY' } @$output) {
  0         0  
455 0         0 die 'delpaths(): paths must be an array of path arrays';
456             }
457              
458 0         0 push @paths, map { _validate_path_array($_, 'delpaths') } @$output;
  0         0  
459             }
460             else {
461 0         0 die 'delpaths(): paths must be an array of path arrays';
462             }
463             }
464             }
465              
466 5 50       9 return $value unless @paths;
467              
468 5 50       8 if (grep { ref $_ eq 'ARRAY' && !@$_ } @paths) {
  8 100       27  
469 1         6 return undef;
470             }
471              
472 4         24 my $clone = _deep_clone($value);
473 4         1782 my @ordered = _sort_paths_for_deletion(@paths);
474              
475 4         21 for my $path (@ordered) {
476 7 50       16 next unless ref $path eq 'ARRAY';
477 7 50       9 next unless @$path;
478 7         15 _delete_path_inplace($clone, [@$path]);
479             }
480              
481 4         17 return $clone;
482             }
483              
484             sub _sort_paths_for_deletion {
485 4     4   11 my (@paths) = @_;
486              
487             return sort {
488 4         15 my $depth_cmp = @$b <=> @$a;
  3         7  
489 3 100       21 return $depth_cmp if $depth_cmp;
490              
491 2         5 my $prefix_cmp = _path_prefix_key($a) cmp _path_prefix_key($b);
492 2 50       4 return $prefix_cmp if $prefix_cmp;
493              
494 2         5 return _compare_path_segments($b->[-1], $a->[-1]);
495             } @paths;
496             }
497              
498             sub _path_prefix_key {
499 4     4   6 my ($path) = @_;
500              
501 4 50 33     18 return '' if !$path || @$path < 2;
502              
503 0         0 my @segments = @$path[0 .. $#$path - 1];
504 0         0 return join "\x1f", map { _path_segment_key($_) } @segments;
  0         0  
505             }
506              
507             sub _path_segment_key {
508 0     0   0 my ($segment) = @_;
509              
510 0 0       0 return 'undef' if !defined $segment;
511              
512 0 0       0 if (JSON::PP::is_bool($segment)) {
513 0 0       0 return $segment ? 'bool:true' : 'bool:false';
514             }
515              
516 0 0       0 return ref $segment ? 'ref:' . ref($segment) : "scalar:$segment";
517             }
518              
519             sub _compare_path_segments {
520 2     2   4 my ($left, $right) = @_;
521              
522 2 50 33     25 if (_is_numeric_segment($left) && _is_numeric_segment($right)) {
523 2         5 return _numeric_segment_value($left) <=> _numeric_segment_value($right);
524             }
525              
526 0         0 return _path_segment_key($left) cmp _path_segment_key($right);
527             }
528              
529             sub _numeric_segment_value {
530 4     4   21 my ($segment) = @_;
531              
532 4 50       6 if (JSON::PP::is_bool($segment)) {
533 4 100       109 return $segment ? 1 : 0;
534             }
535              
536 0         0 return int($segment);
537             }
538              
539             sub _deep_clone {
540 15     15   31 my ($value) = @_;
541              
542 15 50       36 return $value if !defined $value;
543 15 100 66     75 return $value if !ref $value || JSON::PP::is_bool($value);
544              
545 5         82 my $json = _encode_json($value);
546 5         804 return _decode_json($json);
547             }
548              
549             sub _delete_path_inplace {
550 7     7   9 my ($value, $path) = @_;
551              
552 7 50 66     19 return unless ref $value eq 'HASH' || ref $value eq 'ARRAY';
553 7 50       9 return unless ref $path eq 'ARRAY';
554 7 50       8 return unless @$path;
555              
556 7         12 my @segments = @$path;
557 7         11 my $last = pop @segments;
558              
559 7         9 my $cursor = $value;
560 7         9 for my $segment (@segments) {
561 1 50       2 if (ref $cursor eq 'HASH') {
562 1         4 my $key = _coerce_hash_key($segment);
563 1 50       3 return unless defined $key;
564 1 50       2 return unless exists $cursor->{$key};
565 1         2 $cursor = $cursor->{$key};
566 1         2 next;
567             }
568              
569 0 0       0 if (ref $cursor eq 'ARRAY') {
570 0         0 my $index = _normalize_array_index($segment, scalar @$cursor);
571 0 0       0 return if !defined $index;
572 0         0 $cursor = $cursor->[$index];
573 0         0 next;
574             }
575              
576 0         0 return;
577             }
578              
579 7 100       28 if (ref $cursor eq 'HASH') {
580 3         7 my $key = _coerce_hash_key($last);
581 3 50       14 return unless defined $key;
582 3         6 delete $cursor->{$key};
583 3         6 return;
584             }
585              
586 4 50       9 if (ref $cursor eq 'ARRAY') {
587 4         7 my $index = _normalize_array_index($last, scalar @$cursor);
588 4 50       8 return if !defined $index;
589 4         11 splice @$cursor, $index, 1;
590             }
591             }
592              
593             sub _normalize_array_index {
594 4     4   7 my ($value, $length) = @_;
595              
596 4 50       10 return if !defined $value;
597              
598 4 100       6 if (JSON::PP::is_bool($value)) {
599 2 100       10 $value = $value ? 1 : 0;
600             }
601              
602 4 50       26 return if ref $value;
603              
604 4 50       17 return if $value !~ /^-?\d+$/;
605              
606 4         9 my $index = int($value);
607 4 50       8 $index += $length if $index < 0;
608              
609 4 50 33     10 return if $index < 0 || $index >= $length;
610              
611 4         5 return $index;
612             }
613              
614             sub _normalize_entry {
615 1     1   3 my ($entry) = @_;
616              
617 1 50       15 if (ref $entry eq 'HASH') {
618 1 50       4 return unless exists $entry->{key};
619 1         6 return { key => $entry->{key}, value => $entry->{value} };
620             }
621              
622 0 0       0 if (ref $entry eq 'ARRAY') {
623 0 0       0 return unless @$entry >= 2;
624 0         0 return { key => $entry->[0], value => $entry->[1] };
625             }
626              
627 0         0 return;
628             }
629              
630             sub _apply_coalesce {
631 8     8   23 my ($self, $value, $lhs_expr, $rhs_expr) = @_;
632              
633 8         80 my @lhs_values = _evaluate_coalesce_operand($self, $value, $lhs_expr);
634 8         20 for my $candidate (@lhs_values) {
635 6 100       23 return $candidate if defined $candidate;
636             }
637              
638 5         14 my @rhs_values = _evaluate_coalesce_operand($self, $value, $rhs_expr);
639 5         12 for my $candidate (@rhs_values) {
640 5 50       28 return $candidate if defined $candidate;
641             }
642              
643 0         0 return undef;
644             }
645              
646             sub _evaluate_coalesce_operand {
647 13     13   35 my ($self, $context, $expr) = @_;
648              
649 13 50       30 return () unless defined $expr;
650              
651 13         24 my $copy = $expr;
652 13         78 $copy =~ s/^\s+|\s+$//g;
653 13 50       39 return () if $copy eq '';
654              
655 13         40 while ($copy =~ /^\((.*)\)$/) {
656 0         0 $copy = $1;
657 0         0 $copy =~ s/^\s+|\s+$//g;
658             }
659              
660 13 100       140 if ($copy =~ /^(.*?)\s*\/\/\s*(.+)$/) {
661 1         5 my ($lhs, $rhs) = ($1, $2);
662 1         6 my $result = _apply_coalesce($self, $context, $lhs, $rhs);
663 1         4 return ($result);
664             }
665              
666 12 50       49 if ($copy eq '.') {
667 0         0 return ($context);
668             }
669              
670 12         24 my $decoded = eval { _decode_json($copy) };
  12         36  
671 12 100       3101 if (!$@) {
672 2         8 return ($decoded);
673             }
674              
675 10 50       61 if ($copy =~ /^'(.*)'$/) {
676 0         0 my $text = $1;
677 0         0 $text =~ s/\\'/'/g;
678 0         0 return ($text);
679             }
680              
681 10 50       26 return () unless defined $context;
682              
683 10         19 my $path = $copy;
684 10         33 $path =~ s/^\.//;
685              
686 10         35 return _traverse($context, $path);
687             }
688              
689             sub _traverse {
690 748     748   2013 my ($data, $query) = @_;
691 748         3503 my @steps = split /\./, $query;
692 748         1924 my @stack = ($data);
693              
694 748         1552 for my $step (@steps) {
695 776         2125 my $optional = ($step =~ s/\?$//);
696 776         1389 my @next_stack;
697              
698 776         1417 for my $item (@stack) {
699 785 100       2023 next if !defined $item;
700              
701             # direct index access: [index]
702 784 100       10415 if ($step =~ /^\[(\d+)\]$/) {
    100          
    100          
    100          
703 4         8 my $index = $1;
704 4 50 33     30 if (ref $item eq 'ARRAY' && defined $item->[$index]) {
705 4         12 push @next_stack, $item->[$index];
706             }
707             }
708             # array expansion without key: []
709             elsif ($step eq '[]') {
710 2 50       8 if (ref $item eq 'ARRAY') {
    0          
711 2         6 push @next_stack, @$item;
712             }
713             elsif (ref $item eq 'HASH') {
714 0         0 push @next_stack, values %$item;
715             }
716             }
717             # index access: key[index]
718             elsif ($step =~ /^(.*?)\[(\d+)\]$/) {
719 19         95 my ($key, $index) = ($1, $2);
720 19 50 33     192 if (ref $item eq 'HASH' && exists $item->{$key}) {
721 19         54 my $val = $item->{$key};
722 19 50 33     159 push @next_stack, $val->[$index]
723             if ref $val eq 'ARRAY' && defined $val->[$index];
724             }
725             }
726             # array expansion: key[]
727             elsif ($step =~ /^(.*?)\[\]$/) {
728 51         267 my $key = $1;
729 51 100 66     405 if (ref $item eq 'HASH' && exists $item->{$key}) {
    50          
730 49         114 my $val = $item->{$key};
731 49 100       288 if (ref $val eq 'ARRAY') {
    50          
732 48         208 push @next_stack, @$val;
733             }
734             elsif (ref $val eq 'HASH') {
735 1         5 push @next_stack, values %$val;
736             }
737             }
738             elsif (ref $item eq 'ARRAY') {
739 0         0 for my $sub (@$item) {
740 0 0 0     0 if (ref $sub eq 'HASH' && exists $sub->{$key}) {
741 0         0 my $val = $sub->{$key};
742 0 0       0 if (ref $val eq 'ARRAY') {
    0          
743 0         0 push @next_stack, @$val;
744             }
745             elsif (ref $val eq 'HASH') {
746 0         0 push @next_stack, values %$val;
747             }
748             }
749             }
750             }
751             }
752             # standard access: key
753             else {
754 708 100 100     4149 if (ref $item eq 'HASH' && exists $item->{$step}) {
    50          
755 677         1993 push @next_stack, $item->{$step};
756             }
757             elsif (ref $item eq 'ARRAY') {
758 0         0 for my $sub (@$item) {
759 0 0 0     0 if (ref $sub eq 'HASH' && exists $sub->{$step}) {
760 0         0 push @next_stack, $sub->{$step};
761             }
762             }
763             }
764             }
765             }
766              
767             # allow empty results if optional
768 776         1872 @stack = @next_stack;
769 776 100 100     2507 last if !@stack && !$optional;
770             }
771              
772 748         2727 return @stack;
773             }
774              
775             sub _evaluate_condition {
776 66     66   232 my ($item, $cond) = @_;
777              
778             # support for numeric expressions like: select(.a + 5 > 10)
779 66 100       480 if ($cond =~ /^\s*(\.\w+)\s*([\+\-\*\/%])\s*(-?\d+(?:\.\d+)?)\s*(==|!=|>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/) {
780 4         25 my ($path, $op1, $rhs1, $cmp, $rhs2) = ($1, $2, $3, $4, $5);
781 4         15 my @values = _traverse($item, substr($path, 1));
782 4         7 my $lhs = $values[0];
783            
784 4 50 33     19 return 0 unless defined $lhs && $lhs =~ /^-?\d+(?:\.\d+)?$/;
785            
786 4         229 my $expr = eval "$lhs $op1 $rhs1";
787 4         171 return eval "$expr $cmp $rhs2";
788             }
789              
790             # support for multiple conditions: split and evaluate recursively
791 62 50       320 if ($cond =~ /\s+and\s+/i) {
792 0         0 my @conds = split /\s+and\s+/i, $cond;
793 0         0 for my $c (@conds) {
794 0 0       0 return 0 unless _evaluate_condition($item, $c);
795             }
796 0         0 return 1;
797             }
798 62 50       268 if ($cond =~ /\s+or\s+/i) {
799 0         0 my @conds = split /\s+or\s+/i, $cond;
800 0         0 for my $c (@conds) {
801 0 0       0 return 1 if _evaluate_condition($item, $c);
802             }
803 0         0 return 0;
804             }
805              
806             # support for the contains operator: select(.tags contains "perl")
807 62 100       231 if ($cond =~ /^\s*\.(.+?)\s+contains\s+"(.*?)"\s*$/) {
808 3         9 my ($path, $want) = ($1, $2);
809 3         7 my @vals = _traverse($item, $path);
810              
811 3         5 for my $val (@vals) {
812 3 100 33     12 if (ref $val eq 'ARRAY') {
    50          
813 2 100       5 return 1 if grep { $_ eq $want } @$val;
  6         12  
814             }
815             elsif (!ref $val && index($val, $want) >= 0) {
816 1         3 return 1;
817             }
818             }
819 1         4 return 0;
820             }
821              
822             # support for the has operator: select(.meta has "key")
823 59 100       204 if ($cond =~ /^\s*\.(.+?)\s+has\s+"(.*?)"\s*$/) {
824 2         9 my ($path, $key) = ($1, $2);
825 2         9 my @vals = _traverse($item, $path);
826              
827 2         4 for my $val (@vals) {
828 2 100 66     12 if (ref $val eq 'HASH' && exists $val->{$key}) {
829 1         7 return 1;
830             }
831             }
832 1         6 return 0;
833             }
834              
835             # support for the match operator (with optional 'i' flag)
836 57 100       228 if ($cond =~ /^\s*\.(.+?)\s+match\s+"(.*?)"(i?)\s*$/) {
837 16         57 my ($path, $pattern, $ignore_case) = ($1, $2, $3);
838 16         61 my ($re, $error) = _build_regex($pattern, $ignore_case);
839 16 100       36 if ($error) {
840 1         7 $error =~ s/[\r\n]+$//;
841 1         15 die "match(): invalid regular expression - $error";
842             }
843              
844 15         34 my @vals = _traverse($item, $path);
845 15         25 for my $val (@vals) {
846 15 50       28 next if ref $val;
847 15 100       90 return 1 if $val =~ $re;
848             }
849 12         61 return 0;
850             }
851              
852             # support for the =~ operator: select(. =~ "pattern")
853 41 100       129 if ($cond =~ /^\s*\.(.+?)\s*=~\s*"(.*?)"(i?)\s*$/) {
854 2         7 my ($path, $pattern, $ignore_case) = ($1, $2, $3);
855 2         4 my ($re, $error) = _build_regex($pattern, $ignore_case);
856 2 100       5 if ($error) {
857 1         6 $error =~ s/[\r\n]+$//;
858 1         11 die "=~: invalid regular expression - $error";
859             }
860              
861 1         3 my @vals = _traverse($item, $path);
862 1         2 for my $val (@vals) {
863 0 0       0 next if ref $val;
864 0 0       0 return 1 if $val =~ $re;
865             }
866              
867 1         5 return 0;
868             }
869            
870             # pattern for a single condition
871 39 100       359 if ($cond =~ /^\s*\.(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+?)\s*$/) {
872 34         208 my ($path, $op, $value_raw) = ($1, $2, $3);
873              
874 34         86 my $value;
875 34 100       358 if ($value_raw =~ /^"(.*)"$/) {
    50          
    50          
    100          
876 2         4 $value = $1;
877             } elsif ($value_raw eq 'true') {
878 0         0 $value = JSON::PP::true;
879             } elsif ($value_raw eq 'false') {
880 0         0 $value = JSON::PP::false;
881             } elsif ($value_raw =~ /^-?\d+(?:\.\d+)?$/) {
882 29         90 $value = 0 + $value_raw;
883             } else {
884 3         7 $value = $value_raw;
885             }
886              
887 34         126 my @values = _traverse($item, $path);
888 34 100       99 return 0 unless @values;
889              
890 31         60 for my $field_val (@values) {
891 31 50       91 next unless defined $field_val;
892              
893 31   66     417 my $is_number = (!ref($field_val) && $field_val =~ /^-?\d+(?:\.\d+)?$/)
894             && (!ref($value) && $value =~ /^-?\d+(?:\.\d+)?$/);
895              
896 31 50       166 if ($op eq '==') {
    100          
    50          
897 0 0       0 return 1 if $is_number ? ($field_val == $value) : ($field_val eq $value);
    0          
898             } elsif ($op eq '!=') {
899 2 50       11 return 1 if $is_number ? ($field_val != $value) : ($field_val ne $value);
    100          
900             } elsif ($is_number) {
901             # perform numeric comparisons only when applicable
902 29 100       88 if ($op eq '>') {
    50          
    0          
    0          
903 22 100       142 return 1 if $field_val > $value;
904             } elsif ($op eq '>=') {
905 7 100       40 return 1 if $field_val >= $value;
906             } elsif ($op eq '<') {
907 0 0       0 return 1 if $field_val < $value;
908             } elsif ($op eq '<=') {
909 0 0       0 return 1 if $field_val <= $value;
910             }
911             }
912             }
913             }
914              
915 22         94 return 0;
916             }
917              
918             sub _smart_cmp {
919             return sub {
920 50     50   98 my ($a, $b) = @_;
921              
922 50         203 my $num_a = ($a =~ /^-?\d+(?:\.\d+)?$/);
923 50         124 my $num_b = ($b =~ /^-?\d+(?:\.\d+)?$/);
924              
925 50 100 66     168 if ($num_a && $num_b) {
926 36         187 return $a <=> $b;
927             } else {
928 14         60 return "$a" cmp "$b"; # explicitly perform string comparison
929             }
930 26     26   183 };
931             }
932              
933             sub _extreme_by {
934 6     6   17 my ($array_ref, $key_path, $use_entire_item, $mode) = @_;
935              
936 6 50       18 return undef unless ref $array_ref eq 'ARRAY';
937 6 50       16 return undef unless @$array_ref;
938              
939 6         17 my $cmp = _smart_cmp();
940 6         13 my ($best_item, $best_key);
941              
942 6         18 for my $element (@$array_ref) {
943 20         54 my $candidate = _extract_extreme_key($element, $key_path, $use_entire_item);
944 20 100       62 next unless defined $candidate;
945              
946 17 100       35 if (!defined $best_item) {
947 5         28 ($best_item, $best_key) = ($element, $candidate);
948 5         14 next;
949             }
950              
951 12         27 my $comparison = $cmp->($candidate, $best_key);
952 12 100 100     80 if (($mode eq 'max' && $comparison > 0)
      100        
      100        
953             || ($mode eq 'min' && $comparison < 0)) {
954 5         15 ($best_item, $best_key) = ($element, $candidate);
955             }
956             }
957              
958 6 100       62 return defined $best_item ? $best_item : undef;
959             }
960              
961             sub _extract_extreme_key {
962 20     20   45 my ($element, $key_path, $use_entire_item) = @_;
963              
964 20 100       54 my @values = $use_entire_item ? ($element) : _traverse($element, $key_path);
965 20 100       46 return undef unless @values;
966              
967 17         28 my $value = $values[0];
968 17         38 return _value_to_comparable($value);
969             }
970              
971             sub _value_to_comparable {
972 17     17   32 my ($value) = @_;
973              
974 17 50       33 return undef unless defined $value;
975              
976 17 50       51 if (JSON::PP::is_bool($value)) {
977 0 0       0 return $value ? 1 : 0;
978             }
979              
980 17 50       137 if (!ref $value) {
981 17         47 return $value;
982             }
983              
984 0 0 0     0 if (ref($value) eq 'HASH' || ref($value) eq 'ARRAY') {
985 0         0 return _encode_json($value);
986             }
987              
988 0         0 return undef;
989             }
990              
991             sub _normalize_path_argument {
992 25     25   73 my ($raw_path) = @_;
993              
994 25 50       85 $raw_path = '' unless defined $raw_path;
995 25         102 $raw_path =~ s/^\s+|\s+$//g;
996 25         60 $raw_path =~ s/^['"](.*)['"]$/$1/;
997              
998 25   66     95 my $use_entire_item = ($raw_path eq '' || $raw_path eq '.');
999 25         48 my $key_path = $raw_path;
1000 25 100       109 $key_path =~ s/^\.// unless $use_entire_item;
1001              
1002 25         130 return ($key_path, $use_entire_item);
1003             }
1004              
1005             sub _project_numeric_values {
1006 19     19   42 my ($element, $key_path, $use_entire_item) = @_;
1007              
1008 19 100       39 my @values = $use_entire_item
1009             ? ($element)
1010             : _traverse($element, $key_path);
1011              
1012 19         24 my @numbers;
1013 19         45 for my $value (@values) {
1014 19 50       31 next unless defined $value;
1015              
1016 19 100       39 if (JSON::PP::is_bool($value)) {
1017 3 100       65 push @numbers, $value ? 1 : 0;
1018 3         30 next;
1019             }
1020              
1021 16 50       80 next if ref $value;
1022 16 100       42 next unless looks_like_number($value);
1023              
1024 12         32 push @numbers, 0 + $value;
1025             }
1026              
1027 19         42 return @numbers;
1028             }
1029              
1030             sub _uniq {
1031 1     1   2 my %seen;
1032 1         2 return grep { !$seen{_key($_)}++ } @_;
  4         8  
1033             }
1034              
1035             # generate a unique key for hash, array, or scalar values
1036             sub _key {
1037 81     81   130 my ($val) = @_;
1038 81 100       200 if (ref $val eq 'HASH') {
    100          
1039 6         12 return join(",", sort map { "$_=" . _key($val->{$_}) } keys %$val);
  12         26  
1040             } elsif (ref $val eq 'ARRAY') {
1041 6         12 return join(",", map { _key($_) } @$val);
  12         20  
1042             } else {
1043 69         226 return "$val";
1044             }
1045             }
1046              
1047             sub _group_by {
1048 5     5   8 my ($array_ref, $path) = @_;
1049 5 100       24 die 'group_by(): input must be an array' unless ref $array_ref eq 'ARRAY';
1050              
1051 4         8 my ($key_path, $use_entire_item) = _normalize_path_argument($path);
1052              
1053 4         9 my @entries;
1054 4         4 my $index = 0;
1055 4         10 for my $item (@$array_ref) {
1056 12         13 my $key_value;
1057 12 50       18 if ($use_entire_item) {
1058 0         0 $key_value = $item;
1059             } else {
1060 12         20 my @values = _traverse($item, $key_path);
1061 12 50       21 $key_value = @values ? $values[0] : undef;
1062             }
1063              
1064 12 100       18 my $signature = defined $key_value ? _key($key_value) : "\0__JQ_LITE_UNDEF__";
1065 12         36 push @entries, {
1066             item => $item,
1067             signature => $signature,
1068             index => $index++,
1069             };
1070             }
1071              
1072 4         7 my $cmp = _smart_cmp();
1073             my @sorted = sort {
1074 4         14 my $order = $cmp->($a->{signature}, $b->{signature});
  12         18  
1075 12 100       21 $order = $a->{index} <=> $b->{index} if $order == 0;
1076             $order;
1077             } @entries;
1078              
1079 4         7 my @groups;
1080             my $current_signature;
1081 4         4 for my $entry (@sorted) {
1082 12 100 100     31 if (!defined $current_signature || $entry->{signature} ne $current_signature) {
1083 8         10 push @groups, [];
1084 8         10 $current_signature = $entry->{signature};
1085             }
1086 12         12 push @{ $groups[-1] }, $entry->{item};
  12         17  
1087             }
1088              
1089 4         31 return \@groups;
1090             }
1091              
1092             sub _flatten_all {
1093 8     8   20 my ($value) = @_;
1094              
1095 8 50       39 return $value unless ref $value eq 'ARRAY';
1096              
1097 8         13 my @flattened;
1098 8         17 for my $item (@$value) {
1099 17 100       35 if (ref $item eq 'ARRAY') {
1100 6         65 my $flattened = _flatten_all($item);
1101 6 50       17 if (ref $flattened eq 'ARRAY') {
1102 6         14 push @flattened, @$flattened;
1103             } else {
1104 0         0 push @flattened, $flattened;
1105             }
1106             } else {
1107 11         27 push @flattened, $item;
1108             }
1109             }
1110              
1111 8         24 return \@flattened;
1112             }
1113              
1114             sub _flatten_depth {
1115 22     22   39 my ($value, $depth) = @_;
1116              
1117 22 50       40 return $value unless ref $value eq 'ARRAY';
1118 22 100       43 return $value if $depth <= 0;
1119              
1120 8         11 my @flattened;
1121 8         22 for my $item (@$value) {
1122 22 100       45 if (ref $item eq 'ARRAY') {
1123 15         48 my $flattened = _flatten_depth($item, $depth - 1);
1124 15 50       27 if (ref $flattened eq 'ARRAY') {
1125 15         32 push @flattened, @$flattened;
1126             } else {
1127 0         0 push @flattened, $flattened;
1128             }
1129             } else {
1130 7         14 push @flattened, $item;
1131             }
1132             }
1133              
1134 8         28 return \@flattened;
1135             }
1136              
1137             sub _apply_string_predicate {
1138 37     37   74 my ($value, $needle, $mode) = @_;
1139              
1140 37 100       77 if (ref $value eq 'ARRAY') {
1141 6         13 return [ map { _apply_string_predicate($_, $needle, $mode) } @$value ];
  19         89  
1142             }
1143              
1144 31 100       74 return JSON::PP::false if !_is_string_scalar($needle);
1145              
1146 29         60 return _string_predicate_result($value, $needle, $mode);
1147             }
1148              
1149             sub _string_predicate_result {
1150 29     29   51 my ($value, $needle, $mode) = @_;
1151              
1152 29 100       47 return JSON::PP::false if !defined $value;
1153 28 100       47 return JSON::PP::false if ref $value;
1154 27 100       40 return JSON::PP::false if !_is_string_scalar($value);
1155              
1156 24   50     44 $needle //= '';
1157 24         34 my $len = length $needle;
1158              
1159 24 100       46 if ($mode eq 'start') {
1160 11 100 100     54 return JSON::PP::true if $len == 0 || index($value, $needle) == 0;
1161 5         13 return JSON::PP::false;
1162             }
1163              
1164 13 50       43 if ($mode eq 'end') {
1165 13 100       30 return JSON::PP::true if $len == 0;
1166 9 50       22 return JSON::PP::false if length($value) < $len;
1167 9 100       30 return JSON::PP::true if substr($value, -$len) eq $needle;
1168 5         13 return JSON::PP::false;
1169             }
1170              
1171 0         0 return JSON::PP::false;
1172             }
1173              
1174             sub _apply_test {
1175 16     16   38 my ($value, $pattern, $flags) = @_;
1176              
1177 16         47 my ($regex, $error) = _build_regex($pattern, $flags);
1178 16 100       36 if ($error) {
1179 3         16 $error =~ s/[\r\n]+$//;
1180 3         51 die "test(): invalid regular expression - $error";
1181             }
1182              
1183 13         34 return _test_against_regex($value, $regex);
1184             }
1185              
1186             sub _apply_match {
1187 10     10   16 my ($value, $pattern, $flags) = @_;
1188              
1189 10         29 my ($regex, $error) = _build_regex($pattern, $flags);
1190 10 100       19 if ($error) {
1191 3         14 $error =~ s/[\r\n]+$//;
1192 3         34 die "match(): invalid regular expression - $error";
1193             }
1194              
1195 7         12 return _match_against_regex($value, $regex);
1196             }
1197              
1198             sub _test_against_regex {
1199 26     26   46 my ($value, $regex) = @_;
1200              
1201 26 100       54 if (ref $value eq 'ARRAY') {
1202 6         13 return [ map { _test_against_regex($_, $regex) } @$value ];
  13         74  
1203             }
1204              
1205 20 100       42 return JSON::PP::false if !defined $value;
1206              
1207 19 100       57 if (JSON::PP::is_bool($value)) {
1208 2 100       28 $value = $value ? 'true' : 'false';
1209             }
1210              
1211 19 100       135 return JSON::PP::false if ref $value;
1212              
1213 18 100       122 return $value =~ $regex ? JSON::PP::true : JSON::PP::false;
1214             }
1215              
1216             sub _match_against_regex {
1217 7     7   11 my ($value, $regex) = @_;
1218              
1219 7 50       12 if (ref $value eq 'ARRAY') {
1220 0         0 return [ map { _match_against_regex($_, $regex) } @$value ];
  0         0  
1221             }
1222              
1223 7 50       10 return undef if !defined $value;
1224              
1225 7 50       15 if (JSON::PP::is_bool($value)) {
1226 0 0       0 $value = $value ? 'true' : 'false';
1227             }
1228              
1229 7 50       35 return undef if ref $value;
1230              
1231 7         10 my $text = "$value";
1232 7 100       37 return undef unless $text =~ $regex;
1233              
1234 4         9 my $offset = $-[0];
1235 4         23 my $length = $+[0] - $-[0];
1236 4         8 my $string = substr($text, $offset, $length);
1237              
1238 4         5 my @captures;
1239 4         8 my $capture_count = $#-;
1240 4         9 for my $index (1 .. $capture_count) {
1241 2 50 33     12 if (defined $-[$index] && $-[$index] >= 0) {
1242 2         4 my $capture_offset = $-[$index];
1243 2         4 my $capture_length = $+[$index] - $-[$index];
1244 2         5 my $capture_string = substr($text, $capture_offset, $capture_length);
1245 2         7 push @captures, {
1246             offset => $capture_offset,
1247             length => $capture_length,
1248             string => $capture_string,
1249             };
1250             } else {
1251 0         0 push @captures, {
1252             offset => undef,
1253             length => undef,
1254             string => undef,
1255             };
1256             }
1257             }
1258              
1259             return {
1260 4         27 offset => $offset,
1261             length => $length,
1262             string => $string,
1263             captures => \@captures,
1264             };
1265             }
1266              
1267             sub _build_regex {
1268 44     44   103 my ($pattern, $flags) = @_;
1269              
1270 44 50       85 $pattern = '' unless defined $pattern;
1271 44 50       86 $flags = '' unless defined $flags;
1272              
1273 44         68 my %allowed = map { $_ => 1 } qw(i m s x);
  176         346  
1274 44         87 my $modifiers = '';
1275 44         106 for my $flag (split //, $flags) {
1276 12 100       43 return (undef, "unknown regex flag '$flag'") unless $allowed{$flag};
1277 10 50       31 next if index($modifiers, $flag) >= 0;
1278 10         22 $modifiers .= $flag;
1279             }
1280              
1281 42         60 my $escaped = $pattern;
1282 42         71 $escaped =~ s/'/\\'/g;
1283              
1284 42         3423 my $regex = eval "qr'$escaped'$modifiers";
1285 42 100       178 if ($@) {
1286 6         22 return (undef, $@);
1287             }
1288              
1289 36         127 return ($regex, undef);
1290             }
1291              
1292             sub _parse_string_argument {
1293 99     99   300 my ($raw) = @_;
1294              
1295 99 50       253 return '' if !defined $raw;
1296              
1297 99         168 my $parsed = eval { _decode_json($raw) };
  99         235  
1298 99 50       11161 if (!$@) {
1299 99 50       184 $parsed = '' if !defined $parsed;
1300 99         252 return $parsed;
1301             }
1302              
1303 0         0 $raw =~ s/^\s+|\s+$//g;
1304 0         0 $raw =~ s/^['"]//;
1305 0         0 $raw =~ s/['"]$//;
1306 0         0 return $raw;
1307             }
1308              
1309             sub _parse_literal_argument {
1310 28     28   109 my ($raw) = @_;
1311              
1312 28 50       68 return undef if !defined $raw;
1313              
1314 28         65 my $parsed = eval { _decode_json($raw) };
  28         83  
1315 28 50       6821 return $parsed if !$@;
1316              
1317 0         0 $raw =~ s/^\s+|\s+$//g;
1318 0         0 $raw =~ s/^['"]//;
1319 0         0 $raw =~ s/['"]$//;
1320 0         0 return $raw;
1321             }
1322              
1323             sub _apply_csv {
1324 3     3   8 my ($value) = @_;
1325              
1326 3 100       11 if (ref $value eq 'ARRAY') {
1327 2         7 my @fields = map { _format_csv_field($_) } @$value;
  6         16  
1328 2         12 return join(',', @fields);
1329             }
1330              
1331 1         5 return _format_csv_field($value);
1332             }
1333              
1334             sub _apply_tsv {
1335 3     3   6 my ($value) = @_;
1336              
1337 3 100       6 if (ref $value eq 'ARRAY') {
1338 2         4 my @fields = map { _format_tsv_field($_) } @$value;
  6         20  
1339 2         7 return join("\t", @fields);
1340             }
1341              
1342 1         3 return _format_tsv_field($value);
1343             }
1344              
1345             sub _apply_base64 {
1346 6     6   22 my ($value) = @_;
1347              
1348 6         9 my $text;
1349              
1350 6 100 66     31 if (!defined $value) {
    50          
    100          
    50          
1351 1         3 $text = 'null';
1352             }
1353             elsif (JSON::PP::is_bool($value)) {
1354 0 0       0 $text = $value ? 'true' : 'false';
1355             }
1356             elsif (!ref $value) {
1357 2         20 $text = "$value";
1358             }
1359             elsif (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1360 3         72 $text = _encode_json($value);
1361             }
1362             else {
1363 0         0 $text = "$value";
1364             }
1365              
1366 6         495 return encode_base64($text, '');
1367             }
1368              
1369             sub _apply_base64d {
1370 7     7   19 my ($value) = @_;
1371              
1372 7         11 my $text;
1373              
1374 7 50 0     33 if (!defined $value) {
    50          
    50          
    0          
1375 0         0 $text = '';
1376             }
1377             elsif (JSON::PP::is_bool($value)) {
1378 0 0       0 $text = $value ? 'true' : 'false';
1379             }
1380             elsif (!ref $value) {
1381 7 100       57 die '@base64d(): input must be base64 text'
1382             if !_is_string_scalar($value);
1383 6         14 $text = "$value";
1384             }
1385             elsif (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1386 0         0 $text = _encode_json($value);
1387             }
1388             else {
1389 0         0 $text = "$value";
1390             }
1391              
1392 6         16 $text =~ s/\s+//g;
1393              
1394 6 100       74 die '@base64d(): input must be base64 text'
1395             if length($text) % 4 != 0;
1396              
1397 4 100       41 die '@base64d(): input must be base64 text'
1398             if $text !~ /^[A-Za-z0-9+\/]*={0,2}$/;
1399              
1400 3 50 66     20 die '@base64d(): input must be base64 text'
1401             if $text =~ /=/ && $text !~ /=+$/;
1402              
1403 3         15 my $decoded = decode_base64($text);
1404 3         10 my $reencoded = encode_base64($decoded, '');
1405              
1406 3 50       8 die '@base64d(): input must be base64 text'
1407             if $reencoded ne $text;
1408              
1409 3         14 return $decoded;
1410             }
1411              
1412             sub _apply_uri {
1413 8     8   23 my ($value) = @_;
1414              
1415 8         14 my $text;
1416              
1417 8 100 33     61 if (!defined $value) {
    50          
    100          
    50          
1418 1         5 $text = 'null';
1419             }
1420             elsif (JSON::PP::is_bool($value)) {
1421 0 0       0 $text = $value ? 'true' : 'false';
1422             }
1423             elsif (!ref $value) {
1424 5         57 $text = "$value";
1425             }
1426             elsif (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1427 2         50 $text = _encode_json($value);
1428             }
1429             else {
1430 0         0 $text = "$value";
1431             }
1432              
1433 8 100       382 my $encoded = is_utf8($text, 1)
1434             ? encode('UTF-8', $text)
1435             : $text;
1436 8         255 $encoded =~ s/([^A-Za-z0-9\-._~])/sprintf('%%%02X', ord($1))/ge;
  76         308  
1437 8         47 return $encoded;
1438             }
1439              
1440             sub _format_csv_field {
1441 7     7   15 my ($value) = @_;
1442              
1443 7 50       17 return '' if !defined $value;
1444              
1445 7 50       24 if (JSON::PP::is_bool($value)) {
1446 0 0       0 return $value ? 'true' : 'false';
1447             }
1448              
1449 7 50 33     65 if (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1450 0         0 my $encoded = _encode_json($value);
1451 0         0 return _quote_csv_text($encoded);
1452             }
1453              
1454 7 50       19 if (ref $value) {
1455 0         0 my $stringified = "$value";
1456 0         0 return _quote_csv_text($stringified);
1457             }
1458              
1459 7 100       14 if (_is_unquoted_csv_number($value)) {
1460 1         7 return "$value";
1461             }
1462              
1463 6         22 my $text = "$value";
1464 6         14 return _quote_csv_text($text);
1465             }
1466              
1467             sub _format_tsv_field {
1468 7     7   9 my ($value) = @_;
1469              
1470 7 50       11 return '' if !defined $value;
1471              
1472 7 100       14 if (JSON::PP::is_bool($value)) {
1473 1 50       55 return $value ? 'true' : 'false';
1474             }
1475              
1476 6 100 66     34 if (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1477 1         3 my $encoded = _encode_json($value);
1478 1         107 return _escape_tsv_text($encoded);
1479             }
1480              
1481 5 50       6 if (ref $value) {
1482 0         0 my $stringified = "$value";
1483 0         0 return _escape_tsv_text($stringified);
1484             }
1485              
1486 5         7 my $text = "$value";
1487 5         7 return _escape_tsv_text($text);
1488             }
1489              
1490             sub _quote_csv_text {
1491 6     6   13 my ($text) = @_;
1492              
1493 6 50       14 $text = '' unless defined $text;
1494 6         16 $text =~ s/"/""/g;
1495 6         26 return '"' . $text . '"';
1496             }
1497              
1498             sub _escape_tsv_text {
1499 6     6   8 my ($text) = @_;
1500              
1501 6 50       6 $text = '' unless defined $text;
1502 6         17 $text =~ s/\\/\\\\/g;
1503 6         10 $text =~ s/\t/\\t/g;
1504 6         10 $text =~ s/\r/\\r/g;
1505 6         11 $text =~ s/\n/\\n/g;
1506 6         11 return $text;
1507             }
1508              
1509             sub _is_unquoted_csv_number {
1510 7     7   16 my ($value) = @_;
1511              
1512 7 50       16 return 0 if !defined $value;
1513 7 50       16 return 0 if ref $value;
1514              
1515 7         29 my $sv = B::svref_2object(\$value);
1516 7         34 my $flags = $sv->FLAGS;
1517              
1518 7 100       37 return ($flags & (B::SVp_IOK() | B::SVp_NOK())) ? 1 : 0;
1519             }
1520              
1521             sub _apply_split {
1522 29     29   50 my ($value, $separator) = @_;
1523              
1524 29 100       72 if (ref $value eq 'ARRAY') {
1525 4         5 my @parts;
1526              
1527 4         7 for my $element (@$value) {
1528 11 100       15 if (JSON::PP::is_bool($element)) {
1529 5 100       54 my $stringified = $element ? 'true' : 'false';
1530 5         30 my $result = _apply_split($stringified, $separator);
1531 5 50       12 push @parts, ref($result) eq 'ARRAY' ? @$result : $result;
1532 5         8 next;
1533             }
1534              
1535 6         25 my $result = _apply_split($element, $separator);
1536 6 100       17 push @parts, ref($result) eq 'ARRAY' ? @$result : $result;
1537             }
1538              
1539 4         9 return \@parts;
1540             }
1541              
1542 25 100       65 return undef if !defined $value;
1543 22 100       50 if (JSON::PP::is_bool($value)) {
    100          
1544 1 50       15 $value = $value ? 'true' : 'false';
1545             }
1546             elsif (ref $value) {
1547 1         4 return $value;
1548             }
1549              
1550 21 50       147 $separator = '' unless defined $separator;
1551              
1552 21 100       38 if ($separator eq '') {
1553 3         24 return [ split(//, $value) ];
1554             }
1555              
1556 18         44 my $pattern = quotemeta $separator;
1557 18         189 my @parts = split /$pattern/, $value, -1;
1558 18         63 return [ @parts ];
1559             }
1560              
1561             sub _apply_explode {
1562 13     13   31 my ($value) = @_;
1563              
1564 13 100       31 if (ref $value eq 'ARRAY') {
1565 2         7 return [ map { _apply_explode($_) } @$value ];
  8         20  
1566             }
1567              
1568 11 100       23 return undef if !defined $value;
1569              
1570 9 100       93 if (JSON::PP::is_bool($value)) {
1571 1 50       24 $value = $value ? 'true' : 'false';
1572             }
1573              
1574 9 100       71 return $value if ref $value;
1575              
1576 7         25 my @chars = split(//u, "$value");
1577 7         11 return [ map { ord($_) } @chars ];
  24         53  
1578             }
1579              
1580             sub _apply_implode {
1581 10     10   17 my ($value) = @_;
1582              
1583 10 100       19 return undef if !defined $value;
1584              
1585 9 100       21 if (ref $value eq 'ARRAY') {
1586 8         15 my $has_nested = grep { ref $_ } @$value;
  24         44  
1587              
1588 8 100       16 if ($has_nested) {
1589 2         6 return [ map { _apply_implode($_) } @$value ];
  6         13  
1590             }
1591              
1592 6         11 my $string = '';
1593 6         11 for my $code (@$value) {
1594 18 50       28 next unless defined $code;
1595 18 50       40 next unless looks_like_number($code);
1596 18         48 $string .= chr(int($code));
1597             }
1598 6         27 return $string;
1599             }
1600              
1601 1         26 return $value;
1602             }
1603              
1604             sub _apply_substr {
1605 16     16   33 my ($value, @args) = @_;
1606              
1607 16 100       38 if (ref $value eq 'ARRAY') {
1608 2         6 return [ map { _apply_substr($_, @args) } @$value ];
  4         12  
1609             }
1610              
1611 14 100       37 return undef if !defined $value;
1612 13 100       32 if (JSON::PP::is_bool($value)) {
    50          
1613 3 100       69 $value = $value ? 'true' : 'false';
1614             }
1615             elsif (ref $value) {
1616 0         0 return $value;
1617             }
1618              
1619 13         106 my ($start, $length) = @args;
1620 13 100 100     60 if (defined $start && !looks_like_number($start)) {
1621 1         32 die 'substr(): start index must be numeric';
1622             }
1623 12 100       21 $start = 0 unless defined $start;
1624 12         17 $start = int($start);
1625              
1626 12 100       22 if (defined $length) {
1627 8 100       19 if (!looks_like_number($length)) {
1628 1         16 die 'substr(): length must be numeric';
1629             }
1630 7         12 $length = int($length);
1631 7         33 return substr($value, $start, $length);
1632             }
1633              
1634 4         14 return substr($value, $start);
1635             }
1636              
1637             sub _apply_slice {
1638 9     9   25 my ($value, @args) = @_;
1639              
1640 9 50       25 return undef if !defined $value;
1641              
1642 9 100       29 if (ref $value eq 'ARRAY') {
1643 8         14 my $array = $value;
1644 8         17 my $size = @$array;
1645              
1646 8 50       22 return [] if $size == 0;
1647              
1648 8 50       19 my $raw_start = @args ? $args[0] : 0;
1649 8         15 my $start = 0;
1650              
1651 8 100 66     70 if (defined $raw_start && !looks_like_number($raw_start)) {
1652 1         21 die 'slice(): start must be numeric';
1653             }
1654              
1655 7 50 33     35 if (defined $raw_start && looks_like_number($raw_start)) {
1656 7         32 $start = int($raw_start);
1657             }
1658              
1659 7 100       20 $start += $size if $start < 0;
1660 7 100       16 $start = 0 if $start < 0;
1661 7 100       23 return [] if $start >= $size;
1662              
1663 6         11 my $length;
1664 6 100 66     77 if (@args > 1 && defined $args[1] && !looks_like_number($args[1])) {
      100        
1665 1         22 die 'slice(): length must be numeric';
1666             }
1667              
1668 5 50 66     64 if (@args > 1 && defined $args[1] && looks_like_number($args[1])) {
      66        
1669 4         9 $length = int($args[1]);
1670             }
1671              
1672 5         7 my $end;
1673 5 100       14 if (defined $length) {
1674 4 100       14 return [] if $length <= 0;
1675 3         13 $end = $start + $length;
1676             }
1677             else {
1678 1         4 $end = $size;
1679             }
1680              
1681 4 50       10 $end = $size if $end > $size;
1682              
1683 4 50       10 return [] if $end <= $start;
1684              
1685 4         35 return [ @$array[$start .. $end - 1] ];
1686             }
1687              
1688 1         5 return $value;
1689             }
1690              
1691             sub _apply_replace {
1692 17     17   46 my ($value, $search, $replacement) = @_;
1693              
1694 17 100       44 if (ref $value eq 'ARRAY') {
1695 2         7 return [ map { _apply_replace($_, $search, $replacement) } @$value ];
  6         40  
1696             }
1697              
1698 15 100       48 return $value if !defined $value;
1699 14 100       32 return $value if ref $value;
1700              
1701 13 100       61 return $value if looks_like_number($value);
1702              
1703 11 50       31 $search = defined $search ? "$search" : '';
1704 11 50       25 $replacement = defined $replacement ? "$replacement" : '';
1705              
1706 11 100       32 return $value if $search eq '';
1707              
1708 10         23 my $pattern = quotemeta $search;
1709 10         214 (my $copy = "$value") =~ s/$pattern/$replacement/g;
1710 10         58 return $copy;
1711             }
1712              
1713             sub _apply_pick {
1714 5     5   8 my ($value, $keys) = @_;
1715              
1716 5 50       14 return $value unless @$keys;
1717              
1718 5 100       13 if (ref $value eq 'HASH') {
1719 3         3 my %subset;
1720 3         4 for my $key (@$keys) {
1721 6 50       10 next unless defined $key;
1722 6 100       10 next unless exists $value->{$key};
1723 5         14 $subset{$key} = $value->{$key};
1724             }
1725 3         14 return \%subset;
1726             }
1727              
1728 2 100       5 if (ref $value eq 'ARRAY') {
1729 1         2 return [ map { _apply_pick($_, $keys) } @$value ];
  2         13  
1730             }
1731              
1732 1         2 return $value;
1733             }
1734              
1735             sub _parse_arguments {
1736 118     118   351 my ($raw) = @_;
1737              
1738 118 50       279 return () unless defined $raw;
1739              
1740 118         246 my $parsed = eval { _decode_json("[$raw]") };
  118         554  
1741 118 50 33     28146 if (!$@ && ref $parsed eq 'ARRAY') {
1742 118         554 return @$parsed;
1743             }
1744              
1745 0         0 my @parts = split /,/, $raw;
1746             return map {
1747 0         0 my $part = $_;
  0         0  
1748 0         0 $part =~ s/^\s+|\s+$//g;
1749 0         0 $part;
1750             } @parts;
1751             }
1752              
1753             sub _split_semicolon_arguments {
1754 37     37   135 my ($raw, $expected) = @_;
1755              
1756 37   50     80 $raw //= '';
1757              
1758 37         50 my @segments;
1759 37         67 my $current = '';
1760 37         63 my $depth = 0;
1761 37         46 my $in_single = 0;
1762 37         53 my $in_double = 0;
1763 37         49 my $escape = 0;
1764              
1765 37         254 for my $char (split //, $raw) {
1766 458 100       789 if ($escape) {
1767 1         3 $current .= $char;
1768 1         2 $escape = 0;
1769 1         3 next;
1770             }
1771              
1772 457 100 66     890 if ($char eq '\\' && $in_double) {
1773 1         3 $current .= $char;
1774 1         2 $escape = 1;
1775 1         2 next;
1776             }
1777              
1778 456 100 66     1097 if ($char eq '"' && !$in_single) {
1779 110         167 $in_double = !$in_double;
1780 110         159 $current .= $char;
1781 110         173 next;
1782             }
1783              
1784 346 50 33     708 if ($char eq "'" && !$in_double) {
1785 0         0 $in_single = !$in_single;
1786 0         0 $current .= $char;
1787 0         0 next;
1788             }
1789              
1790 346 100 66     992 if (!$in_single && !$in_double) {
1791 113 100 66     599 if ($char =~ /[\[\{\(]/) {
    100 66        
    100          
1792 10         19 $depth++;
1793             }
1794             elsif ($char =~ /[\]\}\)]/ && $depth > 0) {
1795 10         19 $depth--;
1796             }
1797             elsif ($char eq ';' && $depth == 0) {
1798 19         41 my $segment = $current;
1799 19         125 $segment =~ s/^\s+|\s+$//g;
1800 19 50       67 push @segments, length $segment ? $segment : undef;
1801 19         54 $current = '';
1802 19         40 next;
1803             }
1804             }
1805              
1806 327         558 $current .= $char;
1807             }
1808              
1809 37         139 my $final = $current;
1810 37         178 $final =~ s/^\s+|\s+$//g;
1811 37 50       102 push @segments, length $final ? $final : undef;
1812              
1813 37 50       74 if (defined $expected) {
1814 37         86 $expected = int($expected);
1815 37 100       95 if ($expected > @segments) {
1816 18         32 push @segments, (undef) x ($expected - @segments);
1817             }
1818             }
1819              
1820 37         171 return @segments;
1821             }
1822              
1823             sub _parse_range_arguments {
1824 9     9   20 my ($raw) = @_;
1825              
1826 9 50       19 return () unless defined $raw;
1827              
1828 9         43 $raw =~ s/^\s+|\s+$//g;
1829 9 50       21 return () if $raw eq '';
1830              
1831 9         15 my @segments;
1832 9         15 my $current = '';
1833 9         13 my $in_single = 0;
1834 9         16 my $in_double = 0;
1835 9         57 my $escape = 0;
1836              
1837 9         47 for my $char (split //, $raw) {
1838 61 50       116 if ($escape) {
1839 0         0 $current .= $char;
1840 0         0 $escape = 0;
1841 0         0 next;
1842             }
1843              
1844 61 50 33     160 if ($char eq '\\' && $in_double) {
1845 0         0 $current .= $char;
1846 0         0 $escape = 1;
1847 0         0 next;
1848             }
1849              
1850 61 100 66     136 if ($char eq '"' && !$in_single) {
1851 6         12 $in_double = !$in_double;
1852 6         9 $current .= $char;
1853 6         13 next;
1854             }
1855              
1856 55 50 33     169 if ($char eq "'" && !$in_double) {
1857 0         0 $in_single = !$in_single;
1858 0         0 $current .= $char;
1859 0         0 next;
1860             }
1861              
1862 55 50 66     165 if ($char eq ';' && !$in_single && !$in_double) {
      66        
1863 11         29 push @segments, $current;
1864 11         19 $current = '';
1865 11         23 next;
1866             }
1867              
1868 44         106 $current .= $char;
1869             }
1870              
1871 9         29 push @segments, $current;
1872              
1873 9         14 my @args;
1874 9         40 for my $segment (@segments) {
1875 20 50       44 next unless defined $segment;
1876 20         38 my $clean = $segment;
1877 20         112 $clean =~ s/^\s+|\s+$//g;
1878 20 50       52 next if $clean eq '';
1879              
1880 20         50 my @values = _parse_arguments($clean);
1881 20 50       49 my $value = @values ? $values[0] : undef;
1882 20         56 push @args, $value;
1883             }
1884              
1885 9         42 return @args;
1886             }
1887              
1888             sub _apply_range {
1889 9     9   22 my ($value, $args_ref) = @_;
1890              
1891 9         28 my $sequence = _build_range_sequence($args_ref);
1892 4         20 return @$sequence;
1893             }
1894              
1895             sub _build_range_sequence {
1896 9     9   19 my ($args_ref) = @_;
1897              
1898 9         24 my @args = @$args_ref;
1899 9 50       20 die 'range(): bounds must be numeric' unless @args;
1900              
1901 9 50       23 @args = @args[0 .. 2] if @args > 3;
1902              
1903 9         44 my ($start, $end, $step);
1904              
1905 9 100       31 if (@args == 1) {
    100          
1906 2         4 $start = 0;
1907 2         8 $end = _coerce_range_number($args[0]);
1908 2         5 $step = 1;
1909             }
1910             elsif (@args == 2) {
1911 3         10 $start = _coerce_range_number($args[0]);
1912 3         9 $end = _coerce_range_number($args[1]);
1913 3         8 $step = 1;
1914             }
1915             else {
1916 4         12 $start = _coerce_range_number($args[0]);
1917 4         10 $end = _coerce_range_number($args[1]);
1918 4         25 $step = _coerce_range_number($args[2]);
1919             }
1920              
1921 9 100 100     92 die 'range(): bounds must be numeric' unless defined $start && defined $end;
1922 6 100       75 die 'range(): step must be numeric' if !defined $step;
1923 4 50       10 return [] if $step == 0;
1924              
1925 4 100       11 if ($step > 0) {
1926 3 50       8 return [] if $start >= $end;
1927 3         6 my @sequence;
1928 3         7 for (my $current = $start; $current < $end; $current += $step) {
1929 11         29 push @sequence, 0 + $current;
1930             }
1931 3         19 return \@sequence;
1932             }
1933              
1934             # negative step
1935 1 50       6 return [] if $start <= $end;
1936              
1937 1         3 my @sequence;
1938 1         4 for (my $current = $start; $current > $end; $current += $step) {
1939 3         9 push @sequence, 0 + $current;
1940             }
1941              
1942 1         4 return \@sequence;
1943             }
1944              
1945             sub _coerce_range_number {
1946 20     20   38 my ($value) = @_;
1947              
1948 20 50       42 return undef if !defined $value;
1949 20 100       51 return undef if ref $value;
1950 18 100       44 return undef if _is_string_scalar($value);
1951              
1952 15 50       54 return looks_like_number($value) ? 0 + $value : undef;
1953             }
1954              
1955             sub _apply_contains {
1956 17     17   41 my ($value, $needle) = @_;
1957              
1958 17 100       57 return _deep_contains($value, $needle, 'legacy') ? JSON::PP::true : JSON::PP::false;
1959             }
1960              
1961             sub _apply_contains_subset {
1962 11     11   40 my ($value, $needle) = @_;
1963              
1964 11 100       36 return _deep_contains($value, $needle, 'subset') ? JSON::PP::true : JSON::PP::false;
1965             }
1966              
1967             sub _apply_inside {
1968 7     7   18 my ($value, $container) = @_;
1969              
1970 7         23 return _apply_contains($container, $value);
1971             }
1972              
1973             sub _apply_indices {
1974 9     9   19 my ($value, $needle) = @_;
1975              
1976 9 100       20 if (ref $value eq 'ARRAY') {
1977 6         9 my @matches;
1978 6         17 for my $i (0 .. $#$value) {
1979 23 100       58 push @matches, $i if _values_equal($value->[$i], $needle);
1980             }
1981 6         24 return \@matches;
1982             }
1983              
1984 3 50       6 return [] if !defined $value;
1985              
1986 3 50 33     22 if (!ref $value || JSON::PP::is_bool($value)) {
1987 3 100       7 return [] unless defined $needle;
1988              
1989 2         3 my $haystack = "$value";
1990 2         5 my $fragment = "$needle";
1991              
1992 2         2 my @positions;
1993 2 100       5 if ($fragment eq '') {
1994 1         4 @positions = (0 .. length($haystack));
1995             }
1996             else {
1997 1         2 my $pos = -1;
1998 1         1 while (1) {
1999 3         5 $pos = index($haystack, $fragment, $pos + 1);
2000 3 100       6 last if $pos == -1;
2001 2         6 push @positions, $pos;
2002             }
2003             }
2004              
2005 2         5 return \@positions;
2006             }
2007              
2008 0         0 return [];
2009             }
2010              
2011             sub _apply_has {
2012 7     7   19 my ($value, $needle) = @_;
2013              
2014 7 50       22 return JSON::PP::false if !defined $needle;
2015              
2016 7 100       23 if (ref $value eq 'HASH') {
2017 2 100       14 return exists $value->{$needle} ? JSON::PP::true : JSON::PP::false;
2018             }
2019              
2020 5 50       17 if (ref $value eq 'ARRAY') {
2021 5 50       11 return JSON::PP::false if ref $needle;
2022              
2023 5         59 my $sv = B::svref_2object(\$needle);
2024 5         59 my $flags = $sv->FLAGS;
2025 5 100       21 return JSON::PP::false unless ($flags & (B::SVp_IOK() | B::SVp_NOK()));
2026              
2027 3         7 my $index = int($needle);
2028 3 100 66     52 return ($index >= 0 && $index < @$value)
2029             ? JSON::PP::true
2030             : JSON::PP::false;
2031             }
2032              
2033 0         0 return JSON::PP::false;
2034             }
2035              
2036             sub _values_equal {
2037 62     62   135 my ($left, $right) = @_;
2038              
2039 62 50 66     152 return 1 if !defined $left && !defined $right;
2040 60 100 66     218 return 0 if !defined $left || !defined $right;
2041              
2042 58 100 66     157 if (JSON::PP::is_bool($left) && JSON::PP::is_bool($right)) {
2043 6         244 return (!!$left) == (!!$right);
2044             }
2045              
2046 52 50 66     429 if (!ref $left && !ref $right) {
2047 44 100 66     271 if (looks_like_number($left) && looks_like_number($right)) {
2048 11         83 return $left == $right;
2049             }
2050 33         239 return "$left" eq "$right";
2051             }
2052              
2053 8 50 33     38 if (ref $left eq 'ARRAY' && ref $right eq 'ARRAY') {
2054 0 0       0 return 0 if @$left != @$right;
2055 0         0 for (my $i = 0; $i < @$left; $i++) {
2056 0 0       0 return 0 unless _values_equal($left->[$i], $right->[$i]);
2057             }
2058 0         0 return 1;
2059             }
2060              
2061 8 50 33     213 if (ref $left eq 'HASH' && ref $right eq 'HASH') {
2062 8 100       150 return 0 if keys(%$left) != keys(%$right);
2063 7         25 for my $key (keys %$left) {
2064 9 100 66     59 return 0 unless exists $right->{$key} && _values_equal($left->{$key}, $right->{$key});
2065             }
2066 5         23 return 1;
2067             }
2068              
2069 0         0 return 0;
2070             }
2071              
2072             sub _deep_contains {
2073 63     63   111 my ($value, $needle, $mode) = @_;
2074              
2075 63   50     104 $mode ||= 'legacy';
2076              
2077 63 100 100     155 return 1 if !defined $value && !defined $needle;
2078 61 100       112 return 0 if !defined $value;
2079              
2080 59 100       148 if (ref $value eq 'ARRAY') {
2081 14         46 return _array_contains($value, $needle, $mode);
2082             }
2083              
2084 45 100       81 if (ref $value eq 'HASH') {
2085 13         42 return _hash_contains($value, $needle, $mode);
2086             }
2087              
2088 32         65 return _scalar_contains($value, $needle);
2089             }
2090              
2091             sub _array_contains {
2092 14     14   34 my ($haystack, $needle, $mode) = @_;
2093              
2094 14 100 66     132 if ($mode eq 'subset' && ref $needle eq 'ARRAY') {
2095 8         11 my @used;
2096 8         26 NEEDLE: for my $expected (@$needle) {
2097 12         42 for my $i (0 .. $#$haystack) {
2098 29 100       63 next if $used[$i];
2099 26 100       46 if (_deep_contains($haystack->[$i], $expected, $mode)) {
2100 9         13 $used[$i] = 1;
2101 9         20 next NEEDLE;
2102             }
2103             }
2104 3         16 return 0;
2105             }
2106 5         28 return 1;
2107             }
2108              
2109 6         18 for my $item (@$haystack) {
2110 11 100       31 return 1 if _values_equal($item, $needle);
2111             }
2112              
2113 2         14 return 0;
2114             }
2115              
2116             sub _hash_contains {
2117 13     13   23 my ($value, $needle, $mode) = @_;
2118              
2119 13 100       35 if (ref $needle eq 'HASH') {
2120 12         34 for my $key (keys %$needle) {
2121 13 50       47 return 0 unless exists $value->{$key};
2122              
2123 13 100       30 if ($mode eq 'legacy') {
2124 4 100       14 return 0 unless _values_equal($value->{$key}, $needle->{$key});
2125             }
2126             else {
2127 9 100       16 return 0 unless _deep_contains($value->{$key}, $needle->{$key}, $mode);
2128             }
2129             }
2130 6         114 return 1;
2131             }
2132              
2133 1 50       7 return exists $value->{$needle} ? 1 : 0;
2134             }
2135              
2136             sub _scalar_contains {
2137 32     32   49 my ($value, $needle) = @_;
2138              
2139 32 50       51 return 0 if !defined $value;
2140 32 100       149 return 0 if !defined $needle;
2141              
2142 30 50 33     64 if (!ref $value || JSON::PP::is_bool($value)) {
2143 30         45 my $haystack = "$value";
2144 30         67 my $fragment = "$needle";
2145 30 100       121 return index($haystack, $fragment) >= 0 ? 1 : 0;
2146             }
2147              
2148 0         0 return 0;
2149             }
2150              
2151             sub _ceil {
2152 9     9   21 my ($number) = @_;
2153              
2154 9 100       37 return $number if int($number) == $number;
2155 6 100       33 return $number > 0 ? int($number) + 1 : int($number);
2156             }
2157              
2158             sub _floor {
2159 10     10   24 my ($number) = @_;
2160              
2161 10 100       43 return $number if int($number) == $number;
2162 7 100       42 return $number > 0 ? int($number) : int($number) - 1;
2163             }
2164              
2165             sub _round {
2166 14     14   32 my ($number) = @_;
2167              
2168 14 100       80 return $number if int($number) == $number;
2169 10 100       54 return $number >= 0 ? int($number + 0.5) : int($number - 0.5);
2170             }
2171              
2172             sub _group_count {
2173 4     4   13 my ($array_ref, $path) = @_;
2174 4 50       14 return {} unless ref $array_ref eq 'ARRAY';
2175              
2176 4         15 my ($key_path, $use_entire_item) = _normalize_path_argument($path);
2177              
2178 4         10 my %counts;
2179 4         11 for my $item (@$array_ref) {
2180 18         28 my $key_value;
2181 18 100       38 if ($use_entire_item) {
2182 3         7 $key_value = $item;
2183             }
2184             else {
2185 15         35 my @values = _traverse($item, $key_path);
2186 15 100       34 $key_value = @values ? $values[0] : undef;
2187             }
2188              
2189 18 100       47 my $key = defined $key_value ? _key($key_value) : 'null';
2190 18         48 $counts{$key}++;
2191             }
2192              
2193 4         20 return \%counts;
2194             }
2195              
2196             1;