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