File Coverage

lib/JSON/LINQ.pm
Criterion Covered Total %
statement 648 835 77.6
branch 215 376 57.1
condition 35 78 44.8
subroutine 123 152 80.9
pod 61 63 96.8
total 1082 1504 71.9


line stmt bran cond sub pod time code
1             package JSON::LINQ;
2             ######################################################################
3             #
4             # JSON::LINQ - LINQ-style query interface for JSON, JSONL, and LTSV files
5             #
6             # https://metacpan.org/dist/JSON-LINQ
7             #
8             # Copyright (c) 2026 INABA Hitoshi
9             ######################################################################
10             #
11             # Compatible : Perl 5.005_03 and later
12             # Platform : Windows and UNIX/Linux
13             #
14             ######################################################################
15              
16 9     9   186544 use 5.00503; # Universal Consensus 1998 for primetools
  9         26  
17             # Perl 5.005_03 compatibility for historical toolchains
18             # use 5.008001; # Lancaster Consensus 2013 for toolchains
19              
20 9     9   37 use strict;
  9         39  
  9         612  
21 9 50 33 9   214 BEGIN { if ($] < 5.006 && !defined(&warnings::import)) { $INC{'warnings.pm'} = 'stub'; eval 'package warnings; sub import {}' } }
  0         0  
  0         0  
22 9     9   36 use warnings; local $^W = 1;
  9         10  
  9         590  
23 9 50   9   215 BEGIN { pop @INC if $INC[-1] eq '.' }
24              
25 9     9   36 use Carp qw(croak);
  9         11  
  9         436  
26              
27 9     9   34 use vars qw($VERSION);
  9         17  
  9         579  
28             $VERSION = '1.01';
29             $VERSION = $VERSION;
30             # $VERSION self-assignment suppresses "used only once" warning under strict.
31              
32             ###############################################################################
33             # JSON Boolean type objects (merged from mb::JSON)
34             ###############################################################################
35              
36             package JSON::LINQ::Boolean;
37 9     9   37 use vars qw($VERSION);
  9         11  
  9         845  
38             $VERSION = '1.01';
39             $VERSION = $VERSION;
40              
41             use overload
42 2     2   3 '0+' => sub { ${ $_[0] } },
  2         5  
43 2 100   2   3 q{""} => sub { ${ $_[0] } ? 'true' : 'false' },
  2         5  
44 6     6   56 'bool' => sub { ${ $_[0] } },
  6         20  
45 9     9   4523 fallback => 1;
  9         11053  
  9         100  
46              
47             package JSON::LINQ;
48              
49 9     9   798 use vars qw($true $false);
  9         15  
  9         81607  
50             {
51             my $_t = 1; $true = bless \$_t, 'JSON::LINQ::Boolean';
52             my $_f = 0; $false = bless \$_f, 'JSON::LINQ::Boolean';
53             }
54              
55 2     2 1 35 sub true { $true }
56 2     2 1 11 sub false { $false }
57              
58             ###############################################################################
59             # Internal JSON encoder/decoder (merged from mb::JSON 0.06)
60             ###############################################################################
61              
62             # UTF-8 multibyte pattern
63             my $utf8_pat = join '|', (
64             '[\x00-\x7F\x80-\xBF\xC0-\xC1\xF5-\xFF]',
65             '[\xC2-\xDF][\x80-\xBF]',
66             '[\xE0][\xA0-\xBF][\x80-\xBF]',
67             '[\xE1-\xEC][\x80-\xBF][\x80-\xBF]',
68             '[\xED][\x80-\x9F][\x80-\xBF]',
69             '[\xEE-\xEF][\x80-\xBF][\x80-\xBF]',
70             '[\xF0][\x90-\xBF][\x80-\xBF][\x80-\xBF]',
71             '[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]',
72             '[\xF4][\x80-\x8F][\x80-\xBF][\x80-\xBF]',
73             '[\x00-\xFF]',
74             );
75              
76             sub _json_decode {
77 52 50   52   344 my $json = defined $_[0] ? $_[0] : $_;
78 52         285 my $r = \$json;
79 52         127 my $val = _parse_value($r);
80 52         100 $$r =~ s/\A\s+//s;
81 52 50       130 croak "JSON::LINQ::_json_decode: trailing garbage: " . substr($$r, 0, 20)
82             if length $$r;
83 52         282 return $val;
84             }
85              
86             sub _parse_value {
87 380     380   643 my ($r) = @_;
88 380         801 $$r =~ s/\A\s+//s;
89 380 50       785 croak "JSON::LINQ::_json_decode: unexpected end of input" unless length $$r;
90              
91 380         1112 my $c = substr($$r, 0, 1);
92              
93 380 100       1693 if ($c eq '{') { return _parse_object($r) }
  100 100       185  
    100          
    100          
    100          
    100          
    50          
94 25         62 elsif ($c eq '[') { return _parse_array($r) }
95 115         209 elsif ($c eq '"') { return _parse_string($r) }
96 1         4 elsif ($$r =~ s/\Anull(?=[^a-zA-Z0-9_]|$)//s) { return undef }
97 12         32 elsif ($$r =~ s/\Atrue(?=[^a-zA-Z0-9_]|$)//s) { return $true }
98 6         124 elsif ($$r =~ s/\Afalse(?=[^a-zA-Z0-9_]|$)//s) { return $false }
99             elsif ($$r =~ s/\A(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)//s) {
100 121         400 return $1 + 0;
101             }
102             else {
103 0         0 croak "JSON::LINQ::_json_decode: unexpected token: " . substr($$r, 0, 20);
104             }
105             }
106              
107             sub _parse_object {
108 100     100   147 my ($r) = @_;
109 100         323 $$r =~ s/\A\{//s;
110 100         152 my %obj;
111 100         180 $$r =~ s/\A\s+//s;
112 100 50       253 if ($$r =~ s/\A\}//s) { return { %obj } }
  0         0  
113 100         128 while (1) {
114 255         405 $$r =~ s/\A\s+//s;
115 255 50       749 croak "JSON::LINQ::_json_decode: expected string key in object"
116             unless $$r =~ /\A"/;
117 255         489 my $key = _parse_string($r);
118 255         529 $$r =~ s/\A\s+//s;
119 255 50       1018 $$r =~ s/\A://s
120             or croak "JSON::LINQ::_json_decode: expected ':' after key '$key'";
121 255         491 my $val = _parse_value($r);
122 255         690 $obj{$key} = $val;
123 255         463 $$r =~ s/\A\s+//s;
124 255 100       750 if ($$r =~ s/\A,//s) { next }
  155 50       258  
125 100         202 elsif ($$r =~ s/\A\}//s) { last }
126 0         0 else { croak "JSON::LINQ::_json_decode: expected ',' or '}' in object" }
127             }
128 100         581 return { %obj };
129             }
130              
131             sub _parse_array {
132 25     25   1000 my ($r) = @_;
133 25         125 $$r =~ s/\A\[//s;
134 25         42 my @arr;
135 25         63 $$r =~ s/\A\s+//s;
136 25 50       90 if ($$r =~ s/\A\]//s) { return [ @arr ] }
  0         0  
137 25         36 while (1) {
138 73         185 push @arr, _parse_value($r);
139 73         143 $$r =~ s/\A\s+//s;
140 73 100       225 if ($$r =~ s/\A,//s) { next }
  48 50       70  
141 25         50 elsif ($$r =~ s/\A\]//s) { last }
142 0         0 else { croak "JSON::LINQ::_json_decode: expected ',' or ']' in array" }
143             }
144 25         1495 return [ @arr ];
145             }
146              
147             my %UNESC = (
148             '"' => '"', '\\' => '\\', '/' => '/',
149             'b' => "\x08", 'f' => "\x0C",
150             'n' => "\n", 'r' => "\r", 't' => "\t",
151             );
152              
153             sub _parse_string {
154 370     370   547 my ($r) = @_;
155 370         968 $$r =~ s/\A"//s;
156 370         613 my $s = '';
157 370         439 while (1) {
158 1797 100       11689 if ($$r =~ s/\A"//s) { last }
  370 50       1168  
    50          
    50          
159 0         0 elsif ($$r =~ s/\A\\(["\\\/bfnrt])//s) { $s .= $UNESC{$1} }
160             elsif ($$r =~ s/\A\\u([0-9a-fA-F]{4})//s) {
161 0         0 $s .= _cp_to_utf8(hex($1));
162             }
163 1427         2758 elsif ($$r =~ s/\A($utf8_pat)//s) { $s .= $1 }
164 0         0 else { croak "JSON::LINQ::_json_decode: unterminated string" }
165             }
166 370         917 return $s;
167             }
168              
169             sub _cp_to_utf8 {
170 0     0   0 my ($cp) = @_;
171 0 0       0 return chr($cp) if $cp <= 0x7F;
172 0 0       0 if ($cp <= 0x7FF) {
173 0         0 return chr(0xC0|($cp>>6)) . chr(0x80|($cp&0x3F));
174             }
175 0         0 return chr(0xE0|($cp>>12))
176             . chr(0x80|(($cp>>6)&0x3F))
177             . chr(0x80|($cp&0x3F));
178             }
179              
180             sub _json_encode {
181 7     7   15 my ($data) = @_;
182 7         18 return _enc_value($data);
183             }
184              
185             sub _enc_value {
186 21     21   43 my ($v) = @_;
187 21 50       44 return 'null' unless defined $v;
188 21 0       50 if (ref $v eq 'JSON::LINQ::Boolean') { return $$v ? 'true' : 'false' }
  0 50       0  
189 21 50       49 if (ref $v eq 'ARRAY') { return '[' . join(',', map { _enc_value($_) } @$v) . ']' }
  0         0  
  0         0  
190 21 100       65 if (ref $v eq 'HASH') {
191 7         39 my @pairs = map { _enc_string($_) . ':' . _enc_value($v->{$_}) }
  14         30  
192             sort keys %$v;
193 7         58 return '{' . join(',', @pairs) . '}';
194             }
195             # number: matches JSON number pattern exactly
196 14 100 66     104 if (!ref $v && $v =~ /\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\z/s) {
197 10         49 return $v;
198             }
199 4         8 return _enc_string($v);
200             }
201              
202             sub _enc_string {
203 18     18   35 my ($s) = @_;
204 18         36 $s =~ s/\\/\\\\/g;
205 18         27 $s =~ s/"/\\"/g;
206 18         55 $s =~ s/\x08/\\b/g;
207 18         35 $s =~ s/\x0C/\\f/g;
208 18         26 $s =~ s/\n/\\n/g;
209 18         29 $s =~ s/\r/\\r/g;
210 18         30 $s =~ s/\t/\\t/g;
211 18         41 $s =~ s/([\x00-\x1F])/sprintf('\\u%04X', ord($1))/ge;
  0         0  
212 18         75 return '"' . $s . '"';
213             }
214              
215             ###############################################################################
216             # Constructor and Iterator Infrastructure
217             ###############################################################################
218              
219             sub new {
220 223     223 0 459 my($class, $iterator) = @_;
221 223         2379 return bless { iterator => $iterator }, $class;
222             }
223              
224             sub iterator {
225 265     265 0 358 my $self = $_[0];
226             # If this object was created by _from_snapshot, _factory provides
227             # a fresh iterator closure each time iterator() is called.
228 265 100       597 if (exists $self->{_factory}) {
229 43         72 return $self->{_factory}->();
230             }
231 222         359 return $self->{iterator};
232             }
233              
234             ###############################################################################
235             # Data Source Methods
236             ###############################################################################
237              
238             # From - create query from array
239             sub From {
240 94     94 1 805443 my($class, $source) = @_;
241              
242 94 50       274 if (ref($source) eq 'ARRAY') {
243 94         115 my $i = 0;
244             return $class->new(sub {
245 438 100   438   723 return undef if $i >= scalar(@$source);
246 353         547 return $source->[$i++];
247 94         344 });
248             }
249              
250 0         0 die "From() requires ARRAY reference";
251             }
252              
253             # FromJSON - read a JSON file containing a top-level array of objects
254             # Each element of the array becomes one item in the sequence.
255             # The file must contain a single JSON array: [ {...}, {...}, ... ]
256             # or a single JSON object (treated as a one-element sequence).
257             sub FromJSON {
258 19     19 1 215184 my($class, $file) = @_;
259              
260 19         39 my $fh;
261 19 50       105 if ($] >= 5.006) {
262 19 50       1995 eval q{ open($fh, '<', $file) } or die "Cannot open '$file': $!";
263             }
264             else {
265 0         0 $fh = \do { local *_ };
  0         0  
266 0 0       0 open($fh, "< $file") or die "Cannot open '$file': $!";
267             }
268 19         129 binmode $fh; # Raw bytes; handles UTF-8 without UTF-8 flag
269              
270 19         52 my $content = do { local $/; <$fh> };
  19         101  
  19         834  
271 19         220 close $fh;
272              
273 19         41 my $data = eval { _json_decode($content) };
  19         66  
274 19 50       49 die "JSON::LINQ::FromJSON: cannot parse '$file': $@" if $@;
275              
276 19         25 my $records;
277 19 50       54 if (ref($data) eq 'ARRAY') {
    0          
278 19         30 $records = $data;
279             }
280             elsif (ref($data) eq 'HASH') {
281 0         0 $records = [ $data ];
282             }
283             else {
284 0         0 die "JSON::LINQ::FromJSON: '$file' must contain a JSON array or object";
285             }
286              
287 19         27 my $i = 0;
288             return $class->new(sub {
289 74 100   74   254 return undef if $i >= scalar(@$records);
290 55         112 return $records->[$i++];
291 19         301 });
292             }
293              
294             # FromJSONL - read a JSONL (JSON Lines) file
295             # Each line is a separate JSON value (typically an object).
296             # Empty lines and lines beginning with '#' are skipped.
297             # This is memory-efficient for large files: one line at a time.
298             sub FromJSONL {
299 8     8 1 844 my($class, $file) = @_;
300              
301 8         16 my $fh;
302 8 50       25 if ($] >= 5.006) {
303 8 50       1637 eval q{ open($fh, '<', $file) } or die "Cannot open '$file': $!";
304             }
305             else {
306 0         0 $fh = \do { local *_ };
  0         0  
307 0 0       0 open($fh, "< $file") or die "Cannot open '$file': $!";
308             }
309 8         110 binmode $fh; # Raw bytes; handles UTF-8 without UTF-8 flag
310              
311             return $class->new(sub {
312 33     33   632 while (my $line = <$fh>) {
313 27         53 chomp $line;
314 27         57 $line =~ s/\r\z//; # Strip CR for CRLF files
315 27 100       64 next unless length $line;
316 26 50       100 next if $line =~ /\A\s*\z/; # Skip blank lines
317 26 50       67 next if $line =~ /\A\s*#/; # Skip comment lines
318              
319 26         43 my $val = eval { _json_decode($line) };
  26         64  
320 26 50       58 if ($@) {
321 0         0 warn "JSON::LINQ::FromJSONL: skipping invalid JSON line: $@";
322 0         0 next;
323             }
324 26         86 return $val;
325             }
326 7         114 close $fh;
327 7         27 return undef;
328 8         155 });
329             }
330              
331             # FromJSONString - create query from a JSON string (array or object)
332             sub FromJSONString {
333 7     7 1 119 my($class, $json) = @_;
334              
335 7         10 my $data = eval { _json_decode($json) };
  7         18  
336 7 50       16 die "JSON::LINQ::FromJSONString: cannot parse JSON: $@" if $@;
337              
338 7         11 my $records;
339 7 100       18 if (ref($data) eq 'ARRAY') {
    50          
340 6         10 $records = $data;
341             }
342             elsif (ref($data) eq 'HASH') {
343 1         2 $records = [ $data ];
344             }
345             else {
346 0         0 $records = [ $data ];
347             }
348              
349 7         10 my $i = 0;
350             return $class->new(sub {
351 26 100   26   49 return undef if $i >= scalar(@$records);
352 19         37 return $records->[$i++];
353 7         38 });
354             }
355              
356             # FromLTSV - read an LTSV (Labeled Tab-Separated Values) file
357             # Each line is a record of "label:value" fields separated by tabs.
358             # Empty lines are skipped. Memory-efficient: one line at a time.
359             # This method is provided so JSON::LINQ can JOIN with LTSV data sources
360             # without requiring LTSV::LINQ to be installed.
361             sub FromLTSV {
362 21     21 1 251020 my($class, $file) = @_;
363              
364 21         35 my $fh;
365 21 50       54 if ($] >= 5.006) {
366             # Avoid "Too many arguments for open at" error when running with Perl 5.005_03
367 21 50       1555 eval q{ open($fh, '<', $file) } or die "Cannot open '$file': $!";
368             }
369             else {
370 0         0 $fh = \do { local *_ };
  0         0  
371 0 0       0 open($fh, "< $file") or die "Cannot open '$file': $!";
372             }
373 21         111 binmode $fh; # Treat as raw bytes; handles all multibyte encodings
374             # and prevents \r\n -> \n translation on Windows
375              
376             return $class->new(sub {
377 89     89   686 while (my $line = <$fh>) {
378 77         103 chomp $line;
379 77         140 $line =~ s/\r\z//; # Remove CR for CRLF files on any platform
380 77 100       160 next unless length $line;
381              
382             my %record = map {
383 69 50       163 /\A(.+?):(.*)\z/ ? ($1, $2) : ()
  192         953  
384             } split /\t/, $line;
385              
386 69 50       379 return { %record } if %record;
387             }
388 20         194 close $fh;
389 20         45 return undef;
390 21         177 });
391             }
392              
393             # Range - generate sequence of integers
394             sub Range {
395 1     1 1 12 my($class, $start, $count) = @_;
396              
397 1         8 my $current = $start;
398 1         1 my $remaining = $count;
399              
400             return $class->new(sub {
401 6 100   6   9 return undef if $remaining <= 0;
402 5         5 $remaining--;
403 5         6 return $current++;
404 1         5 });
405             }
406              
407             # Empty - return empty sequence
408             sub Empty {
409 4     4 1 54 my($class) = @_;
410              
411             return $class->new(sub {
412 5     5   4 return undef;
413 4         11 });
414             }
415              
416             # Repeat - repeat element specified number of times
417             sub Repeat {
418 0     0 1 0 my($class, $element, $count) = @_;
419              
420 0         0 my $remaining = $count;
421              
422             return $class->new(sub {
423 0 0   0   0 return undef if $remaining <= 0;
424 0         0 $remaining--;
425 0         0 return $element;
426 0         0 });
427             }
428              
429             ###############################################################################
430             # Filtering Methods
431             ###############################################################################
432              
433             # Where - filter elements
434             sub Where {
435 13     13 1 40 my($self, @args) = @_;
436 13         34 my $iter = $self->iterator;
437 13         27 my $class = ref($self);
438              
439             # Support both code reference and DSL form
440 13         20 my $cond;
441 13 100 66     105 if (@args == 1 && ref($args[0]) eq 'CODE') {
442 8         20 $cond = $args[0];
443             }
444             else {
445             # DSL form: Where(key => value, ...)
446 5         14 my %match = @args;
447             $cond = sub {
448 24     24   33 my $row = shift;
449 24         68 for my $k (keys %match) {
450 27 100       65 return 0 unless defined $row->{$k};
451 22 100       73 return 0 unless $row->{$k} eq $match{$k};
452             }
453 9         36 return 1;
454 5         22 };
455             }
456              
457             return $class->new(sub {
458 43     43   80 while (1) {
459 70         128 my $item = $iter->();
460 70 100       156 return undef unless defined $item;
461 57 100       99 return $item if $cond->($item);
462             }
463 13         63 });
464             }
465              
466             ###############################################################################
467             # Projection Methods
468             ###############################################################################
469              
470             # Select - transform elements
471             sub Select {
472 23     23 1 81 my($self, $selector) = @_;
473 23         53 my $iter = $self->iterator;
474 23         54 my $class = ref($self);
475              
476             return $class->new(sub {
477 114     114   158 my $item = $iter->();
478 114 100       238 return undef unless defined $item;
479 91         191 return $selector->($item);
480 23         146 });
481             }
482              
483             # SelectMany - flatten sequences
484             sub SelectMany {
485 2     2 1 3 my($self, $selector) = @_;
486 2         3 my $iter = $self->iterator;
487 2         3 my $class = ref($self);
488              
489 2         2 my @buffer;
490              
491             return $class->new(sub {
492 12     12   6 while (1) {
493 18 100       21 if (@buffer) {
494 10         13 return shift @buffer;
495             }
496              
497 8         8 my $item = $iter->();
498 8 100       10 return undef unless defined $item;
499              
500 6         9 my $result = $selector->($item);
501 6 50       14 unless (ref($result) eq 'ARRAY') {
502 0         0 die "SelectMany: selector must return an ARRAY reference";
503             }
504 6         7 @buffer = @$result;
505             }
506 2         14 });
507             }
508              
509             # Concat - concatenate two sequences
510             sub Concat {
511 1     1 1 2 my($self, $second) = @_;
512 1         2 my $class = ref($self);
513              
514 1         18 my $first_iter = $self->iterator;
515 1         2 my $second_iter;
516 1         1 my $first_done = 0;
517              
518             return $class->new(sub {
519 9 100   9   11 if (!$first_done) {
520 5         4 my $item = $first_iter->();
521 5 100       17 if (defined $item) {
522 4         6 return $item;
523             }
524 1         2 $first_done = 1;
525 1         5 $second_iter = $second->iterator;
526             }
527              
528 5 50       8 return $second_iter ? $second_iter->() : undef;
529 1         5 });
530             }
531              
532             # Zip - combine two sequences element-wise
533             sub Zip {
534 0     0 1 0 my($self, $second, $result_selector) = @_;
535              
536 0         0 my $iter1 = $self->iterator;
537 0         0 my $iter2 = $second->iterator;
538 0         0 my $class = ref($self);
539              
540             return $class->new(sub {
541 0     0   0 my $item1 = $iter1->();
542 0         0 my $item2 = $iter2->();
543              
544             # Return undef if either sequence ends
545 0 0 0     0 return undef unless defined($item1) && defined($item2);
546              
547 0         0 return $result_selector->($item1, $item2);
548 0         0 });
549             }
550              
551             ###############################################################################
552             # Partitioning Methods
553             ###############################################################################
554              
555             # Take - take first N elements
556             sub Take {
557 4     4 1 12 my($self, $count) = @_;
558 4         12 my $iter = $self->iterator;
559 4         11 my $class = ref($self);
560 4         9 my $taken = 0;
561              
562             return $class->new(sub {
563 13 100   13   37 return undef if $taken >= $count;
564 9         18 my $item = $iter->();
565 9 50       27 return undef unless defined $item;
566 9         12 $taken++;
567 9         49 return $item;
568 4         53 });
569             }
570              
571             # Skip - skip first N elements
572             sub Skip {
573 1     1 1 2 my($self, $count) = @_;
574 1         2 my $iter = $self->iterator;
575 1         2 my $class = ref($self);
576 1         1 my $skipped = 0;
577              
578             return $class->new(sub {
579 4     4   6 while ($skipped < $count) {
580 2         5 my $item = $iter->();
581 2 50       4 return undef unless defined $item;
582 2         705 $skipped++;
583             }
584 4         6 return $iter->();
585 1         3 });
586             }
587              
588             # TakeWhile - take while condition is true
589             sub TakeWhile {
590 0     0 1 0 my($self, $predicate) = @_;
591 0         0 my $iter = $self->iterator;
592 0         0 my $class = ref($self);
593 0         0 my $done = 0;
594              
595             return $class->new(sub {
596 0 0   0   0 return undef if $done;
597 0         0 my $item = $iter->();
598 0 0       0 return undef unless defined $item;
599              
600 0 0       0 if ($predicate->($item)) {
601 0         0 return $item;
602             }
603             else {
604 0         0 $done = 1;
605 0         0 return undef;
606             }
607 0         0 });
608             }
609              
610             # SkipWhile - skip elements while predicate is true
611             sub SkipWhile {
612 0     0 1 0 my($self, $predicate) = @_;
613 0         0 my $iter = $self->iterator;
614 0         0 my $class = ref($self);
615 0         0 my $skipping = 1;
616              
617             return $class->new(sub {
618 0     0   0 while (1) {
619 0         0 my $item = $iter->();
620 0 0       0 return undef unless defined $item;
621              
622 0 0       0 if ($skipping) {
623 0 0       0 if (!$predicate->($item)) {
624 0         0 $skipping = 0;
625 0         0 return $item;
626             }
627             }
628             else {
629 0         0 return $item;
630             }
631             }
632 0         0 });
633             }
634              
635             ###############################################################################
636             # Ordering Methods
637             ###############################################################################
638              
639             # OrderBy - sort ascending (smart: numeric when both keys look numeric)
640             sub OrderBy {
641 3     3 1 8 my($self, $key_selector) = @_;
642 3         40 my @items = $self->ToArray();
643 3         48 return JSON::LINQ::Ordered->_new_ordered(
644             [ @items ],
645             [{ sel => $key_selector, dir => 1, type => 'smart' }]
646             );
647             }
648              
649             # OrderByDescending - sort descending (smart comparison)
650             sub OrderByDescending {
651 2     2 1 4 my($self, $key_selector) = @_;
652 2         6 my @items = $self->ToArray();
653 2         26 return JSON::LINQ::Ordered->_new_ordered(
654             [ @items ],
655             [{ sel => $key_selector, dir => -1, type => 'smart' }]
656             );
657             }
658              
659             # OrderByStr - sort ascending by string comparison
660             sub OrderByStr {
661 7     7 1 15 my($self, $key_selector) = @_;
662 7         18 my @items = $self->ToArray();
663 7         45 return JSON::LINQ::Ordered->_new_ordered(
664             [ @items ],
665             [{ sel => $key_selector, dir => 1, type => 'str' }]
666             );
667             }
668              
669             # OrderByStrDescending - sort descending by string comparison
670             sub OrderByStrDescending {
671 0     0 1 0 my($self, $key_selector) = @_;
672 0         0 my @items = $self->ToArray();
673 0         0 return JSON::LINQ::Ordered->_new_ordered(
674             [ @items ],
675             [{ sel => $key_selector, dir => -1, type => 'str' }]
676             );
677             }
678              
679             # OrderByNum - sort ascending by numeric comparison
680             sub OrderByNum {
681 5     5 1 15 my($self, $key_selector) = @_;
682 5         17 my @items = $self->ToArray();
683 5         50 return JSON::LINQ::Ordered->_new_ordered(
684             [ @items ],
685             [{ sel => $key_selector, dir => 1, type => 'num' }]
686             );
687             }
688              
689             # OrderByNumDescending - sort descending by numeric comparison
690             sub OrderByNumDescending {
691 1     1 1 3 my($self, $key_selector) = @_;
692 1         3 my @items = $self->ToArray();
693 1         7 return JSON::LINQ::Ordered->_new_ordered(
694             [ @items ],
695             [{ sel => $key_selector, dir => -1, type => 'num' }]
696             );
697             }
698              
699             # Reverse - reverse order
700             sub Reverse {
701 1     1 1 3 my($self) = @_;
702 1         3 my @items = reverse $self->ToArray();
703 1         3 my $class = ref($self);
704 1         4 return $class->From([ @items ]);
705             }
706              
707             ###############################################################################
708             # Grouping Methods
709             ###############################################################################
710              
711             # GroupBy - group elements by key
712             sub GroupBy {
713 6     6 1 11 my($self, $key_selector, $element_selector) = @_;
714 6   66 19   33 $element_selector ||= sub { $_[0] };
  19         35  
715              
716 6         7 my %groups;
717             my @key_order;
718              
719             $self->ForEach(sub {
720 23     23   41 my $item = shift;
721 23         36 my $key = $key_selector->($item);
722 23 50       63 $key = '' unless defined $key;
723 23 100       34 unless (exists $groups{$key}) {
724 15         37 push @key_order, $key;
725             }
726 23         21 push @{$groups{$key}}, $element_selector->($item);
  23         37  
727 6         22 });
728              
729 6         23 my @result;
730 6         11 for my $key (@key_order) {
731             push @result, {
732             Key => $key,
733 15         33 Elements => $groups{$key},
734             };
735             }
736              
737 6         8 my $class = ref($self);
738 6         16 return $class->From([ @result ]);
739             }
740              
741             ###############################################################################
742             # Set Operations
743             ###############################################################################
744              
745             # Distinct - remove duplicates
746             sub Distinct {
747 5     5 1 10 my($self, $key_selector) = @_;
748 5         9 my $iter = $self->iterator;
749 5         9 my $class = ref($self);
750 5         7 my %seen;
751              
752             return $class->new(sub {
753 22     22   22 while (1) {
754 32         35 my $item = $iter->();
755 32 100       75 return undef unless defined $item;
756              
757 27 100       43 my $key = $key_selector ? $key_selector->($item) : _make_key($item);
758 27 50       39 $key = '' unless defined $key;
759              
760 27 100       58 unless ($seen{$key}++) {
761 17         38 return $item;
762             }
763             }
764 5         17 });
765             }
766              
767             # Internal helper for set operations - make key from item
768             sub _make_key {
769 40     40   37 my($item) = @_;
770              
771 40 50       47 return '' unless defined $item;
772              
773 40 50       75 if (ref($item) eq 'HASH') {
    50          
774 0         0 my @pairs = ();
775 0         0 for my $k (sort keys %$item) {
776 0 0       0 my $v = defined($item->{$k}) ? $item->{$k} : '';
777 0         0 push @pairs, "$k\x1F$v"; # \x1F = Unit Separator
778             }
779 0         0 return join("\x1E", @pairs); # \x1E = Record Separator
780             }
781             elsif (ref($item) eq 'ARRAY') {
782 0 0       0 return join("\x1E", map { defined($_) ? $_ : '' } @$item);
  0         0  
783             }
784             else {
785 40         46 return $item;
786             }
787             }
788              
789             # _from_snapshot - internal helper for GroupJoin.
790             sub _from_snapshot {
791 24     24   29 my($class_or_self, $aref) = @_;
792              
793 24   33     52 my $class = ref($class_or_self) || $class_or_self;
794              
795             my $iter_factory = sub {
796 48     48   50 my $i = 0;
797             return sub {
798 46 100       83 return undef if $i >= scalar(@$aref);
799 22         29 return $aref->[$i++];
800 48         105 };
801 24         49 };
802              
803 24         29 my $obj = bless {
804             iterator => $iter_factory->(),
805             _factory => $iter_factory,
806             }, $class;
807              
808 24         36 return $obj;
809             }
810              
811             # Union - set union with distinct
812             sub Union {
813 1     1 1 2 my($self, $second, $key_selector) = @_;
814              
815 1         5 return $self->Concat($second)->Distinct($key_selector);
816             }
817              
818             # Intersect - set intersection
819             sub Intersect {
820 1     1 1 3 my($self, $second, $key_selector) = @_;
821              
822 1         1 my %second_set = ();
823             $second->ForEach(sub {
824 4     4   4 my $item = shift;
825 4 50       7 my $key = $key_selector ? $key_selector->($item) : _make_key($item);
826 4         7 $second_set{$key} = $item;
827 1         5 });
828              
829 1         17 my $class = ref($self);
830 1         2 my $iter = $self->iterator;
831 1         1 my %seen = ();
832              
833             return $class->new(sub {
834 3     3   22 while (defined(my $item = $iter->())) {
835 4 50       9 my $key = $key_selector ? $key_selector->($item) : _make_key($item);
836              
837 4 50       7 next if $seen{$key}++;
838 4 100       8 return $item if exists $second_set{$key};
839             }
840 1         2 return undef;
841 1         3 });
842             }
843              
844             # Except - set difference
845             sub Except {
846 1     1 1 2 my($self, $second, $key_selector) = @_;
847              
848 1         2 my %second_set = ();
849             $second->ForEach(sub {
850 4     4   4 my $item = shift;
851 4 50       5 my $key = $key_selector ? $key_selector->($item) : _make_key($item);
852 4         5 $second_set{$key} = 1;
853 1         3 });
854              
855 1         3 my $class = ref($self);
856 1         1 my $iter = $self->iterator;
857 1         2 my %seen = ();
858              
859             return $class->new(sub {
860 3     3   3 while (defined(my $item = $iter->())) {
861 4 50       6 my $key = $key_selector ? $key_selector->($item) : _make_key($item);
862              
863 4 50       6 next if $seen{$key}++;
864 4 100       8 return $item unless exists $second_set{$key};
865             }
866 1         2 return undef;
867 1         5 });
868             }
869              
870             # Join - correlates elements of two sequences
871             sub Join {
872 10     10 1 24 my($self, $inner, $outer_key_selector, $inner_key_selector, $result_selector) = @_;
873              
874 10         17 my %inner_hash = ();
875             $inner->ForEach(sub {
876 24     24   28 my $item = shift;
877 24         37 my $key = $inner_key_selector->($item);
878 24 50       79 $key = _make_key($key) if ref($key);
879 24         25 push @{$inner_hash{$key}}, $item;
  24         62  
880 10         39 });
881              
882 10         43 my $class = ref($self);
883 10         19 my $iter = $self->iterator;
884 10         15 my @buffer = ();
885              
886             return $class->new(sub {
887 41     41   47 while (1) {
888 80 100       256 return shift @buffer if @buffer;
889              
890 49         63 my $outer_item = $iter->();
891 49 100       93 return undef unless defined $outer_item;
892              
893 39         61 my $key = $outer_key_selector->($outer_item);
894 39 50       116 $key = _make_key($key) if ref($key);
895              
896 39 100       66 if (exists $inner_hash{$key}) {
897 29         31 for my $inner_item (@{$inner_hash{$key}}) {
  29         49  
898 31         53 push @buffer, $result_selector->($outer_item, $inner_item);
899             }
900             }
901             }
902 10         40 });
903             }
904              
905             # GroupJoin - group join (LEFT OUTER JOIN-like operation)
906             sub GroupJoin {
907 6     6 1 12 my($self, $inner, $outer_key_selector, $inner_key_selector, $result_selector) = @_;
908 6         7 my $class = ref($self);
909 6         11 my $outer_iter = $self->iterator;
910              
911 6         9 my %inner_lookup = ();
912             $inner->ForEach(sub {
913 20     20   18 my $item = shift;
914 20         25 my $key = $inner_key_selector->($item);
915 20 50       47 $key = _make_key($key) if ref($key);
916 20 50       25 $key = '' unless defined $key;
917 20         20 push @{$inner_lookup{$key}}, $item;
  20         45  
918 6         20 });
919              
920             return $class->new(sub {
921 30     30   38 my $outer_item = $outer_iter->();
922 30 100       45 return undef unless defined $outer_item;
923              
924 24         34 my $key = $outer_key_selector->($outer_item);
925 24 50       75 $key = _make_key($key) if ref($key);
926 24 50       31 $key = '' unless defined $key;
927              
928 24 100       52 my $matched_inners = exists $inner_lookup{$key} ? $inner_lookup{$key} : [];
929              
930 24         32 my @snapshot = @$matched_inners;
931 24         44 my $inner_group = $class->_from_snapshot([ @snapshot ]);
932              
933 24         34 return $result_selector->($outer_item, $inner_group);
934 6         33 });
935             }
936              
937             ###############################################################################
938             # Quantifier Methods
939             ###############################################################################
940              
941             # All - test if all elements satisfy condition
942             sub All {
943 2     2 1 3 my($self, $predicate) = @_;
944 2         3 my $iter = $self->iterator;
945              
946 2         3 while (defined(my $item = $iter->())) {
947 6 100       7 return 0 unless $predicate->($item);
948             }
949 1         2 return 1;
950             }
951              
952             # Any - test if any element satisfies condition
953             sub Any {
954 6     6 1 7 my($self, $predicate) = @_;
955 6         7 my $iter = $self->iterator;
956              
957 6 100       8 if ($predicate) {
958 4         5 while (defined(my $item = $iter->())) {
959 11 100       10 return 1 if $predicate->($item);
960             }
961 2         12 return 0;
962             }
963             else {
964 2         2 my $item = $iter->();
965 2 100       5 return defined($item) ? 1 : 0;
966             }
967             }
968              
969             # Contains - check if sequence contains element
970             sub Contains {
971 2     2 1 3 my($self, $value, $comparer) = @_;
972              
973 2 50       4 if ($comparer) {
974 0     0   0 return $self->Any(sub { $comparer->($_[0], $value) });
  0         0  
975             }
976             else {
977             return $self->Any(sub {
978 5     5   4 my $item = $_[0];
979 5   33     29 return (!defined($item) && !defined($value)) ||
980             (defined($item) && defined($value) && $item eq $value);
981 2         4 });
982             }
983             }
984              
985             # SequenceEqual - compare two sequences for equality
986             sub SequenceEqual {
987 2     2 1 2 my($self, $second, $comparer) = @_;
988             $comparer ||= sub {
989 6     6   8 my($a, $b) = @_;
990 6   33     26 return (!defined($a) && !defined($b)) ||
991             (defined($a) && defined($b) && $a eq $b);
992 2   33     9 };
993              
994 2         3 my $iter1 = $self->iterator;
995 2         3 my $iter2 = $second->iterator;
996              
997 2         2 while (1) {
998 7         9 my $item1 = $iter1->();
999 7         8 my $item2 = $iter2->();
1000              
1001 7 50 66     16 return 1 if !defined($item1) && !defined($item2);
1002 6 50 33     9 return 0 if !defined($item1) || !defined($item2);
1003 6 100       12 return 0 unless $comparer->($item1, $item2);
1004             }
1005             }
1006              
1007             ###############################################################################
1008             # Element Access Methods
1009             ###############################################################################
1010              
1011             # First - get first element
1012             sub First {
1013 1     1 1 16 my($self, $predicate) = @_;
1014 1         2 my $iter = $self->iterator;
1015              
1016 1 50       2 if ($predicate) {
1017 1         2 while (defined(my $item = $iter->())) {
1018 2 100       4 return $item if $predicate->($item);
1019             }
1020 0         0 die "No element satisfies the condition";
1021             }
1022             else {
1023 0         0 my $item = $iter->();
1024 0 0       0 return $item if defined $item;
1025 0         0 die "Sequence contains no elements";
1026             }
1027             }
1028              
1029             # FirstOrDefault - get first element or default
1030             sub FirstOrDefault {
1031 0     0 1 0 my $self = shift;
1032 0         0 my($predicate, $default);
1033              
1034 0 0       0 if (@_ >= 2) {
    0          
1035 0         0 ($predicate, $default) = @_;
1036             }
1037             elsif (@_ == 1) {
1038 0 0       0 if (ref($_[0]) eq 'CODE') {
1039 0         0 $predicate = $_[0];
1040             }
1041             else {
1042 0         0 $default = $_[0];
1043             }
1044             }
1045              
1046 0         0 my $result = eval { $self->First($predicate) };
  0         0  
1047 0 0       0 return $@ ? $default : $result;
1048             }
1049              
1050             # Last - get last element
1051             sub Last {
1052 1     1 1 3 my($self, $predicate) = @_;
1053 1         4 my @items = $self->ToArray();
1054              
1055 1 50       4 if ($predicate) {
1056 1         6 for (my $i = $#items; $i >= 0; $i--) {
1057 1 50       3 return $items[$i] if $predicate->($items[$i]);
1058             }
1059 0         0 die "No element satisfies the condition";
1060             }
1061             else {
1062 0 0       0 die "Sequence contains no elements" unless @items;
1063 0         0 return $items[-1];
1064             }
1065             }
1066              
1067             # LastOrDefault - return last element or default
1068             sub LastOrDefault {
1069 0     0 1 0 my $self = shift;
1070 0         0 my($predicate, $default);
1071              
1072 0 0       0 if (@_ >= 2) {
    0          
1073 0         0 ($predicate, $default) = @_;
1074             }
1075             elsif (@_ == 1) {
1076 0 0       0 if (ref($_[0]) eq 'CODE') {
1077 0         0 $predicate = $_[0];
1078             }
1079             else {
1080 0         0 $default = $_[0];
1081             }
1082             }
1083              
1084 0         0 my @items = $self->ToArray();
1085              
1086 0 0       0 if ($predicate) {
1087 0         0 for (my $i = $#items; $i >= 0; $i--) {
1088 0 0       0 return $items[$i] if $predicate->($items[$i]);
1089             }
1090 0         0 return $default;
1091             }
1092             else {
1093 0 0       0 return @items ? $items[-1] : $default;
1094             }
1095             }
1096              
1097             # Single - return the only element
1098             sub Single {
1099 0     0 1 0 my($self, $predicate) = @_;
1100 0         0 my $iter = $self->iterator;
1101 0         0 my $found;
1102 0         0 my $count = 0;
1103              
1104 0         0 while (defined(my $item = $iter->())) {
1105 0 0 0     0 next if $predicate && !$predicate->($item);
1106              
1107 0         0 $count++;
1108 0 0       0 if ($count > 1) {
1109 0         0 die "Sequence contains more than one element";
1110             }
1111 0         0 $found = $item;
1112             }
1113              
1114 0 0       0 die "Sequence contains no elements" if $count == 0;
1115 0         0 return $found;
1116             }
1117              
1118             # SingleOrDefault - return the only element or undef
1119             sub SingleOrDefault {
1120 0     0 1 0 my($self, $predicate) = @_;
1121 0         0 my $iter = $self->iterator;
1122 0         0 my $found;
1123 0         0 my $count = 0;
1124              
1125 0         0 while (defined(my $item = $iter->())) {
1126 0 0 0     0 next if $predicate && !$predicate->($item);
1127              
1128 0         0 $count++;
1129 0 0       0 if ($count > 1) {
1130 0         0 return undef;
1131             }
1132 0         0 $found = $item;
1133             }
1134              
1135 0 0       0 return $count == 1 ? $found : undef;
1136             }
1137              
1138             # ElementAt - return element at specified index
1139             sub ElementAt {
1140 0     0 1 0 my($self, $index) = @_;
1141 0 0       0 die "Index must be non-negative" if $index < 0;
1142              
1143 0         0 my $iter = $self->iterator;
1144 0         0 my $current = 0;
1145              
1146 0         0 while (defined(my $item = $iter->())) {
1147 0 0       0 return $item if $current == $index;
1148 0         0 $current++;
1149             }
1150              
1151 0         0 die "Index out of range";
1152             }
1153              
1154             # ElementAtOrDefault - return element at index or undef
1155             sub ElementAtOrDefault {
1156 0     0 1 0 my($self, $index) = @_;
1157 0 0       0 return undef if $index < 0;
1158              
1159 0         0 my $iter = $self->iterator;
1160 0         0 my $current = 0;
1161              
1162 0         0 while (defined(my $item = $iter->())) {
1163 0 0       0 return $item if $current == $index;
1164 0         0 $current++;
1165             }
1166              
1167 0         0 return undef;
1168             }
1169              
1170             ###############################################################################
1171             # Aggregation Methods
1172             ###############################################################################
1173              
1174             # Count - count elements
1175             sub Count {
1176 17     17 1 60 my($self, $predicate) = @_;
1177              
1178 17 100       29 if ($predicate) {
1179 1         2 return $self->Where($predicate)->Count();
1180             }
1181              
1182 16         19 my $count = 0;
1183 16         20 my $iter = $self->iterator;
1184 16         25 $count++ while defined $iter->();
1185 16         72 return $count;
1186             }
1187              
1188             # Sum - calculate sum
1189             sub Sum {
1190 10     10 1 36 my($self, $selector) = @_;
1191 10   66 5   42 $selector ||= sub { $_[0] };
  5         5  
1192              
1193 10         16 my $sum = 0;
1194             $self->ForEach(sub {
1195 24     24   61 $sum += $selector->(shift);
1196 10         47 });
1197 10         68 return $sum;
1198             }
1199              
1200             # Min - find minimum
1201             sub Min {
1202 1     1 1 2 my($self, $selector) = @_;
1203 1   33 0   6 $selector ||= sub { $_[0] };
  0         0  
1204              
1205 1         1 my $min;
1206             $self->ForEach(sub {
1207 5     5   7 my $val = $selector->(shift);
1208 5 100 100     22 $min = $val if !defined($min) || $val < $min;
1209 1         5 });
1210 1         3 return $min;
1211             }
1212              
1213             # Max - find maximum
1214             sub Max {
1215 1     1 1 2 my($self, $selector) = @_;
1216 1   33 0   3 $selector ||= sub { $_[0] };
  0         0  
1217              
1218 1         1 my $max;
1219             $self->ForEach(sub {
1220 5     5   6 my $val = $selector->(shift);
1221 5 100 100     17 $max = $val if !defined($max) || $val > $max;
1222 1         4 });
1223 1         3 return $max;
1224             }
1225              
1226             # Average - calculate average
1227             sub Average {
1228 1     1 1 2 my($self, $selector) = @_;
1229 1   33 0   3 $selector ||= sub { $_[0] };
  0         0  
1230              
1231 1         1 my $sum = 0;
1232 1         2 my $count = 0;
1233             $self->ForEach(sub {
1234 5     5   8 $sum += $selector->(shift);
1235 5         11 $count++;
1236 1         15 });
1237              
1238 1 50       4 die "Sequence contains no elements" if $count == 0;
1239 1         3 return $sum / $count;
1240             }
1241              
1242             # AverageOrDefault - calculate average or return undef if empty
1243             sub AverageOrDefault {
1244 1     1 1 1 my($self, $selector) = @_;
1245 1   33 0   6 $selector ||= sub { $_[0] };
  0         0  
1246              
1247 1         1 my $sum = 0;
1248 1         1 my $count = 0;
1249             $self->ForEach(sub {
1250 0     0   0 $sum += $selector->(shift);
1251 0         0 $count++;
1252 1         4 });
1253              
1254 1 50       8 return undef if $count == 0;
1255 0         0 return $sum / $count;
1256             }
1257              
1258             # Aggregate - apply accumulator function over sequence
1259             sub Aggregate {
1260 3     3 1 4 my($self, @args) = @_;
1261              
1262 3         3 my($seed, $func, $result_selector);
1263              
1264 3 100       7 if (@args == 1) {
    100          
    50          
1265 1         2 $func = $args[0];
1266 1         1 my $iter = $self->iterator;
1267 1         2 $seed = $iter->();
1268 1 50       2 die "Sequence contains no elements" unless defined $seed;
1269              
1270 1         1 while (defined(my $item = $iter->())) {
1271 3         5 $seed = $func->($seed, $item);
1272             }
1273             }
1274             elsif (@args == 2) {
1275 1         2 ($seed, $func) = @args;
1276             $self->ForEach(sub {
1277 3     3   4 $seed = $func->($seed, shift);
1278 1         2 });
1279             }
1280             elsif (@args == 3) {
1281 1         2 ($seed, $func, $result_selector) = @args;
1282             $self->ForEach(sub {
1283 3     3   4 $seed = $func->($seed, shift);
1284 1         6 });
1285             }
1286             else {
1287 0         0 die "Invalid number of arguments for Aggregate";
1288             }
1289              
1290 3 100       9 return $result_selector ? $result_selector->($seed) : $seed;
1291             }
1292              
1293             ###############################################################################
1294             # Conversion Methods
1295             ###############################################################################
1296              
1297             # ToArray - convert to array
1298             sub ToArray {
1299 114     114 1 211 my($self) = @_;
1300 114         154 my @result;
1301 114         204 my $iter = $self->iterator;
1302              
1303 114         204 while (defined(my $item = $iter->())) {
1304 377         950 push @result, $item;
1305             }
1306 114         504 return @result;
1307             }
1308              
1309             # ToList - convert to array reference
1310             sub ToList {
1311 0     0 1 0 my($self) = @_;
1312 0         0 return [$self->ToArray()];
1313             }
1314              
1315             # ToDictionary - convert sequence to hash reference
1316             sub ToDictionary {
1317 1     1 1 2 my($self, $key_selector, $value_selector) = @_;
1318              
1319 1   33 0   3 $value_selector ||= sub { $_[0] };
  0         0  
1320              
1321 1         2 my %dictionary = ();
1322              
1323             $self->ForEach(sub {
1324 4     4   4 my $item = shift;
1325 4         909 my $key = $key_selector->($item);
1326 4         12 my $value = $value_selector->($item);
1327              
1328 4 50       11 $key = '' unless defined $key;
1329 4         8 $dictionary{$key} = $value;
1330 1         4 });
1331              
1332 1         7 return { %dictionary };
1333             }
1334              
1335             # ToLookup - convert sequence to hash of arrays
1336             sub ToLookup {
1337 2     2 1 3 my($self, $key_selector, $value_selector) = @_;
1338              
1339 2   33 0   5 $value_selector ||= sub { $_[0] };
  0         0  
1340              
1341 2         3 my %lookup = ();
1342              
1343             $self->ForEach(sub {
1344 8     8   6 my $item = shift;
1345 8         10 my $key = $key_selector->($item);
1346 8         37 my $value = $value_selector->($item);
1347              
1348 8 50       19 $key = '' unless defined $key;
1349 8         6 push @{$lookup{$key}}, $value;
  8         14  
1350 2         8 });
1351              
1352 2         8 return { %lookup };
1353             }
1354              
1355             # DefaultIfEmpty - return default value if empty
1356             sub DefaultIfEmpty {
1357 2     2 1 3 my($self, $default_value) = @_;
1358 2         3 my $has_default_arg = @_ > 1;
1359 2 50       3 if (!$has_default_arg) {
1360 0         0 $default_value = undef;
1361             }
1362              
1363 2         2 my $class = ref($self);
1364 2         3 my $iter = $self->iterator;
1365 2         2 my $has_elements = 0;
1366 2         3 my $returned_default = 0;
1367              
1368             return $class->new(sub {
1369 6     6   7 my $item = $iter->();
1370 6 100       7 if (defined $item) {
1371 3         2 $has_elements = 1;
1372 3         4 return $item;
1373             }
1374              
1375 3 100 100     7 if (!$has_elements && !$returned_default) {
1376 1         1 $returned_default = 1;
1377 1         2 return $default_value;
1378             }
1379              
1380 2         4 return undef;
1381 2         4 });
1382             }
1383              
1384             # ToJSON - write sequence as a JSON array file
1385             # Each element is encoded as JSON; the result is a JSON array.
1386             sub ToJSON {
1387 2     2 1 8 my($self, $filename) = @_;
1388              
1389 2         4 my $fh;
1390 2 50       8 if ($] >= 5.006) {
1391 2 50       167 eval q{ open($fh, '>', $filename) } or die "Cannot open '$filename': $!";
1392             }
1393             else {
1394 0         0 $fh = \do { local *_ };
  0         0  
1395 0 0       0 open($fh, "> $filename") or die "Cannot open '$filename': $!";
1396             }
1397 2         18 binmode $fh; # Write raw UTF-8 bytes
1398              
1399 2         26 print $fh "[\n";
1400 2         5 my $first = 1;
1401             $self->ForEach(sub {
1402 5     5   8 my $record = shift;
1403 5 100       19 print $fh ",\n" unless $first;
1404 5         11 $first = 0;
1405 5         16 print $fh _json_encode($record);
1406 2         21 });
1407 2         13 print $fh "\n]\n";
1408              
1409 2         173 close $fh;
1410 2         22 return 1;
1411             }
1412              
1413             # ToJSONL - write sequence as a JSONL (JSON Lines) file
1414             # Each element is encoded as one line of JSON.
1415             # This is streaming-friendly and memory-efficient.
1416             sub ToJSONL {
1417 1     1 1 3 my($self, $filename) = @_;
1418              
1419 1         4 my $fh;
1420 1 50       4 if ($] >= 5.006) {
1421 1 50       102 eval q{ open($fh, '>', $filename) } or die "Cannot open '$filename': $!";
1422             }
1423             else {
1424 0         0 $fh = \do { local *_ };
  0         0  
1425 0 0       0 open($fh, "> $filename") or die "Cannot open '$filename': $!";
1426             }
1427 1         9 binmode $fh; # Write raw UTF-8 bytes
1428              
1429             $self->ForEach(sub {
1430 2     2   3 my $record = shift;
1431 2         6 print $fh _json_encode($record), "\n";
1432 1         11 });
1433              
1434 1         100 close $fh;
1435 1         13 return 1;
1436             }
1437              
1438             # ToLTSV - write sequence as an LTSV (Labeled Tab-Separated Values) file.
1439             # Each element must be a HASH reference; keys are emitted alphabetically.
1440             # Tab/CR/LF in values are sanitized to a single space to keep the file
1441             # structurally valid. This method is provided so a JSON::LINQ pipeline
1442             # can emit LTSV output without requiring LTSV::LINQ.
1443             sub ToLTSV {
1444 5     5 1 9 my($self, $filename) = @_;
1445              
1446 5         7 my $fh;
1447 5 50       11 if ($] >= 5.006) {
1448             # Avoid "Too many arguments for open at" error when running with Perl 5.005_03
1449 5 50       430 eval q{ open($fh, '>', $filename) } or die "Cannot open '$filename': $!";
1450             }
1451             else {
1452 0         0 $fh = \do { local *_ };
  0         0  
1453 0 0       0 open($fh, "> $filename") or die "Cannot open '$filename': $!";
1454             }
1455 5         39 binmode $fh; # Write raw bytes; prevents \r\n translation on Windows
1456             # and is consistent with FromLTSV
1457              
1458             $self->ForEach(sub {
1459 8     8   14 my $record = shift;
1460             # LTSV spec: tab is the field separator; newline terminates the record.
1461             # Sanitize values to prevent structural corruption of the output file.
1462             my $line = join("\t", map {
1463 8 100       39 my $v = defined($record->{$_}) ? $record->{$_} : '';
  17         53  
1464 17         40 $v =~ s/[\t\n\r]/ /g;
1465 17         51 "$_:$v"
1466             } sort keys %$record);
1467 8         52 print $fh $line, "\n";
1468 5         47 });
1469              
1470 5         244 close $fh;
1471 5         35 return 1;
1472             }
1473              
1474             ###############################################################################
1475             # Utility Methods
1476             ###############################################################################
1477              
1478             # ForEach - execute action for each element
1479             sub ForEach {
1480 51     51 1 84 my($self, $action) = @_;
1481 51         89 my $iter = $self->iterator;
1482              
1483 51         126 while (defined(my $item = $iter->())) {
1484 147         196 $action->($item);
1485             }
1486 51         81 return;
1487             }
1488              
1489             1;
1490              
1491             ######################################################################
1492             #
1493             # JSON::LINQ::Ordered - Ordered query supporting ThenBy/ThenByDescending
1494             #
1495             # Returned by OrderBy* methods. Inherits all JSON::LINQ methods via @ISA.
1496             # Stability guarantee: Schwartzian-Transform stable sort, all Perl versions.
1497             #
1498             ######################################################################
1499              
1500             package JSON::LINQ::Ordered;
1501              
1502             @JSON::LINQ::Ordered::ISA = ('JSON::LINQ');
1503              
1504             sub _new_ordered {
1505 24     24   49 my($class, $items, $specs) = @_;
1506             return bless {
1507             _items => $items,
1508             _specs => $specs,
1509             _factory => sub {
1510 19     19   46 my @sorted = _perform_sort($items, $specs);
1511 19         30 my $i = 0;
1512 19 100       92 return sub { $i < scalar(@sorted) ? $sorted[$i++] : undef };
  103         1146  
1513             },
1514 24         241 }, $class;
1515             }
1516              
1517             sub _perform_sort {
1518 19     19   33 my($items, $specs) = @_;
1519              
1520             my @decorated = map {
1521 88         112 my $idx = $_;
1522 88         122 my $item = $items->[$idx];
1523 88         141 my @keys = map { _extract_key($_->{sel}->($item), $_->{type}) } @{$specs};
  118         252  
  88         148  
1524 88         242 [$idx, [ @keys ], $item]
1525 19         64 } 0 .. $#{$items};
  19         53  
1526              
1527             my @sorted_dec = sort {
1528 19         87 my $r = 0;
  129         200  
1529 129         156 for my $i (0 .. $#{$specs}) {
  129         282  
1530 150         310 my $cmp = _compare_keys($a->[1][$i], $b->[1][$i], $specs->[$i]{type});
1531 150 100       296 if ($specs->[$i]{dir} < 0) { $cmp = -$cmp }
  26         35  
1532 150 100       243 if ($cmp != 0) { $r = $cmp; last }
  122         141  
  122         157  
1533             }
1534 129 100       268 $r != 0 ? $r : ($a->[0] <=> $b->[0]);
1535             } @decorated;
1536              
1537 19         40 return map { $_->[2] } @sorted_dec;
  88         220  
1538             }
1539              
1540             sub _extract_key {
1541 118     118   375 my($val, $type) = @_;
1542 118 50       203 $val = '' unless defined $val;
1543 118 100       238 if ($type eq 'num') {
    100          
1544 36 50 33     218 return defined($val) && length($val) ? $val + 0 : 0;
1545             }
1546             elsif ($type eq 'str') {
1547 56         161 return "$val";
1548             }
1549             else {
1550 26         34 my $t = $val;
1551 26         78 $t =~ s/^\s+|\s+$//g;
1552 26 100       93 if ($t =~ /^[+-]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/) {
1553 16         45 return [0, $t + 0];
1554             }
1555             else {
1556 10         31 return [1, "$val"];
1557             }
1558             }
1559             }
1560              
1561             sub _compare_keys {
1562 150     150   229 my($ka, $kb, $type) = @_;
1563 150 100       268 if ($type eq 'num') {
    100          
1564 43         72 return $ka <=> $kb;
1565             }
1566             elsif ($type eq 'str') {
1567 69         121 return $ka cmp $kb;
1568             }
1569             else {
1570 38         58 my $fa = $ka->[0]; my $va = $ka->[1];
  38         44  
1571 38         52 my $fb = $kb->[0]; my $vb = $kb->[1];
  38         40  
1572 38 100 66     108 if ($fa == 0 && $fb == 0) { return $va <=> $vb }
  23 50 33     36  
1573 15         49 elsif ($fa == 1 && $fb == 1) { return $va cmp $vb }
1574 0         0 else { return $fa <=> $fb }
1575             }
1576             }
1577              
1578             sub _thenby {
1579 6     6   11 my($self, $key_selector, $dir, $type) = @_;
1580 6         8 my @new_specs = (@{$self->{_specs}}, { sel => $key_selector, dir => $dir, type => $type });
  6         21  
1581 6         15 return JSON::LINQ::Ordered->_new_ordered($self->{_items}, [ @new_specs ]);
1582             }
1583              
1584 0     0   0 sub ThenBy { my($s, $k)=@_; $s->_thenby($k, 1, 'smart') }
  0         0  
1585 0     0   0 sub ThenByDescending { my($s, $k)=@_; $s->_thenby($k, -1, 'smart') }
  0         0  
1586 4     4   12 sub ThenByStr { my($s, $k)=@_; $s->_thenby($k, 1, 'str') }
  4         11  
1587 0     0   0 sub ThenByStrDescending { my($s, $k)=@_; $s->_thenby($k, -1, 'str') }
  0         0  
1588 0     0   0 sub ThenByNum { my($s, $k)=@_; $s->_thenby($k, 1, 'num') }
  0         0  
1589 2     2   11 sub ThenByNumDescending { my($s, $k)=@_; $s->_thenby($k, -1, 'num') }
  2         18  
1590              
1591             1;
1592              
1593             =encoding utf-8
1594              
1595             =head1 NAME
1596              
1597             JSON::LINQ - LINQ-style query interface for JSON and JSONL files
1598              
1599             =head1 VERSION
1600              
1601             Version 1.01
1602              
1603             =head1 SYNOPSIS
1604              
1605             use JSON::LINQ;
1606              
1607             # Read JSON file (array of objects) and query
1608             my @results = JSON::LINQ->FromJSON("users.json")
1609             ->Where(sub { $_[0]{age} >= 18 })
1610             ->Select(sub { $_[0]{name} })
1611             ->Distinct()
1612             ->ToArray();
1613              
1614             # Read JSONL (JSON Lines) file - one JSON object per line
1615             my @errors = JSON::LINQ->FromJSONL("events.jsonl")
1616             ->Where(sub { $_[0]{level} eq 'ERROR' })
1617             ->ToArray();
1618              
1619             # DSL syntax for simple filtering
1620             my @active = JSON::LINQ->FromJSON("users.json")
1621             ->Where(status => 'active')
1622             ->ToArray();
1623              
1624             # Grouping and aggregation
1625             my @stats = JSON::LINQ->FromJSON("orders.json")
1626             ->GroupBy(sub { $_[0]{category} })
1627             ->Select(sub {
1628             my $g = shift;
1629             return {
1630             Category => $g->{Key},
1631             Count => scalar(@{$g->{Elements}}),
1632             Total => JSON::LINQ->From($g->{Elements})
1633             ->Sum(sub { $_[0]{amount} }),
1634             };
1635             })
1636             ->OrderByDescending(sub { $_[0]{Total} })
1637             ->ToArray();
1638              
1639             # Write results back as JSON or JSONL
1640             JSON::LINQ->From(\@results)->ToJSON("output.json");
1641             JSON::LINQ->From(\@results)->ToJSONL("output.jsonl");
1642              
1643             # Read/write LTSV files (Labeled Tab-Separated Values)
1644             my @rows = JSON::LINQ->FromLTSV("access.ltsv")
1645             ->Where(sub { $_[0]{status} eq '200' })
1646             ->ToArray();
1647             JSON::LINQ->From(\@rows)->ToLTSV("filtered.ltsv");
1648              
1649             # JOIN a JSON file (main) with an LTSV file (sub-table)
1650             my $depts = JSON::LINQ->FromLTSV("departments.ltsv");
1651             my @joined = JSON::LINQ->FromJSON("employees.json")
1652             ->Join($depts,
1653             sub { $_[0]{dept_id} },
1654             sub { $_[0]{id} },
1655             sub { { name => $_[0]{name}, dept => $_[1]{name} } })
1656             ->ToArray();
1657              
1658             # JOIN an LTSV file (main) with a JSON file (sub-table)
1659             my $prices = JSON::LINQ->FromJSON("prices.json");
1660             my @priced = JSON::LINQ->FromLTSV("orders.ltsv")
1661             ->Join($prices,
1662             sub { $_[0]{sku} },
1663             sub { $_[0]{sku} },
1664             sub { { order_id => $_[0]{id},
1665             amount => $_[0]{qty} * $_[1]{price} } })
1666             ->ToArray();
1667              
1668             # Boolean values
1669             my $rec = { active => JSON::LINQ::true, count => 0 };
1670             JSON::LINQ->From([$rec])->ToJSON("output.json");
1671             # ToJSON encodes as: {"active":true,"count":0}
1672              
1673             =head1 TABLE OF CONTENTS
1674              
1675             =over 4
1676              
1677             =item * L
1678              
1679             =item * L -- eg/ samples and doc/ cheat sheets
1680              
1681             =item * L -- Complete method reference (65 methods)
1682              
1683             =item * L -- Practical examples
1684              
1685             =item * L -- Lazy evaluation, method chaining, DSL
1686              
1687             =item * L -- Iterator design, execution flow
1688              
1689             =item * L -- Perl 5.005+ support, pure Perl
1690              
1691             =item * L -- Error messages
1692              
1693             =item * L
1694              
1695             =item * L
1696              
1697             =back
1698              
1699             =head1 DESCRIPTION
1700              
1701             JSON::LINQ provides a LINQ-style query interface for JSON, JSONL
1702             (JSON Lines), and LTSV (Labeled Tab-Separated Values) files. It is
1703             the JSON counterpart of L, sharing the same LINQ API and
1704             adding JSON-specific I/O methods.
1705              
1706             Key features:
1707              
1708             =over 4
1709              
1710             =item * B - O(1) memory for JSONL and LTSV streaming;
1711             JSON arrays are loaded once then iterated lazily
1712              
1713             =item * B - Fluent, readable query composition
1714              
1715             =item * B - Simple key-value filtering
1716              
1717             =item * B<65 LINQ methods> - including JSON I/O (FromJSON, FromJSONL,
1718             FromJSONString, ToJSON, ToJSONL), LTSV I/O (FromLTSV, ToLTSV), and
1719             all 60 methods from L
1720              
1721             =item * B - No XS dependencies
1722              
1723             =item * B - Works on ancient and modern Perl
1724              
1725             =item * B - No CPAN JSON module required
1726              
1727             =back
1728              
1729             =head2 Supported Data Sources
1730              
1731             =over 4
1732              
1733             =item * B - JSON file containing a top-level array or object
1734              
1735             =item * B - JSONL file (one JSON value per line)
1736              
1737             =item * B - JSON string (array or object)
1738              
1739             =item * B - LTSV file (Labeled Tab-Separated Values)
1740              
1741             =item * B - In-memory Perl array
1742              
1743             =item * B - Integer sequence
1744              
1745             =item * B - Empty sequence
1746              
1747             =item * B - Repeated element
1748              
1749             =back
1750              
1751             =head2 What is JSONL?
1752              
1753             JSONL (JSON Lines, also known as ndjson - newline-delimited JSON) is a
1754             text format where each line is a valid JSON value (typically an object).
1755             It is particularly suited for log files and streaming data because:
1756              
1757             =over 4
1758              
1759             =item * One record per line enables streaming with O(1) memory usage
1760              
1761             =item * Compatible with standard Unix tools (grep, sed, awk)
1762              
1763             =item * Easily appendable without rewriting the whole file
1764              
1765             =item * Each line is independently parseable
1766              
1767             =back
1768              
1769             B
1770              
1771             {"time":"2026-04-20T10:00:00","host":"192.0.2.1","status":200,"url":"/"}
1772             {"time":"2026-04-20T10:00:01","host":"192.0.2.2","status":404,"url":"/missing"}
1773              
1774             C reads these files lazily (one line at a time), matching the
1775             memory efficiency of C's C.
1776              
1777             =head2 What is LINQ?
1778              
1779             LINQ (Language Integrated Query) is the Microsoft .NET query API.
1780             This module brings the same LINQ interface to JSON data in Perl.
1781             See L for a detailed description of the LINQ design philosophy.
1782              
1783             =head1 INCLUDED DOCUMENTATION
1784              
1785             The C directory contains sample programs:
1786              
1787             eg/01_json_query.pl FromJSON/Where/Select/OrderByDescending/Distinct/ToLookup
1788             eg/02_jsonl_query.pl FromJSONL streaming, GroupBy, aggregation, ToJSONL
1789             eg/03_grouping.pl GroupBy, ToLookup, GroupJoin, SelectMany, Join
1790             eg/04_sorting.pl OrderBy/ThenBy multi-key sort, OrderByNum vs OrderByStr
1791             eg/05_json_ltsv_join.pl JOIN main JSON x sub-table LTSV
1792             eg/06_ltsv_json_join.pl JOIN main LTSV x sub-table JSON
1793              
1794             The C directory contains JSON::LINQ cheat sheets in 21 languages:
1795              
1796             doc/json_linq_cheatsheet.EN.txt English
1797             doc/json_linq_cheatsheet.JA.txt Japanese
1798             doc/json_linq_cheatsheet.ZH.txt Chinese (Simplified)
1799             doc/json_linq_cheatsheet.TW.txt Chinese (Traditional)
1800             doc/json_linq_cheatsheet.KO.txt Korean
1801             doc/json_linq_cheatsheet.FR.txt French
1802             doc/json_linq_cheatsheet.ID.txt Indonesian
1803             doc/json_linq_cheatsheet.VI.txt Vietnamese
1804             doc/json_linq_cheatsheet.TH.txt Thai
1805             doc/json_linq_cheatsheet.HI.txt Hindi
1806             doc/json_linq_cheatsheet.BN.txt Bengali
1807             doc/json_linq_cheatsheet.TR.txt Turkish
1808             doc/json_linq_cheatsheet.MY.txt Burmese
1809             doc/json_linq_cheatsheet.TL.txt Filipino
1810             doc/json_linq_cheatsheet.KM.txt Khmer
1811             doc/json_linq_cheatsheet.MN.txt Mongolian
1812             doc/json_linq_cheatsheet.NE.txt Nepali
1813             doc/json_linq_cheatsheet.SI.txt Sinhala
1814             doc/json_linq_cheatsheet.UR.txt Urdu
1815             doc/json_linq_cheatsheet.UZ.txt Uzbek
1816             doc/json_linq_cheatsheet.BM.txt Malay
1817              
1818             =head1 METHODS
1819              
1820             =head2 Complete Method Reference
1821              
1822             This module implements 65 LINQ methods organized into 15 categories.
1823             In addition, C and C boolean accessor functions are provided.
1824              
1825             =over 4
1826              
1827             =item * B: From, FromJSON, FromJSONL, FromJSONString, FromLTSV, Range, Empty, Repeat
1828              
1829             =item * B: Where (with DSL)
1830              
1831             =item * B: Select, SelectMany
1832              
1833             =item * B: Concat, Zip
1834              
1835             =item * B: Take, Skip, TakeWhile, SkipWhile
1836              
1837             =item * B: OrderBy, OrderByDescending, OrderByStr, OrderByStrDescending, OrderByNum, OrderByNumDescending, Reverse, ThenBy, ThenByDescending, ThenByStr, ThenByStrDescending, ThenByNum, ThenByNumDescending
1838              
1839             =item * B: GroupBy
1840              
1841             =item * B: Distinct, Union, Intersect, Except
1842              
1843             =item * B: Join, GroupJoin
1844              
1845             =item * B: All, Any, Contains
1846              
1847             =item * B: SequenceEqual
1848              
1849             =item * B: First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, ElementAt, ElementAtOrDefault
1850              
1851             =item * B: Count, Sum, Min, Max, Average, AverageOrDefault, Aggregate
1852              
1853             =item * B: ToArray, ToList, ToDictionary, ToLookup, ToJSON, ToJSONL, ToLTSV, DefaultIfEmpty
1854              
1855             =item * B: ForEach
1856              
1857             =back
1858              
1859             =head2 JSON-Specific Data Source Methods
1860              
1861             =over 4
1862              
1863             =item B
1864              
1865             Read a JSON file containing a top-level array of values. Each element of
1866             the array becomes one item in the sequence.
1867              
1868             my $q = JSON::LINQ->FromJSON("users.json");
1869              
1870             If the file contains a single JSON object (not an array), it is treated
1871             as a one-element sequence.
1872              
1873             B
1874              
1875             [
1876             {"name": "Alice", "age": 30},
1877             {"name": "Bob", "age": 25}
1878             ]
1879              
1880             The entire file is read into memory and parsed once. For large files,
1881             consider JSONL format with C for streaming access.
1882              
1883             =item B
1884              
1885             Read a JSONL (JSON Lines) file. Each non-empty line is parsed as a
1886             separate JSON value. Empty lines and lines starting with C<#> are skipped.
1887              
1888             my $q = JSON::LINQ->FromJSONL("events.jsonl");
1889              
1890             B
1891              
1892             {"event":"login","user":"alice","ts":1713600000}
1893             {"event":"purchase","user":"alice","ts":1713600060,"amount":29.99}
1894             {"event":"logout","user":"alice","ts":1713600120}
1895              
1896             C reads lazily (one line at a time), providing O(1) memory
1897             usage for arbitrarily large files.
1898              
1899             Invalid JSON lines produce a warning and are skipped rather than
1900             aborting the entire sequence.
1901              
1902             =item B
1903              
1904             Create a query from a JSON string. Accepts a JSON array (each element
1905             becomes one sequence item) or a JSON object (single-element sequence).
1906              
1907             my $q = JSON::LINQ->FromJSONString('[{"id":1},{"id":2}]');
1908             my $q = JSON::LINQ->FromJSONString('{"id":1,"name":"Alice"}');
1909              
1910             =back
1911              
1912             =head2 LTSV Interoperability
1913              
1914             To make it easy to JOIN JSON data with LTSV master/lookup tables (or vice
1915             versa) without requiring L to be installed, JSON::LINQ ships
1916             with built-in LTSV I/O methods. The LTSV format is described at
1917             L.
1918              
1919             =over 4
1920              
1921             =item B
1922              
1923             Read an LTSV (Labeled Tab-Separated Values) file. Each line is split on
1924             TAB, and each field is split on the first colon to produce a label/value
1925             pair. The result is a sequence of hash references.
1926              
1927             my $q = JSON::LINQ->FromLTSV("departments.ltsv");
1928              
1929             B
1930              
1931             id:1name:Engineeringhead:Alice
1932             id:2name:Saleshead:Bob
1933              
1934             C reads lazily (one line at a time), so memory usage is O(1)
1935             even for very large files. Empty lines are skipped. CR is stripped to
1936             handle CRLF files on any platform.
1937              
1938             =item B
1939              
1940             Write the sequence as an LTSV file. Each element must be a HASH reference;
1941             its keys are emitted alphabetically so output is deterministic. TAB, CR,
1942             and LF in values are sanitized to a single space to keep the file
1943             structurally valid.
1944              
1945             $query->ToLTSV("output.ltsv");
1946              
1947             B
1948              
1949             age:30name:Alice
1950             age:25name:Bob
1951              
1952             =back
1953              
1954             =head2 JSON-Specific Conversion Methods
1955              
1956             =over 4
1957              
1958             =item B
1959              
1960             Write the sequence as a JSON file containing a JSON array. Each element
1961             is encoded as JSON. The output is a valid JSON array.
1962              
1963             $query->ToJSON("output.json");
1964              
1965             B
1966              
1967             [
1968             {"age":30,"name":"Alice"},
1969             {"age":25,"name":"Bob"}
1970             ]
1971              
1972             Hash keys are sorted alphabetically for deterministic output.
1973              
1974             =item B
1975              
1976             Write the sequence as a JSONL file. Each element is written as one line
1977             of JSON. This is the streaming counterpart of C.
1978              
1979             $query->ToJSONL("output.jsonl");
1980              
1981             B
1982              
1983             {"age":30,"name":"Alice"}
1984             {"age":25,"name":"Bob"}
1985              
1986             =back
1987              
1988             =head2 Boolean Values
1989              
1990             JSON::LINQ provides boolean singleton objects compatible with JSON encoding:
1991              
1992             JSON::LINQ::true # stringifies as "true", numifies as 1
1993             JSON::LINQ::false # stringifies as "false", numifies as 0
1994              
1995             Use these when creating data structures that will be serialised to JSON:
1996              
1997             my $rec = { active => JSON::LINQ::true, count => 0 };
1998             # ToJSON encodes as: {"active":true,"count":0}
1999              
2000             When C or C decode a JSON C or C,
2001             the result is a C object that behaves as 1 or 0
2002             in numeric and boolean context.
2003              
2004             =head2 All Other Methods
2005              
2006             All other LINQ methods are inherited from L and behave
2007             identically. Please refer to L for complete documentation of:
2008              
2009             Where, Select, SelectMany, Concat, Zip, Take, Skip, TakeWhile,
2010             SkipWhile, OrderBy, OrderByDescending, OrderByStr, OrderByStrDescending,
2011             OrderByNum, OrderByNumDescending, Reverse, ThenBy, ThenByDescending,
2012             ThenByStr, ThenByStrDescending, ThenByNum, ThenByNumDescending, GroupBy,
2013             Distinct, Union, Intersect, Except, Join, GroupJoin, All, Any, Contains,
2014             SequenceEqual, First, FirstOrDefault, Last, LastOrDefault, Single,
2015             SingleOrDefault, ElementAt, ElementAtOrDefault, Count, Sum, Min, Max,
2016             Average, AverageOrDefault, Aggregate, ToArray, ToList, ToDictionary,
2017             ToLookup, DefaultIfEmpty, ForEach.
2018              
2019             =head1 EXAMPLES
2020              
2021             =head2 Basic JSON File Query
2022              
2023             use JSON::LINQ;
2024              
2025             # users.json: [{"name":"Alice","age":30}, {"name":"Bob","age":25}, ...]
2026             my @adults = JSON::LINQ->FromJSON("users.json")
2027             ->Where(sub { $_[0]{age} >= 18 })
2028             ->OrderBy(sub { $_[0]{name} })
2029             ->ToArray();
2030              
2031             =head2 JSONL Streaming
2032              
2033             # events.jsonl: one JSON object per line
2034             my $error_count = JSON::LINQ->FromJSONL("events.jsonl")
2035             ->Count(sub { $_[0]{level} eq 'ERROR' });
2036              
2037             JSON::LINQ->FromJSONL("events.jsonl")
2038             ->Where(sub { $_[0]{level} eq 'ERROR' })
2039             ->ForEach(sub { print $_[0]{message}, "\n" });
2040              
2041             =head2 Aggregation
2042              
2043             my $avg = JSON::LINQ->FromJSON("orders.json")
2044             ->Where(sub { $_[0]{status} eq 'completed' })
2045             ->Average(sub { $_[0]{amount} });
2046              
2047             printf "Average order: %.2f\n", $avg;
2048              
2049             =head2 Grouping
2050              
2051             my @by_category = JSON::LINQ->FromJSON("products.json")
2052             ->GroupBy(sub { $_[0]{category} })
2053             ->Select(sub {
2054             my $g = shift;
2055             {
2056             Category => $g->{Key},
2057             Count => scalar(@{$g->{Elements}}),
2058             MaxPrice => JSON::LINQ->From($g->{Elements})
2059             ->Max(sub { $_[0]{price} }),
2060             }
2061             })
2062             ->OrderByDescending(sub { $_[0]{Count} })
2063             ->ToArray();
2064              
2065             =head2 Transform and Write
2066              
2067             # Read JSON, transform, write back as JSONL
2068             JSON::LINQ->FromJSON("input.json")
2069             ->Select(sub {
2070             my $r = shift;
2071             return { %$r, processed => JSON::LINQ::true };
2072             })
2073             ->ToJSONL("output.jsonl");
2074              
2075             =head2 JOIN: JSON (main) with LTSV (sub-table)
2076              
2077             A common pattern: the primary records live in a JSON file, and a small
2078             lookup table is maintained in LTSV format. The example below reads
2079             employees from a JSON file and joins them against a department lookup
2080             table in LTSV format.
2081              
2082             # employees.json
2083             # [
2084             # {"id":1,"name":"Alice","dept_id":10},
2085             # {"id":2,"name":"Bob", "dept_id":20},
2086             # {"id":3,"name":"Carol","dept_id":10}
2087             # ]
2088             #
2089             # departments.ltsv
2090             # id:10name:Engineering
2091             # id:20name:Sales
2092              
2093             my $depts = JSON::LINQ->FromLTSV("departments.ltsv");
2094              
2095             my @joined = JSON::LINQ->FromJSON("employees.json")
2096             ->Join($depts,
2097             sub { $_[0]{dept_id} }, # outer key (JSON side)
2098             sub { $_[0]{id} }, # inner key (LTSV side)
2099             sub { { name => $_[0]{name},
2100             dept => $_[1]{name} } })
2101             ->OrderBy(sub { $_[0]{name} })
2102             ->ToArray();
2103              
2104             # @joined == ({name=>"Alice", dept=>"Engineering"},
2105             # {name=>"Bob", dept=>"Sales"},
2106             # {name=>"Carol", dept=>"Engineering"})
2107              
2108             =head2 JOIN: LTSV (main) with JSON (sub-table)
2109              
2110             The opposite pattern: the primary records are in an LTSV log file (often
2111             high-volume, append-only), and the lookup table is in JSON.
2112              
2113             # orders.ltsv
2114             # id:1001sku:A100qty:2
2115             # id:1002sku:B200qty:1
2116             # id:1003sku:A100qty:5
2117             #
2118             # prices.json
2119             # [
2120             # {"sku":"A100","price":300},
2121             # {"sku":"B200","price":1200}
2122             # ]
2123              
2124             my $prices = JSON::LINQ->FromJSON("prices.json");
2125              
2126             my @priced = JSON::LINQ->FromLTSV("orders.ltsv")
2127             ->Join($prices,
2128             sub { $_[0]{sku} }, # outer key (LTSV)
2129             sub { $_[0]{sku} }, # inner key (JSON)
2130             sub { { order_id => $_[0]{id},
2131             amount => $_[0]{qty} * $_[1]{price} } })
2132             ->ToArray();
2133              
2134             # @priced == ({order_id=>1001, amount=>600},
2135             # {order_id=>1002, amount=>1200},
2136             # {order_id=>1003, amount=>1500})
2137              
2138             C builds a hash from the inner (sub-table) sequence, so it is
2139             efficient even when the outer sequence is large and read lazily.
2140              
2141             =head2 In-Memory Array Query
2142              
2143             my @data = (
2144             {name => 'Alice', score => 95},
2145             {name => 'Bob', score => 72},
2146             {name => 'Carol', score => 88},
2147             );
2148              
2149             my @top = JSON::LINQ->From(\@data)
2150             ->Where(sub { $_[0]{score} >= 80 })
2151             ->OrderByDescending(sub { $_[0]{score} })
2152             ->ToArray();
2153              
2154             =head1 FEATURES
2155              
2156             =head2 Lazy Evaluation
2157              
2158             C reads one line at a time. Combined with C and C,
2159             only the needed records are ever in memory simultaneously.
2160              
2161             C reads the whole file once but then iterates the array lazily.
2162              
2163             =head2 Built-in JSON Parser
2164              
2165             JSON::LINQ contains its own JSON encoder/decoder (derived from mb::JSON 0.06).
2166             No CPAN JSON module is required. The parser handles:
2167              
2168             =over 4
2169              
2170             =item * UTF-8 multibyte strings (output as-is, not \uXXXX-escaped)
2171              
2172             =item * C<\uXXXX> escape sequences on input (converted to UTF-8)
2173              
2174             =item * All JSON types: object, array, string, number, true, false, null
2175              
2176             =item * Nested structures of arbitrary depth
2177              
2178             =back
2179              
2180             =head1 ARCHITECTURE
2181              
2182             =head2 Relationship to LTSV::LINQ
2183              
2184             JSON::LINQ and LTSV::LINQ are parallel modules sharing the same LINQ API.
2185              
2186             LTSV::LINQ - LINQ for LTSV (Labeled Tab-Separated Values) files
2187             JSON::LINQ - LINQ for JSON and JSONL files
2188              
2189             Both share the same LINQ API. JSON::LINQ adds the following I/O methods
2190             on top of LTSV::LINQ's interface:
2191              
2192             FromJSON($file) - read JSON array file
2193             FromJSONL($file) - read JSONL file (streaming)
2194             FromJSONString($json) - read JSON string
2195             FromLTSV($file) - read LTSV file (streaming)
2196             ToJSON($file) - write JSON array file
2197             ToJSONL($file) - write JSONL file
2198             ToLTSV($file) - write LTSV file (streaming)
2199              
2200             C and C are provided primarily so a JSON::LINQ pipeline
2201             can JOIN against (or emit into) LTSV master tables and log files without
2202             requiring LTSV::LINQ to be installed.
2203              
2204             The internal iterator architecture is identical: each operator returns a
2205             new query object wrapping a closure.
2206              
2207             =head2 Memory Characteristics
2208              
2209             FromJSONL - O(1) per record: one line at a time
2210             FromJSON - O(n): entire file loaded once, then lazy iteration
2211             FromLTSV - O(1) per record: one line at a time
2212             ToJSON - O(n): entire sequence collected for array output
2213             ToJSONL - O(1) per record: streaming write
2214             ToLTSV - O(1) per record: streaming write
2215              
2216             =head1 COMPATIBILITY
2217              
2218             =head2 Perl Version Support
2219              
2220             Compatible with B. See L for the
2221             full compatibility rationale (Universal Consensus 1998 / Perl 5.005_03).
2222              
2223             =head2 Pure Perl Implementation
2224              
2225             No XS dependencies. No CPAN module dependencies. Works on any Perl
2226             installation with only the standard core.
2227              
2228             =head2 JSON Limitations
2229              
2230             The built-in parser has the same limitations as mb::JSON 0.06:
2231              
2232             =over 4
2233              
2234             =item * Surrogate pairs (C<\uD800>-C<\uDFFF>) are not supported
2235              
2236             =item * Circular references in encoding cause infinite recursion
2237              
2238             =item * Non-ARRAY/HASH references are stringified
2239              
2240             =back
2241              
2242             =head2 Iterator Protocol and JSON null
2243              
2244             The internal iterator protocol uses C to signal end-of-sequence.
2245             As a consequence, an C value (i.e. a decoded JSON C) cannot
2246             appear as a I of a sequence: it would be
2247             indistinguishable from EOF and the sequence would be silently truncated
2248             at that point.
2249              
2250             This affects C
2251             for some elements will terminate the sequence early.
2252              
2253             # JSON: [{"v":1},{"v":null},{"v":3}]
2254             JSON::LINQ->FromJSON("data.json")
2255             ->Select(sub { $_[0]{v} })
2256             ->ToArray;
2257             # returns (1) - sequence stops at the undef from the second record
2258              
2259             C is unaffected when filtering hash records (the hashref itself
2260             is the element, not its C field), but a C
2261             nullable field will be truncated at the first C. Workarounds:
2262              
2263             =over 4
2264              
2265             =item * Project to a sentinel value: C<< Select(sub { defined $_[0]{v} ? $_[0]{v} : '' }) >>
2266              
2267             =item * Wrap each element in a hashref so the element itself is never undef.
2268              
2269             =back
2270              
2271             C is similarly affected: a default of C
2272             is silently lost. Use a non-undef sentinel (C<0>, C<''>, C<{}>) instead.
2273              
2274             =head1 DIAGNOSTICS
2275              
2276             =over 4
2277              
2278             =item C
2279              
2280             The file exists but does not contain valid JSON.
2281              
2282             =item C
2283              
2284             The file contains valid JSON but the top-level value is a string, number,
2285             or boolean, not an array or object.
2286              
2287             =item C
2288              
2289             A line in a JSONL file could not be parsed. The line is skipped with a
2290             warning; processing continues.
2291              
2292             =item C
2293              
2294             The supplied JSON string is not valid JSON.
2295              
2296             =item C
2297              
2298             Internal JSON parsing error. The message includes the specific unexpected
2299             token or an indication of where parsing stopped.
2300              
2301             =item C
2302              
2303             The JSON array was not properly terminated or separated.
2304              
2305             =item C
2306              
2307             A JSON object was not properly terminated or separated.
2308              
2309             =item C
2310              
2311             The colon separator was missing after a JSON object key.
2312              
2313             =item C
2314              
2315             A JSON object key was not a quoted string.
2316              
2317             =item C
2318              
2319             Extra text was found after a successfully parsed top-level JSON value.
2320             The message is followed by the first 20 characters of the unexpected text.
2321              
2322             =item C
2323              
2324             The JSON text ended before a complete value was parsed.
2325              
2326             =item C
2327              
2328             An unrecognised token was encountered while parsing JSON.
2329             The message is followed by the first 20 characters of the unexpected text.
2330              
2331             =item C
2332              
2333             A JSON string was not closed with a double-quote.
2334              
2335             =item C
2336              
2337             Thrown by C, C, or C when the input file
2338             cannot be opened.
2339              
2340             =item C
2341              
2342             Thrown by C, C, or C when the output file
2343             cannot be opened.
2344              
2345             =item C
2346              
2347             Thrown by C when the argument is not an array reference.
2348              
2349             =item C
2350              
2351             Thrown by C when the supplied index is less than zero.
2352              
2353             =item C
2354              
2355             Thrown by C when the index is beyond the end of the sequence.
2356             Use C to avoid this error.
2357              
2358             =item C
2359              
2360             Thrown by C when called with an argument count other than 1, 2, or 3.
2361              
2362             =item C
2363              
2364             Thrown by C, C, C, C (no-seed form), and
2365             C when the sequence is empty or no element satisfies the predicate.
2366              
2367             =item C
2368              
2369             Thrown by C when more than one element (or matching element) is found.
2370              
2371             =item C
2372              
2373             Thrown by C or C with a predicate when no element matches.
2374              
2375             =item C
2376              
2377             Thrown by C when the selector function returns a non-array value.
2378              
2379             =back
2380              
2381             All other error messages are identical to L.
2382              
2383             =head1 BUGS
2384              
2385             Please report bugs to C.
2386              
2387             =head1 SEE ALSO
2388              
2389             =over 4
2390              
2391             =item * L - The LTSV counterpart of this module
2392              
2393             =item * L - The JSON encoder/decoder this module's parser is derived from
2394              
2395             =item * JSONL specification: L
2396              
2397             =item * Microsoft LINQ documentation: L
2398              
2399             =back
2400              
2401             =head1 AUTHOR
2402              
2403             INABA Hitoshi Eina@cpan.orgE
2404              
2405             =head1 COPYRIGHT AND LICENSE
2406              
2407             Copyright (c) 2026 INABA Hitoshi
2408              
2409             This library is free software; you can redistribute it and/or modify
2410             it under the same terms as Perl itself.
2411              
2412             =head1 DISCLAIMER OF WARRANTY
2413              
2414             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
2415             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT
2416             WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
2417             PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND,
2418             EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
2419             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
2420             THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS
2421             WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF
2422             ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
2423              
2424             =cut