File Coverage

blib/lib/Data/Bucketeer.pm
Criterion Covered Total %
statement 36 36 100.0
branch 8 10 80.0
condition 1 3 33.3
subroutine 9 9 100.0
pod 2 3 66.6
total 56 61 91.8


line stmt bran cond sub pod time code
1 1     1   60570 use strict;
  1         11  
  1         23  
2 1     1   5 use warnings;
  1         1  
  1         34  
3             package Data::Bucketeer 0.006;
4             # ABSTRACT: sort data into buckets based on thresholds
5              
6 1     1   4 use Carp qw(croak);
  1         1  
  1         32  
7 1     1   5 use Scalar::Util ();
  1         1  
  1         14  
8 1     1   4 use List::Util qw(first);
  1         1  
  1         629  
9              
10             #pod =head1 OVERVIEW
11             #pod
12             #pod Data::Bucketeer lets you easily map values in ranges to results. It's for
13             #pod doing table lookups where you're looking for the key in a range, not a list of
14             #pod fixed values.
15             #pod
16             #pod For example, you sell widgets with prices based on quantity:
17             #pod
18             #pod YOU ORDER | YOU PAY, EACH
19             #pod -------------+---------------
20             #pod 1 - 100 | 10 USD
21             #pod 101 - 200 | 5 USD
22             #pod 201 - 500 | 4 USD
23             #pod 501 - 1000 | 3 USD
24             #pod 1001+ | 2 USD
25             #pod
26             #pod This can be easily turned into a bucketeer:
27             #pod
28             #pod use Data::Bucketeer;
29             #pod
30             #pod my $buck = Data::Bucketeer->new({
31             #pod 0 => 10,
32             #pod 100 => 5,
33             #pod 200 => 4,
34             #pod 500 => 3,
35             #pod 1000 => 2,
36             #pod });
37             #pod
38             #pod my $cost = $buck->result_for( 701 ); # cost is 3
39             #pod
40             #pod By default, the values I. For example, above, you end up
41             #pod with a result of C<3> by having an input C 500, and
42             #pod C 500. If you want to use a different operator, you can
43             #pod specify it like this:
44             #pod
45             #pod my $buck = Data::Bucketeer->new( '>=', {
46             #pod 1 => 10,
47             #pod 101 => 5,
48             #pod 201 => 4,
49             #pod 501 => 3,
50             #pod 1001 => 2,
51             #pod });
52             #pod
53             #pod my $cost = $buck->result_for( 701 ); # cost is 3
54             #pod
55             #pod This distinction can be useful when dealing with non-integers. The understood
56             #pod operators are:
57             #pod
58             #pod =for :list
59             #pod * >
60             #pod * >=
61             #pod * <=
62             #pod * <
63             #pod
64             #pod If the result value is a code reference, it will be invoked with C<$_> set to
65             #pod the input. This can be used for dynamically generating results, or to throw
66             #pod exceptions. Here is a contrived example of exception-throwing:
67             #pod
68             #pod my $greeting = Data::Bucketeer->new( '>=', {
69             #pod '-Inf' => sub { die "secs-into-day must be between 0 and 86399; got $_" },
70             #pod
71             #pod 0 => "Good evening.",
72             #pod 28_800 => "Good morning.",
73             #pod 43_200 => "Good afternoon.",
74             #pod 61_200 => "Good evening.",
75             #pod
76             #pod 86_400 => sub { die "secs-into-day must be between 0 and 86399; got $_" },
77             #pod });
78             #pod
79             #pod =cut
80              
81             sub new {
82 3     3 0 4486 my ($class, @rest) = @_;
83 3 100       12 unshift @rest, '>' if ref $rest[0];
84              
85 3         7 my ($type, $buckets) = @rest;
86              
87 3 50       13 my @non_num = grep { ! Scalar::Util::looks_like_number($_) or /NaN/i }
  15         57  
88             keys %$buckets;
89              
90 3 50       10 croak "non-numeric bucket boundaries: @non_num" if @non_num;
91              
92 3         10 my $guts = bless {
93             buckets => $buckets,
94             picker => $class->__picker_for($type),
95             };
96              
97 3         10 return bless $guts => $class;
98             }
99              
100             my %operator = (
101             '>' => sub {
102             my ($self, $this) = @_;
103             first { $this > $_ } sort { $b <=> $a } keys %{ $self->{buckets} };
104             },
105             '>=' => sub {
106             my ($self, $this) = @_;
107             first { $this >= $_ } sort { $b <=> $a } keys %{ $self->{buckets} };
108             },
109              
110             '<=' => sub {
111             my ($self, $this) = @_;
112             first { $this <= $_ } sort { $a <=> $b } keys %{ $self->{buckets} };
113             },
114             '<' => sub {
115             my ($self, $this) = @_;
116             first { $this < $_ } sort { $a <=> $b } keys %{ $self->{buckets} };
117             },
118             );
119              
120             sub __picker_for {
121 3     3   8 my ($self, $type) = @_;
122 3   33     16 return($operator{ $type } || croak("unknown bucket operator: $type"));
123             }
124              
125             #pod =method result_for
126             #pod
127             #pod my $result = $buck->result_for( $input );
128             #pod
129             #pod This returns the result for the given input, as described L.
130             #pod
131             #pod =cut
132              
133             sub result_for {
134 42     42 1 24815 my ($self, $input) = @_;
135              
136 42         73 my ($bound, $result) = $self->bound_and_result_for($input);
137              
138 38         203 return $result;
139             }
140              
141             #pod =method bound_and_result_for
142             #pod
143             #pod my ($bound, $result) = $buck->bound_and_result_for( $input );
144             #pod
145             #pod This returns two values: the boundary key whose result was used, and the
146             #pod result itself.
147             #pod
148             #pod Using the item quantity price above, for example:
149             #pod
150             #pod my $buck = Data::Bucketeer->new({
151             #pod 0 => 10,
152             #pod 100 => 5,
153             #pod 200 => 4,
154             #pod 500 => 3,
155             #pod 1000 => 2,
156             #pod });
157             #pod
158             #pod my ($bound, $cost) = $buck->bound_and_result_for( 701 );
159             #pod
160             #pod # $bound is 500
161             #pod # $cost is 3
162             #pod
163             #pod =cut
164              
165             sub bound_and_result_for {
166 80     80 1 121 my ($self, $input) = @_;
167              
168 80         148 my $bound = $self->{picker}->($self, $input);
169 80 100       322 return (undef, undef) unless defined $bound;
170              
171 52         87 my $bucket = $self->{buckets}->{$bound};
172             my $result = ref $bucket
173 52 100       102 ? do { local $_ = $input; $bucket->($input) }
  20         33  
  20         35  
174             : $bucket;
175              
176 48         224 return ($bound, $result);
177             }
178              
179             1;
180              
181             __END__