numeracy2.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <?php
  2. /**
  3. * Numeracy question generator (addition, subtraction, multiplication,
  4. * division, rounding).
  5. *
  6. * Changes from the original:
  7. * - the expected answer is stored server-side (TkAnswerStore) and the form
  8. * carries only an opaque token in `checkerData`
  9. * - difficulty() always returns ['percent' => .., 'count' => ..]
  10. * - Division() used $numbers[1]/$numbers[2] (index 2 doesn't exist, so the
  11. * divisor was null -> division by zero); now uses [0]/[1] and never divides
  12. * by zero
  13. * - Rounding() always produced 1.00 (int cast of an array); now scales with
  14. * the user's accuracy and works in whole cents
  15. * - subtraction can no longer go negative (numbers are always ordered)
  16. * - mt_rand() always receives integers (float args are deprecated in 8.1+)
  17. * - user/subject are escaped in SQL, and the hard-coded `e107.` database
  18. * name is replaced by MPREFIX
  19. * - the previous tkLastQuestion row is removed before inserting the new one
  20. */
  21. class numeracy2 {
  22. private $user;
  23. private $subSubject;
  24. /**
  25. * @param string $user User to supply the question for.
  26. * @param string $sub Sub-subject: addition|subtraction|multiplication|division|rounding
  27. */
  28. public function __construct($user, $sub) {
  29. $this->user = $user;
  30. $this->subSubject = $sub;
  31. }
  32. public function getNumeracy() {
  33. $difficulty = $this->difficulty();
  34. $question = $this->numeracySwitch($difficulty);
  35. if ($question === null) {
  36. e107::getRender()->tablerender('Numeracy', 'Unknown numeracy subject.');
  37. return;
  38. }
  39. $questionHtml = $this->htmlify($question);
  40. $db = e107::getDb();
  41. $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
  42. $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
  43. e107::getRender()->tablerender('Numeracy', $questionHtml);
  44. }
  45. private function numeracySwitch($difficulty) {
  46. switch ($this->subSubject) {
  47. case 'addition':
  48. return $this->basic('+', $difficulty);
  49. case 'subtraction':
  50. return $this->basic('-', $difficulty);
  51. case 'multiplication':
  52. return $this->basic('x', $difficulty);
  53. case 'rounding':
  54. return $this->Rounding($difficulty);
  55. case 'division':
  56. return $this->Division($difficulty);
  57. }
  58. return null;
  59. }
  60. /**
  61. * Difficulty based on the last month of answers for this sub-subject.
  62. *
  63. * @return array ['percent' => float 0-100, 'count' => int]
  64. */
  65. private function difficulty() {
  66. $db = e107::getDb();
  67. $stats = $db->retrieve(
  68. "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count
  69. FROM `" . MPREFIX . "tkReport`
  70. WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
  71. AND username = '" . $db->escape($this->user) . "'
  72. AND subject = '" . $db->escape($this->subSubject) . "'"
  73. );
  74. if (!is_array($stats) || (int) $stats['count'] === 0) {
  75. return ['percent' => 0, 'count' => 0];
  76. }
  77. return ['percent' => (float) $stats['percent'], 'count' => (int) $stats['count']];
  78. }
  79. /**
  80. * Two random whole numbers, larger first.
  81. */
  82. private function getNumbers($difficulty, $sign) {
  83. //TODO add finer grain control for multiplication here
  84. $count = (float) $difficulty['count'];
  85. $percent = (float) $difficulty['percent'];
  86. if ($sign == 'x' || $sign == '/') {
  87. $count = floor($count / 4);
  88. $percent = $percent / 6;
  89. }
  90. if ($count >= 300) {
  91. $max = (int) pow($count, ($percent / 100) * 2);
  92. } elseif ($count >= 200) {
  93. $max = (int) ($count + ($percent / 100) * 2);
  94. } elseif ($count >= 100) {
  95. $max = (int) ($percent * 2);
  96. } else {
  97. $max = (int) $count + 1;
  98. }
  99. return $this->flipNumbers(mt_rand(0, $max), mt_rand(0, $max));
  100. }
  101. private function flipNumbers($startingNumber, $endingNumber) {
  102. if ($startingNumber > $endingNumber) {
  103. return [$startingNumber, $endingNumber];
  104. }
  105. return [$endingNumber, $startingNumber];
  106. }
  107. private function basic($sign, $difficulty) {
  108. $numbers = $this->getNumbers($difficulty, $sign);
  109. $firstNumber = $numbers[0];
  110. $secondNumber = $numbers[1];
  111. $solution = [
  112. '+' => $firstNumber + $secondNumber,
  113. '-' => $firstNumber - $secondNumber,
  114. 'x' => $firstNumber * $secondNumber,
  115. ];
  116. return ['answer1' => $solution[$sign], 'question' => [$firstNumber, $sign, $secondNumber], 'type' => $this->subSubject];
  117. }
  118. private function Division($difficulty) {
  119. $numbers = $this->getNumbers($difficulty, '/');
  120. $quotient = max(1, $numbers[0]);
  121. $divisor = max(1, $numbers[1]);
  122. $dividend = $quotient * $divisor;
  123. return ['answer1' => $quotient, 'question' => [$dividend, '/', $divisor], 'type' => $this->subSubject];
  124. }
  125. /**
  126. * Round an amount of money to the nearest 5 cents. The amount grows with
  127. * the user's accuracy: up to $5.00 at 0%, up to $25.00 at 100%.
  128. */
  129. private function Rounding($difficulty) {
  130. $maxCents = (5 + (int) floor($difficulty['percent'] / 10) * 2) * 100;
  131. $cents = mt_rand(100, $maxCents);
  132. $roundedCents = (int) (round($cents / 5) * 5);
  133. return [
  134. 'answer1' => sprintf('%01.2f', $roundedCents / 100),
  135. 'question' => [sprintf('%01.2f', $cents / 100), 'r'],
  136. 'type' => $this->subSubject,
  137. ];
  138. }
  139. private function htmlify($question) {
  140. // Expected answer stays on the server; the form only carries a token.
  141. $token = TkAnswerStore::issue($this->user, $question);
  142. $instruction = ($this->subSubject === 'rounding')
  143. ? 'Round to the nearest 5 cents.'
  144. : 'Solve the following equation.';
  145. $frm = e107::getForm();
  146. $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
  147. $fString .= $frm->label('<h4>' . htmlspecialchars(ucfirst($this->subSubject)) . '</h4>') . "</br>\n";
  148. $fString .= $frm->label($instruction) . "\n";
  149. $fString .= '</br>';
  150. foreach ($question['question'] as $seq) {
  151. $fString .= $frm->label($seq . " &nbsp; ") . "\n";
  152. }
  153. $fString .= '</br></br>';
  154. $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
  155. $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
  156. $fString .= $frm->hidden('subject', $this->subSubject, ['id' => 'subject']);
  157. $fString .= '</br>' . "\n";
  158. $fString .= $frm->button('Answer', 'Check answer') . "\n";
  159. $fString .= $frm->close();
  160. $fString .= '</br></br>';
  161. return $fString;
  162. }
  163. }