line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
=head1 NAME |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
Test::Approx - compare two things for approximate equality |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 SYNOPSIS |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
use Test::Approx 'no_plan'; |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
is_approx( 'abcd', 'abcd', 'equal strings' ); |
10
|
|
|
|
|
|
|
is_approx( 1234, 1234, 'equal integers' ); |
11
|
|
|
|
|
|
|
is_approx( 1.234, 1.234, 'equal decimal numbers' ); |
12
|
|
|
|
|
|
|
is_approx( '1.234000', '1.234', 'equal decimal numbers, extra zeros' ); |
13
|
|
|
|
|
|
|
is_approx( 1.0, 1, 'equal decimal number & integer' ); |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
is_approx( 'abcdefgh', 'abcdefg', 'approx strings' ); |
16
|
|
|
|
|
|
|
is_approx( 1, 1.001, 'approx given decimal number & integer' ); |
17
|
|
|
|
|
|
|
is_approx( 51.60334, 51.603335, 'approx decimal numbers' ); |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
# default Levenshtein edit tolerance is 5% of avg string length: |
20
|
|
|
|
|
|
|
is_approx( 'abcdefg', 'abcgfe', 'str tolerance' ); # fail |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
# default difference tolerance is 5% of first number: |
23
|
|
|
|
|
|
|
is_approx( 1, 1.04, 'num tolerance' ); # fail |
24
|
|
|
|
|
|
|
is_approx( 1, 1.05, 'num tolerance' ); # fail |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
# default difference tolerance is 5% of first integer, or 1: |
27
|
|
|
|
|
|
|
is_approx( 1, 2, 'int tolerance' ); # pass |
28
|
|
|
|
|
|
|
is_approx( 100, 105, 'int tolerance' ); # pass |
29
|
|
|
|
|
|
|
is_approx( 100, 106, 'int tolerance' ); # fail |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# you can set the tolerance yourself: |
32
|
|
|
|
|
|
|
is_approx( 'abcdefg', 'abcgfe', 'diff strings', '50%' ); # pass |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
# you can set tolerance as a number too: |
35
|
|
|
|
|
|
|
is_approx( 'abcdefg', 'abcgfe', 'diff strings', 6 ); |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
# you can force compare as string, number, or integer: |
38
|
|
|
|
|
|
|
is_approx_str( '1.001', '1.901', 'pass as string' ); |
39
|
|
|
|
|
|
|
is_approx_num( '1.001', '1.901', 'fail as num' ); |
40
|
|
|
|
|
|
|
is_approx_int( '1.001', '1.901', 'pass as int' ); # not rounded! |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
=cut |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
package Test::Approx; |
45
|
|
|
|
|
|
|
|
46
|
1
|
|
|
1
|
|
1975
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
44
|
|
47
|
1
|
|
|
1
|
|
6
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
37
|
|
48
|
|
|
|
|
|
|
|
49
|
1
|
|
|
1
|
|
3171
|
use POSIX qw( strtod strtol ); |
|
1
|
|
|
|
|
34874
|
|
|
1
|
|
|
|
|
10
|
|
50
|
1
|
|
|
1
|
|
2341
|
use Text::LevenshteinXS qw(distance); |
|
1
|
|
|
|
|
4337
|
|
|
1
|
|
|
|
|
77
|
|
51
|
1
|
|
|
1
|
|
8
|
use Test::Builder; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
29
|
|
52
|
|
|
|
|
|
|
|
53
|
1
|
|
|
1
|
|
5
|
use base 'Exporter'; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
1240
|
|
54
|
|
|
|
|
|
|
our @EXPORT = qw( is_approx is_approx_str is_approx_num is_approx_int ); |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
our $VERSION = 0.03; |
57
|
|
|
|
|
|
|
our %DEFAULT_TOLERANCE = ( |
58
|
|
|
|
|
|
|
str => '5%', |
59
|
|
|
|
|
|
|
num => '5%', |
60
|
|
|
|
|
|
|
int => '5%', |
61
|
|
|
|
|
|
|
); |
62
|
|
|
|
|
|
|
our $Test = Test::Builder->new; |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
sub import { |
65
|
1
|
|
|
1
|
|
8
|
my $self = shift; |
66
|
1
|
|
|
|
|
3
|
my $pack = caller; |
67
|
|
|
|
|
|
|
|
68
|
1
|
|
|
|
|
7
|
$Test->exported_to($pack); |
69
|
1
|
50
|
|
|
|
11
|
$Test->plan(@_) if (@_); |
70
|
|
|
|
|
|
|
|
71
|
1
|
|
|
|
|
140
|
$self->export_to_level(1, $self, @EXPORT); |
72
|
|
|
|
|
|
|
} |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub check_type { |
75
|
32
|
|
|
32
|
0
|
40
|
my $arg = shift; |
76
|
|
|
|
|
|
|
|
77
|
32
|
|
|
|
|
89
|
local $! = 0; |
78
|
32
|
|
|
|
|
186
|
my ($num, $unparsed) = strtod( $arg ); |
79
|
32
|
100
|
66
|
|
|
262
|
return 'str' if (($arg eq '') || ($unparsed != 0) || $!); |
|
|
|
66
|
|
|
|
|
80
|
22
|
100
|
|
|
|
141
|
return 'num' if $num =~ /\.\d*\z/; |
81
|
|
|
|
|
|
|
|
82
|
14
|
|
|
|
|
41
|
return 'int'; |
83
|
|
|
|
|
|
|
} |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
sub is_approx { |
86
|
16
|
|
|
16
|
1
|
21179
|
my ($arg1, $arg2, $msg, $tolerance) = @_; |
87
|
|
|
|
|
|
|
|
88
|
16
|
|
|
|
|
29
|
local $Test::Builder::Level = $Test::Builder::Level + 1; |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
# clean input & avoid warnings |
91
|
16
|
50
|
|
|
|
39
|
$arg1 = '' unless defined $arg1; |
92
|
16
|
50
|
|
|
|
35
|
$arg2 = '' unless defined $arg2; |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
# check inputs types and call appropriate sub |
95
|
16
|
|
|
|
|
36
|
my $arg1_type = check_type( $arg1 ); |
96
|
16
|
|
|
|
|
34
|
my $arg2_type = check_type( $arg2 ); |
97
|
|
|
|
|
|
|
|
98
|
16
|
100
|
|
|
|
63
|
if ($arg1_type eq 'int') { |
|
|
100
|
|
|
|
|
|
99
|
7
|
100
|
|
|
|
31
|
return is_approx_int( @_ ) if $arg2_type eq 'int'; |
100
|
1
|
50
|
|
|
|
7
|
return is_approx_num( @_ ) if $arg2_type eq 'num'; |
101
|
|
|
|
|
|
|
} elsif ($arg1_type eq 'num') { |
102
|
4
|
50
|
66
|
|
|
42
|
return is_approx_num( @_ ) |
103
|
|
|
|
|
|
|
if ($arg2_type eq 'int') or ($arg2_type eq 'num'); |
104
|
|
|
|
|
|
|
} |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
# default behaviour, compare as strings: |
107
|
5
|
|
|
|
|
12
|
return is_approx_str( @_ ); |
108
|
|
|
|
|
|
|
} |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
sub is_approx_str { |
111
|
7
|
|
|
7
|
1
|
1643
|
my ($str1, $str2, $msg, $tolerance) = @_; |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
# clean input & avoid warnings |
114
|
7
|
50
|
|
|
|
20
|
$str1 = '' unless defined $str1; |
115
|
7
|
50
|
|
|
|
16
|
$str2 = '' unless defined $str2; |
116
|
7
|
100
|
|
|
|
22
|
$tolerance = $DEFAULT_TOLERANCE{str} unless defined( $tolerance ); |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
# build some diagnostics info |
119
|
7
|
100
|
|
|
|
42
|
my $short1 = length($str1) > 8 ? substr($str1, 0, 5) . '...' : $str1; |
120
|
7
|
100
|
|
|
|
16
|
my $short2 = length($str2) > 8 ? substr($str2, 0, 5) . '...' : $str2; |
121
|
7
|
|
|
|
|
19
|
my $msg2 = "'$short1' =~ '$short2'"; |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
# set default message |
124
|
7
|
50
|
|
|
|
16
|
$msg = $msg2 unless defined($msg); |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
# figure out what to use as the threshold |
127
|
7
|
|
|
|
|
9
|
my $threshold; |
128
|
7
|
50
|
|
|
|
33
|
if ($tolerance =~ /^(.+)%$/) { |
129
|
|
|
|
|
|
|
# tolerance is a percentage |
130
|
7
|
|
|
|
|
21
|
my $percent = $1 / 100; |
131
|
|
|
|
|
|
|
# calculate threshold from a percentage: |
132
|
|
|
|
|
|
|
# x% of average string length, or 1 |
133
|
7
|
|
100
|
|
|
37
|
$threshold = int(( (length($str1)+length($str2))/2 )*$percent) || 1; |
134
|
|
|
|
|
|
|
} else { |
135
|
|
|
|
|
|
|
# tolerance is already a threshold |
136
|
0
|
|
|
|
|
0
|
$threshold = $tolerance; |
137
|
|
|
|
|
|
|
} |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
# we've got a threshold, now do the test: |
141
|
7
|
|
|
|
|
64305
|
my $dist = distance($str1, $str2); |
142
|
7
|
100
|
|
|
|
36
|
unless ($Test->ok($dist <= $threshold, $msg)) { |
143
|
3
|
50
|
|
|
|
1617
|
$Test->diag(" test: $msg2") if ($msg ne $msg2); |
144
|
3
|
|
|
|
|
207
|
$Test->diag(" error: edit distance ($dist) was greater than threshold ($threshold)"); |
145
|
|
|
|
|
|
|
} |
146
|
|
|
|
|
|
|
} |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
sub is_approx_num { |
149
|
13
|
|
|
13
|
1
|
12417
|
my ($num1, $num2, $msg, $tolerance) = @_; |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
# clean input & avoid warnings |
152
|
13
|
50
|
|
|
|
270
|
$num1 = strtod( defined $num1 ? $num1 : '' ); # ignore any errors |
153
|
13
|
50
|
|
|
|
76
|
$num2 = strtod( defined $num2 ? $num2 : '' ); # ignore any errors |
154
|
13
|
100
|
|
|
|
45
|
$tolerance = $DEFAULT_TOLERANCE{num} unless defined( $tolerance ); |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
# build some diagnostics info |
157
|
13
|
50
|
|
|
|
254
|
my $short1 = length($num1) > 8 ? substr($num1, 0, 5) . '...' : $num1; |
158
|
13
|
100
|
|
|
|
51
|
my $short2 = length($num2) > 8 ? substr($num2, 0, 5) . '...' : $num2; |
159
|
13
|
|
|
|
|
221
|
my $msg2 = "'$short1' =~ '$short2'"; |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
# set default message |
162
|
13
|
50
|
|
|
|
31
|
$msg = $msg2 unless defined($msg); |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
# figure out what to use as the threshold |
165
|
13
|
|
|
|
|
16
|
my $threshold; |
166
|
13
|
100
|
|
|
|
227
|
if ($tolerance =~ /^(.+)%$/) { |
167
|
|
|
|
|
|
|
# tolerance is a percentage |
168
|
10
|
|
|
|
|
28
|
my $percent = $1 / 100; |
169
|
|
|
|
|
|
|
# calculate threshold from a percentage: x% of num1 |
170
|
|
|
|
|
|
|
# strtod() to get around weird bug: |
171
|
|
|
|
|
|
|
# $dist = 0.05; $threshold = 0.05; $dist <= $threshold; # false ??!? |
172
|
10
|
|
|
|
|
227
|
$threshold = strtod( abs( $num1 * $percent ) ); |
173
|
|
|
|
|
|
|
} else { |
174
|
|
|
|
|
|
|
# tolerance is already a threshold |
175
|
3
|
|
|
|
|
5
|
$threshold = $tolerance; |
176
|
|
|
|
|
|
|
} |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
# we've got a threshold, now do the test: |
179
|
|
|
|
|
|
|
# strtod() to get around weird bug: |
180
|
|
|
|
|
|
|
# $dist = 0.05; $threshold = 0.05; $dist <= $threshold; # false ??!? |
181
|
13
|
|
|
|
|
71
|
my $dist = strtod( abs($num2 - $num1) ); |
182
|
13
|
100
|
|
|
|
223
|
unless ($Test->ok($dist <= $threshold, $msg)) { |
183
|
3
|
50
|
|
|
|
1719
|
$Test->diag(" test: $msg2") if ($msg ne $msg2); |
184
|
3
|
|
|
|
|
237
|
$Test->diag(" error: distance ($dist) was greater than threshold ($threshold)"); |
185
|
|
|
|
|
|
|
} |
186
|
|
|
|
|
|
|
} |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
sub is_approx_int { |
189
|
14
|
|
|
14
|
1
|
3094
|
my ($int1, $int2, $msg, $tolerance) = @_; |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
# clean input & avoid warnings |
192
|
14
|
50
|
|
|
|
9474
|
$int1 = strtol( defined $int1 ? $int1 : '' ); # ignore any errors |
193
|
14
|
50
|
|
|
|
67
|
$int2 = strtol( defined $int2 ? $int2 : '' ); # ignore any errors |
194
|
14
|
100
|
|
|
|
51
|
$tolerance = $DEFAULT_TOLERANCE{int} unless defined( $tolerance ); |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# build some diagnostics info |
197
|
14
|
50
|
|
|
|
40
|
my $short1 = length($int1) > 8 ? substr($int1, 0, 5) . '...' : $int1; |
198
|
14
|
50
|
|
|
|
29
|
my $short2 = length($int2) > 8 ? substr($int2, 0, 5) . '...' : $int2; |
199
|
14
|
|
|
|
|
34
|
my $msg2 = "'$short1' =~ '$short2'"; |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
# set default message |
202
|
14
|
50
|
|
|
|
32
|
$msg = $msg2 unless defined($msg); |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
# figure out what to use as the threshold |
205
|
14
|
|
|
|
|
15
|
my $threshold; |
206
|
14
|
100
|
|
|
|
74
|
if ($tolerance =~ /^(.+)%$/) { |
207
|
|
|
|
|
|
|
# tolerance is a percentage |
208
|
13
|
|
|
|
|
37
|
my $percent = $1 / 100; |
209
|
|
|
|
|
|
|
# calculate threshold from a percentage: x% of num1 || 1 |
210
|
|
|
|
|
|
|
# strtod() to get around weird bug: |
211
|
|
|
|
|
|
|
# $dist = 0.05; $threshold = 0.05; $dist <= $threshold; # false ??!? |
212
|
13
|
|
100
|
|
|
107
|
$threshold = strtod( abs( int( $int1 * $percent ) ) ) || 1; |
213
|
|
|
|
|
|
|
} else { |
214
|
|
|
|
|
|
|
# tolerance is already a threshold |
215
|
1
|
|
|
|
|
5
|
$threshold = $tolerance; |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
# we've got a threshold, now do the test: |
219
|
|
|
|
|
|
|
# strtod() to get around weird bug: |
220
|
|
|
|
|
|
|
# $dist = 0.05; $threshold = 0.05; $dist <= $threshold; # false ??!? |
221
|
14
|
|
|
|
|
59
|
my $dist = strtod( abs($int2 - $int1) ); |
222
|
14
|
100
|
|
|
|
74
|
unless ($Test->ok($dist <= $threshold, $msg)) { |
223
|
3
|
50
|
|
|
|
1570
|
$Test->diag(" test: $msg2") if ($msg ne $msg2); |
224
|
3
|
|
|
|
|
209
|
$Test->diag(" error: distance ($dist) was greater than threshold ($threshold)"); |
225
|
|
|
|
|
|
|
} |
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
1; |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
__END__ |