File Coverage

blib/lib/Algorithm/Bucketizer.pm
Criterion Covered Total %
statement 108 124 87.1
branch 25 38 65.7
condition 13 15 86.6
subroutine 17 21 80.9
pod 0 9 0.0
total 163 207 78.7


line stmt bran cond sub pod time code
1             ##################################################
2             package Algorithm::Bucketizer;
3             ##################################################
4             # Documentation attached as POD below
5             ##################################################
6              
7 2     2   52221 use 5.006;
  2         6  
  2         77  
8 2     2   11 use strict;
  2         3  
  2         62  
9 2     2   12 use warnings;
  2         7  
  2         3598  
10              
11             our $VERSION = '0.13';
12              
13             ##################################################
14             sub new {
15             ##################################################
16 10778     10778 0 23372 my($class, @options) = @_;
17              
18 10778         46728 my $self = { # Overwritable parameters
19             bucketsize => 100,
20             algorithm => "simple",
21             add_buckets => 1,
22              
23             @options,
24            
25             # Internal stuff
26              
27             # index (0-..) of bucket we're currently
28             # inserting items into
29             cur_bucket_idx => 0,
30              
31             buckets => [],
32             };
33              
34 10778         31115 bless $self, $class;
35             }
36              
37             ##################################################
38             sub add_item {
39             ##################################################
40 43129     43129 0 56057 my($self, $item, $size) = @_;
41              
42             # in 'simple' mode, we continue with the bucket we
43             # inserted the last item into
44 43129         60460 my $first = $self->{cur_bucket_idx};
45              
46             # retry tries all buckets
47 43129 100       89942 $first = 0 if $self->{algorithm} eq 'retry';
48              
49             # Check if it fits in any existing bucket.
50 43129         106814 for(my $idx = $first; exists $self->{buckets}->[$idx]; $idx++) {
51              
52 43138         61244 my $bucket = $self->{buckets}->[$idx];
53              
54 43138 100       93175 if($bucket->probe_item($item, $size)) {
55 17036         28958 $bucket->add_item($item, $size);
56 17036         22353 $self->{ cur_bucket_idx } = $idx;
57 17036         38836 return $bucket;
58             }
59             }
60              
61             # It didn't fit anywhere. Create a new bucket.
62 26093 50       49118 return undef unless $self->{add_buckets};
63 26093         43790 my $bucket = $self->add_bucket();
64              
65 26093 50       48566 if($bucket->probe_item($item, $size)) {
66 26093         44373 $bucket->add_item($item, $size);
67 26093         38803 $self->{ cur_bucket_idx } = $bucket->{ idx };
68 26093         54818 return $bucket;
69             }
70              
71             # It didn't even fit in a new bucket. Forget it.
72 0         0 return undef;
73             }
74              
75             ##################################################
76             sub current_bucket_idx {
77             ##################################################
78 0     0 0 0 my($self, $idx ) = @_;
79              
80 0 0       0 if( defined $idx ) {
81 0         0 $self->{ cur_bucket_idx } = $idx;
82             }
83              
84 0         0 return $self->{ cur_bucket_idx };
85             }
86              
87             ###########################################
88             sub add_bucket {
89             ###########################################
90 26096     26096 0 35280 my($self, @options) = @_;
91              
92             my $bucket = Algorithm::Bucketizer::Bucket->new(
93             maxsize => $self->{bucketsize},
94 26096         34773 idx => $#{ $self->{ buckets } } + 1,
  26096         81322  
95             @options,
96             );
97              
98             # adding a bucket won't increase the current bucket index,
99             # just append it to the end of the chain
100 26096         34149 push @{$self->{buckets}}, $bucket;
  26096         52071  
101              
102 26096         44480 return $bucket;
103             }
104              
105             ##################################################
106             sub buckets {
107             ##################################################
108 10780     10780 0 12129 my($self) = @_;
109            
110 10780         10209 return @{$self->{buckets}};
  10780         20902  
111             }
112              
113             ##################################################
114             sub prefill_bucket {
115             ##################################################
116 2     2 0 14 my($self, $bucket_idx, $item, $size) = @_;
117            
118 2         3 my $bucket = $self->{buckets}->[$bucket_idx];
119              
120             # Create the bucket if it doesn't exist yet
121 2 50       8 if(!exists $self->{buckets}->[$bucket_idx]) {
122 2         18 $bucket = Algorithm::Bucketizer::Bucket->new(
123             maxsize => $self->{bucketsize},
124             idx => $bucket_idx,
125             );
126 2         5 $self->{buckets}->[$bucket_idx] = $bucket;
127 2         4 $self->{cur_bucket_idx} = $bucket_idx;
128             }
129              
130 2         5 $bucket->add_item($item, $size);
131 2         3 return $bucket;
132             }
133              
134             ##################################################
135             sub optimize {
136             ##################################################
137 2     2 0 14 my($self, %options) = @_;
138              
139 2 50       7 $options{algorithm} = "random" unless defined $options{algorithm};
140 2 100 66     13 $options{maxtime} = 3 if exists $options{maxtime} and
141             $options{maxtime} < 3;
142 2         24 my($next);
143              
144 2         7 my @items = $self->items();
145              
146             # Create next() closure for appropriate variation algorithm
147 2 50       18 if($options{algorithm} eq "brute_force") {
    50          
148 0         0 require Algorithm::Permute;
149 0         0 my $p = Algorithm::Permute->new([@items]);
150 0     0   0 $next = sub { return $p->next };
  0         0  
151             } elsif($options{algorithm} eq "random") {
152             # fisher-yates shuffle
153 2     10771   10 $next = sub { $self->shuffle(@items) };
  10771         21976  
154 2 50 66     11 die "Need maxrounds|maxtime for 'random' optimizer"
155             if !exists $options{maxrounds} and !exists $options{maxtime};
156             }
157              
158 2         4 my $round = 0;
159              
160 2         2 my $minbuckets;
161             my @minitems;
162 2         8 my $start_time = time();
163              
164             # Run through different setups and determine the one
165             # requiring a minimum of buckets.
166 2         5 while (my @res = $next->()) {
167              
168 10771         27655 my $b = Algorithm::Bucketizer->new(bucketsize => $self->{bucketsize},
169             algorithm => 'retry');
170 10771         17481 for (@res) {
171 43084         55986 my($name, $weight) = @$_;
172 43084         76992 $b->add_item($name, $weight);
173             }
174              
175 10771         23062 my $nof_buckets = scalar $b->buckets;
176              
177 10771 100 100     46379 if(! defined $minbuckets or $nof_buckets < $minbuckets) {
178 3         4 $minbuckets = $nof_buckets;
179 3         6 @minitems = @res;
180             }
181              
182 10771         10829 ++$round;
183 10771 100 100     24612 last if exists $options{maxrounds} and $round >= $options{maxrounds};
184 10770 100 100     101064 last if exists $options{maxtime} and
185             time() > $start_time + $options{maxtime};
186             }
187              
188             # We should have a ideal distribution now, nuke all buckets and refill
189 2         7 $self->{buckets} = [];
190 2         13 $self->{cur_bucket_idx} = 0;
191 2         5 $self->{algorithm} = "retry"; # We're optimizing
192              
193 2         3 for (@minitems) {
194 8         11 my($name, $weight) = @$_;
195 8         15 $self->add_item($name, $weight);
196             }
197             }
198              
199             ##################################################
200             sub items {
201             ##################################################
202 2     2 0 3 my($self) = @_;
203              
204 2         3 my @items = ();
205              
206 2         2 for my $bucket (@{$self->{buckets}}) {
  2         6  
207 6         17 for(my $idx = 0; exists $bucket->{items}->[$idx]; $idx++) {
208 8         34 push @items, [$bucket->{items}->[$idx], $bucket->{sizes}->[$idx]];
209             }
210             }
211              
212 2         8 return @items;
213             }
214              
215             ###########################################
216             sub shuffle {
217             ###########################################
218 10771     10771 0 18561 my($self, @array) = @_;
219              
220 10771         25366 for(my $i=@array; --$i; ) {
221 32313         50007 my $j = int rand ($i+1);
222 32313 100       68380 next if $i == $j;
223 20670         55567 @array[$i,$j] = @array[$j,$i];
224             }
225              
226 10771         38158 return @array;
227             }
228              
229             ##################################################
230             package Algorithm::Bucketizer::Bucket;
231             ##################################################
232              
233             ##################################################
234             sub new {
235             ##################################################
236 26098     26098   48229 my($class, @options) = @_;
237              
238 26098         116325 my $self = { size => 0,
239             items => [],
240             sizes => [],
241             maxsize => undef,
242             maxitems => undef,
243             idx => 0,
244             @options,
245             };
246              
247 26098         82570 bless $self, $class;
248             }
249              
250             ##################################################
251             sub serial {
252             ##################################################
253 21     21   64 my($self) = @_;
254              
255 21         71 return ($self->{idx} + 1);
256             }
257              
258             ##################################################
259             sub level {
260             ##################################################
261 0     0   0 my($self) = @_;
262              
263 0         0 return ($self->{size});
264             }
265              
266             ##################################################
267             sub idx {
268             ##################################################
269 0     0   0 my($self) = @_;
270              
271 0         0 return ($self->{idx});
272             }
273              
274             ##################################################
275             sub add_item {
276             ##################################################
277 43131     43131   59036 my($self, $item, $size) = @_;
278              
279             # Does item fit in container?
280 43131 50       68922 if($self->probe_item($item, $size)) {
281             # Add it
282 43131         40398 push @{$self->{items}}, $item;
  43131         83786  
283 43131         52370 push @{$self->{sizes}}, $size;
  43131         69122  
284 43131         66003 $self->{size} += $size;
285 43131         58015 return 1;
286             }
287              
288 0         0 return undef;
289             }
290              
291             ##################################################
292             sub probe_item {
293             ##################################################
294 112362     112362   143112 my($self, $item, $size) = @_;
295              
296             # Does item fit in container?
297 112362 50       247326 if($self->{maxitems}) {
298 0 0       0 if(scalar $self->{items} >= $self->{maxitems}) {
299 0         0 return 0;
300             }
301             }
302              
303 112362 100       214417 if($self->{size} + $size <= $self->{maxsize}) {
304 86260         202103 return 1;
305             } else {
306 26102         110845 return 0;
307             }
308             }
309              
310             ##################################################
311             sub items {
312             ##################################################
313 18     18   68 my($self) = @_;
314              
315 18         17 return @{$self->{items}};
  18         65  
316             }
317              
318             1;
319              
320             __END__