File Coverage

blib/lib/Set/Cluster.pm
Criterion Covered Total %
statement 135 136 99.2
branch 19 24 79.1
condition 9 15 60.0
subroutine 20 20 100.0
pod 6 10 60.0
total 189 205 92.2


line stmt bran cond sub pod time code
1             package Set::Cluster::Result;
2              
3 1     1   21311 use 5.004;
  1         4  
  1         34  
4              
5 1     1   4 use strict;
  1         2  
  1         27  
6 1     1   5 use warnings;
  1         5  
  1         39  
7 1     1   106558 use Storable qw(dclone);
  1         3221  
  1         82  
8 1     1   7 use Carp;
  1         2  
  1         169  
9              
10             sub copy {
11 34     34   42 my $self = shift;
12 34 50       67 croak "can't copy class $self" unless (ref $self);
13 34         1481 my $copy = Storable::dclone($self);
14 34         69 return $copy;
15             }
16              
17             sub new {
18 8     8   16 my $class = shift;
19 8         14 my $self = {};
20 8         22 return bless $self, $class;
21             }
22              
23              
24             package Set::Cluster;
25              
26 1     1   6 use strict;
  1         2  
  1         33  
27 1     1   4 use warnings;
  1         1  
  1         29  
28              
29 1     1   4 use Carp;
  1         2  
  1         1310  
30              
31             our $VERSION = '0.02';
32              
33             sub new {
34 7     7 0 2860 my $class = shift;
35 7         32 my $self = { results => {}, node_name_lookup => {}, item_lookup => {}, item_type => "" };
36 7         25 return bless $self, $class;
37             }
38              
39             sub setup {
40 4     4 1 800 my $self = shift;
41 4         12 my %args = @_;
42 4   33     18 $self->{nodes} = $args{nodes} || croak "No nodes specified";
43 4 50       11 $args{items} || croak "No items specified";
44 4 100       16 if (ref $args{items} eq "ARRAY") {
    50          
45 2         5 $self->{item_type} = "ARRAY";
46 2         2 foreach my $object (@{$args{items}}) {
  2         5  
47 6         45 $self->{item_lookup}->{"$object"} = $object;
48 6 100       181 $object->can("weight") or croak "Object $object does not have a weight method";
49 5         97 $self->{items}->{"$object"} = $object->weight;
50             }
51             } elsif (ref $args{items} eq "HASH") {
52 2         4 $self->{item_type} = "HASH";
53 2         6 $self->{items} = $args{items};
54             } else {
55 0         0 croak "items is not correct type: ".ref $args{items};
56             }
57             }
58              
59 27     27 1 95 sub results { shift->{results}; }
60              
61             sub calculate {
62 7     7 1 557 my $self = shift;
63 7   100     21 my $levels = shift || 0;
64              
65 7 100       9 $levels = scalar @{$self->{nodes}} - 1 if ($levels >= scalar @{$self->{nodes}});
  2         3  
  7         22  
66              
67             # Setup nodes and distribute first time
68 7         18 my $result = Set::Cluster::Result->new;
69 7         9 foreach my $n (@{$self->{nodes}}) {
  7         16  
70 19         24 my $stringified = "$n";
71 19         77 $self->{node_name_lookup}->{$stringified} = $n;
72 19         44 $result->{$stringified} = [];
73             }
74 7         11 $self->distribute($result, [keys %{$self->{items}}]);
  7         40  
75              
76 7         26 $self->process("", $result, $levels);
77             }
78              
79             sub process {
80 41     41 0 61 my ($self, $scenario, $result, $levels) = @_;
81 41         73 $self->{results}->{$scenario} = $result;
82 41 100       193 if ($levels > 0) {
83 15         28 foreach my $failed_node (keys %$result) {
84 34         66 my $new_state = $result->copy;
85 34         79 my $scenario = join(",", split(",",$scenario), $failed_node);
86 34         40 my @items = @{$new_state->{$failed_node}};
  34         86  
87 34         68 delete $new_state->{$failed_node};
88 34         102 $self->distribute($new_state, [@items]);
89 34         99 $self->process($scenario, $new_state, $levels-1);
90             }
91             }
92             }
93              
94             # Logic - sort items by weight. From highest, add item to
95             # node with lowest total weight
96             sub distribute {
97 41     41 0 52 my ($self, $state, $items) = @_;
98              
99 41         93 my @nodes = keys %$state;
100 41 50       119 my @items = sort { $self->{items}->{$a} <=> $self->{items}->{$b} || $a cmp $b } @$items;
  190         521  
101              
102 41         100 while (my $i = pop @items) {
103 166         312 my $node = $self->lowest($state);
104 166         177 push @{$state->{$node}}, $i;
  166         546  
105             }
106 41         73 return $state;
107             }
108              
109             # TODO: Should keep total with the node, to save recalculating each time
110             sub lowest {
111 168     168 0 603 my $self = shift;
112 168         210 my $state = shift;
113 168         167 my %totals;
114 168         286 foreach my $n (keys %$state) {
115 295         392 $totals{$n} = 0;
116 295         298 map { $totals{$n} += $self->{items}->{$_} } @{$state->{$n}};
  811         1656  
  295         472  
117             }
118 168 50       394 my @a = sort { $totals{$a} <=> $totals{$b} || $a cmp $b } keys %totals;
  154         414  
119 168         405 return shift @a;
120             }
121              
122             sub items {
123 4     4 1 958 my $self = shift;
124 4         12 my %args = @_;
125 4   100     20 $args{fail} = $args{fail} || "";
126 4         9 my $scenario = "$args{fail}";
127 4   33     16 my $node = "$args{node}" || croak "Must specify node";
128 4 100       12 if ($self->{item_type} eq "HASH") {
129 2         2 return @{$self->results->{$scenario}->{$node}};
  2         4  
130             } else {
131 2         3 return map {$_ = $self->{item_lookup}->{$_}} @{$self->results->{$scenario}->{$node}};
  3         16  
  2         6  
132             }
133             }
134              
135             sub takeover {
136 3     3 1 1012 my $self = shift;
137 3         11 my %args = @_;
138 3   33     14 my $node = "$args{node}" || croak "Must specify node";
139 3         13 my @fail = split (",", "$args{fail}");
140 3         6 pop @fail;
141 3         6 my $fail = join(",", @fail);
142 3         7 my $prior = $self->results->{$fail}->{$node};
143 3         8 my $now = $self->results->{$args{fail}}->{$node};
144 3         5 my %seen;
145 3         7 map {$seen{$_}++} @$now;
  15         31  
146 3         4 map {$seen{$_}--} @$prior;
  8         16  
147 3         4 my @result = ();
148 3 100       9 map { push @result, $_ if $seen{$_}==1 } keys %seen;
  15         51  
149 3 100       11 if ($self->{item_type} eq "HASH") {
150 2         14 return @result;
151             } else {
152 1         10 return map { $_ = $self->{item_lookup}->{$_}} @result;
  1         11  
153             }
154             }
155              
156             sub hash_by_item {
157 4     4 1 1335 my $self = shift;
158 4         9 my %args = @_;
159 4   100     17 my $fail = $args{fail} || "";
160 4         8 my $hash = {};
161 4         6 foreach my $node (keys %{$self->results->{$fail}}) {
  4         8  
162 8         16 my $items = $self->results->{$fail}->{$node};
163 8         8 foreach my $i (@{$items}) {
  8         14  
164 27         99 $hash->{$i} = $self->{node_name_lookup}->{$node};
165             }
166             }
167 4         14 return $hash;
168             }
169              
170             1;
171             __END__