File Coverage

lib/JSON/LINQ.pm
Criterion Covered Total %
statement 848 1024 82.8
branch 247 394 62.6
condition 50 98 51.0
subroutine 153 182 84.0
pod 63 65 96.9
total 1361 1763 77.2


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