File Coverage

blib/lib/Array/Tour.pm
Criterion Covered Total %
statement 143 239 59.8
branch 17 76 22.3
condition 5 11 45.4
subroutine 30 43 69.7
pod 14 15 93.3
total 209 384 54.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Array::Tour - Base class for Array Tours.
4              
5             =head1 SYNOPSIS
6              
7             #
8             # For a new package. Add extra methods and internal attributes afterwards.
9             #
10             package Array::Tour::NewTypeOfTour
11             use base qw(Array::Tour);
12            
13             # (Code goes here).
14              
15             or
16              
17             #
18             # Make use of the constants in the package.
19             #
20             use Array::Tour qw(:directions);
21             use Array::Tour qw(:status);
22              
23             or
24              
25             #
26             # Use Array::Tour for its default 'typewriter' tour of the array.
27             #
28             use Array::Tour;
29            
30             my $by_row = Array::Tour->new(dimensions => [24, 80, 1]);
31            
32              
33             =head1 PREREQUISITES
34              
35             Perl 5.8 or later. This is the version of perl under which this module
36             was developed.
37              
38             =head1 DESCRIPTION
39              
40             Array::Tour is a base class for iterators that traverse the cells of an
41             array. This class should provide most of the methods needed for any type
42             of tour, whether it needs to visit each cell or not, and whether
43             the tour needs to be a continuous path or not.
44              
45             The iterator provides coordinates and directions. It does not define
46             the array. This leaves the user of the tour object free to
47             define the form of the array or the data structure behind it without
48             restrictions from the tour object.
49              
50             By itself without any subclassing or options, the Array::Tour class traverses a
51             simple left-to-right, top-to-bottom typewriter path. There are options to change
52             the direction or rotation of the path.
53              
54             =cut
55              
56             package Array::Tour;
57 9     9   14829 use 5.008;
  9         33  
  9         362  
58 9     9   49 use strict;
  9         16  
  9         258  
59 9     9   58 use warnings;
  9         20  
  9         334  
60 9     9   841 use integer;
  9         19  
  9         43  
61              
62 9     9   342 use vars qw(@ISA);
  9         17  
  9         889  
63             require Exporter;
64              
65             @ISA = qw(Exporter);
66              
67 9     9   53 use vars qw(%EXPORT_TAGS @EXPORT_OK);
  9         13  
  9         1515  
68             %EXPORT_TAGS = (
69             'directions' => [ qw ( NoDirection
70             North NorthWest West SouthWest Ceiling
71             South SouthEast East NorthEast Floor
72             SetPosition
73             )],
74             'status' => [ qw (START TOURING STOP)]
75             );
76              
77             @EXPORT_OK = ( @{ $EXPORT_TAGS{'directions'} }, @{ $EXPORT_TAGS{'status'} } );
78              
79             our $VERSION = '0.06';
80              
81             #
82             # Directions.
83             #
84             # The eight possible directions that one can move from a cell, including
85             # "null" direction NoDirection, and position change indicator SetPosition.
86             #
87 9     9   46 use constant NoDirection => 0x0000;
  9         14  
  9         1462  
88 9     9   46 use constant North => 0x0001; # 0;
  9         17  
  9         509  
89 9     9   43 use constant NorthWest => 0x0002; # 1;
  9         16  
  9         485  
90 9     9   47 use constant West => 0x0004; # 2;
  9         12  
  9         332  
91 9     9   44 use constant SouthWest => 0x0008; # 3;
  9         14  
  9         370  
92 9     9   44 use constant Ceiling => 0x0010; # 4;
  9         13  
  9         391  
93 9     9   44 use constant South => 0x0020; # 5;
  9         24  
  9         373  
94 9     9   51 use constant SouthEast => 0x0040; # 6;
  9         15  
  9         356  
95 9     9   40 use constant East => 0x0080; # 7;
  9         16  
  9         312  
96 9     9   40 use constant NorthEast => 0x0100; # 8;
  9         17  
  9         391  
97 9     9   52 use constant Floor => 0x0200; # 9;
  9         18  
  9         316  
98 9     9   42 use constant SetPosition => 0x8000; # 15;
  9         28  
  9         730  
99              
100             #
101             # {tourstatus} constants.
102             #
103 9     9   44 use constant START => 0;
  9         14  
  9         397  
104 9     9   40 use constant TOURING => 1;
  9         93  
  9         336  
105 9     9   55 use constant STOP => 2;
  9         30  
  9         24108  
106              
107              
108             =head2 Tour Object Methods
109              
110             =head3 new([ => value, ...])
111              
112             Creates the object with its attributes.
113              
114             With the exception of C and C, attributes are set using the
115             internal method _set(). This means that subclasses should not override new(),
116             but instead provide their own _set() method to handle their own attributes.
117              
118             In addition to C and C, new() also creates internal
119             attributes that may be used by subclasses. See the Attributes section for more
120             details.
121              
122             =cut
123              
124             sub new
125             {
126 209     209 1 83075 my $class = shift;
127 209         403 my $self = {};
128              
129             #
130             # We are copying from an existing Tour object?
131             #
132 209 50       602 if (ref $class)
133             {
134 0 0       0 if ($class->isa("Array::Tour"))
135             {
136 0         0 $class->_copy($self, @_);
137 0         0 return bless($self, ref $class);
138             }
139              
140 0         0 warn "Attempts to create an Array Touring object from a '",
141             ref $class, "' object fail.\n";
142 0         0 return undef;
143             }
144              
145             #
146             # Starting from scratch.
147             #
148 209         1043 bless($self, $class);
149 209         979 my %attributes = @_;
150 209         888 $self->_set_dimensions(%attributes);
151 209         832 $self->_set_offset(%attributes);
152 209         762 delete @attributes{qw(dimensions offset)};
153 209         609 $self->{position} = [0, 0, 0];
154 209         510 $self->{start} = [0, 0, 0];
155 209         369 $self->{array} = undef;
156 209         325 $self->{tourlength} = 1;
157 209         484 map {$self->{tourlength} *= $_} $self->get_dimensions();
  627         1034  
158 209         483 $self->{tourstatus} = START;
159 209         548 $self->{odometer} = 0;
160 209         934 $self->_set(%attributes);
161              
162 209         904 return $self;
163             }
164              
165             =head3 reset()
166              
167             $tour->reset([ => value, ...])
168              
169             Reset the object by returning its internal state to its original form.
170             Optionally change some of the characteristics using the same parameters
171             found in the new() method.
172              
173             =cut
174              
175             sub reset
176             {
177 0     0 1 0 my $self = shift;
178 0         0 my %newargs = @_;
179              
180 0         0 my %params = $self->describe();
181 0         0 $params{position} = [0, 0, 0];
182 0         0 $params{tourlength} = 1;
183 0         0 $params{tourstatus} = START;
184 0         0 $params{odometer} = 0;
185 0 0       0 $params{array} = undef if ($self->_uses_array());
186              
187             #
188             # Apply any options passed in.
189             #
190 0         0 map {$params{$_} = $newargs{$_}} keys %newargs;
  0         0  
191              
192 0         0 return $self->_set(%params);
193             }
194              
195             =head3 has_next()
196              
197             Returns 1 if there is more to the tour, 0 if finished.
198              
199             =cut
200              
201             sub has_next
202             {
203 8338     8338 1 15023 my $self = shift;
204 8338 100       34271 return ($self->{tourstatus} == STOP)? 0: 1;
205             }
206              
207             =head3 get_dimensions()
208              
209             Returns an array of the dimensions.
210              
211             =cut
212              
213             sub get_dimensions
214             {
215 418     418 1 1885 my $self = shift;
216 418         432 return @{$self->{dimensions}};
  418         1473  
217             }
218              
219             =head3 direction()
220              
221             Returns the current direction as found in the :directions EXPORT tag.
222              
223             =cut
224              
225             sub direction
226             {
227 0     0 1 0 my $self = shift;
228 0 0       0 return (${$self->{position}}[0] == 0)? NoDirection: East;
  0         0  
229             }
230              
231             =head3 opposite_direction()
232              
233             Return the direction opposite from the current direction.
234              
235             =cut
236              
237             sub opposite_direction
238             {
239 0     0 1 0 my $self = shift;
240 0         0 my $dir = $self->direction();
241 0 0       0 return NoDirection if ($dir == NoDirection);
242 0 0       0 return ($dir <= Ceiling )? ($dir << 5): ($dir >> 5);
243             }
244              
245             =head3 say_direction()
246              
247             Return the name in English of the current direction.
248              
249             =cut
250              
251             sub say_direction
252             {
253 0     0 1 0 my $self = shift;
254 0         0 my $dir = $self->direction();
255              
256 0         0 return $self->direction_name($dir);
257             }
258              
259             =head3 direction_name()
260              
261             Return the name in English of the direction passed in.
262              
263             print $tour->direction_name(NorthWest), " is ", NorthWest, "\n";
264              
265             =cut
266              
267             sub direction_name
268             {
269 0     0 1 0 my $self = shift;
270 0         0 my($dir) = @_;
271              
272 0 0       0 return q(NoDirection) if ($dir == NoDirection);
273 0 0       0 return q(North) if ($dir == North);
274 0 0       0 return q(NorthWest) if ($dir == NorthWest);
275 0 0       0 return q(West) if ($dir == West);
276 0 0       0 return q(SouthWest) if ($dir == SouthWest);
277 0 0       0 return q(Ceiling) if ($dir == Ceiling);
278 0 0       0 return q(South) if ($dir == South);
279 0 0       0 return q(SouthEast) if ($dir == SouthEast);
280 0 0       0 return q(East) if ($dir == East);
281 0 0       0 return q(NorthEast) if ($dir == NorthEast);
282 0 0       0 return q(Floor) if ($dir == Floor);
283 0 0       0 if ($dir == SetPosition)
284             {
285 0         0 my @p = @{$self->get_position()};
  0         0  
286 0         0 return q(SetPosition) . "[" . join(", ", @p) . "]";
287             }
288              
289 0         0 return q(unknown direction);
290             };
291              
292             =head3 get_position()
293              
294             Return a reference to an array of coordinates of the current position.
295              
296             @absolute_pos = @{$self->get_position()};
297              
298             =cut
299              
300             sub get_position
301             {
302 0     0 1 0 my $self = shift;
303 0         0 return $self->{position};
304             }
305              
306             =head3 get_offset()
307              
308             Return a reference to an array of offsets to be added to the current position.
309              
310             @offset = @{$self->get_offset()};
311              
312             =cut
313              
314             sub get_offset
315             {
316 0     0 1 0 my $self = shift;
317 0         0 return $self->{offset};
318             }
319              
320             =head3 adjusted_position()
321              
322             Return a reference to an array of coordinates that are created from the position
323             plus the offset. Used by the next() method.
324              
325             @current_pos = @{$self->adjusted_position()};
326              
327             =cut
328              
329             sub adjusted_position
330             {
331 8129     8129 1 9478 my $self = shift;
332              
333 8129         17698 my @position = @{ $self->{position} };
  8129         32354  
334 8129         9992 my @offset = @{ $self->{offset} };
  8129         16195  
335 8129         15565 map {$position[$_] += $offset[$_]} (0..$#position);
  16283         27846  
336 8129         25668 return \@position;
337             }
338              
339             =head3 next()
340              
341             Returns an array reference to the next coordinates to use. Returns
342             undef if the iterator is finished.
343              
344             my $ctr = 1;
345             my $tour = Array::Tour->new(dimensions => 64);
346              
347             while (my $cref = $tour->next())
348             {
349             my($x_coord, $y_coord, $z_coord) = @{$cref};
350             $grid[$y_coord, $x_coord] = isprime($ctr++);
351             }
352              
353             The above example would look like a completed Sieve of Eratothenes in the array
354             @grid.
355              
356             =cut
357              
358             sub next
359             {
360 26     26 1 149 my $self = shift;
361              
362 26 100       36 return undef unless ($self->has_next());
363              
364             #
365             # Set up the conditions for the pacing.
366             #
367 25 100       42 if ($self->{tourstatus} == START)
368             {
369 1         1 $self->{tourstatus} = TOURING;
370             }
371             else
372             {
373             #
374             # Move to the next cell, checking to see if we've
375             # reached the end of the row/plane/cube.
376             #
377 24         14 my($dim, $lastdim) = (0, scalar @{$self->{dimensions}});
  24         36  
378 24   66     47 while ($dim < $lastdim and ${$self->{position}}[$dim] == ${$self->{dimensions}}[$dim] - 1)
  28         38  
  28         75  
379             {
380 4         5 ${$self->{position}}[$dim++] = 0;
  4         9  
381             }
382 24 50       43 ${$self->{position}}[$dim] += 1 unless ($dim == $lastdim);
  24         28  
383             }
384              
385 25 100       50 $self->{tourstatus} = STOP if (++$self->{odometer} == $self->{tourlength});
386 25         36 return $self->adjusted_position();
387             }
388              
389             =head3 get_array()
390              
391             Return a reference to the internally generated array.
392              
393             $arrayref = $self->get_array()
394              
395             =cut
396              
397             sub get_array
398             {
399 0     0 1 0 my $self = shift;
400 0 0       0 $self->_make_array() unless (defined $self->{array});
401 0         0 return $self->{array};
402             }
403              
404             =head3 describe()
405              
406             Returns as a hash the attributes of the tour object. The hash may be
407             used to create a new object.
408              
409             =cut
410              
411             sub describe
412             {
413 96     96 1 570 my $self = shift;
414 96         135 return map {$_, $self->{$_}} grep(/^[a-z]/, keys %{$self});
  1536         15593  
  96         1310  
415             }
416              
417             =head2 Internal Tour Object Methods
418              
419             =head3 _set_dimensions()
420              
421             my $tour = Array::Tour->new(dimensions => [12, 16]);
422              
423             This works identically as
424              
425             my $tour = Array::Tour->new(dimensions => [12, 16, 1]);
426              
427             If the grid is going to be square, a single integer is sufficient:
428              
429             my $tour = Array::Tour->new(dimensions => 16);
430              
431             In both cases, the new() member funcntion calls _set_dimensions() and sets the
432             C attribute with a reference to a three dimensional array. The third
433             dimension is set to 1 if no value is given for it.
434              
435             =cut
436              
437             sub _set_dimensions
438             {
439 209     209   284 my $self = shift;
440 209         712 my(%params) = @_;
441 209   50     1532 my $dim = $params{dimensions} || [1, 1, 1];
442              
443 209         250 my @dimensions;
444              
445 209 100       550 if (ref $dim eq 'ARRAY')
446             {
447 184   50     232 @dimensions = map {$_ ||= 1} @{$dim};
  464         1365  
  184         548  
448 184 50       499 push @dimensions, 1 if (@dimensions < 1);
449 184 50       456 push @dimensions, $dimensions[0] if (@dimensions < 2);
450             }
451             else
452             {
453             #
454             # Square grid if only one dimension is defined.
455             #
456 25         68 @dimensions = ($dim) x 2;
457             }
458 209 100       775 push @dimensions, 1 if (@dimensions < 3);
459 209         720 $self->{dimensions} = \@dimensions;
460              
461 209         647 return $self;
462             }
463              
464             =head3 _set_offset()
465              
466             The new() member funcntion calls _set_offset() and sets the C attribute
467             with a reference to an array of coordinates. This method matches the size of the
468             C array to the size of C, so _set_dimensions() must be called
469             beforhand.
470              
471             =cut
472              
473             sub _set_offset
474             {
475 209     209   289 my $self = shift;
476 209         1283 my(%params) = @_;
477 209   50     1171 my $offsetref = $params{offset} || [0, 0, 0];
478              
479 209         388 $self->{offset} = $offsetref;
480              
481 209         239 my $dims = scalar @{$self->{dimensions}};
  209         370  
482 209         300 my $offsets = scalar @{$self->{offset}};
  209         353  
483 209 50       517 push @{$self->{offset}}, (0) x ($dims - $offsets) if ($dims > $offsets);
  0         0  
484 209         215 return @{$self->{offset}};
  209         655  
485             }
486              
487             =head3 _move_to()
488              
489             $position = $self->_move_to($direction); # [$c, $r, $l]
490              
491             Return a new position depending upon the direction taken. This does not set a
492             new position.
493              
494             =cut
495              
496             sub _move_to
497             {
498 0     0   0 my $self = shift;
499 0         0 my($dir) = @_;
500 0         0 my($c, $r, $l) = @{ $self->{position} };
  0         0  
501              
502 0 0       0 --$r if ($dir & (North | NorthWest | NorthEast));
503 0 0       0 ++$r if ($dir & (South | SouthWest | SouthEast));
504 0 0       0 ++$c if ($dir & (East | NorthEast | SouthEast));
505 0 0       0 --$c if ($dir & (West | NorthWest | SouthWest));
506 0 0       0 ++$l if ($dir & Floor);
507 0 0       0 --$l if ($dir & Ceiling);
508 0         0 return [$c, $r, $l];
509             }
510              
511             =head3 _make_array()
512              
513             $self->_make_array();
514             or
515             $self->_make_array($value);
516              
517             Make an internal array for reference purposes. If no value to set the array cels
518             with is passed in, the array cells are set to zero by default.
519              
520             =cut
521              
522             sub _make_array
523             {
524 0     0   0 my $self = shift;
525 0 0       0 my $dflt = (scalar @_)? $_[0]: 0;
526 0         0 my($cols, $rows, $lvls) = map {$_ - 1} @{$self->{dimensions}};
  0         0  
  0         0  
527              
528 0         0 my $m = $self->{array} = ([]);
529 0         0 foreach my $l (0..$lvls)
530             {
531 0         0 foreach my $r (0..$rows)
532             {
533 0         0 foreach my $c (0..$cols)
534             {
535 0         0 $$m[$l][$r][$c] = $dflt;
536             }
537             }
538             }
539 0         0 return $self;
540             }
541              
542             =head3 _set()
543              
544             $self->_set(%attributes);
545              
546             Take the parameters provided to new() and use them to set the
547             attributes of the touring object.
548              
549             =cut
550              
551             sub _set()
552             {
553 1     1   3 my $self = shift;
554 1         2 my(%params) = @_;
555              
556 1         2 warn "Unknown paramter $_" foreach (grep{$_ !~ /reverse/} (keys %params));
  1         7  
557 1         2 return $self;
558             }
559              
560             =head3 _uses_array()
561              
562             Returns 0 or 1 depending upon whether there's an internal array to return.
563              
564             =cut
565              
566 0     0     sub _uses_array {my $self = shift; return 0;}
  0            
567              
568             #
569             # dump_array
570             #
571             # @xlvls = $obj->dump_array($spr_fmt);
572             # $xstr = $obj->dump_array($spr_fmt);
573             #
574             # Returns a formatted string of all the cell values. By default,
575             # the format string is " %04x", so the default output strings will
576             # be rows of hexadecimal numbers separated by a space.
577             #
578             # If called in a list context, returns a list of strings, each one
579             # representing a level. If called in a scalar context, returns a single
580             # string, each level separated by a single newline.
581             #
582             sub dump_array
583             {
584 0     0 0   my $self = shift;
585 0   0       my $format = $_[0] || " %04x";
586 0           my($cols, $rows, $lvls) = map {$_ - 1} @{$self->{dimensions}};
  0            
  0            
587 0           my $m = $self->{array};
588 0           my @levels;
589              
590 0           foreach my $l (0..$lvls)
591             {
592 0           my $vxstr = "";
593 0           foreach my $r (0..$rows)
594             {
595 0           foreach my $c (0..$cols)
596             {
597 0           $vxstr .= sprintf($format, $$m[$l][$r][$c]);
598             }
599 0           $vxstr .= "\n";
600             }
601              
602 0           push @levels, $vxstr;
603             }
604              
605 0 0         return wantarray? @levels: join("\n", @levels);
606             }
607              
608             #
609             # $class->_copy($self);
610             #
611             # Duplicate the iterator.
612             #
613             sub _copy
614             {
615 0     0     my($other, $self) = @_;
616 0           foreach my $k (grep($_ !~ /_array/, keys %{$other}))
  0            
617             {
618 0           $self->{$k} = $other->{$k};
619             }
620 0 0         if ($other->uses_array())
621             {
622             # copy it.
623             }
624             }
625              
626             1;
627             __END__