File Coverage

blib/lib/Math/VarRate.pm
Criterion Covered Total %
statement 71 71 100.0
branch 24 24 100.0
condition 4 4 100.0
subroutine 10 10 100.0
pod 4 4 100.0
total 113 113 100.0


line stmt bran cond sub pod time code
1 3     3   202092 use strict;
  3         35  
  3         92  
2 3     3   15 use warnings;
  3         6  
  3         122  
3             package Math::VarRate 0.100001;
4             # ABSTRACT: deal with linear, variable rates of increase
5              
6 3     3   16 use Carp ();
  3         6  
  3         37  
7 3     3   13 use Scalar::Util ();
  3         6  
  3         2431  
8              
9             #pod =head1 DESCRIPTION
10             #pod
11             #pod Math::VarRate is a very, very poor man's calculus. A Math::VarRate object
12             #pod represents an accumulator that increases at a varying rate over time. The rate
13             #pod may change, it is always a linear, positive rate of change.
14             #pod
15             #pod You can imagine the rate as representing "units gained per time." You can then
16             #pod interrogate the Math::VarRate object for the total units accumulated at any
17             #pod given offset in time, or for the time at which a given number of units will
18             #pod have first been accumulated.
19             #pod
20             #pod =method new
21             #pod
22             #pod my $varrate = Math::VarRate->new(\%arg);
23             #pod
24             #pod Valid arguments to C are:
25             #pod
26             #pod rate_changes - a hashref in which keys are offsets and values are rates
27             #pod starting_value - the value at offset 0 (defaults to 0)
28             #pod
29             #pod =cut
30              
31             sub new {
32 15     15 1 3508 my ($class, $arg) = @_;
33              
34 15   100     56 my $changes = $arg->{rate_changes} || {};
35             my $self = bless {
36             rate_changes => $changes,
37 15   100     72 starting_value => $arg->{starting_value} || 0,
38             } => $class;
39              
40 15         39 $self->_sanity_check_rate_changes;
41 11         36 $self->_precompute_offsets;
42              
43 11         53 return $self;
44             }
45              
46             sub _sanity_check_rate_changes {
47 15     15   32 my ($self) = @_;
48 15         30 my $rc = $self->{rate_changes};
49              
50 15         87 my %check = (
51             rates => [ values %$rc ],
52             offsets => [ keys %$rc ],
53             );
54              
55 15         63 while (my ($k, $v) = each %check) {
56             Carp::confess("non-numeric $k are not allowed")
57 30 100       60 if grep { ! Scalar::Util::looks_like_number("$_") } @$v;
  46         364  
58 28 100       55 Carp::confess("negative $k are not allowed") if grep { $_ < 0 } @$v;
  44         464  
59             }
60             }
61              
62             #pod =method starting_value
63             #pod
64             #pod This method returns the value of the accumulator at offset 0.
65             #pod
66             #pod =cut
67              
68 40 100   40 1 4603 sub starting_value { $_[0]->{starting_value} || 0 }
69              
70             #pod =method offset_for
71             #pod
72             #pod my $offset = $varrate->offset_for($value);
73             #pod
74             #pod This method returns the offset (positive, from 0) at which the given value is
75             #pod reached. If the given value will never be reached, undef will be returned.
76             #pod
77             #pod =cut
78              
79             sub offset_for {
80 22     22 1 645 my ($self, $value) = @_;
81              
82 22 100       177 Carp::croak("illegal value: non-numeric")
83             unless Scalar::Util::looks_like_number("$value");
84              
85 21 100       174 Carp::croak("illegal value: negative") unless $value >= 0;
86              
87 20         29 $value += 0;
88              
89 20 100       40 return 0 if $value == $self->starting_value;
90              
91 16         32 my $ko = $self->{known_offsets};
92 16         43 my ($offset) = sort { $b <=> $a } grep { $ko->{ $_ } < $value } keys %$ko;
  19         44  
  35         96  
93              
94 16 100       49 return unless defined $offset;
95              
96 14         25 my $rate = $self->{rate_changes}{ $offset };
97              
98             # If we stopped for good, we can never reach the target. -- rjbs, 2009-05-11
99 14 100       36 return undef if $rate == 0;
100              
101 12         22 my $to_go = $value - $ko->{ $offset };
102 12         23 my $dur = $to_go / $rate;
103              
104 12         67 return $offset + $dur;
105             }
106              
107             #pod =method value_at
108             #pod
109             #pod my $value = $varrate->value_at($offset);
110             #pod
111             #pod This returns the value in the accumulator at the given offset.
112             #pod
113             #pod =cut
114              
115             sub value_at {
116 32     32 1 9013 my ($self, $offset) = @_;
117              
118 32 100       239 Carp::croak("illegal offset: non-numeric")
119             unless Scalar::Util::looks_like_number("$offset");
120              
121 31 100       146 Carp::croak("illegal offset: negative") unless $offset >= 0;
122              
123 30         50 $offset += 0;
124              
125 30         48 my $known_offsets = $self->{known_offsets};
126              
127 30 100       91 return $known_offsets->{ $offset } if exists $known_offsets->{ $offset };
128              
129 22         67 my ($max) = sort { $b <=> $a } grep { $_ < $offset } keys %$known_offsets;
  13         37  
  44         130  
130              
131 22 100       56 return $self->starting_value unless defined $max;
132              
133 16         27 my $start = $known_offsets->{ $max };
134 16         28 my $rate = $self->{rate_changes}{ $max };
135 16         25 my $dur = $offset - $max;
136              
137 16         79 return $start + $rate * $dur;
138             }
139              
140             sub _precompute_offsets {
141 11     11   19 my ($self) = @_;
142              
143 11         27 my $value = $self->starting_value;
144 11         23 my $v_at_o = {};
145 11         15 my %changes = %{ $self->{rate_changes} };
  11         34  
146 11         21 my $prev = 0;
147 11         14 my $rate = 0;
148              
149 11         42 for my $offset (sort { $a <=> $b } keys %changes) {
  13         30  
150 19         33 my $duration = $offset - $prev;
151              
152 19         32 $value += $duration * $rate;
153 19         72 $v_at_o->{ $offset } = $value;
154              
155 19         23 $rate = $changes{ $offset };
156 19         34 $prev = $offset;
157             }
158              
159 11         29 $self->{known_offsets} = $v_at_o;
160             }
161              
162             1;
163              
164             __END__