| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- <?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;
- }
- }
|