user = $user; } /** * Make a question, show it, and remember it as the student's open question. */ public function getFractions() { $problem = $this->getProblem(); $questionHtml = $this->htmlify($problem); $db = e107::getDb(); $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'"); $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]); e107::getRender()->tablerender('Fractions', $questionHtml); } /** * Make a question without showing it. * * @return array ['answer1' => string, 'question' => string, 'type' => 'fractions', * 'format' => 'simplest'|'mixed', 'instruction' => string] */ public function getProblem() { $this->level = $this->difficulty(); $kinds = ['simplify', 'sameDenominators', 'fractionOf']; if ($this->level >= 20) { $kinds[] = 'mixedNumber'; $kinds[] = 'oneDenominatorMultiple'; } if ($this->level >= 45) { $kinds[] = 'unlikeDenominators'; $kinds[] = 'multiply'; } if ($this->level >= 70) { $kinds[] = 'divide'; } $kind = $kinds[array_rand($kinds)]; return $this->$kind(); } /** * How hard to make the questions, 0-100: last month's accuracy, scaled up * over the first 30 answers. * * @return float */ 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 = '" . self::SUBJECT . "'" ); if (!is_array($stats) || (int) $stats['count'] === 0) { return 0.0; } $sample = min(1.0, ((int) $stats['count']) / 30); return max(0.0, min(100.0, (float) $stats['percent'])) * $sample; } /** * A random whole number from $min up to $maxAtFull. At level 0 the top of * the range is a quarter of the way there; at level 100 it is all of it. */ private function num($min, $maxAtFull) { $span = max(0, $maxAtFull - $min); $max = $min + (int) ceil($span * (0.25 + 0.75 * $this->level / 100)); return mt_rand($min, max($min, $max)); } private function gcd($a, $b) { $a = abs($a); $b = abs($b); while ($b > 0) { $t = $a % $b; $a = $b; $b = $t; } return $a; } /** * A numerator from 1 to $d - 1 that shares no factor with $d. */ private function coprimeNumerator($d) { for ($i = 0; $i < 50; $i++) { $n = mt_rand(1, $d - 1); if ($this->gcd($n, $d) === 1) { return $n; } } return 1; } private function problem($question, Fraction $answer, $instruction, $format = 'simplest') { return [ 'answer1' => (string) $answer, 'question' => $question, 'type' => self::SUBJECT, 'format' => $format, 'instruction' => $instruction, ]; } // ---- the questions /** Simplify 6/8 -> 3/4 */ private function simplify() { $d = $this->num(3, 12); $n = $this->coprimeNumerator($d); $k = $this->num(2, 6); return $this->problem( 'Simplify ' . ($n * $k) . '/' . ($d * $k), new Fraction($n, $d), 'Write this fraction in its simplest form.' ); } /** 3/8 + 1/8, 5/9 - 2/9 */ private function sameDenominators() { $d = $this->num(4, 12); if (mt_rand(0, 1) === 1) { if ($this->level >= 20) { $a = mt_rand(1, $d - 1); $b = mt_rand(1, $d - 1); // the total can be more than 1 } else { $a = mt_rand(1, $d - 2); $b = mt_rand(1, $d - 1 - $a); // the total stays under 1 } $answer = (new Fraction($a, $d))->add(new Fraction($b, $d)); $question = $a . '/' . $d . ' + ' . $b . '/' . $d; } else { $a = mt_rand(2, $d - 1); $b = mt_rand(1, $a - 1); $answer = (new Fraction($a, $d))->subtract(new Fraction($b, $d)); $question = $a . '/' . $d . ' − ' . $b . '/' . $d; } return $this->problem($question, $answer, 'Work it out and write your answer in its simplest form.'); } /** 3/4 of 20 -> 15 */ private function fractionOf() { $d = $this->num(2, 10); $n = mt_rand(1, $d - 1); $k = $this->num(2, 12); return $this->problem( $n . '/' . $d . ' of ' . ($d * $k), new Fraction($n * $k), 'Work it out. Your answer is a whole number.' ); } /** 17/5 as a mixed number -> 3 2/5 */ private function mixedNumber() { $d = $this->num(2, 10); $r = $this->coprimeNumerator($d); $whole = $this->num(1, 6); $n = $whole * $d + $r; return $this->problem( $n . '/' . $d . ' as a mixed number', new Fraction($n, $d), 'Write this improper fraction as a mixed number, like 2 1/3.', 'mixed' ); } /** 1/2 + 3/8: one denominator is a multiple of the other */ private function oneDenominatorMultiple() { for ($i = 0; $i < 20; $i++) { $d1 = $this->num(2, 6); $d2 = $d1 * $this->num(2, 4); $first = [mt_rand(1, $d1 - 1), $d1]; $second = [mt_rand(1, $d2 - 1), $d2]; if (mt_rand(0, 1) === 1) { list($first, $second) = [$second, $first]; } $problem = $this->addOrSubtract($first, $second); if ($problem !== null) { return $problem; } } return $this->addOrSubtract($first, $second, true); } /** 2/3 + 1/4: different denominators, neither a multiple of the other */ private function unlikeDenominators() { for ($i = 0; $i < 20; $i++) { $d1 = $this->num(2, 8); $d2 = $this->num(2, 8); if ($d1 === $d2 || $d1 % $d2 === 0 || $d2 % $d1 === 0) { continue; } $first = [mt_rand(1, $d1 - 1), $d1]; $second = [mt_rand(1, $d2 - 1), $d2]; $problem = $this->addOrSubtract($first, $second); if ($problem !== null) { return $problem; } } return $this->addOrSubtract([1, 3], [1, 4], true); } /** 2/3 × 3/4 */ private function multiply() { $b = $this->num(2, 9); $d = $this->num(2, 9); $a = mt_rand(1, $b - 1); $c = mt_rand(1, $d - 1); return $this->problem( $a . '/' . $b . ' × ' . $c . '/' . $d, (new Fraction($a, $b))->multiply(new Fraction($c, $d)), 'Work it out and write your answer in its simplest form.' ); } /** 1/2 ÷ 1/4 */ private function divide() { $b = $this->num(2, 9); $d = $this->num(2, 9); $a = mt_rand(1, $b - 1); $c = mt_rand(1, $d - 1); return $this->problem( $a . '/' . $b . ' ÷ ' . $c . '/' . $d, (new Fraction($a, $b))->divide(new Fraction($c, $d)), 'Work it out and write your answer in its simplest form.' ); } /** * Add or subtract two fractions, each given as [numerator, denominator]. * For a subtraction the larger fraction goes first so the answer is above * zero; two equal fractions give null (nothing to subtract). * * @return array|null */ private function addOrSubtract(array $first, array $second, $forceAdd = false) { $add = $forceAdd || (mt_rand(0, 1) === 1); if (!$add) { $left = $first[0] * $second[1]; $right = $second[0] * $first[1]; if ($left === $right) { return null; } if ($left < $right) { list($first, $second) = [$second, $first]; } } $one = new Fraction($first[0], $first[1]); $two = new Fraction($second[0], $second[1]); if ($add) { $answer = $one->add($two); $operator = ' + '; } else { $answer = $one->subtract($two); $operator = ' − '; } return $this->problem( $first[0] . '/' . $first[1] . $operator . $second[0] . '/' . $second[1], $answer, 'Work it out and write your answer in its simplest form.' ); } private function htmlify($problem) { // Expected answer stays on the server; the form only carries a token. $token = TkAnswerStore::issue($this->user, $problem); $frm = e107::getForm(); $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']); $fString .= $frm->label('

Fractions

') . "
\n"; $fString .= $frm->label(htmlspecialchars($problem['instruction'])) . "\n"; $fString .= '
'; $fString .= '

' . htmlspecialchars($problem['question']) . '

'; $fString .= '

Type fractions like 3/4 and mixed numbers like 2 1/3.

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

'; return $fString; } }