|
@@ -0,0 +1,340 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Fractions ("fractions"): simplify, add, subtract, multiply, divide, mixed
|
|
|
|
|
+ * numbers and "a fraction of a number".
|
|
|
|
|
+ *
|
|
|
|
|
+ * Built on the Fraction class in fraction/src (it does the arithmetic and
|
|
|
|
|
+ * puts every answer in its simplest form). The questions available grow with
|
|
|
|
|
+ * the student's level (accuracy this month, scaled up over their first 30
|
|
|
|
|
+ * answers):
|
|
|
|
|
+ *
|
|
|
|
|
+ * from the start simplify, add/subtract with the same denominator,
|
|
|
|
|
+ * a fraction of a number
|
|
|
|
|
+ * level 20+ improper fraction to mixed number, add/subtract where one
|
|
|
|
|
+ * denominator is a multiple of the other
|
|
|
|
|
+ * level 45+ add/subtract with different denominators, multiply
|
|
|
|
|
+ * level 70+ divide
|
|
|
|
|
+ *
|
|
|
|
|
+ * The expected answer is stored server-side (TkAnswerStore); the checker
|
|
|
|
|
+ * (answerChecker2::checkFractions) accepts any correct form but wants the
|
|
|
|
|
+ * answer in its simplest form.
|
|
|
|
|
+ */
|
|
|
|
|
+require_once dirname(__DIR__, 2) . '/fraction/src/Fraction.php';
|
|
|
|
|
+
|
|
|
|
|
+class fractions {
|
|
|
|
|
+
|
|
|
|
|
+ const SUBJECT = 'fractions';
|
|
|
|
|
+
|
|
|
|
|
+ private $user;
|
|
|
|
|
+ private $level = 0; // 0-100: how hard to make the questions
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param string $user User to supply the question for.
|
|
|
|
|
+ */
|
|
|
|
|
+ public function __construct($user) {
|
|
|
|
|
+ $this->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('<h4>Fractions</h4>') . "</br>\n";
|
|
|
|
|
+ $fString .= $frm->label(htmlspecialchars($problem['instruction'])) . "\n";
|
|
|
|
|
+ $fString .= '</br>';
|
|
|
|
|
+ $fString .= '<p style="font-size:140%;font-weight:bold;">' . htmlspecialchars($problem['question']) . '</p>';
|
|
|
|
|
+ $fString .= '<p class="smalltext">Type fractions like 3/4 and mixed numbers like 2 1/3.</p>';
|
|
|
|
|
+ $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 .= '</br>' . "\n";
|
|
|
|
|
+ $fString .= $frm->button('Answer', 'Check answer') . "\n";
|
|
|
|
|
+ $fString .= $frm->close();
|
|
|
|
|
+ $fString .= '</br></br>';
|
|
|
|
|
+
|
|
|
|
|
+ return $fString;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|