File Coverage

blib/lib/Random/Skew.pm
Criterion Covered Total %
statement 85 87 97.7
branch 41 46 89.1
condition 4 7 57.1
subroutine 8 8 100.0
pod 5 5 100.0
total 143 153 93.4


line stmt bran cond sub pod time code
1             package Random::Skew;
2              
3             #use 5.028001;
4 8     8   293394 use strict;
  8         91  
  8         286  
5 8     8   43 use warnings;
  8         15  
  8         3084  
6              
7             our $VERSION = '0.09';
8              
9             our $GRAIN = 72; # default
10             sub GRAIN {
11 9 50   9 1 3040 if ( @_ ) {
12 9         24 my $grain = shift( @_ );
13 9 100       57 if ( $grain =~ /\D/ ) {
    100          
14 2         17 die "\$Random::Skew::GRAIN must be a positive integer >= 2";
15             } elsif ( $grain >= 2 ) {
16 6         19 $GRAIN = int( $grain );
17             } else {
18 1         8 die "\$Random::Skew::GRAIN must be >= 2";
19             }
20             }
21 6         13 return $GRAIN;
22             }
23              
24             our $ROUNDING = 0.5; # default
25             sub ROUNDING {
26 3 50   3 1 889 if ( @_ ) {
27 3         8 my $rounding = shift( @_ );
28 3 100 66     36 if ( $rounding =~ /[^0-9.]/ ) {
    100          
29 1         9 die "\$Random::Skew::ROUNDING must be decimal-point and digits only (floating point)";
30             } elsif ( $rounding < 0.0 or $rounding > 1.0 ) {
31 1         8 die "\$Random::Skew::ROUNDING must be between 0.0 and 1.0";
32             } else {
33 1         3 $ROUNDING = $rounding;
34             }
35             }
36 1         3 return $ROUNDING;
37             }
38              
39              
40              
41             sub new {
42 16     16 1 1222 my $class = shift;
43 16 100       84 my %params = @_ or die "Random::Skew->new: No parameters?";
44              
45 15         24 my $tot = 0;
46 15         23 my @bad;
47 15 50       36 push @bad, "Random::Skew::GRAIN ($GRAIN) must be larger than 2" unless $GRAIN > 2;
48 15         60 for ( keys %params ) {
49 8     8   60 no warnings qw/numeric/;
  8         22  
  8         5633  
50 101         135 my $v = $params{ $_ } + 0.0;
51 101 100       156 if ( $v >= 1 ) {
52 99         138 $tot += $v;
53             } else {
54 2         29 push @bad,"Value '$params{$_}' for key '$_' must be a number >= 1";
55             }
56             }
57 15 100       57 die @bad if @bad;
58              
59 13         118 my $self = bless {
60             _set => [],
61             _tot => $tot,
62             _grain => $GRAIN,
63             _params => { %params },
64             _unique => scalar( keys %params ),
65             _fraction => 0, # for when we have fine-grained details
66             }, $class;
67              
68 13         27 my $small_skew;
69             # Do we need to $scale our population down to fit $Random::Skew::GRAIN buckets?
70 13 100       35 if ( $tot > $GRAIN ) {
71              
72             # Biggest to smallest
73 9 50       38 my @ordered = sort { $params{ $b } <=> $params{ $a } or $a cmp $b } keys %params;
  255         450  
74              
75             # so that we fit in 1..GRAIN buckets
76 9         25 my $scale = $GRAIN / $tot;
77 9         17 my %big;
78             my %small;
79 9         21 my $running_tot = $tot * $scale;
80             #$DB::single = 1;
81 9         17 foreach my $item ( @ordered ) {
82              
83 93         195 my $vec = $scale * $params{ $item };
84              
85 93 100       165 if ( $running_tot < 1.0 ) {
    100          
86              
87             # Remaining items are, all together, smaller than one bucket...
88             # Small items get their own Random::Skew
89 14         27 $small{ $item } = $params{ $item }; # original weighting
90 14 100       37 if ( not $self->{ _fraction } ) {
91 5         8 $self->{ _fraction } = $running_tot;
92             }
93              
94             } elsif ( $vec < 1.0 ) {
95              
96             # We are looking at contents smaller than one bucket at this scale...
97             # (e.g. 30 items with a weight of 10 each, with GRAIN=20 f'rinstance)
98 16 50       24 if ( %big ) {
99 16         25 $small{ $item } = $params{ $item }; # original weighting
100 16 100       43 if ( not $self->{ _fraction } ) {
101 2         4 $self->{ _fraction } = $running_tot;
102             }
103             } else {
104 0         0 my $try = int( $GRAIN / $vec + 0.999999 );
105             # GRAIN=12
106             # POP=15 10s [10 10 10 10 10 10 10 10 10 10 10 10 10 10 10]
107             # grain needs to be 15, or 150(tot) / 10(biggest score)
108             # $tot / $params{ $item }
109 0         0 die "\$Random::Skew::GRAIN ($GRAIN) too small for this population (try >$try)";
110             }
111              
112             } else {
113              
114             # Big items are $scale'd to 0..$GRAIN (+.5 for rounding)
115 63         97 $big{ $item } = $vec + $ROUNDING; # scaled weighting
116              
117             }
118              
119 93         136 $running_tot -= $vec;
120              
121             }
122              
123 9         76 %params = %big;
124 9 100       93 $small_skew = Random::Skew->new( %small ) if %small;
125              
126             }
127              
128             # Load up our set with items, one of which gets returned each ->item() call at random
129 13         43 foreach my $item ( keys %params ) {
130 68         123 my $bucket = int( $params{ $item } );
131 68         79 push @{ $self->{_set} }, ( $item ) x $bucket;
  68         525  
132             }
133              
134 13 100       51 if ( $small_skew ) {
135              
136             # In case the big items are numerous and similar, they may
137             # not be very large related to the $tot, so pad extra items
138             # to get us up to $GRAIN buckets
139 7         16 my $fraction = int( $self->{_fraction} ) - 1; # leave room for $small_skew
140 7 100       17 $fraction = 0 if $fraction < 0;
141 7         17 my @blanks = ('') x $fraction;
142              
143             # Item [0] will be the zoom-in small set, for recursion
144 7         13 unshift @{ $self->{_set} }, $small_skew, @blanks;
  7         32  
145              
146             }
147              
148 13         21 $self->{_pop} = scalar @{ $self->{_set} };
  13         27  
149              
150 13         52 return $self;
151              
152             }
153              
154              
155              
156             sub item {
157 4398848     4398848 1 10557565 my $self = shift;
158              
159 4398848   50     7293637 my $fraction = $self->{_fraction} // 0;
160 4398848         5165410 my $set = $self->{_set};
161 4398848         5290571 my $pop = scalar @$set;
162              
163             RANDOMIZE: {
164              
165             # Pick a floating point random number from 0.0 up to $pop
166 4398848         4908074 my $ix = rand( $pop );
  4480583         5685946  
167              
168             # Shortcut for when there's no smaller scale to recurse thru:
169 4480583 100       7603404 return $set->[ $ix ] unless $fraction;
170              
171 3434637 100       5723687 if ( $ix <= $fraction ) {
    100          
172             # Calls the smaller-set Random::Skew object
173 396345         619911 return $set->[0]->item(); # RECURSION: zoom in, get an item from the smaller subset
174              
175             } elsif ( $ix >= 1.0 ) {
176 2956557         5917210 return $set->[ $ix ];
177              
178             } else {
179             # rand(0.0 .. _pop) is in the gap, > _fraction and < 1.0
180 81735         109141 redo RANDOMIZE;
181             }
182              
183             }
184              
185             }
186              
187              
188              
189             sub items {
190 2     2 1 796 my $self = shift;
191 2   50     7 my $ct = shift || 1;
192              
193 2         3 my @v;
194 2         7 push @v, $self->item # standard Random::Skew->item call
195             while $ct -- > 0;
196              
197 2         488 return @v;
198             }
199              
200              
201              
202             return $VERSION;
203              
204             __END__