.., 'count' => ..] * - Division() used $numbers[1]/$numbers[2] (index 2 doesn't exist, so the * divisor was null -> division by zero); now uses [0]/[1] and never divides * by zero * - Rounding() always produced 1.00 (int cast of an array); now scales with * the user's accuracy and works in whole cents * - subtraction can no longer go negative (numbers are always ordered) * - mt_rand() always receives integers (float args are deprecated in 8.1+) * - user/subject are escaped in SQL, and the hard-coded `e107.` database * name is replaced by MPREFIX * - the previous tkLastQuestion row is removed before inserting the new one */ class numeracy2 { private $user; private $subSubject; /** * @param string $user User to supply the question for. * @param string $sub Sub-subject: addition|subtraction|multiplication|division|rounding */ public function __construct($user, $sub) { $this->user = $user; $this->subSubject = $sub; } public function getNumeracy() { $difficulty = $this->difficulty(); $question = $this->numeracySwitch($difficulty); if ($question === null) { e107::getRender()->tablerender('Numeracy', 'Unknown numeracy subject.'); return; } $questionHtml = $this->htmlify($question); $db = e107::getDb(); $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'"); $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]); e107::getRender()->tablerender('Numeracy', $questionHtml); } private function numeracySwitch($difficulty) { switch ($this->subSubject) { case 'addition': return $this->basic('+', $difficulty); case 'subtraction': return $this->basic('-', $difficulty); case 'multiplication': return $this->basic('x', $difficulty); case 'rounding': return $this->Rounding($difficulty); case 'division': return $this->Division($difficulty); } return null; } /** * Difficulty based on the last month of answers for this sub-subject. * * @return array ['percent' => float 0-100, 'count' => int] */ private function difficulty() { $db = e107::getDb(); $stats = $db->retrieve( "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count FROM `" . MPREFIX . "tkReport` WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH) AND username = '" . $db->escape($this->user) . "' AND subject = '" . $db->escape($this->subSubject) . "'" ); if (!is_array($stats) || (int) $stats['count'] === 0) { return ['percent' => 0, 'count' => 0]; } return ['percent' => (float) $stats['percent'], 'count' => (int) $stats['count']]; } /** * Two random whole numbers, larger first. */ private function getNumbers($difficulty, $sign) { //TODO add finer grain control for multiplication here $count = (float) $difficulty['count']; $percent = (float) $difficulty['percent']; if ($sign == 'x' || $sign == '/') { $count = floor($count / 4); $percent = $percent / 6; } if ($count >= 300) { $max = (int) pow($count, ($percent / 100) * 2); } elseif ($count >= 200) { $max = (int) ($count + ($percent / 100) * 2); } elseif ($count >= 100) { $max = (int) ($percent * 2); } else { $max = (int) $count + 1; } return $this->flipNumbers(mt_rand(0, $max), mt_rand(0, $max)); } private function flipNumbers($startingNumber, $endingNumber) { if ($startingNumber > $endingNumber) { return [$startingNumber, $endingNumber]; } return [$endingNumber, $startingNumber]; } private function basic($sign, $difficulty) { $numbers = $this->getNumbers($difficulty, $sign); $firstNumber = $numbers[0]; $secondNumber = $numbers[1]; $solution = [ '+' => $firstNumber + $secondNumber, '-' => $firstNumber - $secondNumber, 'x' => $firstNumber * $secondNumber, ]; return ['answer1' => $solution[$sign], 'question' => [$firstNumber, $sign, $secondNumber], 'type' => $this->subSubject]; } private function Division($difficulty) { $numbers = $this->getNumbers($difficulty, '/'); $quotient = max(1, $numbers[0]); $divisor = max(1, $numbers[1]); $dividend = $quotient * $divisor; return ['answer1' => $quotient, 'question' => [$dividend, '/', $divisor], 'type' => $this->subSubject]; } /** * Round an amount of money to the nearest 5 cents. The amount grows with * the user's accuracy: up to $5.00 at 0%, up to $25.00 at 100%. */ private function Rounding($difficulty) { $maxCents = (5 + (int) floor($difficulty['percent'] / 10) * 2) * 100; $cents = mt_rand(100, $maxCents); $roundedCents = (int) (round($cents / 5) * 5); return [ 'answer1' => sprintf('%01.2f', $roundedCents / 100), 'question' => [sprintf('%01.2f', $cents / 100), 'r'], 'type' => $this->subSubject, ]; } private function htmlify($question) { // Expected answer stays on the server; the form only carries a token. $token = TkAnswerStore::issue($this->user, $question); $instruction = ($this->subSubject === 'rounding') ? 'Round to the nearest 5 cents.' : 'Solve the following equation.'; $frm = e107::getForm(); $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']); $fString .= $frm->label('

' . htmlspecialchars(ucfirst($this->subSubject)) . '

') . "
\n"; $fString .= $frm->label($instruction) . "\n"; $fString .= '
'; foreach ($question['question'] as $seq) { $fString .= $frm->label($seq . "   ") . "\n"; } $fString .= '

'; $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n"; $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']); $fString .= $frm->hidden('subject', $this->subSubject, ['id' => 'subject']); $fString .= '
' . "\n"; $fString .= $frm->button('Answer', 'Check answer') . "\n"; $fString .= $frm->close(); $fString .= '

'; return $fString; } }