#!/usr/bin/perl -w
# prtaddsub.pl - Generate whole number addition and subtraction worksheets
#
# To do: Add probabilities that operands themselves are negative

use CGI qw(escapeHTML);

my $cgi = new CGI();

# When to give up trying to generate a unique problem
my $max_tries = 1000;

#======================================================================
# Subroutines

#----------------------------------------------------------------------
sub fixed_format
{
	my ($num, $decimals) = @_;
	my $fraction = '';

	if ($decimals || 0)
	{
		$fraction = '.' . substr($num, -$decimals, $decimals, '');
	}

	while ($num =~ s/(\d)(\d\d\d)(,|$)/$1,$2$3/)
	{
	}

	return "$num$fraction";
}

#----------------------------------------------------------------------
sub show_header
{
	print <<HTML;
Content-type: text/html

<html>
<head>
<title>Addition and Subtraction Worksheets</title>
<meta name="description" value="Generate free, printable addition and subtraction worksheet problems and answers with fine control over the type and complexity." />
<meta name="keywords" value="addition worksheets,printable addition problems,subtraction worksheets,printable subtraction problems,addition drills,subtraction drills,math tools,math drills,teaching tools,carry,borrow,negative" />
<style type='text/css'>
body { font-family: sans-serif; }
h1 { font-size: 20pt; }
.problems { font-family: monospace; }
.problems thead { font-size: large; }
.problems tbody td { text-align: right; }
table.problems { margin: 0 5%; width: 90%; }
\@media print {
.noprint { display: none; }
}
</style>
</head>
<body>
HTML
}

#----------------------------------------------------------------------
sub show_footer
{
	print <<HTML;
</body>
</html>
HTML
}

#----------------------------------------------------------------------
my $checkboxes = [ ];

sub show_form
{
	my $numrows = $cgi->textfield('-name' => 'numrows', '-size' => 3,
	  '-default' => 7);
	my $numcols = $cgi->textfield('-name' => 'numcols', '-size' => 3,
	  '-default' => 5);

	my $fontsize = $cgi->textfield('-name' => 'fontsize', '-size' => 6,
	  '-default' => '20pt');

	my $probsubtract = $cgi->textfield('-name' => 'probsubtract',
	  '-size' => 3, '-default' => 50);

	my $maxdigits = $cgi->textfield('-name' => 'maxdigits', '-size' => 3,
	  '-default' => 2);

	my $minvalue = $cgi->textfield('-name' => 'minvalue', '-size' => 3,
	  '-default' => 1);

	my $decimals = $cgi->textfield('-name' => 'decimals', '-size' => 3,
	  '-default' => 0);

	my $probcarry = $cgi->textfield('-name' => 'probcarry',
	  '-size' => 3, '-default' => 70);

	my $probborrow = $cgi->textfield('-name' => 'probborrow',
	  '-size' => 3, '-default' => 70);

	my $probnegative = $cgi->textfield('-name' => 'probnegative',
	  '-size' => 3, '-default' => 0);

	my $heading = $cgi->textfield('-name' => 'heading', '-size' => 60);

	print <<HTML;
<h1>Generate Printable Addition and/or Subtraction Problems</h1>
<form method='POST'>
<table>
<tr><td title='# of rows down the page'>Rows of problems:</td>
<td>$numrows</td></tr>
<tr><td title='# of columns across the page'>Columns of problems:</td>
<td>$numcols</td></tr>
<tr><td title='1 pt = 1/72"; 1 px = 1 screen pixel'>Font size &amp; units (pt or px):</td>
<td>$fontsize</td></tr>
<tr><td title='Probability of a subtraction problem (i.e. 0 = all addition; 100 = all subtraction)'>Subtraction probability:</td>
<td>$probsubtract\%</td></tr>
<tr><td title='Maximum # of digits in an operand (not result)'>Maximum # of digits:</td>
<td>$maxdigits</td></tr>
<tr><td title='Minimum operand (often 0 or 1)'>Minimum operand:</td>
<td>$minvalue</td></tr>
<tr><td title='Number of fixed decimal places'>Decimal places:</td>
<td>$decimals</td></tr>
<tr><td title='Probability of carry in addition'>Carry probability:</td>
<td>$probcarry</td></tr>
<tr><td title='Probability of borrow in subtraction'>Borrow probability:</td><td>$probborrow</td></tr>
<tr><td title='Probability of a negative difference in subtraction (0 = never; 100 = always)'>Negative difference probability:</td>
<td>$probnegative\%</td></tr>
<tr><td title='Optional text to appear at the top of the worksheet'>Page heading:</td>
<td>$heading</td></tr>
<tr><td colspan='2'><input type='submit' value='Generate Problems' /></td></tr>
</table>
<input type='hidden' name='action' value='generate' />
</form>
HTML
}

#----------------------------------------------------------------------
sub generate_problems
{
	my @problems;
	my %problems;
	my $count = ($cgi->param('numrows') || 1) * ($cgi->param('numcols') || 1);
	my $tries = 0;
	my $probsubtract = $cgi->param('probsubtract');
	my $maxdigits = $cgi->param('maxdigits') || 1;
	my $minvalue = $cgi->param('minvalue') || 0;
	my $probcarry = $cgi->param('probcarry') || 0;
	my $probborrow = $cgi->param('probborrow') || 0;
	my $probnegative = $cgi->param('probnegative') || 0;

	$probborrow = 0 if ($maxdigits < 2);

	# Limit maximum number of problems
	$count = 1000 if ($count > 1000);

	while ($count > 0 && $tries < $max_tries)
	{
		my $and = ($probsubtract && (rand(100) < $probsubtract))? '-': '+';
		my $wantcb = rand(100) < (($and eq '+')? $probcarry: $probborrow);
		my ($op1, $op2, $didcb) = (0, 0, 0);

		for (my $place = 0; $place < $maxdigits; )
		{
			my $dig1 = int(rand 10);
			my $dig2 = int(rand 10);
			my $newcb = 0;

			if ($and eq '+' && $dig1 + $dig2 > 9)
			{
				next if (!$wantcb);
				++$newcb;
			}
			elsif ($and eq '-' && $dig2 > $dig1)
			{
				if (!$wantcb)
				{
					($dig1, $dig2) = ($dig2, $dig1);
				}
				else
				{
					++$newcb;
				}
			}

			#
			# Restart generation if we wanted a carry/borrow
			# and didn't get one.
			#
			if ($place == $maxdigits - 1 && $wantcb && !$didcb &&
			  !$newcb)
			{
				($op1, $op2, $place) = (0, 0, 0);
				next;
			}

			my $nop1 = $op1 + $dig1 * 10 ** $place;
			my $nop2 = $op2 + $dig2 * 10 ** $place;

			#
			# Always generate subtractions with positive results.
			# This allows us to honor probborrow even with
			# negative results.
			#
			next if ($and eq '-' && $place == $maxdigits - 1 &&
			  $nop1 < $nop2);

			($op1, $op2) = ($nop1, $nop2);
			$didcb += $newcb;
			++$place;
		}

		#
		# Now decide if we want a negative answer on subtraction
		#
		($op1, $op2) = ($op2, $op1)
		  if ($and eq '-' && $probnegative &&
		  rand(100) < $probnegative);

		my $answer = ($and eq '+')? ($op1 + $op2): ($op1 - $op2);
		my $problem = "$op1$and$op2=$answer";

		#
		# Make sure the problem is unique, the operands meet the
		# minimum value requirement, and that both operands are
		# not simultaneously 0.
		#
		if (!defined($problems{$problem}) &&
		  $op1 >= $minvalue && $op2 >= $minvalue &&
		  ($op1 != 0 || $op2 != 0))
		{
			push(@problems, $problem);
			$problems{$problem} = 1;
			$tries = 0;
			--$count;
		}
		else
		{
			#
			# Otherwise, just brute-force try to find a
			# satisfactory problem up to the try limit.
			#
			++$tries;
		}
	}

	$cgi->param('problems', join(';', @problems));
}

#----------------------------------------------------------------------
sub show_problems
{
	my ($with_answers) = @_;

	my $other_action = $with_answers? 'show_problems': 'show_answers';
	my $other_label = $with_answers? 'Show Problems': 'Show Answers';

	print <<HTML;
<div class='noprint' style='float: left;'>
<form method='POST'>
<input type='submit' value='New Problems' />
HTML
	show_hiddens($checkboxes, [ qw(action problems) ]);
	print "</form></div>\n";

	print <<HTML;
<div class='noprint' style='float: left;'>
<form method='POST'>
<input type='hidden' name='action' value='$other_action' />
<input type='submit' value='$other_label' />
HTML
	show_hiddens($checkboxes, [ 'action' ]);
	print "</form></div>\n";

	print "<div class='noprint' style='float: left;'>",
	  "&nbsp; Subtraction: ", escapeHTML($cgi->param('probsubtract')), "%",
	  ";&nbsp; Carry: ", escapeHTML($cgi->param('probcarry')), "%",
	  ";&nbsp; Borrow: ", escapeHTML($cgi->param('probborrow')), "%",
	  ";&nbsp; Negative: ", escapeHTML($cgi->param('probnegative')), "%",
	  ";&nbsp; >= ", escapeHTML($cgi->param('minvalue')),
	  "</div>\n";

	print "<br class='noprint' style='clear: both;' />\n";

	my $cols = $cgi->param('numcols') || 1;
	my $problem = $cgi->param('problems');
	my $decimals = $cgi->param('decimals');
	my $fontsize = $cgi->param('fontsize');

	$problem = '' unless (defined($problem));
	my @problems = split(';', $problem);

	print "<table class='problems'>\n";

	my $heading = $cgi->param('heading');
	if (defined($heading) && $heading ne '')
	{
		print "<thead><tr><td colspan='", $cols * 2 - 1, "'>",
		  escapeHTML($heading), "<br /><br /></td></tr></thead>\n";
	}

	$fontsize =~ s/[^\s\w]//g;
	print "<tbody style='font-size: $fontsize;'>\n";

	my $col = 0;
	while (@problems)
	{
		print (($col == 0)? "<tr>": "<td>&nbsp;</td>");
		$problem = shift(@problems);
		my ($op1, $and, $op2, $ans) = ($problem =~ /(\d+)(.)(\d+)=(.*)/);
		$op1 = fixed_format($op1, $decimals);
		$op2 = fixed_format($op2, $decimals);
		$ans = fixed_format($ans, $decimals);
		my $width = (length($ans) > length($op1))?
		  length($ans): length($op1);
		my $andop2 = "$and $op2";
		my $nbsp = '&nbsp;' x ($width - length($andop2));
		print "<td>$op1<br /><u>$nbsp$andop2</u><br />",
		  $with_answers? $ans: '&nbsp;', "<br /><br /></td>\n";

		if (++$col == $cols)
		{
			print "</tr>\n";
			$col = 0;
		}
	}

	print "</tr>\n" if ($col);
	print "</table>\n";
}

#----------------------------------------------------------------------
sub show_hiddens
{
	my ($includes, $excludes) = @_;
	my %includes = map(($_, 1), @{$includes || []});
	my %excludes = map(($_, 1), @{$excludes || []});

	foreach my $name ($cgi->param())
	{
		unless ($excludes{$name})
		{
			print $cgi->hidden('-name' => $name);
			delete $includes{$name};
		}
	}

	foreach my $name (keys %includes)
	{
		print $cgi->hidden('-name' => $name, '-value' => '');
	}
}

#======================================================================
# Main program

my $action = $cgi->param('action') || '';

show_header();

if ($action eq 'generate')
{
	generate_problems();
	show_problems(0);
}
elsif ($action eq 'show_problems')
{
	show_problems(0);
}
elsif ($action eq 'show_answers')
{
	show_problems(1);
}
else
{
	show_form();
}

show_footer();

# END
