File Coverage

blib/lib/Math/InterpolationCompiler.pm
Criterion Covered Total %
statement 76 79 96.2
branch 24 34 70.5
condition 5 9 55.5
subroutine 10 10 100.0
pod 0 2 0.0
total 115 134 85.8


line stmt bran cond sub pod time code
1             package Math::InterpolationCompiler;
2 1     1   484 use 5.006001;
  1         3  
  1         29  
3 1     1   445 use Moo 2;
  1         10946  
  1         6  
4 1     1   1920 use Types::Standard 1;
  1         45094  
  1         8  
5 1     1   373 use Carp;
  1         2  
  1         683  
6              
7             our $VERSION= '0.001000';
8              
9             # ABSTRACT: Compile interpolations into perl coderefs
10              
11              
12             has domain => ( is => 'ro', isa => Types::Standard::ArrayRef, required => 1 );
13             has range => ( is => 'ro', isa => Types::Standard::ArrayRef, required => 1 );
14             has algorithm => ( is => 'ro', default => sub { 'linear' } );
15             has beyond_domain => ( is => 'ro', default => sub { 'clamp' } );
16             has perl_code => ( is => 'lazy' );
17             has fn => ( is => 'lazy' );
18             has sanitize => ( is => 'ro', default => sub { 1 } );
19              
20             sub BUILDARGS {
21 8     8 0 12666 my $self= shift;
22 8         27 my $args= $self->next::method(@_);
23 8 50 33     149 if ($args->{points} && !$args->{domain} && !$args->{range}) {
      33        
24 8         8 my (@domain, @range);
25 8         6 for (@{ delete $args->{points} }) {
  8         17  
26 26         27 push @domain, $_->[0];
27 26         25 push @range, $_->[1];
28             }
29 8         13 $args->{domain}= \@domain;
30 8         10 $args->{range}= \@range;
31             }
32 8         119 return $args;
33             }
34              
35             sub _sanitize_number_array {
36             return [
37 51 50       56 map {
38 16         18 defined $_ or croak " is not a number";
39 51         55 my $n= "$_";
40 51 100       286 $n =~ /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/ or croak "$n is not a number";
41 50         60 $n
42 16     16   12 } @{ $_[0] }
43             ];
44             }
45              
46             sub BUILD {
47 8     8 0 37 my $self= shift;
48 0         0 @{ $self->domain } == @{ $self->range }
  8         17  
  8         19  
49 8 50       8 or croak "Domain and range differ in length (".@{ $self->domain }." != ".@{ $self->range }.")";
  0         0  
50 8 50       8 @{ $self->domain } > 1
  8         14  
51             or croak "Domain does not contain any intervals";
52 8         7 my $prev;
53 8 50       12 if ($self->sanitize) {
54 8         14 $self->{domain}= _sanitize_number_array($self->domain);
55 8         15 $self->{range}= _sanitize_number_array($self->range);
56             }
57 7         6 for (@{ $self->domain }) {
  7         13  
58 24 100 100     142 croak "Domain is not sorted in non-decreasing order"
59             if defined $prev && $_ < $prev;
60 23         23 $prev= $_;
61             }
62 6 50       107 $self->can("_gen_".$self->algorithm)
63             or croak "Unknown algorithm ".$self->algorithm;
64             }
65              
66             sub _build_perl_code {
67 6     6   287 my $self= shift;
68 6         18 my $method= $self->can("_gen_".$self->algorithm);
69 6         13 return $self->$method;
70             }
71              
72             sub _build_fn {
73 6     6   377 my $self= shift;
74 6 50       61 my $sub= eval $self->perl_code
75             or croak "Failed to build function: $@";
76 6         19 return $sub;
77             }
78              
79             # Create a linear interpolation
80             sub _gen_linear {
81 6     6   5 my $self= shift;
82 6         8 my $domain= $self->domain;
83 6         8 my $range= $self->range;
84 6         6 my @expressions;
85 6         15 for (my $i= 1; $i < @$domain; $i++) {
86             # skip discontinuities
87 15 100       34 next if $domain->[$i] == $domain->[$i-1];
88             # calculate slope and offset at x0
89 8         24 my $m= ($range->[$i] - $range->[$i-1]) / ($domain->[$i] - $domain->[$i-1]);
90 8         13 my $b= $range->[$i-1] - $domain->[$i-1] * $m;
91             # generate code
92 8         33 push @expressions, [ $domain->[$i-1], '$x * '.$m.' + '.$b ];
93             }
94 6 100       46 if ($self->beyond_domain eq 'clamp') {
    100          
    100          
    50          
95 2         6 unshift @expressions, [ undef, $range->[0] ];
96 2         4 push @expressions, [ $domain->[-1], $range->[-1] ];
97             }
98             elsif ($self->beyond_domain eq 'extrapolate') {
99             # just let the edge expressions do their thing
100             # ... unless there were discontinuities at the edges
101 1 50       4 unshift @expressions, [ undef, $range->[0] ]
102             if $domain->[0] == $domain->[1];
103 1 50       3 push @expressions, [ $domain->[-1], $range->[-1] ]
104             if $domain->[-1] == $domain->[-2];
105             }
106             elsif ($self->beyond_domain eq 'undef') {
107 1         3 unshift @expressions, [ undef, 'undef' ];
108 1         4 push @expressions, [ $domain->[-1], '$x == '.$domain->[-1].'? ('.$range->[-1].') : undef' ];
109             }
110             elsif ($self->beyond_domain eq 'die') {
111 2         5 unshift @expressions, [ undef, 'die "argument out of bounds (<'.$domain->[0].')"' ];
112 2         6 push @expressions, [ $domain->[-1], '$x == '.$domain->[-1].'? ('.$range->[-1].') : die "argument out of bounds (>'.$domain->[-1].')"' ];
113             }
114             else {
115 0         0 croak "Algorithm 'linear' does not support domain-edge '".$self->beyond_domain."'";
116             }
117             # Now tree-up the expressions
118 6         17 while (@expressions > 1) {
119 11         8 my ($i, $dest);
120 11         19 for ($i= 1, $dest= 0; $i < @expressions; $i+= 2) {
121 12         49 $expressions[$dest++]= [
122             $expressions[$i-1][0],
123             '$x < '.$expressions[$i][0]."?"
124             .' ('.$expressions[$i-1][1].")"
125             .':('.$expressions[$i][1].")"
126             ];
127             }
128             # odd number?
129 11 100       14 if ($i == @expressions) {
130 4         5 $expressions[$dest++]= $expressions[-1];
131             }
132             # truncate list
133 11         27 $#expressions= $dest-1;
134             }
135             # finally, wrap with function
136 6         483 return "sub {\n my \$x= shift;\n return ".$expressions[0][1].";\n}\n";
137             }
138              
139             1;
140              
141             __END__