File Coverage

blib/lib/Acme/Siteswap.pm
Criterion Covered Total %
statement 56 73 76.7
branch 22 36 61.1
condition n/a
subroutine 8 11 72.7
pod 3 3 100.0
total 89 123 72.3


line stmt bran cond sub pod time code
1             package Acme::Siteswap;
2 1     1   20781 use strict;
  1         2  
  1         33  
3 1     1   5 use warnings;
  1         2  
  1         29  
4              
5 1     1   6 use List::Util qw( max reduce );
  1         11  
  1         1256  
6              
7             =head1 NAME
8              
9             Acme::Siteswap - Provide information about Juggling Siteswap patterns
10              
11             =head1 SYNOPSIS
12              
13             use Acme::Siteswap;
14             my $siteswap = Acme::Siteswap->new(
15             pattern => '53142',
16             balls => 3,
17             );
18             print "Awesome!\n" unless $siteswap->valid;
19              
20             =cut
21              
22             our $VERSION = '0.03';
23              
24             =head1 FUNCTIONS
25              
26             =head2 new
27              
28             Create a new Acme::Siteswap object.
29              
30             Options:
31              
32             =over 4
33              
34             =item pattern
35              
36             Mandatory. The siteswap pattern. Should be a series of throws.
37              
38             =item balls
39              
40             Mandatory. The number of balls in the pattern.
41              
42             =back
43              
44             =cut
45              
46             sub new {
47 285     285 1 2585 my $class = shift;
48 285         807 my $self = { @_ };
49 285 50       686 die "A siteswap pattern is required!" unless defined $self->{pattern};
50 285 50       538 die "The number of balls is required!" unless defined $self->{balls};
51 285         472 bless $self, $class;
52 285         551 return $self;
53             }
54              
55             =head2 valid
56              
57             Determines if the specified pattern is valid.
58              
59             =cut
60              
61             sub valid {
62 566     566 1 1499 my $self = shift;
63 566         905 my $pattern = $self->{pattern};
64              
65 566         538 my @throws;
66 566         599 eval { @throws = _pattern_to_throws($pattern) };
  566         916  
67 566 50       1151 if ($@) {
68 0         0 $self->{error} = $@;
69 0         0 return 0;
70             }
71              
72             # Check that the numbers / throws == # of balls
73 566         659 my $total = 0;
74 566         736 for my $t (@throws) {
75 2702 100       3730 if (ref $t eq 'ARRAY') {
76 23         27 foreach my $m_t (@$t) {
77 46         86 $total += $m_t;
78             }
79             }
80             else {
81 2679         4711 $total += $t;
82             }
83             }
84              
85 566         1087 my $avg = $total / @throws;
86 566 100       1522 unless ($avg == $self->{balls}) {
87 1         2 $self->{error} = "sum of throws / # of throws does not equal # of balls!";
88 1         6 return 0;
89             }
90            
91 565         1103 return $self->_check_timing(@throws);
92             }
93              
94             sub _check_timing {
95 565     565   1355 my ($self, @throws) = @_;
96            
97             # foreach non-zero throw, mark where the ball will next be
98             # thrown and make sure that each throw is fed.
99 565 100       3045 my @throw_map = map { ref $_ eq 'ARRAY' ? scalar(@$_)
  2701 100       6564  
100             : ( $_ > 0 ? 1 : 0 ) } @throws;
101 565         1210 my @feeds = (0) x scalar @throws;
102 565         1162 for my $i (0 .. $#throws) {
103 2701 100       6086 my @subthrows = ref $throws[$i] eq 'ARRAY' ? @{$throws[$i]}
  23         44  
104             : ($throws[$i]);
105            
106 2701         3066 foreach my $throw (@subthrows) {
107 2724 100       5570 next if $throws[$i] == 0;
108 2505         4516 my $next_thrown = ($i + $throw) % scalar @throws;
109 2505         6917 $feeds[$next_thrown]++;
110             }
111             }
112              
113 565         1004 for my $i (0 .. $#throws) {
114 2695 100       5132 if ($feeds[$i] != $throw_map[$i]) {
115 3         6 $self->{error} = "Multiple throws would land at the same time.";
116 3         18 return 0;
117             }
118             }
119 562         3890 return 1;
120             }
121              
122             =head2 error
123              
124             Returns an error message or empty string.
125              
126             =cut
127              
128 4 50   4 1 41 sub error { $_[0]->{error} || '' }
129              
130             sub _pattern_to_throws {
131 566     566   561 my $pattern = shift;
132              
133 566         721 my @throw_set = ();
134              
135 566         2536 while ($pattern =~ m/
136             # next block of non-multiplex throws
137             (?: \G (\d+) )
138             # or the next multiplex throw
139             | (?: \G \[(\d+)\] )
140             # or the end of the pattern
141             | (?: \G \z )
142             /xmg) {
143 1153 100       2796 if ( defined $1 ) {
    100          
144 564         4203 push (@throw_set, split (//, $1));
145             }
146             elsif ( defined $2 ) {
147 23         120 push (@throw_set, [ split(//, $2) ]);
148             }
149             else {
150             # if we never get here, the pattern had an issue
151 566         2782 return @throw_set;
152             }
153             }
154            
155 0           die "unable to parse pattern: $pattern";
156             }
157              
158             sub _max_throw {
159 0     0     my ($throws) = @_;
160              
161             my $max_throw = reduce {
162 0 0   0     my $a_1 = ( ref $a eq 'ARRAY' ? max(@$a) : $a );
163 0 0         my $b_1 = ( ref $b eq 'ARRAY' ? max(@$b) : $b );
164 0 0         $a_1 >= $b_1 ? $a_1 : $b_1;
165 0           } @$throws;
166              
167             # if our pattern is a 1-length multiplex pattern,
168             # reduce returns the first element, so correct for
169             # that here
170 0 0         $max_throw = max(@$max_throw) if ref $max_throw eq 'ARRAY';
171              
172 0           return $max_throw;
173             }
174              
175             # extend the pattern by the number of throws equal to the biggest
176             # throw in the pattern, to ensure that every throw in the pattern
177             # lands at least once.
178             sub _expand_throws {
179 0     0     my ($throws) = @_;
180 0           my $max_throw = _max_throw($throws);
181            
182 0           foreach my $i (0 .. $max_throw) {
183             # if it's a multiplex throw, we want to copy it
184 0 0         my $t = ref $throws->[$i] eq 'ARRAY' ? [@{$throws->[$i]}]
  0            
185             : $throws->[$i];
186 0           push @$throws, $t;
187             }
188 0           return $throws;
189             }
190              
191             =head1 AUTHORS
192              
193             Luke Closs, C<< >>
194             Multiplex support by Seamus Campbell, C<< >
195              
196             =head1 BUGS
197              
198             Please report any bugs or feature requests to
199             C, or through the web interface at
200             L.
201             I will be notified, and then you'll automatically be notified of progress on
202             your bug as I make changes.
203              
204             =head1 COPYRIGHT
205              
206             Copyright 2007 Luke Closs, all rights reserved.
207              
208             =head1 LICENSE
209              
210             This program is free software; you can redistribute it and/or modify it
211             under the same terms as Perl itself.
212              
213             =cut
214              
215             1;