'c' function is unnecessary
[catagits/DOM-Tiny.git] / lib / DOM / Tiny / Collection.pm
1 package DOM::Tiny::Collection;
2
3 use strict;
4 use warnings;
5 use Carp 'croak';
6 use List::Util;
7 use Scalar::Util 'blessed';
8
9 our $VERSION = '0.001';
10
11 sub new {
12   my $class = shift;
13   return bless [@_], ref $class || $class;
14 }
15
16 sub TO_JSON { [@{shift()}] }
17
18 sub compact {
19   my $self = shift;
20   return $self->new(grep { defined && (ref || length) } @$self);
21 }
22
23 sub each {
24   my ($self, $cb) = @_;
25   return @$self unless $cb;
26   my $i = 1;
27   $_->$cb($i++) for @$self;
28   return $self;
29 }
30
31 sub first {
32   my ($self, $cb) = (shift, shift);
33   return $self->[0] unless $cb;
34   return List::Util::first { $_ =~ $cb } @$self if ref $cb eq 'Regexp';
35   return List::Util::first { $_->$cb(@_) } @$self;
36 }
37
38 sub flatten { $_[0]->new(_flatten(@{$_[0]})) }
39
40 sub grep {
41   my ($self, $cb) = (shift, shift);
42   return $self->new(grep { $_ =~ $cb } @$self) if ref $cb eq 'Regexp';
43   return $self->new(grep { $_->$cb(@_) } @$self);
44 }
45
46 sub join {
47   join $_[1] // '', map {"$_"} @{$_[0]};
48 }
49
50 sub last { shift->[-1] }
51
52 sub map {
53   my ($self, $cb) = (shift, shift);
54   return $self->new(map { $_->$cb(@_) } @$self);
55 }
56
57 sub reduce {
58   my $self = shift;
59   @_ = (@_, @$self);
60   goto &List::Util::reduce;
61 }
62
63 sub reverse { $_[0]->new(reverse @{$_[0]}) }
64
65 sub shuffle { $_[0]->new(List::Util::shuffle @{$_[0]}) }
66
67 sub size { scalar @{$_[0]} }
68
69 sub slice {
70   my $self = shift;
71   return $self->new(@$self[@_]);
72 }
73
74 sub sort {
75   my ($self, $cb) = @_;
76
77   return $self->new(sort @$self) unless $cb;
78
79   my $caller = caller;
80   no strict 'refs';
81   my @sorted = sort {
82     local (*{"${caller}::a"}, *{"${caller}::b"}) = (\$a, \$b);
83     $a->$cb($b);
84   } @$self;
85   return $self->new(@sorted);
86 }
87
88 sub tap {
89   my ($self, $cb) = (shift, shift);
90   $_->$cb(@_) for $self;
91   return $self;
92 }
93
94 sub to_array { [@{shift()}] }
95
96 sub uniq {
97   my ($self, $cb) = (shift, shift);
98   my %seen;
99   return $self->new(grep { !$seen{$_->$cb(@_)}++ } @$self) if $cb;
100   return $self->new(grep { !$seen{$_}++ } @$self);
101 }
102
103 sub _flatten {
104   map { _ref($_) ? _flatten(@$_) : $_ } @_;
105 }
106
107 sub _ref { ref $_[0] eq 'ARRAY' || blessed $_[0] && $_[0]->isa(__PACKAGE__) }
108
109 1;
110
111 =encoding utf8
112
113 =head1 NAME
114
115 DOM::Tiny::Collection - Collection
116
117 =head1 SYNOPSIS
118
119   use DOM::Tiny::Collection;
120
121   # Manipulate collection
122   my $collection = DOM::Tiny::Collection->new(qw(just works));
123   unshift @$collection, 'it';
124   say $collection->join("\n");
125
126   # Chain methods
127   $collection->map(sub { ucfirst })->shuffle->each(sub {
128     my ($word, $num) = @_;
129     say "$num: $word";
130   });
131
132 =head1 DESCRIPTION
133
134 L<DOM::Tiny::Collection> is an array-based container for collections of
135 L<DOM::Tiny> nodes or other items based on L<Mojo::Collection>.
136
137   # Access array directly to manipulate collection
138   my $collection = DOM::Tiny::Collection->new(1 .. 25);
139   $collection->[23] += 100;
140   say for @$collection;
141
142 =head1 METHODS
143
144 L<DOM::Tiny::Collection> implements the following methods.
145
146 =head2 new
147
148   my $collection = DOM::Tiny::Collection->new(1, 2, 3);
149
150 Construct a new array-based L<DOM::Tiny::Collection> object.
151
152 =head2 TO_JSON
153
154   my $array = $collection->TO_JSON;
155
156 Alias for L</"to_array">.
157
158 =head2 compact
159
160   my $new = $collection->compact;
161
162 Create a new collection with all elements that are defined and not an empty
163 string.
164
165   # "0, 1, 2, 3"
166   DOM::Tiny::Collection->new(0, 1, undef, 2, '', 3)->compact->join(', ');
167
168 =head2 each
169
170   my @elements = $collection->each;
171   $collection  = $collection->each(sub {...});
172
173 Evaluate callback for each element in collection or return all elements as a
174 list if none has been provided. The element will be the first argument passed
175 to the callback and is also available as C<$_>.
176
177   # Make a numbered list
178   $collection->each(sub {
179     my ($e, $num) = @_;
180     say "$num: $e";
181   });
182
183 =head2 first
184
185   my $first = $collection->first;
186   my $first = $collection->first(qr/foo/);
187   my $first = $collection->first(sub {...});
188   my $first = $collection->first($method);
189   my $first = $collection->first($method, @args);
190
191 Evaluate regular expression/callback for, or call method on, each element in
192 collection and return the first one that matched the regular expression, or for
193 which the callback/method returned true. The element will be the first argument
194 passed to the callback and is also available as C<$_>.
195
196   # Longer version
197   my $first = $collection->first(sub { $_->$method(@args) });
198
199   # Find first value that contains the word "dom"
200   my $interesting = $collection->first(qr/dom/i);
201
202   # Find first value that is greater than 5
203   my $greater = $collection->first(sub { $_ > 5 });
204
205 =head2 flatten
206
207   my $new = $collection->flatten;
208
209 Flatten nested collections/arrays recursively and create a new collection with
210 all elements.
211
212   # "1, 2, 3, 4, 5, 6, 7"
213   DOM::Tiny::Collection->new(1, [2, [3, 4], 5, [6]], 7)->flatten->join(', ');
214
215 =head2 grep
216
217   my $new = $collection->grep(qr/foo/);
218   my $new = $collection->grep(sub {...});
219   my $new = $collection->grep($method);
220   my $new = $collection->grep($method, @args);
221
222 Evaluate regular expression/callback for, or call method on, each element in
223 collection and create a new collection with all elements that matched the
224 regular expression, or for which the callback/method returned true. The element
225 will be the first argument passed to the callback and is also available as
226 C<$_>.
227
228   # Longer version
229   my $new = $collection->grep(sub { $_->$method(@args) });
230
231   # Find all values that contain the word "dom"
232   my $interesting = $collection->grep(qr/dom/i);
233
234   # Find all values that are greater than 5
235   my $greater = $collection->grep(sub { $_ > 5 });
236
237 =head2 join
238
239   my $stream = $collection->join;
240   my $stream = $collection->join("\n");
241
242 Turn collection into string.
243
244   # Join all values with commas
245   $collection->join(', ')->say;
246
247 =head2 last
248
249   my $last = $collection->last;
250
251 Return the last element in collection.
252
253 =head2 map
254
255   my $new = $collection->map(sub {...});
256   my $new = $collection->map($method);
257   my $new = $collection->map($method, @args);
258
259 Evaluate callback for, or call method on, each element in collection and create
260 a new collection from the results. The element will be the first argument
261 passed to the callback and is also available as C<$_>.
262
263   # Longer version
264   my $new = $collection->map(sub { $_->$method(@args) });
265
266   # Append the word "dom" to all values
267   my $domified = $collection->map(sub { $_ . 'dom' });
268
269 =head2 reduce
270
271   my $result = $collection->reduce(sub {...});
272   my $result = $collection->reduce(sub {...}, $initial);
273
274 Reduce elements in collection with callback, the first element will be used as
275 initial value if none has been provided.
276
277   # Calculate the sum of all values
278   my $sum = $collection->reduce(sub { $a + $b });
279
280   # Count how often each value occurs in collection
281   my $hash = $collection->reduce(sub { $a->{$b}++; $a }, {});
282
283 =head2 reverse
284
285   my $new = $collection->reverse;
286
287 Create a new collection with all elements in reverse order.
288
289 =head2 slice
290
291   my $new = $collection->slice(4 .. 7);
292
293 Create a new collection with all selected elements.
294
295   # "B C E"
296   DOM::Tiny::Collection->new('A', 'B', 'C', 'D', 'E')->slice(1, 2, 4)->join(' ');
297
298 =head2 shuffle
299
300   my $new = $collection->shuffle;
301
302 Create a new collection with all elements in random order.
303
304 =head2 size
305
306   my $size = $collection->size;
307
308 Number of elements in collection.
309
310 =head2 sort
311
312   my $new = $collection->sort;
313   my $new = $collection->sort(sub {...});
314
315 Sort elements based on return value of callback and create a new collection
316 from the results.
317
318   # Sort values case-insensitive
319   my $case_insensitive = $collection->sort(sub { uc($a) cmp uc($b) });
320
321 =head2 tap
322
323   $collection = $collection->tap(sub {...});
324
325 Equivalent to L<Mojo::Base/"tap">.
326
327 =head2 to_array
328
329   my $array = $collection->to_array;
330
331 Turn collection into array reference.
332
333 =head2 uniq
334
335   my $new = $collection->uniq;
336   my $new = $collection->uniq(sub {...});
337   my $new = $collection->uniq($method);
338   my $new = $collection->uniq($method, @args);
339
340 Create a new collection without duplicate elements, using the string
341 representation of either the elements or the return value of the
342 callback/method.
343
344   # Longer version
345   my $new = $collection->uniq(sub { $_->$method(@args) });
346
347   # "foo bar baz"
348   DOM::Tiny::Collection->new('foo', 'bar', 'bar', 'baz')->uniq->join(' ');
349
350   # "[[1, 2], [2, 1]]"
351   DOM::Tiny::Collection->new([1, 2], [2, 1], [3, 2])->uniq(sub{ $_->[1] })->to_array;
352
353 =head1 BUGS
354
355 Report any issues on the public bugtracker.
356
357 =head1 AUTHOR
358
359 Dan Book <dbook@cpan.org>
360
361 =head1 COPYRIGHT AND LICENSE
362
363 This software is Copyright (c) 2015 by Dan Book.
364
365 This is free software, licensed under:
366
367   The Artistic License 2.0 (GPL Compatible)
368
369 =head1 SEE ALSO
370
371 L<Mojo::Collection>