package Test::Approximate;
# ABSTRACT: Test and deeply test two number is approximate equality
use strict;
use warnings;
our $VERSION = 0.008;
use POSIX qw( strtod );
use Test::Builder;
our $Test = Test::Builder->new;
use base 'Exporter';
our @EXPORT = qw( is_approx approx );
our $DEFAULT_TOLERANCE = '1%';
sub is_approx {
my ( $got, $expected, $msg, $tolerance ) = @_;
$tolerance //= $DEFAULT_TOLERANCE;
# build some diagnostics info
my $short1 = length($got) > 12 ? substr($got, 0, 8) . '...' : $got;
my $short2 = length($expected) > 12 ? substr($expected, 0, 8) . '...' : $expected;
my $msg2 = "'$short1' =~ '$short2'";
#set default message
$msg = $msg2 unless defined($msg);
unless ( $Test->ok(_is_approx($got, $expected, $tolerance), $msg) ) {
$Test->diag(" test: $msg");
if ( check_type($got) eq 'str' or check_type($expected) eq 'str' ) {
$Test->diag(" error: diff between string\n got: $got\nexpected: $expected");
return;
}
my $diff = $got - $expected;
if ( $tolerance =~ /^(.+)%$/ ) {
my $percentage = ( $diff / $expected ) * 100;
$Test->diag(" error: diff $percentage% is not under tolerance $tolerance");
}
else {
$Test->diag(" error: diff $diff is not under tolerance $tolerance");
}
}
}
sub _is_approx {
my ( $num1, $num2, $tolerance ) = @_;
# borrowed form Test::Approx
my $num1_type = check_type($num1);
my $num2_type = check_type($num2);
if ( $num1_type eq 'str' or $num2_type eq 'str' ) {
return $num1 eq $num2;
}
# figure out what to use as the threshold
my $threshold;
if ( $tolerance =~ /^(.+)%$/ ) {
my $percent = $1 / 100;
$threshold = strtod( abs( $num1 * $percent ) );
} else {
$threshold = $tolerance;
}
my $dist = strtod( abs($num2 - $num1) );
return $dist <= $threshold ? 1 : 0;
}
# borrowed from Test::Approx
sub check_type {
my $arg = shift;
local $! = 0;
my ( $num, $unparsed ) = strtod($arg);
return 'str' if ( ($arg eq '') || ($unparsed != 0) || $! );
return 'num';
}
# deeply test approx
sub approx {
my ( $structure, $torlerance ) = @_;
if ( ref $structure eq '' ) { # value
return Test::Deep::Approximate->new($structure, $torlerance);
}
elsif ( ref $structure eq ref {} ) { # hash
my $hash = {};
foreach my $key ( keys %$structure ) {
$hash->{$key} = approx($structure->{$key}, $torlerance);
}
return $hash;
}
elsif ( ref $structure eq ref [] ) { # array
my $array = [];
for my $item ( @$structure ) {
push @$array, approx($item, $torlerance);
}
return $array;
}
}
{
package Test::Deep::Approximate;
use Test::Deep::Cmp;
sub init {
my ( $self, $expect, $torlerance ) = @_;
$self->{expect} = $expect;
$self->{torlerance} = $torlerance;
}
sub _is_approx {
shift;
return Test::Approximate::_is_approx(@_);
}
sub descend {
my ( $self, $got ) = @_;
return $self->_is_approx($got, $self->{expect}, $self->{torlerance});
}
sub diagnostics {
my ( $self, $where, $last ) = @_;
my $got = $last->{got};
my $diag = <<EOM;
Comparing $where
got : $got
expected : $self->{expect}
EOM
return $diag;
}
}
1;
__END__
=pod
=head1 NAME
Test::Approximate -- compare two number for approximate equality, deeply
=head1 SYNOPSIS
use Test::Approximate;
is_approx(1, 1.0001, 'approximate equal', '1%');
is_approx(0.0001001, '1e-04', 'str vs num', '1%');
is_approx(1000, 1000.01, 'absolute tolerance', '0.1');
use Test::Deep;
use Test::Approximate;
$got = [ 1.00001, 2, 3, 4 ];
$expect = [ 1, 2, 3, 4 ];
cmp_deeply($got, approx($expect, '1%'), 'array');
$got = { a => 1, b => 1e-3, c => [ 1.1, 2.5, 5, 1e-9 ] };
$expect = { a => 1.0001, b => 1e-03, c => [ 1.1, 2.5, 5, 1.00001e-9 ] };
cmp_deeply( $got, approx($expect, '0.01%'), 'hash mix array');
=head1 DESCRIPTION
This module can test two scalar string or number numberic approximate equal, and deeply test two array or hash or array of hash etc.
There is already a nice module do this -- L<Test::Approx>. I wrote this one because L<Test::Approx> can't do a deeply test, and I have not found a module do the same thing.
=head1 FUNCTIONS
=over 2
=item is_approx($got, $expected, [$msg, $tolerance])
Test $got and $expected 's difference.
This function is partly borrowed from L<Test::Approx>, without the string Levenshtein difference.
Only do a numeric difference; If you compare two string, the test will pass only when the two string is equal.
C<$test_name> defaults to C<'got' =~ 'expected'>
C<$tolerance> is used to determine how different the scalars can be, it
defaults to C<1%>. It can also be set as a number representing a threshold.
To determine which:
$tolerance = '6%'; # threshold = calculated at 6%
$tolerance = 0.06; # threshold = 0.06
=item approx($aoh, $tolerance)
This function is used to do a deelpy approximate test, with L<Test::Deep>
cmp_deeply($got, approx($expected, '1%'), 'test msg')
This will do a approximate compare every element of an array, and every value of a hash with the given tolerance, If the data is an complicate structure like hash of array , array of hash etc, it will walk all the element , and do a deep compare as you wish.
It is useful when you want do a deep approximate compare with a big data.
=back
=head1 EXPORTS
C<is_approx>, C<approx>
=head1 AUTHOR
tadegenban <tadegenban@gmail.com>
=head1 COPYRIGHT
Copyright (c) 2014 tadegenban.
Released under the same terms as Perl itself.
=head1 SEE ALSO
L<Text::Approx>,
L<Test::Builder>,
L<Test::Deep::Between>,
=cut