blib/lib/Array/Ordered.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 180 | 196 | 91.8 |
branch | 47 | 68 | 69.1 |
condition | 10 | 21 | 47.6 |
subroutine | 33 | 36 | 91.6 |
pod | 17 | 17 | 100.0 |
total | 287 | 338 | 84.9 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Array::Ordered; | ||||||
2 | |||||||
3 | 2 | 2 | 39419 | use 5.006; | |||
2 | 8 | ||||||
2 | 88 | ||||||
4 | 2 | 2 | 12 | use strict; | |||
2 | 3 | ||||||
2 | 70 | ||||||
5 | 2 | 2 | 10 | use warnings FATAL => 'all'; | |||
2 | 8 | ||||||
2 | 87 | ||||||
6 | 2 | 2 | 1589 | use integer; | |||
2 | 19 | ||||||
2 | 11 | ||||||
7 | 2 | 2 | 1798 | use subs qw( last unshift push shift pop sort ); | |||
2 | 41 | ||||||
2 | 10 | ||||||
8 | 2 | 2 | 152 | use Scalar::Util qw( blessed ); | |||
2 | 3 | ||||||
2 | 181 | ||||||
9 | 2 | 2 | 12 | use Carp; | |||
2 | 4 | ||||||
2 | 5903 | ||||||
10 | |||||||
11 | =head1 NAME | ||||||
12 | |||||||
13 | Array::Ordered - Methods for handling ordered arrays | ||||||
14 | |||||||
15 | =cut | ||||||
16 | |||||||
17 | require Exporter; | ||||||
18 | |||||||
19 | our @ISA = qw( Exporter ); | ||||||
20 | our @EXPORT = qw( order ); | ||||||
21 | our @EXPORT_OK = qw( order ); | ||||||
22 | |||||||
23 | =head1 VERSION | ||||||
24 | |||||||
25 | Version 0.03 | ||||||
26 | |||||||
27 | =cut | ||||||
28 | |||||||
29 | our $VERSION = '0.03'; | ||||||
30 | |||||||
31 | =head1 SYNOPSIS | ||||||
32 | |||||||
33 | use Array::Ordered; | ||||||
34 | |||||||
35 | # Export | ||||||
36 | $array = order [], \&my_comparison; | ||||||
37 | $array = order \@array, \&my_comparison; | ||||||
38 | $array = order $array, \&other_comparison; | ||||||
39 | |||||||
40 | # Utility | ||||||
41 | $size = $array->size; | ||||||
42 | @items = $array->clear; # $array->size == 0 | ||||||
43 | |||||||
44 | # Strictly Ordered: | ||||||
45 | $elem = $array->find $match; | ||||||
46 | $array->insert $item; | ||||||
47 | $elem = $array->find_or_insert $match; | ||||||
48 | $item = $array->remove $match; | ||||||
49 | $pos = $array->position $match; | ||||||
50 | unless( $array->is_reduced ) { | ||||||
51 | $array->reduce; | ||||||
52 | } | ||||||
53 | |||||||
54 | # Unstrictly Ordered: | ||||||
55 | $elem = $array->first $match; | ||||||
56 | $elem = $array->last $match; | ||||||
57 | $array->unshift @items; | ||||||
58 | $array->push @items; | ||||||
59 | $item = $array->shift $match; | ||||||
60 | $item = $array->pop $match; | ||||||
61 | $pos = $array->first_position $match; | ||||||
62 | $pos = $array->last_position $match; | ||||||
63 | $count = $array->occurrences $match; | ||||||
64 | unless( $array->is_sorted ) { | ||||||
65 | $array->sort; | ||||||
66 | } | ||||||
67 | |||||||
68 | # Multi-element: | ||||||
69 | @elems = $array->find_all $match; | ||||||
70 | @elems = $array->heads; | ||||||
71 | @elems = $array->tails; | ||||||
72 | @items = $array->remove_all $match; | ||||||
73 | @items = $array->shift_heads; | ||||||
74 | @items = $array->pop_tails; | ||||||
75 | |||||||
76 | =head1 DESCRIPTION | ||||||
77 | |||||||
78 | The purpose of the Array::Ordered module is to provide the means to access and modify arrays while keeping them sorted. | ||||||
79 | |||||||
80 | At the heart of this module are two symmetrical binary search algorithms: | ||||||
81 | |||||||
82 | =over | ||||||
83 | |||||||
84 | =item 1 | ||||||
85 | |||||||
86 | The first returns the index of the first element equal to or greater than a matching argument. (possibly the array's size) | ||||||
87 | |||||||
88 | =item 2 | ||||||
89 | |||||||
90 | The second returns the index of the last element equal to or less than a matching argument. (possibly -1) | ||||||
91 | |||||||
92 | =back | ||||||
93 | |||||||
94 | Elements are inserted and deleted from the ordered array using 'splice'. | ||||||
95 | |||||||
96 | =head2 TERMINOLOGY | ||||||
97 | |||||||
98 | =head3 Comparison Subroutine | ||||||
99 | |||||||
100 | A I |
||||||
101 | |||||||
102 | =over | ||||||
103 | |||||||
104 | =item | ||||||
105 | |||||||
106 | Negative if the first argument should preceed the second (less than) | ||||||
107 | |||||||
108 | =item | ||||||
109 | |||||||
110 | Zero if they are equivalent (equal to) | ||||||
111 | |||||||
112 | =item | ||||||
113 | |||||||
114 | Positive if the first argument should follow the second (greater than) | ||||||
115 | |||||||
116 | =back | ||||||
117 | |||||||
118 | =head3 Equivalency Sequence | ||||||
119 | |||||||
120 | =begin html | ||||||
121 | |||||||
122 | Consider an array A = <X0, X1, X2, Y0, Z0, Z1> sorted by the rule C such that: |
||||||
123 | |||||||
124 | |
||||||
125 | |
||||||
126 | C (X*, Y*) < 0, | ||||||
127 | C (X*, Z*) < 0, | ||||||
128 | |
||||||
129 | C (Y*, Y*) = 0, | ||||||
130 | C (Y*, Z*) < 0, | ||||||
131 | |
||||||
132 | C (Z*, Y*) > 0, | ||||||
133 | C (Z*, Z*) = 0 | ||||||
134 | |||||||
135 | |||||||
136 | The array A has three equivalency sequences: AX = <X0, X1, X2>, AY = <Y0>, and AZ = <Z0, Z1>. |
||||||
137 | |||||||
138 | =end html | ||||||
139 | |||||||
140 | The length of every equivalency sequence in a strictly ordered array is 1. Only an unstrictly ordered array can have longer equivalency sequences. | ||||||
141 | |||||||
142 | =head1 METHODS | ||||||
143 | |||||||
144 | I have used the following convention for naming variables: | ||||||
145 | |||||||
146 | =over | ||||||
147 | |||||||
148 | =item * | ||||||
149 | |||||||
150 | A variable is named C<$item> or C<@items> if it refers to data introduced into or removed from the ordered array. | ||||||
151 | |||||||
152 | =item * | ||||||
153 | |||||||
154 | A variable is named C<$elem> or C<@elems> if it refers to data accessed and remaining in the ordered array. | ||||||
155 | |||||||
156 | =item * | ||||||
157 | |||||||
158 | An argument is named C<$match> when it is used to fish out one or more equivalent elements from the array. | ||||||
159 | |||||||
160 | =back | ||||||
161 | |||||||
162 | =head2 Export | ||||||
163 | |||||||
164 | =head3 order | ||||||
165 | |||||||
166 | This method takes two arguments: | ||||||
167 | |||||||
168 | =over | ||||||
169 | |||||||
170 | =item 1 | ||||||
171 | |||||||
172 | An array reference, and | ||||||
173 | |||||||
174 | =item 2 | ||||||
175 | |||||||
176 | A reference to a comparison subroutine. | ||||||
177 | |||||||
178 | =back | ||||||
179 | |||||||
180 | The array reference is returned after being tied to the code reference for ordering, the array's contents are sorted, and the reference is blessed. | ||||||
181 | |||||||
182 | The method C |
||||||
183 | |||||||
184 | sub lencmp { length $_[0] <=> length $_[1] } | ||||||
185 | |||||||
186 | $array = order [], \&lencmp; # Empty array orded by 'lencmp' | ||||||
187 | |||||||
188 | order $array, sub { $_[0] cmp $_[1] }; # Now ordered by 'cmp' | ||||||
189 | |||||||
190 | $array = order []; # Okay: Default comparison is sub { 0 } | ||||||
191 | |||||||
192 | my @items = { '3', '001', '02' }; | ||||||
193 | |||||||
194 | $array = order [@items], \&lencmp; # Copy of @items ordered by '&lencmp': | ||||||
195 | # @items is unchanged | ||||||
196 | $array = order \@items, \&lencmp; # $array == \@items: | ||||||
197 | # @items is sorted | ||||||
198 | |||||||
199 | =cut | ||||||
200 | |||||||
201 | my %CMPSUBS; | ||||||
202 | |||||||
203 | sub order { | ||||||
204 | # @_ == ($self, $cmpsub); | ||||||
205 | 165 | 50 | 165 | 1 | 29669 | my @valid = ( | |
50 | |||||||
50 | |||||||
206 | defined $_[0] ? | ||||||
207 | blessed $_[0] ? $_[0]->isa('Array::Ordered') : ref $_[0] eq 'ARRAY' : | ||||||
208 | '', | ||||||
209 | defined $_[1] ? ref $_[1] eq 'CODE' : 1 | ||||||
210 | ); | ||||||
211 | |||||||
212 | 165 | 50 | 33 | 576 | unless ($valid[0] and $valid[1]) { | ||
213 | 0 | 0 | my @msg = ('Array::Ordered::order'); | ||||
214 | 0 | 0 | 0 | (defined $_[0]) or | |||
215 | push @msg, 'missing argument'; | ||||||
216 | 0 | 0 | foreach my $i (0 .. 1) { | ||||
217 | 0 | 0 | 0 | 0 | (!$valid[$i] and defined $_[$i]) and | ||
0 | |||||||
218 | push @msg, 'invalid argument '.(ref $_[0] || $_[0]); | ||||||
219 | } | ||||||
220 | 0 | 0 | croak join( ': ', @msg ); | ||||
221 | } | ||||||
222 | |||||||
223 | 165 | 242 | my ($self, | ||||
224 | $cmpsub) = @_; | ||||||
225 | 165 | 50 | 321 | (defined $cmpsub) or | |||
226 | $cmpsub = \&_default_cmpsub; | ||||||
227 | |||||||
228 | 165 | 50 | 453 | (blessed $self) or bless $self; | |||
229 | |||||||
230 | 165 | 100 | 100 | 604 | unless (exists $CMPSUBS{$self} and | ||
231 | $CMPSUBS{$self} == $cmpsub) { | ||||||
232 | 154 | 383 | $CMPSUBS{$self} = $cmpsub; | ||||
233 | 154 | 318 | $self->sort; | ||||
234 | } | ||||||
235 | |||||||
236 | 165 | 953 | return $self; | ||||
237 | } | ||||||
238 | |||||||
239 | =head2 Utility | ||||||
240 | |||||||
241 | =head3 size | ||||||
242 | |||||||
243 | Returns number of elements in referenced array. | ||||||
244 | |||||||
245 | $size = $array->size; | ||||||
246 | # Same as: | ||||||
247 | $size = scalar @{$array}; | ||||||
248 | |||||||
249 | =cut | ||||||
250 | |||||||
251 | sub size { | ||||||
252 | 1 | 1 | 1 | 6 | return scalar( @{$_[0]} ); | ||
1 | 7 | ||||||
253 | } | ||||||
254 | |||||||
255 | =head3 clear | ||||||
256 | |||||||
257 | Removes and returns all elements from the ordered array. | ||||||
258 | |||||||
259 | @array_contained = $array->clear; | ||||||
260 | # Same as: | ||||||
261 | @array_contained = splice( @{$array}, 0, $array->size ); | ||||||
262 | |||||||
263 | =cut | ||||||
264 | |||||||
265 | sub clear { | ||||||
266 | 7 | 7 | 1 | 29 | return splice( @{$_[0]}, 0, scalar( @{$_[0]} ) ); | ||
7 | 15 | ||||||
7 | 44 | ||||||
267 | } | ||||||
268 | |||||||
269 | =head2 Strictly Ordered | ||||||
270 | |||||||
271 | =head3 find | ||||||
272 | |||||||
273 | Alias for L |
||||||
274 | |||||||
275 | =head3 insert | ||||||
276 | |||||||
277 | Alias for L |
||||||
278 | |||||||
279 | =head3 find_or_insert | ||||||
280 | |||||||
281 | Returns first equivalent item if found, or inserts and returns a new item. | ||||||
282 | |||||||
283 | If no equivalent item is found, then: | ||||||
284 | |||||||
285 | =begin html | ||||||
286 | |||||||
287 | |
||||||
288 | |
||||||
289 | |
||||||
290 | $match . |
||||||
291 | |||||||
292 | |||||||
293 | =end html | ||||||
294 | |||||||
295 | $object = $array->find_or_insert( $match, \&constructor ); | ||||||
296 | $elem = $array->find_or_insert( $match, $default ); | ||||||
297 | $elem = $array->find_or_insert( $match ); | ||||||
298 | |||||||
299 | # Examples: | ||||||
300 | $object = $array->find_or_insert( 'Delta', sub { My::NamedObject->new( 'Delta' ) } ); | ||||||
301 | $elem = $array->find_or_insert( 'DELTA', 'Delta' ); | ||||||
302 | $elem = $array->find_or_insert( 'Delta' ); | ||||||
303 | |||||||
304 | Use C |
||||||
305 | |||||||
306 | =cut | ||||||
307 | |||||||
308 | sub find_or_insert { | ||||||
309 | # @_ == ($self, $match, $constr:undef); | ||||||
310 | 357 | 357 | 1 | 1189 | my ($self, | ||
311 | $match, | ||||||
312 | $constr) = @_; | ||||||
313 | 357 | 627 | my $found = $self->first( $match ); | ||||
314 | |||||||
315 | 357 | 100 | 688 | unless (defined $found) { | |||
316 | 37 | 78 | $found = (defined $constr) ? | ||||
317 | 111 | 100 | 251 | (ref $constr eq 'CODE') ? &{$constr} : $constr : | |||
100 | |||||||
318 | $match; | ||||||
319 | 111 | 261 | $self->push( $found ); | ||||
320 | } | ||||||
321 | |||||||
322 | 357 | 727 | return $found; | ||||
323 | } | ||||||
324 | |||||||
325 | =head3 remove | ||||||
326 | |||||||
327 | Alias for L |
||||||
328 | |||||||
329 | =head3 position | ||||||
330 | |||||||
331 | Alias for L |
||||||
332 | |||||||
333 | =head3 is_reduced | ||||||
334 | |||||||
335 | Returns C<1> if the array is strictly ordered, otherwise C<''>. | ||||||
336 | |||||||
337 | $strictly = $array->is_reduced; | ||||||
338 | |||||||
339 | =cut | ||||||
340 | |||||||
341 | sub is_reduced { | ||||||
342 | # @_ == ($self) | ||||||
343 | 21 | 21 | 1 | 95 | my ($self) = @_; | ||
344 | 21 | 44 | my $cmpsub = $CMPSUBS{$self}; | ||||
345 | 21 | 23 | my $size = scalar @{$self}; | ||||
21 | 38 | ||||||
346 | |||||||
347 | 21 | 64 | for (my $i = 1; $i < $size; $i++) { | ||||
348 | 75 | 100 | 270 | (&{$cmpsub}( $self->[$i-1], $self->[$i] ) < 0) or | |||
75 | 151 | ||||||
349 | return ''; | ||||||
350 | } | ||||||
351 | |||||||
352 | 14 | 80 | return 1; | ||||
353 | } | ||||||
354 | |||||||
355 | =head3 reduce | ||||||
356 | |||||||
357 | Reduces the array into a strictly ordered array. | ||||||
358 | |||||||
359 | Only the last element of each equivalency sequence remains unless a C |
||||||
360 | |||||||
361 | $array->reduce; | ||||||
362 | # Same as: | ||||||
363 | $array->reduce( 0 ); | ||||||
364 | |||||||
365 | # Or use: | ||||||
366 | |||||||
367 | my $preserve_first = 1; | ||||||
368 | $array->reduce( $preserve_first ); | ||||||
369 | |||||||
370 | =cut | ||||||
371 | |||||||
372 | sub reduce { | ||||||
373 | # @_ == ($self, $preserve_first) | ||||||
374 | 14 | 14 | 1 | 48 | my ($self, | ||
375 | $preserve_first) = @_; | ||||||
376 | 14 | 28 | my $cmpsub = $CMPSUBS{$self}; | ||||
377 | 14 | 14 | my $size = scalar @{$self}; | ||||
14 | 19 | ||||||
378 | |||||||
379 | # Default behavior is FIFO: delete first unless otherwise specified | ||||||
380 | 14 | 100 | 49 | my $preserve_last = $preserve_first ? 0 : 1; | |||
381 | |||||||
382 | 14 | 14 | my $i = 1; | ||||
383 | 14 | 31 | while ($i < $size) { | ||||
384 | 224 | 256 | my $cmp = &{$cmpsub}( $self->[$i-1], $self->[$i] ); | ||||
224 | 399 | ||||||
385 | 224 | 100 | 693 | if ($cmp < 0) { | |||
50 | |||||||
386 | 60 | 129 | $i++; | ||||
387 | } | ||||||
388 | elsif ($cmp == 0) { | ||||||
389 | 164 | 166 | splice( @{$self}, $i - $preserve_last, 1 ); | ||||
164 | 208 | ||||||
390 | 164 | 372 | $size--; | ||||
391 | } | ||||||
392 | else { | ||||||
393 | 0 | 0 | my $item = splice @{$self}, $i, 1; | ||||
0 | 0 | ||||||
394 | 0 | 0 | my $index = _search_down( $self, $item, $i - 2 ); | ||||
395 | |||||||
396 | 0 | 0 | 0 | 0 | if ($index < 0 or | ||
0 | 0 | ||||||
397 | &{$cmpsub}( $self->[$index], $item ) < 0) { | ||||||
398 | 0 | 0 | _insert( $self, $item, $index + 1 ); | ||||
399 | } | ||||||
400 | else { # &{$cmpsub}( $item, $self->[$index] ) == 0 | ||||||
401 | 0 | 0 | 0 | $self->[$index] = $item unless ($preserve_first); | |||
402 | 0 | 0 | $size--; | ||||
403 | } | ||||||
404 | } | ||||||
405 | } | ||||||
406 | } | ||||||
407 | |||||||
408 | =head2 Unstrictly Ordered | ||||||
409 | |||||||
410 | =head3 first | ||||||
411 | |||||||
412 | Returns first equivalent item or C |
||||||
413 | |||||||
414 | Optionally returns the position of the item or C |
||||||
415 | |||||||
416 | $elem = $array->first( $match ); | ||||||
417 | ($elem, $pos) = $array->first( $match ); | ||||||
418 | |||||||
419 | =cut | ||||||
420 | |||||||
421 | sub first { | ||||||
422 | # @_ == ($self, $match) | ||||||
423 | 394 | 394 | 1 | 2028 | my ($found, | ||
424 | $equal, | ||||||
425 | $index) = _find( @_, \&_search_up); | ||||||
426 | |||||||
427 | 394 | 100 | 830 | $equal or | |||
428 | ($found, | ||||||
429 | $index) = (undef, undef); | ||||||
430 | |||||||
431 | 394 | 100 | 1003 | return wantarray ? ($found, $index) : $found; | |||
432 | } | ||||||
433 | |||||||
434 | =head3 last | ||||||
435 | |||||||
436 | Returns last equivalent item or C |
||||||
437 | |||||||
438 | Optionally returns the position of the item or C |
||||||
439 | |||||||
440 | $elem = $array->last( $match ); | ||||||
441 | ($elem, $pos) = $array->last( $match ); | ||||||
442 | |||||||
443 | =cut | ||||||
444 | |||||||
445 | sub last { | ||||||
446 | # @_ == ($self, $match) | ||||||
447 | 37 | 37 | 1351 | my ($found, | |||
448 | $equal, | ||||||
449 | $index) = _find( @_, \&_search_down ); | ||||||
450 | |||||||
451 | 37 | 50 | 88 | $equal or | |||
452 | ($found, | ||||||
453 | $index) = (undef, undef); | ||||||
454 | |||||||
455 | 37 | 50 | 169 | return wantarray ? ($found, $index) : $found; | |||
456 | } | ||||||
457 | |||||||
458 | =head3 unshift | ||||||
459 | |||||||
460 | Adds item(s), prepending them to their equivalent peers. | ||||||
461 | |||||||
462 | $array->unshift( $item ); | ||||||
463 | $array->unshift( @items ); | ||||||
464 | |||||||
465 | =cut | ||||||
466 | |||||||
467 | sub unshift { | ||||||
468 | # @_ == ($self, @items) | ||||||
469 | 7 | 7 | 39 | my $self = CORE::shift; | |||
470 | |||||||
471 | 7 | 16 | foreach (@_) { | ||||
472 | 119 | 252 | _insert( $self, $_, _search_up( $self, $_ ) ); | ||||
473 | } | ||||||
474 | } | ||||||
475 | |||||||
476 | =head3 push | ||||||
477 | |||||||
478 | Adds item(s), appending them to their equivalent peers. | ||||||
479 | |||||||
480 | $array->push( $item ); | ||||||
481 | $array->push( @items ); | ||||||
482 | |||||||
483 | =cut | ||||||
484 | |||||||
485 | sub push { | ||||||
486 | # @_ == ($self, @items) | ||||||
487 | 118 | 118 | 171 | my $self = CORE::shift; | |||
488 | |||||||
489 | 118 | 192 | foreach (@_) { | ||||
490 | 230 | 405 | _insert( $self, $_, _search_down( $self, $_ ) + 1 ); | ||||
491 | } | ||||||
492 | } | ||||||
493 | |||||||
494 | =head3 shift | ||||||
495 | |||||||
496 | Removes and returns first equivalent item or C |
||||||
497 | |||||||
498 | $item = $array->shift( $match ); | ||||||
499 | |||||||
500 | =cut | ||||||
501 | |||||||
502 | sub shift { | ||||||
503 | # @_ == ($self, $match) | ||||||
504 | 190 | 190 | 1575 | return _remove( @_, \&_search_up ); | |||
505 | } | ||||||
506 | |||||||
507 | =head3 pop | ||||||
508 | |||||||
509 | Removes and returns last equivalent item or C |
||||||
510 | |||||||
511 | $item = $array->pop( $match ); | ||||||
512 | |||||||
513 | =cut | ||||||
514 | |||||||
515 | sub pop { | ||||||
516 | # @_ == ($self, $match) | ||||||
517 | 190 | 190 | 1484 | return _remove( @_, \&_search_down ); | |||
518 | } | ||||||
519 | |||||||
520 | =head3 first_position | ||||||
521 | |||||||
522 | Returns position of first equivalent item or C |
||||||
523 | |||||||
524 | $pos = $array->first_position( $match ); | ||||||
525 | # Same as: | ||||||
526 | $pos = ($array->first( $match ))[1]; | ||||||
527 | |||||||
528 | =cut | ||||||
529 | |||||||
530 | sub first_position { | ||||||
531 | 0 | 0 | 1 | 0 | return (first( @_ ))[1]; | ||
532 | } | ||||||
533 | |||||||
534 | =head3 last_position | ||||||
535 | |||||||
536 | Returns position of last equivalent item or C |
||||||
537 | |||||||
538 | $pos = $array->last_position( $match ); | ||||||
539 | # Same as: | ||||||
540 | $pos = ($array->last( $match ))[1]; | ||||||
541 | |||||||
542 | =cut | ||||||
543 | |||||||
544 | sub last_position { | ||||||
545 | 0 | 0 | 1 | 0 | return (last( @_ ))[1]; | ||
546 | } | ||||||
547 | |||||||
548 | =head3 occurrences | ||||||
549 | |||||||
550 | Returns number of elements equivalent to C<$match>. | ||||||
551 | |||||||
552 | $count = $array->occurrences( $match ); | ||||||
553 | |||||||
554 | =cut | ||||||
555 | |||||||
556 | sub occurrences { | ||||||
557 | # @_ == ($self, $match) | ||||||
558 | 74 | 74 | 1 | 281 | my ($found, | ||
559 | $equal, | ||||||
560 | $from) = _find( @_, \&_search_up ); | ||||||
561 | |||||||
562 | 74 | 50 | 194 | return $equal ? _search_down( @_ ) - $from + 1 : 0; | |||
563 | } | ||||||
564 | |||||||
565 | =head3 is_sorted | ||||||
566 | |||||||
567 | Returns C<1> if the array is ordered, otherwise C<''>. | ||||||
568 | |||||||
569 | There is no need to call this method as long as the referenced array is modified only via the methods in this module. | ||||||
570 | |||||||
571 | $ordered = $array->is_sorted; | ||||||
572 | |||||||
573 | =cut | ||||||
574 | |||||||
575 | sub is_sorted { | ||||||
576 | # @_ == ($self) | ||||||
577 | 13 | 13 | 1 | 105 | my ($self) = @_; | ||
578 | 13 | 21 | my $cmpsub = $CMPSUBS{$self}; | ||||
579 | 13 | 16 | my $size = scalar @{$self}; | ||||
13 | 17 | ||||||
580 | |||||||
581 | 13 | 31 | for (my $i = 1; $i < $size; $i++) { | ||||
582 | 83 | 100 | 336 | (&{$cmpsub}( $self->[$i-1], $self->[$i] ) > 0) and | |||
83 | 151 | ||||||
583 | return ''; | ||||||
584 | } | ||||||
585 | |||||||
586 | 3 | 17 | return 1; | ||||
587 | } | ||||||
588 | |||||||
589 | =head3 sort | ||||||
590 | |||||||
591 | Sorts the referenced array using its associated comparison subroutine. | ||||||
592 | |||||||
593 | There is no need to call this method as long as the referenced array is modified only via the methods in this module. | ||||||
594 | |||||||
595 | $array->sort; | ||||||
596 | |||||||
597 | =cut | ||||||
598 | |||||||
599 | sub sort { | ||||||
600 | # @_ == ($self) | ||||||
601 | 164 | 164 | 247 | my ($self) = @_; | |||
602 | 164 | 294 | my $cmpsub = $CMPSUBS{$self}; | ||||
603 | 164 | 177 | my $size = scalar @{$self}; | ||||
164 | 258 | ||||||
604 | |||||||
605 | 164 | 454 | for (my $i = 1; $i < $size; $i++) { | ||||
606 | 1816 | 100 | 7736 | if (&{$cmpsub}( $self->[$i], $self->[$i-1] ) < 0) { | |||
1816 | 3666 | ||||||
607 | 252 | 941 | my $item = $self->[$i]; | ||||
608 | 252 | 507 | my $index = _search_down( $self, $item, $i - 2) + 1; | ||||
609 | 252 | 570 | for (my $j = $i; $j > $index; $j--) { | ||||
610 | 1109 | 2582 | $self->[$j] = $self->[$j-1]; | ||||
611 | } | ||||||
612 | 252 | 730 | $self->[$index] = $item; | ||||
613 | } | ||||||
614 | } | ||||||
615 | } | ||||||
616 | |||||||
617 | =head2 Multi-element | ||||||
618 | |||||||
619 | =head3 find_all | ||||||
620 | |||||||
621 | Returns array of all items equivalent to C<$match>. | ||||||
622 | |||||||
623 | @elems = $array->find_all( $match ); | ||||||
624 | |||||||
625 | =cut | ||||||
626 | |||||||
627 | sub find_all { | ||||||
628 | # @_ == ($self, $match) | ||||||
629 | 74 | 74 | 1 | 308 | my ($found, | ||
630 | $equal, | ||||||
631 | $from) = _find( @_, \&_search_up ); | ||||||
632 | |||||||
633 | 74 | 50 | 208 | return $equal ? @{$_[0]}[$from .. _search_down( @_ )] : (); | |||
74 | 350 | ||||||
634 | } | ||||||
635 | |||||||
636 | =head3 heads | ||||||
637 | |||||||
638 | Returns a strictly ordered array containing the first of each equivalency sequence. | ||||||
639 | |||||||
640 | @elems = $array->heads; | ||||||
641 | |||||||
642 | =cut | ||||||
643 | |||||||
644 | sub heads { | ||||||
645 | # @_ == ($self) | ||||||
646 | 7 | 7 | 1 | 23 | my ($self) = @_; | ||
647 | 7 | 9 | my $size = scalar( @{$self} ); | ||||
7 | 10 | ||||||
648 | 7 | 8 | my @heads; | ||||
649 | |||||||
650 | 7 | 17 | for (my $index = 0; $index < $size; | ||||
651 | $index = _search_down( $self, $heads[-1] ) + 1) { | ||||||
652 | 37 | 90 | CORE::push @heads, $self->[$index]; | ||||
653 | } | ||||||
654 | |||||||
655 | 7 | 40 | return @heads; | ||||
656 | } | ||||||
657 | |||||||
658 | =head3 tails | ||||||
659 | |||||||
660 | Returns a strictly ordered array containing the last of each equivalency sequence. | ||||||
661 | |||||||
662 | @elems = $array->tails; | ||||||
663 | |||||||
664 | =cut | ||||||
665 | |||||||
666 | sub tails { | ||||||
667 | # @_ == ($self) | ||||||
668 | 7 | 7 | 1 | 28 | my ($self) = @_; | ||
669 | 7 | 8 | my @tails; | ||||
670 | |||||||
671 | 7 | 11 | for (my $index = scalar( @{$self} ) - 1; $index >= 0; | ||||
7 | 25 | ||||||
672 | $index = _search_up( $self, $tails[0] ) - 1) { | ||||||
673 | 37 | 113 | CORE::unshift @tails, $self->[$index]; | ||||
674 | } | ||||||
675 | |||||||
676 | 7 | 66 | return @tails; | ||||
677 | } | ||||||
678 | |||||||
679 | =head3 remove_all | ||||||
680 | |||||||
681 | Removes all items equivalent to C<$match> and returns them as an array. | ||||||
682 | |||||||
683 | @items = $array->remove_all( $match ); | ||||||
684 | |||||||
685 | =cut | ||||||
686 | |||||||
687 | sub remove_all { | ||||||
688 | # @_ == ($self, $match) | ||||||
689 | 74 | 74 | 1 | 336 | my ($found, | ||
690 | $equal, | ||||||
691 | $from) = _find( @_, \&_search_up ); | ||||||
692 | |||||||
693 | 74 | 191 | return $equal ? | ||||
694 | 74 | 50 | 164 | splice( @{$_[0]}, $from, _search_down( @_ ) - $from + 1 ) : (); | |||
695 | } | ||||||
696 | |||||||
697 | =head3 shift_heads | ||||||
698 | |||||||
699 | Removes the first of each equivalency sequence and returns them as a strictly ordered array. | ||||||
700 | |||||||
701 | @items = $array->shift_heads; | ||||||
702 | |||||||
703 | =cut | ||||||
704 | |||||||
705 | sub shift_heads { | ||||||
706 | # @_ == ($self) | ||||||
707 | 7 | 7 | 1 | 30 | my ($self) = @_; | ||
708 | 7 | 9 | my $size = scalar( @{$self} ); | ||||
7 | 58 | ||||||
709 | 7 | 10 | my @heads; | ||||
710 | |||||||
711 | 7 | 24 | for (my $index = 0; $index < $size; $size--, | ||||
712 | $index = _search_down( $self, $heads[-1] ) + 1) { | ||||||
713 | 37 | 46 | CORE::push @heads, splice( @{$self}, $index, 1 ); | ||||
37 | 123 | ||||||
714 | } | ||||||
715 | |||||||
716 | 7 | 46 | return @heads; | ||||
717 | } | ||||||
718 | |||||||
719 | =head3 pop_tails | ||||||
720 | |||||||
721 | Removes the last of each equivalency sequence and returns them as a strictly ordered array. | ||||||
722 | |||||||
723 | @items = $array->pop_tails; | ||||||
724 | |||||||
725 | =cut | ||||||
726 | |||||||
727 | sub pop_tails { | ||||||
728 | # @_ == ($self) | ||||||
729 | 7 | 7 | 1 | 27 | my ($self) = @_; | ||
730 | 7 | 8 | my @tails; | ||||
731 | |||||||
732 | 7 | 9 | for (my $index = scalar( @{$self} ) - 1; $index >= 0; | ||||
7 | 25 | ||||||
733 | $index = _search_up( $self, $tails[0] ) - 1) { | ||||||
734 | 37 | 46 | CORE::unshift @tails, splice( @{$self}, $index, 1 ); | ||||
37 | 129 | ||||||
735 | } | ||||||
736 | |||||||
737 | 7 | 55 | return @tails; | ||||
738 | } | ||||||
739 | |||||||
740 | # Aliases | ||||||
741 | |||||||
742 | *find = \&first; | ||||||
743 | *remove = \&shift; | ||||||
744 | *insert = \&push; | ||||||
745 | *position = \&first_position; | ||||||
746 | |||||||
747 | # Begin Private Methods | ||||||
748 | |||||||
749 | sub _find { | ||||||
750 | 1033 | 1033 | 1381 | my ($self, | |||
751 | $match, | ||||||
752 | $search) = @_; | ||||||
753 | 1033 | 1120 | my $index = &{$search}( $self, $match ); | ||||
1033 | 1772 | ||||||
754 | 1033 | 1554 | my $found = $self->[$index]; | ||||
755 | 908 | 2595 | my $equal = defined $found ? | ||||
756 | 1033 | 100 | 1708 | &{$CMPSUBS{$self}}( $match, $found ) == 0 : | |||
757 | ''; | ||||||
758 | |||||||
759 | 1033 | 4335 | return ( $found, $equal, $index ); | ||||
760 | } | ||||||
761 | |||||||
762 | sub _insert { | ||||||
763 | 349 | 349 | 537 | my ($self, | |||
764 | $item, | ||||||
765 | $index) = @_; | ||||||
766 | 349 | 383 | my $size = scalar @{$self}; | ||||
349 | 480 | ||||||
767 | |||||||
768 | 349 | 100 | 691 | if ($index < $size / 2) { | |||
769 | 90 | 106 | CORE::unshift @{$self}, splice( @{$self}, 0, $index, $item ); | ||||
90 | 133 | ||||||
90 | 396 | ||||||
770 | } | ||||||
771 | else { | ||||||
772 | 259 | 295 | CORE::push @{$self}, splice( @{$self}, $index, $size - $index, $item ); | ||||
259 | 359 | ||||||
259 | 1075 | ||||||
773 | } | ||||||
774 | } | ||||||
775 | |||||||
776 | sub _remove { | ||||||
777 | # @_ == ($self, $match, $search) | ||||||
778 | 380 | 380 | 669 | my ($found, | |||
779 | $equal, | ||||||
780 | $index) = _find( @_ ); | ||||||
781 | |||||||
782 | 380 | 100 | 888 | return $equal ? splice( @{$_[0]}, $index, 1 ) : undef; | |||
238 | 694 | ||||||
783 | } | ||||||
784 | |||||||
785 | sub _search_up { | ||||||
786 | 999 | 999 | 1377 | my ($self, | |||
787 | $match, | ||||||
788 | $min) = @_; | ||||||
789 | 999 | 1912 | my $cmpsub = $CMPSUBS{$self}; | ||||
790 | 999 | 50 | 2127 | (defined $min) or | |||
791 | $min = 0; | ||||||
792 | 999 | 1106 | my $max = scalar @{$self}; | ||||
999 | 1417 | ||||||
793 | |||||||
794 | 999 | 100 | 2573 | while ($min < $max and &{$cmpsub} ($match, $self->[$min]) > 0) { | |||
2088 | 4578 | ||||||
795 | 1506 | 7357 | my $mid = $min + ($max - $min) / 2; | ||||
796 | 1506 | 100 | 1831 | if (&{$cmpsub} ($match, $self->[$mid]) > 0) { | |||
1506 | 2938 | ||||||
797 | 960 | 4673 | $min = $mid + 1; | ||||
798 | } | ||||||
799 | else { | ||||||
800 | 546 | 2613 | $max = $mid; | ||||
801 | } | ||||||
802 | } | ||||||
803 | |||||||
804 | 999 | 4399 | return $min; | ||||
805 | } | ||||||
806 | |||||||
807 | sub _search_down { | ||||||
808 | 1005 | 1005 | 1357 | my ($self, | |||
809 | $match, | ||||||
810 | $max) = @_; | ||||||
811 | 1005 | 1884 | my $cmpsub = $CMPSUBS{$self}; | ||||
812 | 753 | 1140 | (defined $max) or | ||||
813 | 1005 | 100 | 2605 | $max = scalar @{$self} - 1; | |||
814 | 1005 | 1218 | my $min = -1; | ||||
815 | |||||||
816 | 1005 | 100 | 2580 | while ($max > $min and &{$cmpsub} ($match, $self->[$max]) < 0) { | |||
2521 | 5460 | ||||||
817 | 1963 | 9456 | my $mid = $max + ($min - $max) / 2; | ||||
818 | 1963 | 100 | 2546 | if (&{$cmpsub} ($match, $self->[$mid]) < 0) { | |||
1963 | 3953 | ||||||
819 | 1167 | 5713 | $max = $mid - 1; | ||||
820 | } | ||||||
821 | else { | ||||||
822 | 796 | 3810 | $min = $mid; | ||||
823 | } | ||||||
824 | } | ||||||
825 | |||||||
826 | 1005 | 4732 | return $max; | ||||
827 | } | ||||||
828 | |||||||
829 | 0 | 0 | sub _default_cmpsub { 0 }; | ||||
830 | |||||||
831 | # End Private Methods | ||||||
832 | |||||||
833 | =head1 ACKNOWLEDGMENTS | ||||||
834 | |||||||
835 | This module's framework generated with L |
||||||
836 | |||||||
837 | =head1 AUTHOR | ||||||
838 | |||||||
839 | S. Randall Sawyer, C<< |
||||||
840 | |||||||
841 | =head1 BUGS | ||||||
842 | |||||||
843 | Please report any bugs or feature requests to C |
||||||
844 | |||||||
845 | =head1 SUPPORT | ||||||
846 | |||||||
847 | You can find documentation for this module with the perldoc command. | ||||||
848 | |||||||
849 | perldoc Array::Ordered | ||||||
850 | |||||||
851 | =head1 TODO | ||||||
852 | |||||||
853 | Write an XS version so that 'order' works syntactically like 'tie'. | ||||||
854 | Write a module for handling large sorted arrays using a balanced binary tree as a back-end. | ||||||
855 | |||||||
856 | =head1 SEE ALSO | ||||||
857 | |||||||
858 | L |
||||||
859 | |||||||
860 | =head1 LICENSE AND COPYRIGHT | ||||||
861 | |||||||
862 | Copyright 2013 S. Randall Sawyer. All rights reserved. | ||||||
863 | |||||||
864 | This program is free software; you can redistribute it and/or modify it | ||||||
865 | under the terms of the the Artistic License (2.0). You may obtain a | ||||||
866 | copy of the full license at: | ||||||
867 | |||||||
868 | L |
||||||
869 | |||||||
870 | =cut | ||||||
871 | |||||||
872 | 1; | ||||||
873 | |||||||
874 | __END__ |