| 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__ |