—use
5.006;
use
strict;
use
warnings;
package
LINQ::Collection;
our
$AUTHORITY
=
'cpan:TOBYINK'
;
our
$VERSION
=
'0.000_004'
;
use
Role::Tiny;
use
LINQ::Util::Internal ();
requires
qw( target_class to_list )
;
my
$_coerce
=
sub
{
my
(
$thing
) =
@_
;
if
( Scalar::Util::blessed(
$thing
) and
$thing
->DOES( __PACKAGE__ ) ) {
return
$thing
;
}
if
(
ref
(
$thing
) eq
'ARRAY'
) {
return
LINQ::Array::->new(
$thing
);
}
LINQ::Util::Internal::throw(
"CallerError"
,
message
=>
"Expected a LINQ collection; got '$thing'"
);
};
sub
select
{
my
$self
=
shift
;
my
$map
= LINQ::Util::Internal::assert_code(
@_
);
LINQ::Util::Internal::create_linq(
$self
=> [
map
scalar
(
$map
->(
$_
) ),
$self
->to_list ],
);
}
sub
where {
my
$self
=
shift
;
my
$filter
= LINQ::Util::Internal::assert_code(
@_
);
LINQ::Util::Internal::create_linq(
$self
=> [
grep
$filter
->(
$_
),
$self
->to_list ],
);
}
sub
select_many {
my
$self
=
shift
;
my
$map
= LINQ::Util::Internal::assert_code(
@_
);
LINQ::Util::Internal::create_linq(
$self
=> [
map
$map
->(
$_
)->
$_coerce
->to_list,
$self
->to_list ],
);
}
sub
min {
my
$self
=
shift
;
return
$self
->
select
(
@_
)->min
if
@_
;
&List::Util::min
(
$self
->to_list );
}
sub
max {
my
$self
=
shift
;
return
$self
->
select
(
@_
)->max
if
@_
;
&List::Util::max
(
$self
->to_list );
}
sub
sum {
my
$self
=
shift
;
return
$self
->
select
(
@_
)->sum
if
@_
;
&List::Util::sum
(
$self
->to_list );
}
sub
average {
my
$self
=
shift
;
$self
->sum(
@_
) /
$self
->count();
}
sub
aggregate {
my
$self
=
shift
;
my
$code
= LINQ::Util::Internal::assert_code(
shift
);
my
$wrapper
=
sub
{
$code
->(
$a
,
$b
) };
&List::Util::reduce
(
$wrapper
,
@_
,
$self
->to_list );
}
my
$_prepare_join
=
sub
{
my
$x
=
shift
;
my
$y
=
shift
;
my
$hint
=
ref
(
$_
[0] ) ? -inner :
shift
(
@_
);
my
$x_keys
= LINQ::Util::Internal::assert_code(
shift
);
my
$y_keys
= LINQ::Util::Internal::assert_code(
shift
);
my
$joiner
= LINQ::Util::Internal::assert_code(
@_
);
$hint
=~ /\A-(inner|left|right|outer)\z/ or LINQ::Util::Internal::throw(
"CallerError"
,
message
=>
"Expected a recognized join type; got '$hint'"
);
my
@x_mapped
=
$x
->
select
(
sub
{ [
scalar
(
$x_keys
->(
$_
[0] ) ),
$_
[0] ] } )->to_list;
my
@y_mapped
=
$y
->
select
(
sub
{ [
scalar
(
$y_keys
->(
$_
[0] ) ),
$_
[0] ] } )->to_list;
return
( \
@x_mapped
, \
@y_mapped
,
$hint
,
$joiner
);
};
sub
join
{
my
(
$x_mapped
,
$y_mapped
,
$hint
,
$joiner
) =
$_prepare_join
->(
@_
);
my
@joined
;
my
(
@found_x
,
@found_y
);
for
my
$Xi
( 0 ..
$#$x_mapped
) {
my
$X
=
$x_mapped
->[
$Xi
];
for
my
$Yi
( 0 ..
$#$y_mapped
) {
my
$Y
=
$y_mapped
->[
$Yi
];
if
(
$X
->[0] eq
$Y
->[0] ) {
my
$a
=
$X
->[1];
my
$b
=
$Y
->[1];
$found_x
[
$Xi
]++;
$found_y
[
$Yi
]++;
push
@joined
,
scalar
$joiner
->(
$a
,
$b
);
}
}
#/ for my $Yi ( 0 .. $#$y_mapped)
}
#/ for my $Xi ( 0 .. $#$x_mapped)
if
(
$hint
eq -left or
$hint
eq -outer ) {
for
my
$Xi
( 0 ..
$#$x_mapped
) {
next
if
$found_x
[
$Xi
];
my
$a
=
$x_mapped
->[
$Xi
][1];
my
$b
=
undef
;
push
@joined
,
scalar
$joiner
->(
$a
);
}
}
if
(
$hint
eq -right or
$hint
eq -outer ) {
for
my
$Yi
( 0 ..
$#$y_mapped
) {
next
if
$found_y
[
$Yi
];
my
$a
=
undef
;
my
$b
=
$y_mapped
->[
$Yi
][1];
push
@joined
,
scalar
$joiner
->(
undef
,
$b
);
}
}
LINQ::Util::Internal::create_linq(
$_
[0], \
@joined
);
}
#/ sub join
sub
group_join {
my
(
$x_mapped
,
$y_mapped
,
$hint
,
$joiner
) =
$_prepare_join
->(
@_
);
$hint
=~ /\A-(left|inner)\z/ or LINQ::Util::Internal::throw(
"CallerError"
,
message
=>
"Join type '$hint' not supported for group_join"
,
);
my
@joined
;
my
(
@found_x
,
@found_y
);
for
my
$Xi
( 0 ..
$#$x_mapped
) {
my
$X
=
$x_mapped
->[
$Xi
];
my
@group
=
map
$_
->[1],
grep
$X
->[0] eq
$_
->[0],
@$y_mapped
;
if
(
@group
or
$hint
eq -left ) {
my
$a
=
$X
->[1];
my
$b
= LINQ::Util::Internal::create_linq(
$_
[0], \
@group
);
push
@joined
,
scalar
$joiner
->(
$a
,
$b
);
}
}
#/ for my $Xi ( 0 .. $#$x_mapped)
LINQ::Util::Internal::create_linq(
$_
[0], \
@joined
);
}
#/ sub group_join
sub
take {
my
$self
=
shift
;
my
(
$n
) =
@_
;
$self
->take_while(
sub
{
$n
-- > 0 } );
}
sub
take_while {
my
$self
=
shift
;
my
$filter
= LINQ::Util::Internal::assert_code(
@_
);
my
$stopped
= 0;
my
$idx
= 0;
'LINQ::Iterator'
->new(
sub
{
local
( $@,
$_
);
my
$next
;
my
$ok
=
eval
{
$next
=
$self
->element_at(
$idx
); not
$stopped
};
my
$e
= $@;
if
(
$ok
and
$filter
->(
$_
=
$next
) ) {
++
$idx
;
return
$next
;
}
if
(
defined
$e
and not
$ok
) {
die
(
$e
)
unless
Scalar::Util::blessed(
$e
);
die
(
$e
)
unless
$e
->isa(
'LINQ::Exception::NotFound'
);
}
$stopped
= 1;
return
LINQ::END();
} );
}
#/ sub take_while
sub
skip {
my
$self
=
shift
;
my
(
$n
) =
@_
;
$self
->where(
sub
{
$n
-- <= 0 } );
}
sub
skip_while {
my
$self
=
shift
;
my
$filter
= LINQ::Util::Internal::assert_code(
@_
);
my
$skipping
= 1;
$self
->where(
sub
{
$skipping
= 0
if
$skipping
&& !
$filter
->(
$_
);
not
$skipping
;
}
);
}
#/ sub skip_while
sub
concat {
my
@collections
=
@_
;
my
$idx
= 0;
'LINQ::Iterator'
->new(
sub
{
local
( $@,
$_
);
my
$next
;
FIND_NEXT: {
return
LINQ::END()
if
not
@collections
;
my
$ok
=
eval
{
$next
=
$collections
[0]->element_at(
$idx
); 1 };
my
$e
= $@;
if
(
$ok
) {
++
$idx
;
return
$next
;
}
elsif
(
defined
$e
and not
$ok
) {
die
(
$e
)
unless
Scalar::Util::blessed(
$e
);
die
(
$e
)
unless
$e
->isa(
'LINQ::Exception::NotFound'
);
}
shift
@collections
;
$idx
= 0;
redo
FIND_NEXT;
};
} );
}
sub
order_by {
my
$self
=
shift
;
my
$hint
=
ref
(
$_
[0] ) ? -numeric :
shift
(
@_
);
my
$keygen
=
@_
? LINQ::Util::Internal::assert_code(
@_
) :
undef
;
if
( not
$keygen
) {
if
(
$hint
eq -string ) {
return
LINQ::Util::Internal::create_linq(
$self
=> [
sort
{
$a
cmp
$b
}
$self
->to_list ]
);
}
elsif
(
$hint
eq -numeric ) {
return
LINQ::Util::Internal::create_linq(
$self
=> [
sort
{
$a
<=>
$b
}
$self
->to_list ]
);
}
}
if
(
$hint
eq -string ) {
return
LINQ::Util::Internal::create_linq(
$self
=> [
map
$_
->[1],
sort
{
$a
->[0] cmp
$b
->[0] }
map
[
$keygen
->(
$_
),
$_
],
$self
->to_list
]
);
}
#/ if ( $hint eq -string )
elsif
(
$hint
eq -numeric ) {
return
LINQ::Util::Internal::create_linq(
$self
=> [
map
$_
->[1],
sort
{
$a
->[0] <=>
$b
->[0] }
map
[
$keygen
->(
$_
),
$_
],
$self
->to_list
]
);
}
#/ elsif ( $hint eq -numeric)
LINQ::Util::Internal::throw(
"CallerError"
,
message
=>
"Expected '-numeric' or '-string'; got '$hint'"
);
}
#/ sub order_by
sub
then_by {
LINQ::Util::Internal::throw(
"Unimplemented"
,
method
=>
"then_by"
);
}
sub
order_by_descending {
my
$self
=
shift
;
$self
->order_by(
@_
)->
reverse
;
}
sub
then_by_descending {
LINQ::Util::Internal::throw(
"Unimplemented"
,
method
=>
"then_by_descending"
);
}
sub
reverse
{
my
$self
=
shift
;
LINQ::Util::Internal::create_linq(
$self
=> [
reverse
(
$self
->to_list ) ],
);
}
sub
group_by {
my
$self
=
shift
;
my
$keygen
= LINQ::Util::Internal::assert_code(
@_
);
my
@keys
;
my
%values
;
for
(
$self
->to_list ) {
my
$key
=
$keygen
->(
$_
);
unless
(
$values
{
$key
} ) {
push
@keys
,
$key
;
$values
{
$key
} = [];
}
push
@{
$values
{
$key
} },
$_
;
}
LINQ::Util::Internal::create_linq(
$self
=> [
map
LINQ::Grouping::->new(
key
=>
$_
,
values
=> LINQ::Util::Internal::create_linq(
$self
,
$values
{
$_
} ),
),
@keys
]
);
}
#/ sub group_by
sub
distinct {
my
$self
=
shift
;
my
$compare
=
@_
? LINQ::Util::Internal::assert_code(
@_
) :
sub
{
$_
[0] ==
$_
[1] };
my
@already
;
$self
->where(
sub
{
my
$maybe
=
$_
[0];
for
my
$got
(
@already
) {
return
!!0
if
$compare
->(
$maybe
,
$got
);
}
push
@already
,
$maybe
;
return
!!1;
}
);
}
#/ sub distinct
sub
union {
my
$self
=
shift
;
my
(
$other
,
@compare
) =
@_
;
$self
->concat(
$other
)->distinct(
@compare
);
}
sub
intersect {
my
$self
=
shift
;
my
$other
=
shift
;
my
@compare
=
@_
? LINQ::Util::Internal::assert_code(
@_
) :
sub
{
$_
[0] ==
$_
[1] };
$self
->where(
sub
{
$other
->contains(
$_
,
@compare
) } );
}
sub
except {
my
$self
=
shift
;
my
$other
=
shift
;
my
@compare
=
@_
? LINQ::Util::Internal::assert_code(
@_
) :
sub
{
$_
[0] ==
$_
[1] };
$self
->where(
sub
{ not
$other
->contains(
$_
,
@compare
) } );
}
sub
sequence_equal {
my
$self
=
shift
;
my
(
$other
,
@compare
) =
@_
;
return
!!0
unless
$self
->count ==
$other
->count;
my
@list1
=
$self
->to_list;
my
@list2
=
$other
->to_list;
return
!!0
unless
@list1
==
@list2
;
if
(
@compare
) {
my
$compare
= LINQ::Util::Internal::assert_code(
@compare
);
for
my
$i
( 0 ..
$#list1
) {
return
!!0
unless
$compare
->(
$list1
[
$i
],
$list2
[
$i
] );
}
return
!!1;
}
for
my
$i
( 0 ..
$#list1
) {
return
!!0
unless
$list1
[
$i
] ==
$list2
[
$i
];
}
return
!!1;
}
#/ sub sequence_equal
my
$_with_default
=
sub
{
my
$self
=
shift
;
my
$method
=
shift
;
my
@args
=
@_
;
my
$default
=
pop
(
@args
);
my
$return
;
eval
{
$return
=
$self
->
$method
(
@args
); 1 } or
do
{
my
$e
= $@;
# catch
# Rethrow any non-blessed errors.
die
(
$e
)
unless
Scalar::Util::blessed(
$e
);
# Rethrow any errors of the wrong class.
die
(
$e
)
unless
$e
->isa(
'LINQ::Exception::NotFound'
)
||
$e
->isa(
'LINQ::Exception::MultipleFound'
);
# Rethrow any errors which resulted from the wrong source.
die
(
$e
)
unless
$e
->collection ==
$self
;
return
$default
;
};
return
$return
;
};
sub
first {
my
$self
=
shift
;
my
$found
=
$self
->where(
@_
);
return
$found
->element_at( 0 )
if
$found
->count > 0;
LINQ::Util::Internal::throw(
'NotFound'
,
collection
=>
$self
);
}
sub
first_or_default {
shift
->
$_with_default
(
first
=>
@_
);
}
sub
last
{
my
$self
=
shift
;
my
$found
=
$self
->where(
@_
);
return
$found
->element_at( -1 )
if
$found
->count > 0;
LINQ::Util::Internal::throw(
'NotFound'
,
collection
=>
$self
);
}
sub
last_or_default {
shift
->
$_with_default
(
last
=>
@_
);
}
sub
single {
my
$self
=
shift
;
my
$found
=
$self
->where(
@_
);
return
$found
->element_at( 0 )
if
$found
->count == 1;
$found
->count == 0
? LINQ::Util::Internal::throw(
'NotFound'
,
collection
=>
$self
)
: LINQ::Util::Internal::throw(
'MultipleFound'
,
collection
=>
$self
,
found
=>
$found
);
}
sub
single_or_default {
shift
->
$_with_default
(
single
=>
@_
);
}
sub
element_at {
my
$self
=
shift
;
my
(
$i
) =
@_
;
my
@list
=
$self
->to_list;
if
(
$i
>
$#list
) {
LINQ::Util::Internal::throw(
'NotFound'
,
collection
=>
$self
);
}
if
(
$i
< 0 -
@list
) {
LINQ::Util::Internal::throw(
'NotFound'
,
collection
=>
$self
);
}
$list
[
$i
];
}
#/ sub element_at
sub
element_at_or_default {
shift
->
$_with_default
(
element_at
=>
@_
);
}
sub
any {
my
$self
=
shift
;
@_
?
$self
->where(
@_
)->any
: (
$self
->count > 0 );
}
sub
all {
my
$self
=
shift
;
$self
->where(
@_
)->count ==
$self
->count;
}
sub
contains {
my
$self
=
shift
;
my
(
$x
,
@args
) =
@_
;
if
(
@args
) {
splice
(
@args
, 1, 0,
$x
);
return
$self
->any( LINQ::Util::Internal::assert_code(
@args
) );
}
$self
->any(
sub
{
$_
==
$x
} );
}
#/ sub contains
sub
count {
my
$self
=
shift
;
return
$self
->where(
@_
)->count
if
@_
;
my
@list
=
$self
->to_list;
return
scalar
(
@list
);
}
sub
to_array {
my
$self
=
shift
;
[
$self
->to_list ];
}
sub
to_dictionary {
my
$self
=
shift
;
my
(
$keygen
) = LINQ::Util::Internal::assert_code(
@_
);
+{
map
+(
$keygen
->(
$_
),
$_
),
$self
->to_list };
}
sub
to_lookup {
my
$self
=
shift
;
$self
->to_dictionary(
@_
);
}
sub
to_iterator {
my
$self
=
shift
;
my
@list
=
$self
->to_list;
sub
{
@list
?
shift
(
@list
) : () };
}
sub
cast {
my
$self
=
shift
;
my
(
$type
) =
@_
;
my
$cast
=
$self
->of_type(
@_
);
return
$cast
if
$self
->count ==
$cast
->count;
LINQ::Util::Internal::throw(
"Cast"
,
collection
=>
$self
,
type
=>
$type
);
}
sub
of_type {
my
$self
=
shift
;
my
(
$type
) =
@_
;
unless
( Scalar::Util::blessed(
$type
) and
$type
->can(
'check'
) ) {
LINQ::Util::Internal::throw(
"CallerError"
,
message
=>
"Expected type constraint; got '$type'"
,
);
}
if
(
$type
->isa(
'Type::Tiny'
) ) {
my
$check
=
$type
->compiled_check;
if
(
$type
->has_coercion ) {
my
$coercion
=
$type
->coercion->compiled_coercion;
return
$self
->
select
(
$coercion
)->where(
$check
);
}
return
$self
->where(
$check
);
}
#/ if ( $type->isa( 'Type::Tiny'...))
if
(
$type
->can(
'has_coercion'
) and
$type
->has_coercion ) {
return
$self
->
select
(
sub
{
$type
->coerce(
$_
) } )
->where(
sub
{
$type
->check(
$_
) } );
}
return
$self
->where(
sub
{
$type
->check(
$_
) } );
}
#/ sub of_type
sub
zip {
my
$self
=
shift
;
my
$other
=
shift
;
my
$map
= LINQ::Util::Internal::assert_code(
@_
);
my
@self
=
$self
->to_list;
my
@other
=
$other
->to_list;
my
@results
;
while
(
@self
and
@other
) {
push
@results
,
scalar
$map
->(
shift
(
@self
),
shift
(
@other
) );
}
LINQ::Util::Internal::create_linq(
$self
=> \
@results
);
}
#/ sub zip
sub
default_if_empty {
my
$self
=
shift
;
my
$item
=
shift
;
if
(
$self
->count == 0 ) {
return
LINQ::Util::Internal::create_linq(
$self
=> [
$item
] );
}
return
$self
;
}
#/ sub default_if_empty
1;
__END__
=pod
=encoding utf-8
=head1 NAME
LINQ - the interface which all LINQ collections share
=head1 SYNOPSIS
use feature 'say';
use LINQ 'LINQ';
my $double_even_numbers =
LINQ( [1..100] )
->where( sub { $_ % 2 == 0 } )
->select( sub { $_ * 2 } );
if ( not $double_even_numbers->DOES( 'LINQ::Collection' ) ) {
die "What? But you said they all do it!";
}
for my $n ( $double_even_numbers->to_list ) {
say $n;
}
=head1 DESCRIPTION
Objects returned by the C<< LINQ() >>, C<< LINQ::Repeat() >>, and
C<< LINQ::Range() >> functions all provide the LINQ::Collection interface.
Many of the methods in this interface also return new objects which provide
this interface.
=head1 METHODS
Many methods take a parameter "CALLABLE". This means they can accept a
coderef, an object overloading C<< &{} >>, or an arrayref where the first
item is one of the previous two things and the remainder are treated as
arguments to curry to the first argument. A quoted regexp C<< qr/.../ >> can
also be used as a callable.
If using an arrayref, it is generally permissable to flatten it into a
list, unless otherwise noted. An example of this can be seen in the
documentation for C<select>.
=over
=item C<< select( CALLABLE ) >>
LINQ's version of C<map>, except that the code given is always called in
scalar context, being expected to return exactly one result.
Returns a LINQ::Collection of the results.
my $people = LINQ( [
{ name => "Alice", age => 32 },
{ name => "Bob", age => 31 },
{ name => "Carol", age => 34 },
] );
my $names = $people->select( sub {
return $_->{name};
} );
for my $name ( $names->to_list ) {
print "$name\n";
}
Another way of doing the same thing, using currying:
my $people = LINQ( [
{ name => "Alice", age => 32 },
{ name => "Bob", age => 31 },
{ name => "Carol", age => 34 },
] );
my $BY_HASH_KEY = sub {
my ($key) = @_;
return $_->{$key};
};
my $names = $people->select( $BY_HASH_KEY, 'name' );
for my $name ( $names->to_list ) {
print "$name\n";
}
=item C<< select_many( CALLABLE ) >>
If you wanted C<select> to be able to return a list like C<map> does, then
C<select_many> is what you want. However, rather than returning a Perl list,
your callable should return a LINQ::Collection or an arrayref.
=item C<< where( CALLABLE ) >>
LINQ's version of C<grep>. Returns a LINQ::Collection of the filtered results.
my $people = LINQ( [
{ name => "Alice", age => 32 },
{ name => "Bob", age => 31 },
{ name => "Carol", age => 34 },
] );
my $young_people = $people->where( sub {
return $_->{age} < 33;
} );
=item C<< min( CALLABLE? ) >>
Returns the numerically lowest value in the collection.
my $lowest = LINQ( [ 5, 1, 2, 3 ] )->min; # ==> 1
If a callable is provided, then C<select> will be called and the minimum of the
result will be returned.
my $people = LINQ( [
{ name => "Alice", age => 32 },
{ name => "Bob", age => 31 },
{ name => "Carol", age => 34 },
] );
my $lowest_age = $people->min( sub { $_->{age} } ); # ==> 31
If you need more flexible comparison (e.g. non-numeric comparison), use
C<order_by> followed by C<first>.
=item C<< max( CALLABLE? ) >>
Like C<min>, but returns the numerically highest value.
=item C<< sum( CALLABLE? ) >>
Like C<min>, but returns the sum of all values in the collection.
=item C<< average( CALLABLE? ) >>
Takes C<sum>, and divides by the count of items in the collection.
my $people = LINQ( [
{ name => "Alice", age => 32 },
{ name => "Bob", age => 31 },
{ name => "Carol", age => 34 },
] );
my $average_age = $people->average( sub {
return $_->{age};
} ); # ==> 32.33333
=item C<< aggregate( CALLABLE, INITIAL? ) >>
LINQ's version of C<reduce> (from L<List::Util>). We pass C<< $a >> and
C<< $b >> as the last arguments to CALLABLE, rather than using the package
variables like List::Util does.
The CALLABLE must not be a flattened list, but may still be an arrayref.
INITIAL is an initial value.
my $people = LINQ( [
{ name => "Alice", age => 32 },
{ name => "Bob", age => 31 },
{ name => "Carol", age => 34 },
] );
my dotted_names = $people
->select( sub { $_->{name} } )
->aggregate( sub {
my ( $a, $b ) = @_;
return "$a.$b";
} );
print "$dotted_names\n"; # ==> Alice.Bob.Carol
=item C<< join( Y, HINT?, X_KEYS, Y_KEYS, JOINER ) >>
This is akin to an SQL join.
my $people = LINQ( [
{ name => "Alice", dept => 'Marketing' },
{ name => "Bob", dept => 'IT' },
{ name => "Carol", dept => 'IT' },
] );
my $departments = LINQ( [
{ dept_name => 'Accounts', cost_code => 1 },
{ dept_name => 'IT', cost_code => 7 },
{ dept_name => 'Marketing', cost_code => 8 },
] );
my $BY_HASH_KEY = sub {
my ($key) = @_;
return $_->{$key};
};
my $joined = $people->join(
$departments,
-inner, # inner join
[ $BY_HASH_KEY, 'dept' ], # select from $people
[ $BY_HASH_KEY, 'dept_name' ], # select from $departments
sub {
my ( $person, $dept ) = @_;
return {
name => $person->{name},
dept => $person->{dept},
expense_code => $dept->{cost_code},
};
},
);
Hints C<< -inner >>, C<< -left >>, C<< -right >>, and C<< -outer >> are
supported, analagous to the joins with the same names in SQL.
X_KEYS and Y_KEYS are non-list callables which return the values to join the
two collections by.
JOINER is a callable (which may be a flattened list) which is passed items
from each of the two collections and should return a new item. In the case
of left/right/outer joins, one of those items may be undef.
=item C<< group_join( Y, HINT?, X_KEYS, Y_KEYS, JOINER ) >>
Similar to C<group> except that rather than JOINER being called for every
X/Y combination, all the Ys for a particular X are put in a collection, and
the JOINER is called for each X and passed the collection of Ys.
The only hints supported are C<< -inner >> and C<< -left >>.
This is best explained with a full example:
my $departments = LINQ( [
{ dept_name => 'Accounts', cost_code => 1 },
{ dept_name => 'IT', cost_code => 7 },
{ dept_name => 'Marketing', cost_code => 8 },
] );
my $people = LINQ( [
{ name => "Alice", dept => 'Marketing' },
{ name => "Bob", dept => 'IT' },
{ name => "Carol", dept => 'IT' },
] );
my $BY_HASH_KEY = sub {
my ($key) = @_;
return $_->{$key};
};
my $joined = $departments->group_join(
$people,
-left, # left join
[ $BY_HASH_KEY, 'dept_name' ], # select from $departments
[ $BY_HASH_KEY, 'dept' ], # select from $people
sub {
my ( $dept, $people ) = @_; # $people is a LINQ::Collection
my $names = $people->select( $BY_HASH_KEY, 'name' )->to_array;
return {
dept => $dept->{dept_name},
cost_code => $dept->{cost_code},
people => $names,
};
},
);
# [
# {
# 'cost_code' => 1,
# 'dept' => 'Accounts',
# 'people' => []
# },
# {
# 'cost_code' => 7,
# 'dept' => 'IT',
# 'people' => [
# 'Bob',
# 'Carol'
# ]
# },
# {
# 'cost_code' => 8,
# 'dept' => 'Marketing',
# 'people' => [
# 'Alice'
# ]
# }
=item C<< take( N ) >>
Takes just the first N items from a collection, returning a new collection.
=item C<< take_while( CALLABLE ) >>
Takes items from the collection, stopping at the first item where CALLABLE
returns false.
=item C<< skip( N ) >>
Skips the first N items from a collection, and returns the rest as a new
collection.
=item C<< skip_while( CALLABLE ) >>
Skips the first items from a collection while CALLABLE returns true, and
returns the rest as a new collection.
=item C<< concat( COLLECTION ) >>
Returns a new collection by concatenating this collection with another
collection.
my $deck_of_cards = $red_cards->concat( $black_cards );
=item C<< order_by( HINT?, CALLABLE? ) >>
HINT may be C<< -numeric >> (the default) or C<< -string >>.
If CALLABLE is given, it should return a number or string to sort by.
my $sorted = $people->order_by(
-string,
[ $BY_HASH_KEY, 'name' ] # CALLABLE as an arrayref
);
=item C<< then_by( HINT?, CALLABLE ) >>
Not implemented.
=item C<< order_by_descending( HINT?, CALLABLE ) >>
Like C<order_by> but uses reverse order.
=item C<< then_by_descending( HINT?, CALLABLE ) >>
Not implemented.
=item C<< reverse >>
Reverses the order of the collection.
=item C<< group_by( CALLABLE ) >>
Groups the items by the key returned by CALLABLE.
The collection of groups is a LINQ::Collection and each grouping is a
LINQ::Grouping. LINQ::Grouping provides two accessors: C<key> and C<values>,
with C<values> returning a LINQ::Collection of items.
my $people = LINQ( [
{ name => "Alice", dept => 'Marketing' },
{ name => "Bob", dept => 'IT' },
{ name => "Carol", dept => 'IT' },
] );
my $groups = $people->group_by( sub { $_->{dept} } );
for my $group ( $groups->to_list ) {
print "Dept: ", $group->key, "\n";
for my $person ( $group->values->to_list ) {
print " - ", $person->{name};
}
}
=item C<< distinct( CALLABLE? ) >>
Returns a new collection without any duplicates which were in the original.
If CALLABLE is provided, this will be used to determine when two items are
considered identical/equivalent. Otherwise, numeric equality is used.
=item C<< union( COLLECTION, CALLABLE? ) >>
Returns a new collection formed from the union of both collections.
my $first = LINQ( [ 1, 2, 3, 4 ] );
my $second = LINQ( [ 3, 4, 5, 6 ] );
$first->union( $second )->to_array; # ==> [ 1, 2, 3, 4, 5, 6 ]
If CALLABLE is provided, this will be used to determine when two items are
considered identical/equivalent. Otherwise, numeric equality is used.
=item C<< intersect( COLLECTION, CALLABLE? ) >>
Returns a new collection formed from the union of both collections.
my $first = LINQ( [ 1, 2, 3, 4 ] );
my $second = LINQ( [ 3, 4, 5, 6 ] );
$first->intersect( $second )->to_array; # ==> [ 3, 4 ]
If CALLABLE is provided, this will be used to determine when two items are
considered identical/equivalent. Otherwise, numeric equality is used.
=item C<< except( COLLECTION, CALLABLE? ) >>
Returns a new collection formed from the asymmetric difference of both
collections.
my $first = LINQ( [ 1, 2, 3, 4 ] );
my $second = LINQ( [ 3, 4, 5, 6 ] );
$first->except( $second )->to_array; # ==> [ 1, 2 ]
If CALLABLE is provided, this will be used to determine when two items are
considered identical/equivalent. Otherwise, numeric equality is used.
=item C<< sequence_equal( COLLECTION, CALLABLE? ) >>
Returns true if and only if each item in the first collection is
identical/equivalent to its corresponding item in the second collection,
considered in order, according to CALLABLE.
If CALLABLE isn't given, then numeric equality is used.
=item C<< first( CALLABLE? ) >>
Returns the first item in a collection.
If CALLABLE is provided, returns the first item in the collection where
CALLABLE returns true.
If there is no item to return, does not return undef, but throws a
LINQ::Exception::NotFound exception.
=item C<< first_or_default( CALLABLE?, DEFAULT ) >>
Like C<first>, but instead of throwing an exception, will return the DEFAULT.
=item C<< last( CALLABLE? ) >>
Returns the last item in a collection.
If CALLABLE is provided, returns the last item in the collection where
CALLABLE returns true.
If there is no item to return, does not return undef, but throws a
LINQ::Exception::NotFound exception.
=item C<< last_or_default( CALLABLE?, DEFAULT ) >>
Like C<last>, but instead of throwing an exception, will return the DEFAULT.
=item C<< single( CALLABLE? ) >>
Returns the only item in a collection.
If CALLABLE is provided, returns the only item in the collection where
CALLABLE returns true.
If there is no item to return, does not return undef, but throws a
LINQ::Exception::NotFound exception.
If there are multiple items in the collection, or multiple items where
CALLABLE returns true, throws a LINQ::Exception::MultipleFound exception.
=item C<< single_or_default( CALLABLE?, DEFAULT ) >>
Like C<single> but rather than throwing an exception, will return DEFAULT.
=item C<< element_at( N ) >>
Returns element N within the collection. N may be negative to count from the
end of the collection. Collections are indexed from zero.
If N exceeds the length of the collection, throws a LINQ::Exception::NotFound
exception.
=item C<< element_at_or_default( N, DEFAULT ) >>
Like C<element_at> but rather than throwing an exception, will return DEFAULT.
=item C<< any( CALLABLE? ) >>
Returns true if CALLABLE returns true for any item in the collection.
=item C<< all( CALLABLE? ) >>
Returns true if CALLABLE returns true for every item in the collection.
=item C<< contains( ITEM, CALLABLE? ) >>
Returns true if the collection contains ITEM. By default, this is checked
using numeric equality.
If CALLABLE is given, this is passed two items and should return true
if they should be considered identical/equivalent.
my $SAME_NAME = sub {
$_[0]{name} eq $_[1]{name};
};
if ( $people->contains( { name => "Bob" }, $SAME_NAME ) ) {
print "The collection includes Bob.\n";
}
=item C<< count >>
Returns the size of the collection. (Number of items.)
=item C<< to_list >>
Returns the collection as a list.
=item C<< to_array >>
Returns an arrayref for the collection. This may be a tied arrayref and you
should not assume it will be writable.
=item C<< to_dictionary( CALLABLE ) >>
The CALLABLE will be called for each item in the collection and is expected to
return a string key.
The method will return a hashref mapping the keys to each item in the
collection.
=item C<< to_lookup( CALLABLE ) >>
Alias for C<to_dictionary>.
=item C<< to_iterator >>
Returns a coderef which can be used to iterate through the collection.
my $people = LINQ( [
{ name => "Alice", dept => 'Marketing' },
{ name => "Bob", dept => 'IT' },
{ name => "Carol", dept => 'IT' },
] );
my $next_person = $people->to_iterator;
while ( my $person = $next_person->() ) {
print $person->{name}, "\n";
}
=item C<< cast( TYPE ) >>
Given a type constraint (see L<Type::Tiny>) will attempt to coerce every item
in the collection to the type, and will return the collection of coerced
values. If any item cannot be coerced, throws a LINQ::Exception::Cast
exception.
=item C<< of_type( TYPE ) >>
Given a type constraint (see L<Type::Tiny>) will attempt to coerce every item
in the collection to the type, and will return the collection of coerced
values. Any items which cannot be coerced will be skipped.
=item C<< zip( COLLECTION, CALLABLE ) >>
Will loop through both collections in parallel and pass one item from each
collection to CALLABLE as arguments.
If the two collections are of different sizes, will stop after exhausing the
shorter collection.
=item C<< default_if_empty( ITEM ) >>
If this collection contains one or more items, returns itself.
If the collection is empty, returns a new collection containing just a single
item, given as a parameter.
my $collection = $people->default_if_empty( "Bob" );
# Equivalent to:
my $collection = $people->count ? $people : LINQ( [ "Bob" ] );
=item C<< target_class >>
This returns either a class name or a coderef, which will be the usual way
that the methods defined in LINQ::Collection will create new collections
(for example, when they need to return a collection).
Given a collection, you can create a new collection like this:
my $target_class = $old_collection->target_class;
my $new_collection = ref($target_class)
? $target_class->( @items )
: $target_class->new( @items );
=back
=head1 BUGS
Please report any bugs to
=head1 SEE ALSO
L<LINQ>, L<LINQ::Grouping>.
=head1 AUTHOR
Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
=head1 COPYRIGHT AND LICENCE
This software is copyright (c) 2014, 2021 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=head1 DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.