File Coverage

blib/lib/Acme/Urinal.pm
Criterion Covered Total %
statement 61 68 89.7
branch 34 38 89.4
condition 36 48 75.0
subroutine 8 8 100.0
pod 5 5 100.0
total 144 167 86.2


line stmt bran cond sub pod time code
1             package Acme::Urinal;
2 1     1   985 use strict;
  1         3  
  1         45  
3 1     1   7 use warnings;
  1         2  
  1         34  
4              
5 1     1   15 use Carp;
  1         2  
  1         959  
6              
7             our $VERSION = '1.0';
8              
9             =head1 NAME
10              
11             Acme::Urinal - assign resources using the same algorithm used by men choosing which urinal to use
12              
13             =head1 SYNOPSIS
14              
15             use Acme::Urinal;
16              
17             my $urinals = Acme::Urinal->new(8);
18              
19             say $urinal->pick_one; # prints 1
20             say $urinal->pick_one; # prints 3
21             say $urinal->pick_one; # prints 5
22             say $urinal->pick_one; # prints 7
23             say $urinal->pick_one; # prints 2
24             say $urinal->pick_one; # prints 4
25             say $urinal->pick_one; # prints 6
26             say $urinal->pick_one; # prints 0
27             say $urinal->pick_one; # prints nothing, triggers an uninit warning
28              
29             $urinal->leave(3);
30             $urinal->leave(4);
31             say $urinal->pick_one; # prints 4
32              
33             $urinal->leave(2);
34             $urinal->leave(1);
35             say $urinal->pick_one; # prints 1
36              
37              
38             =head1 DESCRIPTION
39              
40             When men use a bathroom with multiple urinals. The way the urinal to use is
41             chosen is nearly deterministic. This module allocates resources in a way that
42             emulates this process.
43              
44             Basically, a L object keeps track of a list of resources. You can
45             then request these resources be allocated and used by asking for one using the
46             L method. It will return the next resource according to the
47             algorithm. Once finished suing that resource, you may return it using the
48             L method.
49              
50             Each resource is chosen according to the following rules:
51              
52             =over
53              
54             =item 1.
55              
56             If possible, the lowest index resource that has a free resource on either side
57             is chosen.
58              
59             =item 2.
60              
61             Failing that, the lowest index resource with a lesser neighbor free is chosen.
62              
63             =item 3.
64              
65             Failing that, the lowest index resource with a greater neighbor free is chosen.
66              
67             =item 4.
68              
69             Failing that, the lowest index resource that is not at either end is chosen
70             (because those end ones usually tend to be the less preferable low urinal).
71              
72             =item 5.
73              
74             Finally, the lowest index resource that is available is chosen.
75              
76             =back
77              
78             =head1 METHODS
79              
80             =head2 new
81              
82             my $urinal = Acme::Urinal->new($count);
83             my $urinal = Acme::Urinal->new(\@resources);
84              
85             Constructs a new Acme::Urinal object. If the argument is a positive integer, it
86             is the same as if an array reference were passed like this:
87              
88             [ 0 .. $count ]
89              
90             If an array reference is passed, the object will use that array as the list of
91             resources. The array will be copied, so changes to the original, won't change
92             the one used by Acme::Urinal.
93              
94             Anything else should cause an error.
95              
96             =cut
97              
98             sub new {
99 2     2 1 735 my ($class, $resources) = @_;
100              
101 2 50       25 if (ref $resources) {
    100          
102 0         0 return bless [ map { [ 0, $_ ] } @$resources ], $class;
  0         0  
103             }
104             elsif ($resources > 0) {
105 1         4 return bless [ map { [ 0, $_ ] } 0 .. ($resources - 1) ], $class;
  8         37  
106             }
107             else {
108 1         99 croak "incorrect argument";
109             }
110             }
111              
112             =head2 pick_one
113              
114             my $index = Acme::Urinal->pick_one;
115             my ($index, $resource, $comfort_level) = Acme::Urinal->pick_one;
116              
117             This will choose an available resource from those available using the algorithm
118             described in the L. If no resource is available, the return will
119             be C or an empty list.
120              
121             In scalar context, the index of the resource is returned. In list context, a
122             three-element list is returned where the first element is the index, the second
123             is the resource that was allocated, and the third is the comfort level with
124             which the resource was allocated. The higher the level, the better the
125             allocation was (the earlier the rule from the L that was used to
126             make the allocation). Currently, the comfort level will be between 1 and 5.
127              
128             =cut
129              
130             sub pick_one {
131 11     11 1 722 my ($self) = @_;
132              
133 11         14 my $choice_score = 0;
134 11         12 my $best_choice;
135 11         30 for my $i (0 .. $#$self) {
136 71         68 my ($in_use, $resource) = @{ $self->[$i] };
  71         115  
137              
138 71 100       154 next if $in_use;
139              
140 28 100 66     507 if ($choice_score < 5 and $i > 0 and $i < $#$self and not($self->[$i - 1][0]) and not($self->[$i + 1][0])) {
    100 100        
    100 100        
    100 100        
    100 66        
      100        
      33        
      66        
      100        
      66        
141 4         5 $choice_score = 5;
142 4         5 $best_choice = $i;
143 4         6 last;
144             }
145              
146             elsif ($choice_score < 4 and $i > 0 and not $self->[$i - 1][0]) {
147 2         3 $choice_score = 4;
148 2         5 $best_choice = $i;
149             }
150              
151             elsif ($choice_score < 3 and $i < $#$self and not $self->[$i + 1][0]) {
152 6         9 $choice_score = 3;
153 6         11 $best_choice = $i;
154             }
155              
156             elsif ($choice_score < 2 and $i > 0 and $i < $#$self) {
157 5         8 $choice_score = 2;
158 5         8 $best_choice = $i
159             }
160              
161             elsif ($choice_score < 1) {
162 7         10 $choice_score = 1;
163 7         12 $best_choice = $i;
164             }
165             }
166              
167 11 100       29 if (defined $best_choice) {
168 10         15 $self->[$best_choice][0] = 1;
169              
170 10 100       17 if (wantarray) {
171 8         64 return ($best_choice, $self->[$best_choice][1], $choice_score);
172             }
173             else {
174 2         28 return $best_choice;
175             }
176             }
177              
178 1         6 return;
179             }
180              
181             =head2 pick
182              
183             my $resource = $self->pick($index);
184             my ($resource, $comfort_level) = $self->pick($index);
185              
186             Allows you to violate the usual algorithm to pick a urinal explicitly. In scalar
187             context it returns the resource picked. In list context, it returns that and the
188             comfort level your pick has. If the resource picked is already in use, an
189             exception will be thrown.
190              
191             =cut
192              
193             sub pick {
194 3     3 1 6 my ($self, $i) = @_;
195              
196 3 100       8 if ($self->[$i][0]) {
197 1         1043 croak "The resource at index $i is already in use.";
198             }
199              
200 2 50       61 if (wantarray) {
201 2         8 my @r = $self->look($i);
202 2         4 $self->[$i][0] = 1;
203 2         12 return @r;
204             }
205             else {
206 0         0 my $r = $self->look($i);
207 0         0 $self->[$i][0] = 1;
208 0         0 return $r;
209             }
210             }
211              
212             =head2 look
213              
214             my $resource = $self->look($index);
215             my ($resource, $comfort_level) = $self->look($index);
216              
217             In most algorithms, this would be called "peek," but peeking in urinals is, at
218             best, awkward and, at worst, likely to get you beat up.
219              
220             This is the same as L, but does not actually allocate. Also, the
221             C<$comfort_level> returned will be C<0> if the resource is currently in use.
222              
223             =cut
224              
225             sub look {
226 5     5 1 8 my ($self, $i) = @_;
227              
228 5 100       10 if (wantarray) {
229 4         6 my $choice_score = 0;
230 4 100       12 if (not $self->[$i][0]) {
231 3 100 66     86 if ($i > 0 and $i < $#$self and not $self->[$i - 1][0] and not $self->[$i + 1][0]) {
    50 100        
    50 66        
    100 33        
      66        
      66        
232 1         3 $choice_score = 5;
233             }
234              
235             elsif ($i > 0 and not $self->[$i - 1][0]) {
236 0         0 $choice_score = 4;
237             }
238              
239             elsif ($i < $#$self and not $self->[$i + 1][0]) {
240 0         0 $choice_score = 3;
241             }
242              
243             elsif ($i > 0 and $i < $#$self) {
244 1         2 $choice_score = 2;
245             }
246              
247             else {
248 1         3 $choice_score = 1;
249             }
250             }
251              
252 4         17 return ($self->[$i][1], $choice_score);
253             }
254             else {
255 1         5 return $self->[$i][1];
256             }
257             }
258              
259             =head2 leave
260              
261             $self->leave($index);
262              
263             Frees up the resource at the given index. Throws an exception if the resource is
264             not currently in use.
265              
266             =cut
267              
268             sub leave {
269 8     8 1 448 my ($self, $i) = @_;
270              
271 8 100       20 if (not $self->[$i][0]) {
272 1         111 croak "The resource at index $i is not currently in use.";
273             }
274              
275 7         10 $self->[$i][0] = 0;
276 7         12 return;
277             }
278              
279             =head1 AUTHOR
280              
281             Andrew Sterling Hanenkamp C<< hanenkamp@cpan.or >>
282              
283             =head1 COPYRIGHT & LICENSE
284              
285             Copyright 2014 Andrew Sterling Hanenkamp.
286              
287             This is free software and may be copied and distributed under the same terms as
288             Perl itself.
289              
290             =cut
291              
292             1;