| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- <?php
- /**
- * Numeracy question generator (addition, subtraction, multiplication,
- * division, rounding).
- *
- * Changes from the original:
- * - the expected answer is stored server-side (TkAnswerStore) and the form
- * carries only an opaque token in `checkerData`
- * - difficulty() always returns ['percent' => .., '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('<h4>' . htmlspecialchars(ucfirst($this->subSubject)) . '</h4>') . "</br>\n";
- $fString .= $frm->label($instruction) . "\n";
- $fString .= '</br>';
- foreach ($question['question'] as $seq) {
- $fString .= $frm->label($seq . " ") . "\n";
- }
- $fString .= '</br></br>';
- $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 .= '</br>' . "\n";
- $fString .= $frm->button('Answer', 'Check answer') . "\n";
- $fString .= $frm->close();
- $fString .= '</br></br>';
- return $fString;
- }
- }
|