File Coverage

blib/lib/Tie/Hash/Interpolate.pm
Criterion Covered Total %
statement 98 98 100.0
branch 38 42 90.4
condition 6 9 66.6
subroutine 23 23 100.0
pod 1 1 100.0
total 166 173 95.9


line stmt bran cond sub pod time code
1             package Tie::Hash::Interpolate;
2              
3 1     1   27576 use 5.006;
  1         4  
  1         41  
4 1     1   6 use strict;
  1         1  
  1         32  
5 1     1   5 use warnings;
  1         5  
  1         47  
6              
7 1     1   6 use Carp;
  1         1  
  1         293  
8 1     1   9 use Scalar::Util qw/ looks_like_number blessed /;
  1         2  
  1         297  
9              
10 1     1   6 use constant EX_LINEAR => 'linear';
  1         1  
  1         90  
11 1     1   4 use constant EX_CONSTANT => 'constant';
  1         2  
  1         42  
12 1     1   4 use constant EX_FATAL => 'fatal';
  1         2  
  1         36  
13 1     1   4 use constant EX_UNDEF => 'undef';
  1         2  
  1         38  
14              
15 1     1   4 use constant ONE_KEY_FATAL => 'fatal';
  1         1  
  1         41  
16 1     1   4 use constant ONE_KEY_CONSTANT => 'constant';
  1         9  
  1         38  
17 1     1   4 use constant ONE_KEY_UNDEF => 'undef';
  1         7  
  1         1077  
18              
19             our $VERSION = '0.07';
20              
21             sub new
22             {
23 1     1 1 315 my ($class, %opts) = @_;
24 1         2 my $tied = {};
25 1         2 tie %{$tied}, $class, %opts;
  1         6  
26 1         3 return $tied;
27             }
28              
29             sub TIEHASH
30             {
31 8     8   1926 my ($class, %opts) = (shift, extrapolate => EX_LINEAR, one_key => ONE_KEY_FATAL, @_);
32 8 100       242 croak "invalid value for 'extrapolate' option ($opts{'extrapolate'})"
33             unless grep $_ eq $opts{'extrapolate'}, EX_LINEAR, EX_UNDEF, EX_FATAL, EX_CONSTANT;
34 7 50       30 croak "invalid value for 'one_key' option ($opts{'one_key'})"
35             unless grep $_ eq $opts{'one_key'}, ONE_KEY_UNDEF, ONE_KEY_FATAL, ONE_KEY_CONSTANT;
36 7         41 my $self = { _DATA => {}, _KEYS => [], _SORT => 1, _OPTS => \%opts };
37 7         61 bless $self, $class;
38             }
39              
40             sub FIRSTKEY
41             {
42 2     2   349 my $a = scalar keys %{$_[0]->{'_DATA'}};
  2         7  
43 2         5 return each %{$_[0]->{'_DATA'}};
  2         15  
44             }
45              
46             sub NEXTKEY
47             {
48 3     3   7 return each %{$_[0]->{'_DATA'}};
  3         16  
49             }
50              
51             sub EXISTS
52             {
53 2     2   607 return exists $_[0]->{'_DATA'}->{$_[1]};
54             }
55              
56             sub DELETE
57             {
58             ## force a re-sort on next fetch
59 1     1   671 $_[0]->{'_SORT'} = 1;
60 1         7 delete $_[0]->{'_DATA'}->{$_[1]};
61             }
62              
63             sub CLEAR
64             {
65             ## force a re-sort on next fetch
66 5     5   1408 $_[0]->{'_SORT'} = 1;
67 5         7 %{$_[0]->{'_DATA'}} = ();
  5         30  
68             }
69              
70             sub STORE
71             {
72 19     19   3717 my ($self, $key, $val) = @_;
73              
74             ## the key must be a number
75 19 100 66     321 croak "key ($key) must be a number" if ref $key ||
76             ! looks_like_number($key);
77              
78             ## the value must be a number
79 17 100 66     289 croak "val ($val) must be a number" if ref $val ||
80             ! looks_like_number($val);
81              
82             ## force key to number
83 15         22 $key += 0;
84              
85             ## force a re-sort on next fetch
86 15         23 $self->{'_SORT'} = 1;
87              
88 15         68 $self->{'_DATA'}{$key} = $val;
89              
90             }
91              
92             sub FETCH
93             {
94 45     45   18638 my ($self, $key) = @_;
95              
96 45 100 66     489 croak "key ($key) must be a number" if ref $key ||
97             ! looks_like_number($key);
98              
99             ## force key to number
100 43         73 $key += 0;
101              
102             ## return right away for direct hits
103 43 100       180 return $self->{'_DATA'}{$key} if exists $self->{'_DATA'}{$key};
104              
105             ## re-sort keys if necessary
106 40 100       109 _sort_keys($self) if $self->{'_SORT'};
107              
108 40         47 my @keys = @{ $self->{'_KEYS'} };
  40         109  
109              
110             ## be sure we have at least 1 key
111 40 100       427 croak "cannot interpolate/extrapolate with less than two keys"
112             if @keys < 1;
113              
114             ## return constant if only 1 key
115 38 100       79 if (@keys == 1)
116             {
117              
118             ## determine whether we should die, return undef, or extrapolate
119 4         10 my $one_key_opt = $self->{'_OPTS'}{'one_key'};
120              
121 4 50       16 $one_key_opt eq ONE_KEY_FATAL ? croak "cannot extrapolate with only one key" :
    50          
122             $one_key_opt eq ONE_KEY_UNDEF ? return undef : ();
123              
124 4         46 return $self->{'_DATA'}{$keys[0]};
125             }
126              
127             ## begin interpolation/extrapolation search
128 34         41 my ($lower, $upper);
129              
130             ## key is below range of known keys
131 34 100       83 if ($key < $keys[0])
    100          
132             {
133              
134 15         31 my $extrap_opt = $self->{'_OPTS'}{'extrapolate'};
135              
136 15 100       183 $extrap_opt eq EX_CONSTANT ? return $self->{'_DATA'}{$keys[0]} :
    100          
    100          
137             $extrap_opt eq EX_FATAL ? croak "fatal extrapolation with key ($key)" :
138             $extrap_opt eq EX_UNDEF ? return undef : ();
139              
140 12         27 ($lower, $upper) = @keys[0, 1];
141              
142             }
143             ## key is above range of known keys
144             elsif ($key > $keys[-1])
145             {
146              
147 9         20 my $extrap_opt = $self->{'_OPTS'}{'extrapolate'};
148              
149 9 100       167 $extrap_opt eq EX_CONSTANT ? return $self->{'_DATA'}{$keys[-1]} :
    100          
    100          
150             $extrap_opt eq EX_FATAL ? croak "fatal extrapolation with key ($key)" :
151             $extrap_opt eq EX_UNDEF ? return undef : ();
152              
153 6         15 ($lower, $upper) = @keys[-2, -1];
154              
155             }
156             ## key is within range of known keys
157             else
158             {
159              
160 10         27 for my $i (0 .. $#keys - 1)
161             {
162 11         28 ($lower, $upper) = @keys[$i, $i+1];
163 11 100       55 last if $key <= $upper;
164 1 50       6 croak "unable to find bracketing keys" if $i == $#keys - 1;
165             }
166              
167             }
168              
169 28         92 return _mx_plus_b($key, $lower, $upper, $self->{'_DATA'}{$lower},
170             $self->{'_DATA'}{$upper});
171              
172             }
173              
174             ## sort keys and reset flag
175             sub _sort_keys
176             {
177 13     13   17 my ($self) = @_;
178 13         18 $self->{'_KEYS'} = [ sort { $a <=> $b } keys %{ $self->{'_DATA'} } ];
  11         52  
  13         71  
179 13         33 $self->{'_SORT'} = 0;
180             }
181              
182             ## basic equation for a line given 2 points
183             sub _mx_plus_b
184             {
185 28     28   62 my ($x, $x1, $x2, $y1, $y2) = @_;
186 28         55 my $slope = ($y2 - $y1) / ($x2 - $x1);
187 28         43 my $intercept = $y2 - ($slope * $x2);
188 28         156 return $slope * $x + $intercept;
189             }
190              
191             1;
192             __END__