File Coverage

blib/lib/Test/Approximate.pm
Criterion Covered Total %
statement 46 46 100.0
branch 18 18 100.0
condition 8 9 88.8
subroutine 12 12 100.0
pod 3 3 100.0
total 87 88 98.8


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   509559 use 5.008;
  5         61  
10              
11 5     5   30 use strict;
  5         8  
  5         108  
12 5     5   24 use warnings;
  5         10  
  5         136  
13              
14 5     5   36 use Carp 'croak';
  5         12  
  5         349  
15              
16             =head1 VERSION
17              
18             version 0.102
19              
20             =cut
21              
22             our $VERSION = '0.102';
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 13.
73             # Original: 1000, 1100
74             # DOP: 2
75             # Delta: 100
76             # Multiplier: 0.01 (1e-2)
77             # Diff: round(1)
78             # Comparison: 1 == 0
79              
80             B: the untouched values of C<$actual> and C<$expected> as
81             passed in to the test.
82              
83             B: the I used.
84              
85             B: the absolute difference between C<$actual> and C<$expected>.
86              
87             B: factor used to make all DOP occur before the decimal
88             point.
89              
90             B: delta after scaling.
91              
92             B: the comparison used for the test.
93              
94             =cut
95              
96 5     5   2404 use parent 'Exporter';
  5         1487  
  5         28  
97             our @EXPORT = qw( is_approx isnt_approx set_dop );
98              
99 5     5   379 use Test2::API qw'context';
  5         10  
  5         247  
100              
101             use constant {
102 5         2950 K_EQUAL => 'EQ',
103             K_NOTEQUAL => 'NE'
104 5     5   28 };
  5         10  
105              
106             my $MAXDOP = 14;
107             my $_DOP = 3;
108              
109             =head1 FUNCTIONS
110              
111             =head2 is_approx
112              
113             is_approx $actual, $expected, $name;
114              
115             Compares C<$actual> to C<$expected> as described L
116             and generates an appropriate standard pass/fail test result. As is
117             usual with Perl testing modules, C<$name> is optional.
118              
119             =cut
120              
121             sub is_approx($$;$) {
122 43     43 1 20287 return _compare(K_EQUAL, @_);
123             }
124              
125             =head2 isnt_approx
126              
127             isnt_approx $actual, $expected, $name;
128              
129             The opposite of C. The test will pass if C<$actual> and
130             C<$expected> are sufficiently different.
131              
132             =cut
133              
134             sub isnt_approx($$;$) {
135 44     44 1 4106 return _compare(K_NOTEQUAL, @_);
136             }
137              
138             =head2 set_dop($new_dop)
139              
140             set_dop(4); # set precision to 4 DOP
141             print set_dop(); # 4
142              
143             C is used to set the precision of C and
144             C. It takes a single, optional, argument that
145             represents the DOP to use in the comparisons. C<$new_dop> must be an
146             integer between 1 and 14 inclusive and will be shoehorned into that
147             range if necessary. C<$new_dop> is returned as the value of the
148             function.
149              
150             If C<$new_dop> is absent, C just returns the current value of
151             DOP.
152              
153             See L above for more about DOP.
154              
155             =cut
156              
157             sub set_dop {
158 20 100   20 1 44988 if (@_) {
159 17         37 my $dop = shift;
160              
161 17 100 100     190 croak "Positive integer expected"
      100        
162             if ! defined $dop || $dop eq '' || $dop =~ /\D/;
163              
164 12 100       36 $dop = 1 if $dop < 1;
165 12 100       30 $dop = $MAXDOP if $dop > $MAXDOP;
166              
167 12         25 $_DOP = $dop
168             }
169 15         38 return $_DOP;
170             }
171              
172             sub _compare {
173 87     87   200 my ($eq_noteq, $actual, $expected, $name) = @_;
174              
175 87 100       389 my $scale = $expected == 0
176             ? 0
177             : _floor(log(abs $expected) / log 10) + 1;
178              
179 87         185 my $delta = abs $actual - $expected;
180 87         188 my $mult = 10**($_DOP-$scale);
181 87         148 my $to_round = $delta * $mult;
182 87         168 my $diff = int $to_round + 0.5;
183              
184 87 100       1003 my $diag = join '', map { " $_\n" } (
  522         1282  
185             "Original values: $actual, $expected",
186             "DOP: $_DOP",
187             "Delta: $delta",
188             "Multiplier: $mult (1e" . ($_DOP-$scale) . ")",
189             "Diff: round($to_round)",
190             "Comparison: $diff " . ($eq_noteq eq K_EQUAL ? '==' : '!=') . " 0",
191             );
192              
193 87         285 my $ctx = context( level => 1 );
194 87 100       6999 my $ok = $eq_noteq eq K_EQUAL ? $diff == 0 : $diff != 0;
195 87         288 $ctx->ok( $ok, $name );
196 87 100       14613 $ctx->diag($diag) unless $ok;
197 87         1348 $ctx->release;
198              
199 87         2307 return $ok;
200             }
201              
202             sub _floor { # https://stackoverflow.com/questions/37020135
203 83     83   150 my $x = shift;
204 83         182 my $int = int $x;
205 83 100 66     332 return $x < 0 && $int != $x ? $int - 1 : $int;
206             }
207              
208             =head1 AUTHOR & COPYRIGHT
209              
210             Copyright 2020 by Brian Greenfield E briang @ cpan dot org E.
211              
212             This program is free software; you can redistribute it and/or modify
213             it under the same terms as Perl itself.
214              
215             =cut
216              
217             1;