fractions.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <?php
  2. /**
  3. * Fractions ("fractions"): simplify, add, subtract, multiply, divide, mixed
  4. * numbers and "a fraction of a number".
  5. *
  6. * Built on the Fraction class in fraction/src (it does the arithmetic and
  7. * puts every answer in its simplest form). The questions available grow with
  8. * the student's level (accuracy this month, scaled up over their first 30
  9. * answers):
  10. *
  11. * from the start simplify, add/subtract with the same denominator,
  12. * a fraction of a number
  13. * level 20+ improper fraction to mixed number, add/subtract where one
  14. * denominator is a multiple of the other
  15. * level 45+ add/subtract with different denominators, multiply
  16. * level 70+ divide
  17. *
  18. * The expected answer is stored server-side (TkAnswerStore); the checker
  19. * (answerChecker2::checkFractions) accepts any correct form but wants the
  20. * answer in its simplest form.
  21. */
  22. require_once dirname(__DIR__, 2) . '/fraction/src/Fraction.php';
  23. class fractions {
  24. const SUBJECT = 'fractions';
  25. private $user;
  26. private $level = 0; // 0-100: how hard to make the questions
  27. /**
  28. * @param string $user User to supply the question for.
  29. */
  30. public function __construct($user) {
  31. $this->user = $user;
  32. }
  33. /**
  34. * Make a question, show it, and remember it as the student's open question.
  35. */
  36. public function getFractions() {
  37. $problem = $this->getProblem();
  38. $questionHtml = $this->htmlify($problem);
  39. $db = e107::getDb();
  40. $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
  41. $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
  42. e107::getRender()->tablerender('Fractions', $questionHtml);
  43. }
  44. /**
  45. * Make a question without showing it.
  46. *
  47. * @return array ['answer1' => string, 'question' => string, 'type' => 'fractions',
  48. * 'format' => 'simplest'|'mixed', 'instruction' => string]
  49. */
  50. public function getProblem() {
  51. $this->level = $this->difficulty();
  52. $kinds = ['simplify', 'sameDenominators', 'fractionOf'];
  53. if ($this->level >= 20) {
  54. $kinds[] = 'mixedNumber';
  55. $kinds[] = 'oneDenominatorMultiple';
  56. }
  57. if ($this->level >= 45) {
  58. $kinds[] = 'unlikeDenominators';
  59. $kinds[] = 'multiply';
  60. }
  61. if ($this->level >= 70) {
  62. $kinds[] = 'divide';
  63. }
  64. $kind = $kinds[array_rand($kinds)];
  65. return $this->$kind();
  66. }
  67. /**
  68. * How hard to make the questions, 0-100: last month's accuracy, scaled up
  69. * over the first 30 answers.
  70. *
  71. * @return float
  72. */
  73. private function difficulty() {
  74. $db = e107::getDb();
  75. $stats = $db->retrieve(
  76. "SELECT (SUM(correct)/COUNT(correct)*100) AS percent, COUNT(correct) AS count
  77. FROM `" . MPREFIX . "tkReport`
  78. WHERE answereddatestamp >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH)
  79. AND username = '" . $db->escape($this->user) . "' AND subject = '" . self::SUBJECT . "'"
  80. );
  81. if (!is_array($stats) || (int) $stats['count'] === 0) {
  82. return 0.0;
  83. }
  84. $sample = min(1.0, ((int) $stats['count']) / 30);
  85. return max(0.0, min(100.0, (float) $stats['percent'])) * $sample;
  86. }
  87. /**
  88. * A random whole number from $min up to $maxAtFull. At level 0 the top of
  89. * the range is a quarter of the way there; at level 100 it is all of it.
  90. */
  91. private function num($min, $maxAtFull) {
  92. $span = max(0, $maxAtFull - $min);
  93. $max = $min + (int) ceil($span * (0.25 + 0.75 * $this->level / 100));
  94. return mt_rand($min, max($min, $max));
  95. }
  96. private function gcd($a, $b) {
  97. $a = abs($a);
  98. $b = abs($b);
  99. while ($b > 0) {
  100. $t = $a % $b;
  101. $a = $b;
  102. $b = $t;
  103. }
  104. return $a;
  105. }
  106. /**
  107. * A numerator from 1 to $d - 1 that shares no factor with $d.
  108. */
  109. private function coprimeNumerator($d) {
  110. for ($i = 0; $i < 50; $i++) {
  111. $n = mt_rand(1, $d - 1);
  112. if ($this->gcd($n, $d) === 1) {
  113. return $n;
  114. }
  115. }
  116. return 1;
  117. }
  118. private function problem($question, Fraction $answer, $instruction, $format = 'simplest') {
  119. return [
  120. 'answer1' => (string) $answer,
  121. 'question' => $question,
  122. 'type' => self::SUBJECT,
  123. 'format' => $format,
  124. 'instruction' => $instruction,
  125. ];
  126. }
  127. // ---- the questions
  128. /** Simplify 6/8 -> 3/4 */
  129. private function simplify() {
  130. $d = $this->num(3, 12);
  131. $n = $this->coprimeNumerator($d);
  132. $k = $this->num(2, 6);
  133. return $this->problem(
  134. 'Simplify ' . ($n * $k) . '/' . ($d * $k),
  135. new Fraction($n, $d),
  136. 'Write this fraction in its simplest form.'
  137. );
  138. }
  139. /** 3/8 + 1/8, 5/9 - 2/9 */
  140. private function sameDenominators() {
  141. $d = $this->num(4, 12);
  142. if (mt_rand(0, 1) === 1) {
  143. if ($this->level >= 20) {
  144. $a = mt_rand(1, $d - 1);
  145. $b = mt_rand(1, $d - 1); // the total can be more than 1
  146. } else {
  147. $a = mt_rand(1, $d - 2);
  148. $b = mt_rand(1, $d - 1 - $a); // the total stays under 1
  149. }
  150. $answer = (new Fraction($a, $d))->add(new Fraction($b, $d));
  151. $question = $a . '/' . $d . ' + ' . $b . '/' . $d;
  152. } else {
  153. $a = mt_rand(2, $d - 1);
  154. $b = mt_rand(1, $a - 1);
  155. $answer = (new Fraction($a, $d))->subtract(new Fraction($b, $d));
  156. $question = $a . '/' . $d . ' − ' . $b . '/' . $d;
  157. }
  158. return $this->problem($question, $answer, 'Work it out and write your answer in its simplest form.');
  159. }
  160. /** 3/4 of 20 -> 15 */
  161. private function fractionOf() {
  162. $d = $this->num(2, 10);
  163. $n = mt_rand(1, $d - 1);
  164. $k = $this->num(2, 12);
  165. return $this->problem(
  166. $n . '/' . $d . ' of ' . ($d * $k),
  167. new Fraction($n * $k),
  168. 'Work it out. Your answer is a whole number.'
  169. );
  170. }
  171. /** 17/5 as a mixed number -> 3 2/5 */
  172. private function mixedNumber() {
  173. $d = $this->num(2, 10);
  174. $r = $this->coprimeNumerator($d);
  175. $whole = $this->num(1, 6);
  176. $n = $whole * $d + $r;
  177. return $this->problem(
  178. $n . '/' . $d . ' as a mixed number',
  179. new Fraction($n, $d),
  180. 'Write this improper fraction as a mixed number, like 2 1/3.',
  181. 'mixed'
  182. );
  183. }
  184. /** 1/2 + 3/8: one denominator is a multiple of the other */
  185. private function oneDenominatorMultiple() {
  186. for ($i = 0; $i < 20; $i++) {
  187. $d1 = $this->num(2, 6);
  188. $d2 = $d1 * $this->num(2, 4);
  189. $first = [mt_rand(1, $d1 - 1), $d1];
  190. $second = [mt_rand(1, $d2 - 1), $d2];
  191. if (mt_rand(0, 1) === 1) {
  192. list($first, $second) = [$second, $first];
  193. }
  194. $problem = $this->addOrSubtract($first, $second);
  195. if ($problem !== null) {
  196. return $problem;
  197. }
  198. }
  199. return $this->addOrSubtract($first, $second, true);
  200. }
  201. /** 2/3 + 1/4: different denominators, neither a multiple of the other */
  202. private function unlikeDenominators() {
  203. for ($i = 0; $i < 20; $i++) {
  204. $d1 = $this->num(2, 8);
  205. $d2 = $this->num(2, 8);
  206. if ($d1 === $d2 || $d1 % $d2 === 0 || $d2 % $d1 === 0) {
  207. continue;
  208. }
  209. $first = [mt_rand(1, $d1 - 1), $d1];
  210. $second = [mt_rand(1, $d2 - 1), $d2];
  211. $problem = $this->addOrSubtract($first, $second);
  212. if ($problem !== null) {
  213. return $problem;
  214. }
  215. }
  216. return $this->addOrSubtract([1, 3], [1, 4], true);
  217. }
  218. /** 2/3 × 3/4 */
  219. private function multiply() {
  220. $b = $this->num(2, 9);
  221. $d = $this->num(2, 9);
  222. $a = mt_rand(1, $b - 1);
  223. $c = mt_rand(1, $d - 1);
  224. return $this->problem(
  225. $a . '/' . $b . ' × ' . $c . '/' . $d,
  226. (new Fraction($a, $b))->multiply(new Fraction($c, $d)),
  227. 'Work it out and write your answer in its simplest form.'
  228. );
  229. }
  230. /** 1/2 ÷ 1/4 */
  231. private function divide() {
  232. $b = $this->num(2, 9);
  233. $d = $this->num(2, 9);
  234. $a = mt_rand(1, $b - 1);
  235. $c = mt_rand(1, $d - 1);
  236. return $this->problem(
  237. $a . '/' . $b . ' ÷ ' . $c . '/' . $d,
  238. (new Fraction($a, $b))->divide(new Fraction($c, $d)),
  239. 'Work it out and write your answer in its simplest form.'
  240. );
  241. }
  242. /**
  243. * Add or subtract two fractions, each given as [numerator, denominator].
  244. * For a subtraction the larger fraction goes first so the answer is above
  245. * zero; two equal fractions give null (nothing to subtract).
  246. *
  247. * @return array|null
  248. */
  249. private function addOrSubtract(array $first, array $second, $forceAdd = false) {
  250. $add = $forceAdd || (mt_rand(0, 1) === 1);
  251. if (!$add) {
  252. $left = $first[0] * $second[1];
  253. $right = $second[0] * $first[1];
  254. if ($left === $right) {
  255. return null;
  256. }
  257. if ($left < $right) {
  258. list($first, $second) = [$second, $first];
  259. }
  260. }
  261. $one = new Fraction($first[0], $first[1]);
  262. $two = new Fraction($second[0], $second[1]);
  263. if ($add) {
  264. $answer = $one->add($two);
  265. $operator = ' + ';
  266. } else {
  267. $answer = $one->subtract($two);
  268. $operator = ' − ';
  269. }
  270. return $this->problem(
  271. $first[0] . '/' . $first[1] . $operator . $second[0] . '/' . $second[1],
  272. $answer,
  273. 'Work it out and write your answer in its simplest form.'
  274. );
  275. }
  276. private function htmlify($problem) {
  277. // Expected answer stays on the server; the form only carries a token.
  278. $token = TkAnswerStore::issue($this->user, $problem);
  279. $frm = e107::getForm();
  280. $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
  281. $fString .= $frm->label('<h4>Fractions</h4>') . "</br>\n";
  282. $fString .= $frm->label(htmlspecialchars($problem['instruction'])) . "\n";
  283. $fString .= '</br>';
  284. $fString .= '<p style="font-size:140%;font-weight:bold;">' . htmlspecialchars($problem['question']) . '</p>';
  285. $fString .= '<p class="smalltext">Type fractions like 3/4 and mixed numbers like 2 1/3.</p>';
  286. $fString .= $frm->text('a1', '', 20, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
  287. $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
  288. $fString .= $frm->hidden('subject', self::SUBJECT, ['id' => 'subject']);
  289. $fString .= '</br>' . "\n";
  290. $fString .= $frm->button('Answer', 'Check answer') . "\n";
  291. $fString .= $frm->close();
  292. $fString .= '</br></br>';
  293. return $fString;
  294. }
  295. }