File Coverage

blib/lib/Test/Approximate.pm
Criterion Covered Total %
statement 50 50 100.0
branch 18 18 100.0
condition 8 9 88.8
subroutine 12 12 100.0
pod 3 3 100.0
total 91 92 98.9


line stmt bran cond sub pod time code
1             package Test::Approximate;
2              
3             =head1 NAME
4              
5             Test::Approximate - test for approximate numeric equality
6              
7             =cut
8              
9 5     5   512886 use 5.008;
  5         61  
10              
11 5     5   35 use strict;
  5         12  
  5         104  
12 5     5   33 use warnings;
  5         10  
  5         132  
13              
14 5     5   26 use Carp 'croak';
  5         21  
  5         366  
15              
16             =head1 VERSION
17              
18             version 0.101
19              
20             =cut
21              
22             our $VERSION = '0.101';
23              
24             =head1 SYNOPSIS
25              
26             use Test::Approximate;
27              
28             set_dop(3); # this is the default value
29             is_approx 3, 3.001, 'close enough';
30              
31             set_dop(1);
32             is_approx 3.0, 3.4, 'also close enough';
33              
34             set_dop(2);
35             isnt_approx 3.0, 3.4, 'different';
36              
37             =head1 DESCRIPTION
38              
39             Test::Approximate provides tests that can be used to compare
40             floating-point numbers for approximate equality (C) and
41             inequality (C). This is achieved using the concept of
42             I, or DOP. The DOP of a number are a fixed number
43             of significant digits of that number. For example, with 3 DOP
44             1,234,000 would be 1,230,000 and 0.000 1234 would be 0.000 123.
45              
46             By default, 3 DOP are used.
47              
48             =head2 METHOD
49              
50             To compare the two numbers C<$actual> and C<$expected>, a
51             (power-of-ten) scaling factor for C<$expected> is determined, such
52             that, when multiplied by it, there will be DOP digits before the
53             decimal point. The absolute value of the difference between C<$actual>
54             and C<$expected> is multiplied by the scaling factor and rounded to
55             the nearest integer. If this value is zero, the two values are deemed
56             to be approximately the same (or non-zero in the case of
57             C).
58              
59             =head2 DIAGNOSTICS
60              
61             C and C will emit diagnostics whenever a
62             test fails.
63              
64             For example,
65              
66             set_dop(2);
67             is_approx 1000,1100; # fails
68              
69             produces the following output:
70              
71             not ok 1
72             # Failed test at ... line 30.
73             # DOP: 2
74             # Delta: 100
75             # Multiplier: 0.01 (1e-2)
76             # Diff: round(1)
77             # Comparison: 1 == 0
78              
79             B: the I used.
80              
81             B: the absolute difference between C<$actual> and C<$expected>.
82              
83             B: factor used to make all DOP occur before the decimal
84             point.
85              
86             B: delta after scaling.
87              
88             B: the comparison used for the test.
89              
90             =cut
91              
92 5     5   2387 use parent 'Exporter';
  5         1484  
  5         38  
93             our @EXPORT = qw( is_approx isnt_approx set_dop );
94              
95 5     5   375 use Test2::API qw'context';
  5         12  
  5         281  
96              
97             use constant {
98 5         3066 K_EQUAL => 'EQ',
99             K_NOTEQUAL => 'NE'
100 5     5   30 };
  5         9  
101              
102             my $MAXDOP = 14;
103             my $_DOP = 3;
104              
105             =head1 FUNCTIONS
106              
107             =head2 is_approx
108              
109             is_approx $actual, $expected, $name;
110              
111             Compares C<$actual> to C<$expected> as described L
112             and generates an appropriate standard pass/fail test result. As is
113             usual with Perl testing modules, C<$name> is optional.
114              
115             =cut
116              
117             sub is_approx($$;$) {
118 43     43 1 21750 return _compare(K_EQUAL, @_);
119             }
120              
121             =head2 isnt_approx
122              
123             isnt_approx $actual, $expected, $name;
124              
125             The opposite of C. The test will pass if C<$actual> and
126             C<$expected> are sufficiently different.
127              
128             =cut
129              
130             sub isnt_approx($$;$) {
131 44     44 1 5381 return _compare(K_NOTEQUAL, @_);
132             }
133              
134             =head2 set_dop($new_dop)
135              
136             set_dop(4); # set precision to 4 DOP
137             print set_dop(); # 4
138              
139             C is used to set the precision of C and
140             C. It takes a single, optional, argument that
141             represents the DOP to use in the comparisons. C<$new_dop> must be an
142             integer between 1 and 14 inclusive and will be shoehorned into that
143             range if necessary. C<$new_dop> is returned as the value of the
144             function.
145              
146             If C<$new_dop> is absent, C just returns the current value of
147             DOP.
148              
149             See L above for more about DOP.
150              
151             =cut
152              
153             sub set_dop {
154 20 100   20 1 41938 if (@_) {
155 17         36 my $dop = shift;
156              
157 17 100 100     208 croak "Positive integer expected"
      100        
158             if ! defined $dop || $dop eq '' || $dop =~ /\D/;
159              
160 12 100       41 $dop = 1 if $dop < 1;
161 12 100       32 $dop = $MAXDOP if $dop > $MAXDOP;
162              
163 12         27 $_DOP = $dop
164             }
165 15         38 return $_DOP;
166             }
167              
168             sub _compare {
169 87     87   227 my ($eq_noteq, $actual, $expected, $name) = @_;
170              
171 87 100       469 my $scale = $expected == 0
172             ? 0
173             : _floor(log(abs $expected) / log 10) + 1;
174              
175 87         229 my $delta = abs $actual - $expected;
176 87         214 my $mult = 10**($_DOP-$scale);
177 87         161 my $to_round = $delta * $mult;
178 87         174 my $diff = int $to_round + 0.5;
179              
180 87         141 my $diag = '';
181 87         227 $diag .= " DOP: $_DOP\n";
182 87         426 $diag .= " Delta: $delta\n";
183 87         359 $diag .= " Multiplier: $mult (1e" . ($_DOP-$scale) . ")\n";
184 87         261 $diag .= " Diff: round($to_round)\n";
185 87 100       264 $diag .= " Comparison: $diff " . ($eq_noteq eq K_EQUAL ? '==' : '!=') . " 0\n";
186              
187 87         291 my $ctx = context( level => 1 );
188 87 100       8283 my $ok = $eq_noteq eq K_EQUAL ? $diff == 0 : $diff != 0;
189 87         352 $ctx->ok( $ok, $name );
190 87 100       17398 $ctx->diag($diag) unless $ok;
191 87         1400 $ctx->release;
192              
193 87         2765 return $ok;
194             }
195              
196             sub _floor { # https://stackoverflow.com/questions/37020135
197 83     83   169 my $x = shift;
198 83         477 my $int = int $x;
199 83 100 66     402 return $x < 0 && $int != $x ? $int - 1 : $int;
200             }
201              
202             =head1 AUTHOR & COPYRIGHT
203              
204             Copyright 2020 by Brian Greenfield E briang @ cpan dot org E.
205              
206             This program is free software; you can redistribute it and/or modify
207             it under the same terms as Perl itself.
208              
209             =cut
210              
211             1;