Commit | Line | Data |
d6512b50 |
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 | |
927f1351 |
10 | our $VERSION = '0.001'; |
11 | |
d6512b50 |
12 | our @EXPORT_OK = ('c'); |
13 | |
d6512b50 |
14 | sub c { __PACKAGE__->new(@_) } |
15 | |
8152d579 |
16 | sub new { |
17 | my $class = shift; |
18 | return bless [@_], ref $class || $class; |
19 | } |
20 | |
21 | sub TO_JSON { [@{shift()}] } |
22 | |
d6512b50 |
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 { |
91880340 |
52 | join $_[1] // '', map {"$_"} @{$_[0]}; |
d6512b50 |
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 | |
d6512b50 |
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 | |
8152d579 |
124 | use DOM::Tiny::Collection; |
d6512b50 |
125 | |
126 | # Manipulate collection |
8152d579 |
127 | my $collection = DOM::Tiny::Collection->new(qw(just works)); |
d6512b50 |
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 |
8152d579 |
138 | use DOM::Tiny::Collection 'c'; |
d6512b50 |
139 | c(qw(a b c))->join('/')->url_escape->say; |
140 | |
141 | =head1 DESCRIPTION |
142 | |
8152d579 |
143 | L<DOM::Tiny::Collection> is an array-based container for collections of |
144 | L<DOM::Tiny> nodes based on L<Mojo::Collection>. |
d6512b50 |
145 | |
146 | # Access array directly to manipulate collection |
8152d579 |
147 | my $collection = DOM::Tiny::Collection->new(1 .. 25); |
d6512b50 |
148 | $collection->[23] += 100; |
149 | say for @$collection; |
150 | |
151 | =head1 FUNCTIONS |
152 | |
8152d579 |
153 | L<DOM::Tiny::Collection> implements the following functions, which can be imported |
d6512b50 |
154 | individually. |
155 | |
156 | =head2 c |
157 | |
158 | my $collection = c(1, 2, 3); |
159 | |
8152d579 |
160 | Construct a new array-based L<DOM::Tiny::Collection> object. |
d6512b50 |
161 | |
162 | =head1 METHODS |
163 | |
8152d579 |
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. |
d6512b50 |
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" |
8152d579 |
186 | DOM::Tiny::Collection->new(0, 1, undef, 2, '', 3)->compact->join(', '); |
d6512b50 |
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 | |
8152d579 |
219 | # Find first value that contains the word "dom" |
220 | my $interesting = $collection->first(qr/dom/i); |
d6512b50 |
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" |
8152d579 |
233 | DOM::Tiny::Collection->new(1, [2, [3, 4], 5, [6]], 7)->flatten->join(', '); |
d6512b50 |
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 | |
8152d579 |
251 | # Find all values that contain the word "dom" |
252 | my $interesting = $collection->grep(qr/dom/i); |
d6512b50 |
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 | |
8152d579 |
286 | # Append the word "dom" to all values |
287 | my $domified = $collection->map(sub { $_ . 'dom' }); |
d6512b50 |
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" |
8152d579 |
316 | DOM::Tiny::Collection->new('A', 'B', 'C', 'D', 'E')->slice(1, 2, 4)->join(' '); |
d6512b50 |
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" |
8152d579 |
368 | DOM::Tiny::Collection->new('foo', 'bar', 'bar', 'baz')->uniq->join(' '); |
d6512b50 |
369 | |
370 | # "[[1, 2], [2, 1]]" |
8152d579 |
371 | DOM::Tiny::Collection->new([1, 2], [2, 1], [3, 2])->uniq(sub{ $_->[1] })->to_array; |
d6512b50 |
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> |