The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w

# Tests for Number::WithError::LaTeX

use strict;
use lib ();
use File::Spec::Functions ':ALL';
BEGIN {
	$| = 1;
	unless ( $ENV{HARNESS_ACTIVE} ) {
		require FindBin;
		$FindBin::Bin = $FindBin::Bin; # Avoid a warning
		chdir catdir( $FindBin::Bin, updir() );
		lib->import(
			catdir('blib', 'lib'),
			'lib',
			);
	}
}


#####################################################################

use Number::WithError::LaTeX ':all';
use Params::Util qw/_INSTANCE/;
BEGIN {
	require Test::LectroTest;
   	if (defined $ENV{PERL_TEST_ATTEMPTS}) {
		Test::LectroTest->import(trials => $ENV{PERL_TEST_ATTEMPTS}+0);
	}
	else {
		Test::LectroTest->import(trials => 100);
	}
}

sub Error () {
	Frequency(
		[40, Float],
		[40, List(Float, 'length' => 2)],
		[10, List(Float, 'length' => 1)],
		[10, Unit(undef) ],
	)
}

sub WithError () {
	Concat(
		Float,
		List(
			Error,
			'length' => [0, 20]
		)
	)
}

sub WithErrorSmall () {
	Concat(
		Float(range=>[0..20]),
		List(
			Error,
			'length' => [0, 10]
		)
	)
}

sub max {
	my $max = $_[0];
	for (@_) {
		$max = $_ if $_ > $max;
	}
	return $max;
}

sub min {
	my $min = $_[0];
	for (@_) {
		$min = $_ if $_ < $min;
	}
	return $min;
}

use constant EPS => 1e-8;
use constant EPS_UNSTABLE => 1e-6;
my $IsUnstable = 0;

sub numeq ($$) {
	return undef if not defined $_[0] or not defined $_[1];
	if ($IsUnstable) {
		return abs($_[0]-$_[1]) < abs(EPS_UNSTABLE * min($_[0], $_[1])) + EPS;
	}
	return abs($_[0]-$_[1]) < EPS;
}

sub undef_or_eq ($$) {
	if (not defined $_[0]) {
		if (not defined $_[1]) {
			return 1;
		}
		else {
			return undef;
		}
	}
	elsif (not defined $_[1]) {
		return undef;
	}

	if ($IsUnstable) {
		return abs($_[0]-$_[1]) < abs(EPS_UNSTABLE * min($_[0], $_[1])) + EPS;
	}
	return abs($_[0]-$_[1]) < EPS;
}

sub diag {
	print "# " . join('', @_) . "\n";
}

sub test_err_calc {
	my $sub = shift;
	my $res = shift;
	my $o1 = shift;
	my $o2 = shift;

	if (not @{$res->{errors}} == max(scalar(@{$o1->{errors}}), scalar(@{$o2->{errors}}))) {
		diag(
			"Number of errors in result is ",
			scalar(@{$res->{errors}}),
			" but the expected number of errors is ",
		   	max( scalar(@{$o1->{errors}}), scalar(@{$o2->{errors}}) )
		);
		return undef;
	}
	
	foreach my $no (0..$#{$res->{errors}}) {
		my $e1 = $o1->{errors}[$no];
		my $e2 = $o2->{errors}[$no];
		my $eres = $res->{errors}[$no];
		
		if (ref($e1) eq 'ARRAY') {
			return undef if not ref($eres) eq 'ARRAY' and @{$e1}!=1;
			if (ref($e2) eq 'ARRAY') {
				for (0..1) {
					my $cmperr = $sub->($e1->[$_]||0, $e2->[$_]||0, $o1->{num}, $o2->{num});
					if (not numeq( $cmperr||0, $eres->[$_]||0 )) {
						diag(
							"Error number $no (both are arys) is in the result: ",
							$eres->[$_]||0, " The expected result is: ", $cmperr||0
						);
						return undef;
					}
				}
			}
			else {
				for (0..1) {
					my $cmperr = $sub->($e1->[$_]||0, $e2||0, $o1->{num}, $o2->{num});
					if (not numeq( $cmperr||0, $eres->[$_]||0 )) {
						diag(
							"Error number $no (err1 is ary) is in the result: ",
							$eres->[$_]||0, " The expected result is: ", $cmperr||0
						);
						return undef;
					}
				}
			}
		}
		elsif (ref($e2) eq 'ARRAY') {
			return undef if not ref($eres) eq 'ARRAY' and @{$e2} != 1;
			for (0..1) {
				my $cmperr = $sub->($e1||0, $e2->[$_]||0, $o1->{num}, $o2->{num});
				if (not numeq( $cmperr||0, $eres->[$_]||0 )) {
					diag(
						"Error number $no (err2 is ary) is in the result: ",
						$eres->[$_]||0, " The expected result is: ", $cmperr||0
					);
					return undef;
				}
			}
		}
		else {
			my $cmperr =  $sub->($e1||0, $e2||0, $o1->{num}, $o2->{num});
			if ( not numeq( $cmperr||0, $eres||0 ) ) {
				diag("Error number $no is in the result: ", $eres||0, " The expected result is: ", $cmperr||0);
				return undef;
			}
		}
	}
	return 1;
}

my $Operator;

# add
$Operator = 'addition';
Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1->add($o2);
	my $num = $o1->{num} + $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt($_[0]**2 + $_[1]**2) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "add() method" ;

Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1 + $o2;
	my $num = $o1->{num} + $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt($_[0]**2 + $_[1]**2) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "overload: +" ;

Property {
	##[ x <- WithError, y <- Float ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(ref($_)eq'ARRAY' ? @$_ : $_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $y + $o1;
	my $num = $y + $o1->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt($_[0]**2 + $_[1]**2) };
	
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o2, $o1) or return undef;
	1
}, name => "overload: +, number" ;



# subtract
$Operator = 'subtraction';
Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1->subtract($o2);
	my $num = $o1->{num} - $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt($_[0]**2 + $_[1]**2) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "subtract() method" ;

Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1 - $o2;
	my $num = $o1->{num} - $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt($_[0]**2 + $_[1]**2) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "overload: -" ;

Property {
	##[ x <- WithError, y <- Float ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(ref($_)eq'ARRAY' ? @$_ : $_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $y - $o1;
	my $num = $y - $o1->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt($_[0]**2 + $_[1]**2) };
	
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o2, $o1) or return undef;
	1
}, name => "overload: -, number" ;




# multiply
$Operator = 'multiplication';
Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1->multiply($o2);
	my $num = $o1->{num} * $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt(($_[0]*$_[3])**2 + ($_[2]*$_[1])**2) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "multiply() method" ;

Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1 * $o2;
	my $num = $o1->{num} * $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt(($_[0]*$_[3])**2 + ($_[2]*$_[1])**2) };
	
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "overload: *" ;

Property {
	##[ x <- WithError, y <- Float ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(ref($_)eq'ARRAY' ? @$_ : $_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $y * $o1;
	my $num = $y * $o1->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt(($_[0]*$_[3])**2 + ($_[2]*$_[1])**2) };
	
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o2, $o1) or return undef;
	1
}, name => "overload: *, number" ;




# divide
$Operator = 'division';
Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1->divide($o2);
	my $num = $o1->{num} / $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt(($_[0]/$_[3])**2 + ($_[2]*$_[1]/$_[3]**2)**2) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "divide() method" ;

Property {
	##[ x <- WithError, y <- WithError ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $o1 / $o2;
	my $num = $o1->{num} / $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt(($_[0]/$_[3])**2 + ($_[2]*$_[1]/$_[3]**2)**2) };
	
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "overload: /" ;

Property {
	##[ x <- WithError, y <- Float ]##
	$IsUnstable = 0;
	my ($o1, $o2) = map {witherror(ref($_)eq'ARRAY' ? @$_ : $_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);

	my $res = $y / $o1;
	my $num = $y / $o1->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt( $_[0]**2/$_[3]**2 + $_[2]**2*$_[1]**2/$_[3]**4 ) };

	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o2, $o1) or return undef;
	1
}, name => "overload: /, number" ;




# exponentiate
$Operator = 'exponentiation';
Property {
	##[ x <- WithErrorSmall, y <- WithErrorSmall ]##
	$IsUnstable = 1;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);
	
	$tcon->retry if $y->[0] > 10 or $x->[0] > 50 or $y->[0] < 0;
	
	my $res = $o1->exponentiate($o2);
	my $num = $o1->{num} ** $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt( ($_[3]*$_[2]**($_[3]-1)*$_[0])**2 + (log($_[2])*$_[2]**$_[3]*$_[1])**2 ) };

	if ($o1->{num} < 0) {
		return 1 if not defined $res;
		return undef;
	}
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "exponentiate() method" ;

Property {
	##[ x <- WithErrorSmall, y <- WithErrorSmall ]##
	$IsUnstable = 1;
	my ($o1, $o2) = map {witherror(@$_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);
	
	$tcon->retry if $y->[0] > 10 or $x->[0] > 50 or $y->[0] < 0;

	my $res = $o1 ** $o2;
	my $num = $o1->{num} ** $o2->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt( ($_[3]*$_[2]**($_[3]-1)*$_[0])**2 + (log($_[2])*$_[2]**$_[3]*$_[1])**2 ) };
	
	if ($o1->{num} < 0) {
		return 1 if not defined $res;
		return undef;
	}
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o1, $o2) or return undef;
	1
}, name => "overload: **" ;

Property {
	##[ x <- WithErrorSmall, y <- Float(range => [0,10]) ]##
	$IsUnstable = 1;
	my ($o1, $o2) = map {witherror(ref($_)eq'ARRAY' ? @$_ : $_)} ($x, $y);
	return undef if grep {not defined} ($o1, $o2);
	
	$tcon->retry if $y > 10 or $x->[0] > 50 or $y < 0;

	my $res = $y ** $o1;
	my $num = $y ** $o1->{num};
	# parms: err1||0, err2||0, n1, n2
	my $err_calc = sub { sqrt( ($_[3]*$_[2]**($_[3]-1)*$_[0])**2 + (log($_[2])*$_[2]**$_[3]*$_[1])**2 ) };

	if ($y < 0) {
		return 1 if not defined $res;
		return undef;
	}
	return undef if not defined $res;
	return undef if not _INSTANCE($res, 'Number::WithError::LaTeX');

	if ( not numeq($res->{num}, $num) ) {
		diag("Result of $Operator is $res->{num}. Should be $num.");
	   	return undef;
	}

	test_err_calc($err_calc, $res, $o2, $o1) or return undef;
	1
}, name => "overload: **, number" ;


















1;