File Coverage

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


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