|
@@ -1,197 +1,194 @@
|
|
|
<?php
|
|
<?php
|
|
|
|
|
|
|
|
-/*
|
|
|
|
|
- * To change this license header, choose License Headers in Project Properties.
|
|
|
|
|
- * To change this template file, choose Tools | Templates
|
|
|
|
|
- * and open the template in the editor.
|
|
|
|
|
- */
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Description of numeracy
|
|
|
|
|
|
|
+ * Numeracy question generator (addition, subtraction, multiplication,
|
|
|
|
|
+ * division, rounding).
|
|
|
*
|
|
*
|
|
|
- * @author gregory
|
|
|
|
|
|
|
+ * Changes from the original:
|
|
|
|
|
+ * - the expected answer is stored server-side (TkAnswerStore) and the form
|
|
|
|
|
+ * carries only an opaque token in `checkerData`
|
|
|
|
|
+ * - difficulty() always returns ['percent' => .., 'count' => ..]
|
|
|
|
|
+ * - Division() used $numbers[1]/$numbers[2] (index 2 doesn't exist, so the
|
|
|
|
|
+ * divisor was null -> division by zero); now uses [0]/[1] and never divides
|
|
|
|
|
+ * by zero
|
|
|
|
|
+ * - Rounding() always produced 1.00 (int cast of an array); now scales with
|
|
|
|
|
+ * the user's accuracy and works in whole cents
|
|
|
|
|
+ * - subtraction can no longer go negative (numbers are always ordered)
|
|
|
|
|
+ * - mt_rand() always receives integers (float args are deprecated in 8.1+)
|
|
|
|
|
+ * - user/subject are escaped in SQL, and the hard-coded `e107.` database
|
|
|
|
|
+ * name is replaced by MPREFIX
|
|
|
|
|
+ * - the previous tkLastQuestion row is removed before inserting the new one
|
|
|
*/
|
|
*/
|
|
|
class numeracy2 {
|
|
class numeracy2 {
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
private $user;
|
|
private $user;
|
|
|
private $subSubject;
|
|
private $subSubject;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Setup new numbers engine
|
|
|
|
|
- *
|
|
|
|
|
- * @param bool $test Enable debug mode.
|
|
|
|
|
|
|
+ * @param string $user User to supply the question for.
|
|
|
|
|
+ * @param string $sub Sub-subject: addition|subtraction|multiplication|division|rounding
|
|
|
*/
|
|
*/
|
|
|
- public function __construct($user,$sub) {
|
|
|
|
|
-
|
|
|
|
|
|
|
+ public function __construct($user, $sub) {
|
|
|
$this->user = $user;
|
|
$this->user = $user;
|
|
|
$this->subSubject = $sub;
|
|
$this->subSubject = $sub;
|
|
|
-
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public function getNumeracy() {
|
|
public function getNumeracy() {
|
|
|
-
|
|
|
|
|
- //print_a(['user'=>$this->user,'subSubject'=>$this->subSubject]);
|
|
|
|
|
$difficulty = $this->difficulty();
|
|
$difficulty = $this->difficulty();
|
|
|
$question = $this->numeracySwitch($difficulty);
|
|
$question = $this->numeracySwitch($difficulty);
|
|
|
|
|
+
|
|
|
|
|
+ if ($question === null) {
|
|
|
|
|
+ e107::getRender()->tablerender('Numeracy', 'Unknown numeracy subject.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
$questionHtml = $this->htmlify($question);
|
|
$questionHtml = $this->htmlify($question);
|
|
|
-
|
|
|
|
|
- e107::getDb()->insert('tkLastQuestion', ['user'=>$this->user,'LastQuestionHtml'=>base64_encode($questionHtml)]);
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ $db = e107::getDb();
|
|
|
|
|
+ $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'");
|
|
|
|
|
+ $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]);
|
|
|
|
|
+
|
|
|
e107::getRender()->tablerender('Numeracy', $questionHtml);
|
|
e107::getRender()->tablerender('Numeracy', $questionHtml);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
private function numeracySwitch($difficulty) {
|
|
private function numeracySwitch($difficulty) {
|
|
|
switch ($this->subSubject) {
|
|
switch ($this->subSubject) {
|
|
|
-
|
|
|
|
|
case 'addition':
|
|
case 'addition':
|
|
|
- return $this->basic('+',$difficulty);
|
|
|
|
|
|
|
+ return $this->basic('+', $difficulty);
|
|
|
case 'subtraction':
|
|
case 'subtraction':
|
|
|
- return $this->basic('-',$difficulty);
|
|
|
|
|
|
|
+ return $this->basic('-', $difficulty);
|
|
|
case 'multiplication':
|
|
case 'multiplication':
|
|
|
- return $this->basic('x',$difficulty);
|
|
|
|
|
|
|
+ return $this->basic('x', $difficulty);
|
|
|
case 'rounding':
|
|
case 'rounding':
|
|
|
return $this->Rounding($difficulty);
|
|
return $this->Rounding($difficulty);
|
|
|
case 'division':
|
|
case 'division':
|
|
|
return $this->Division($difficulty);
|
|
return $this->Division($difficulty);
|
|
|
}
|
|
}
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Return the difficulty based on previously answered questions.
|
|
|
|
|
|
|
+ * Difficulty based on the last month of answers for this sub-subject.
|
|
|
*
|
|
*
|
|
|
- * @return int Difficulty as percent.
|
|
|
|
|
|
|
+ * @return array ['percent' => float 0-100, 'count' => int]
|
|
|
*/
|
|
*/
|
|
|
private function difficulty() {
|
|
private function difficulty() {
|
|
|
-
|
|
|
|
|
- $stats = e107::getDb()->retrieve("SELECT (SUM(correct)/COUNT(correct)*100) as percent,COUNT(correct) as count
|
|
|
|
|
- FROM e107.`e107_tkReport` WHERE
|
|
|
|
|
- answereddatestamp >= unix_timestamp((NOW()) - INTERVAL 1 MONTH)
|
|
|
|
|
- AND username = '$this->user' AND subject = '$this->subSubject';");
|
|
|
|
|
-
|
|
|
|
|
- if ($stats["count"]==0) {
|
|
|
|
|
- return 1;
|
|
|
|
|
- } else {
|
|
|
|
|
- return $stats;
|
|
|
|
|
|
|
+ $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 = '" . $db->escape($this->subSubject) . "'"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (!is_array($stats) || (int) $stats['count'] === 0) {
|
|
|
|
|
+ return ['percent' => 0, 'count' => 0];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ return ['percent' => (float) $stats['percent'], 'count' => (int) $stats['count']];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function getNumbers($difficulty,$sign){
|
|
|
|
|
-//TODO add finer grain control for multiplication here
|
|
|
|
|
- if($sign=='x'||$sign=='/')
|
|
|
|
|
- {
|
|
|
|
|
- $difficulty['count'] = (int)$difficulty['count']/4;
|
|
|
|
|
- $difficulty['percent'] = (int)$difficulty['percent']/6;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if($difficulty['count']>=300) {
|
|
|
|
|
- $startingNumber = mt_rand(0, (int) pow($difficulty['count'],($difficulty['percent']/100)*2));
|
|
|
|
|
- $endingNumber = mt_rand(0, (int) pow($difficulty['count'],($difficulty['percent']/100)*2));
|
|
|
|
|
- $returnNumbers = [$startingNumber,$endingNumber];
|
|
|
|
|
- }elseif($difficulty['count']>=200){
|
|
|
|
|
- $startingNumber = mt_rand(0, (int) $difficulty['count']+($difficulty['percent']/100)*2);
|
|
|
|
|
- $endingNumber = mt_rand(0, (int) $difficulty['count']+($difficulty['percent']/100)*2);
|
|
|
|
|
- $returnNumbers = $this->flipNumbers($startingNumber,$endingNumber);
|
|
|
|
|
- }elseif($difficulty['count']>=100){
|
|
|
|
|
- $startingNumber = mt_rand(0, (int) ($difficulty['percent'])*2);
|
|
|
|
|
- $endingNumber = mt_rand(0, (int) ($difficulty['percent'])*2);
|
|
|
|
|
- $returnNumbers = $this->flipNumbers($startingNumber,$endingNumber);
|
|
|
|
|
- }else{
|
|
|
|
|
- $startingNumber = mt_rand(0, $difficulty['count']+1);
|
|
|
|
|
- $endingNumber = mt_rand(0, $difficulty['count']+1);
|
|
|
|
|
- $returnNumbers = $this->flipNumbers($startingNumber,$endingNumber);
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Two random whole numbers, larger first.
|
|
|
|
|
+ */
|
|
|
|
|
+ private function getNumbers($difficulty, $sign) {
|
|
|
|
|
+ //TODO add finer grain control for multiplication here
|
|
|
|
|
+ $count = (float) $difficulty['count'];
|
|
|
|
|
+ $percent = (float) $difficulty['percent'];
|
|
|
|
|
+
|
|
|
|
|
+ if ($sign == 'x' || $sign == '/') {
|
|
|
|
|
+ $count = floor($count / 4);
|
|
|
|
|
+ $percent = $percent / 6;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- //print_a(['Original'=>['1'=>$startingNumber,'2'=>$endingNumber],'FlipNumbers'=>$returnNumbers,'user'=>$this->user,'diff'=>$difficulty]);
|
|
|
|
|
- return $returnNumbers;
|
|
|
|
|
|
|
+ if ($count >= 300) {
|
|
|
|
|
+ $max = (int) pow($count, ($percent / 100) * 2);
|
|
|
|
|
+ } elseif ($count >= 200) {
|
|
|
|
|
+ $max = (int) ($count + ($percent / 100) * 2);
|
|
|
|
|
+ } elseif ($count >= 100) {
|
|
|
|
|
+ $max = (int) ($percent * 2);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $max = (int) $count + 1;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ return $this->flipNumbers(mt_rand(0, $max), mt_rand(0, $max));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function flipNumbers($startingNumber,$endingNumber){
|
|
|
|
|
- if($startingNumber>$endingNumber){
|
|
|
|
|
- return [$startingNumber,$endingNumber];
|
|
|
|
|
- }else{
|
|
|
|
|
- return [$endingNumber,$startingNumber];
|
|
|
|
|
|
|
+ private function flipNumbers($startingNumber, $endingNumber) {
|
|
|
|
|
+ if ($startingNumber > $endingNumber) {
|
|
|
|
|
+ return [$startingNumber, $endingNumber];
|
|
|
}
|
|
}
|
|
|
|
|
+ return [$endingNumber, $startingNumber];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private function basic($sign,$difficulty) {
|
|
|
|
|
-
|
|
|
|
|
- $numbers = $this->getNumbers($difficulty,$sign);
|
|
|
|
|
|
|
|
|
|
|
|
+ private function basic($sign, $difficulty) {
|
|
|
|
|
+ $numbers = $this->getNumbers($difficulty, $sign);
|
|
|
$firstNumber = $numbers[0];
|
|
$firstNumber = $numbers[0];
|
|
|
$secondNumber = $numbers[1];
|
|
$secondNumber = $numbers[1];
|
|
|
- $TempQuestion = [];
|
|
|
|
|
-
|
|
|
|
|
- $TempQuestion[] = $firstNumber;
|
|
|
|
|
- $TempQuestion[] = $sign;
|
|
|
|
|
- $TempQuestion[] = $secondNumber;
|
|
|
|
|
-
|
|
|
|
|
- $solution['+'] = ($firstNumber + $secondNumber);
|
|
|
|
|
- $solution['-'] = ($firstNumber - $secondNumber);
|
|
|
|
|
- $solution['x'] = ($firstNumber * $secondNumber);
|
|
|
|
|
-
|
|
|
|
|
- return [ 'answer1'=>$solution[$sign], 'question'=>$TempQuestion, 'type'=>$this->subSubject];
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ $solution = [
|
|
|
|
|
+ '+' => $firstNumber + $secondNumber,
|
|
|
|
|
+ '-' => $firstNumber - $secondNumber,
|
|
|
|
|
+ 'x' => $firstNumber * $secondNumber,
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ return ['answer1' => $solution[$sign], 'question' => [$firstNumber, $sign, $secondNumber], 'type' => $this->subSubject];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private function Division($difficulty) {
|
|
private function Division($difficulty) {
|
|
|
-
|
|
|
|
|
- $numbers = $this->getNumbers($difficulty,'/');
|
|
|
|
|
-
|
|
|
|
|
- $firstNumber = $numbers[1];
|
|
|
|
|
- $secondNumber = $numbers[2];
|
|
|
|
|
- $divNumber = $firstNumber * $secondNumber;
|
|
|
|
|
- $TempQuestion = [];
|
|
|
|
|
-
|
|
|
|
|
- $TempQuestion[] = $divNumber;
|
|
|
|
|
- $TempQuestion[] = '/';
|
|
|
|
|
- $TempQuestion[] = $secondNumber;
|
|
|
|
|
-
|
|
|
|
|
- $solution['/'] = ($divNumber / $secondNumber);
|
|
|
|
|
-
|
|
|
|
|
- return [ 'answer1'=>$solution['/'], 'question'=>$TempQuestion, 'type'=>$this->subSubject];
|
|
|
|
|
-
|
|
|
|
|
|
|
+ $numbers = $this->getNumbers($difficulty, '/');
|
|
|
|
|
+ $quotient = max(1, $numbers[0]);
|
|
|
|
|
+ $divisor = max(1, $numbers[1]);
|
|
|
|
|
+ $dividend = $quotient * $divisor;
|
|
|
|
|
+
|
|
|
|
|
+ return ['answer1' => $quotient, 'question' => [$dividend, '/', $divisor], 'type' => $this->subSubject];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private function Rounding($difficulty) {
|
|
|
|
|
-
|
|
|
|
|
- $firstNumber = sprintf("%01.2f", (float) rand(100, (int) ($difficulty) * 100) / 100);
|
|
|
|
|
- $TempQuestion = [];
|
|
|
|
|
-
|
|
|
|
|
- $TempQuestion[] = $firstNumber;
|
|
|
|
|
- $TempQuestion[] = 'r';
|
|
|
|
|
-
|
|
|
|
|
- $solution['r'] = (sprintf("%01.2f", round($firstNumber / 5, 2) * 5));
|
|
|
|
|
-
|
|
|
|
|
- return [ 'answer1'=>$solution['r'], 'question'=>$TempQuestion, 'type'=>$this->subSubject];
|
|
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Round an amount of money to the nearest 5 cents. The amount grows with
|
|
|
|
|
+ * the user's accuracy: up to $5.00 at 0%, up to $25.00 at 100%.
|
|
|
|
|
+ */
|
|
|
|
|
+ private function Rounding($difficulty) {
|
|
|
|
|
+ $maxCents = (5 + (int) floor($difficulty['percent'] / 10) * 2) * 100;
|
|
|
|
|
+ $cents = mt_rand(100, $maxCents);
|
|
|
|
|
+ $roundedCents = (int) (round($cents / 5) * 5);
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'answer1' => sprintf('%01.2f', $roundedCents / 100),
|
|
|
|
|
+ 'question' => [sprintf('%01.2f', $cents / 100), 'r'],
|
|
|
|
|
+ 'type' => $this->subSubject,
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private function htmlify($question) {
|
|
private function htmlify($question) {
|
|
|
-
|
|
|
|
|
|
|
+ // Expected answer stays on the server; the form only carries a token.
|
|
|
|
|
+ $token = TkAnswerStore::issue($this->user, $question);
|
|
|
|
|
+
|
|
|
|
|
+ $instruction = ($this->subSubject === 'rounding')
|
|
|
|
|
+ ? 'Round to the nearest 5 cents.'
|
|
|
|
|
+ : 'Solve the following equation.';
|
|
|
|
|
+
|
|
|
$frm = e107::getForm();
|
|
$frm = e107::getForm();
|
|
|
|
|
|
|
|
$fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
|
|
$fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']);
|
|
|
- $fString .= $frm->label('<h4>'.ucfirst($this->subSubject).'</h4>') . "</br>\n";
|
|
|
|
|
- $fString .= $frm->label('Solve the following equation.') . "\n";
|
|
|
|
|
|
|
+ $fString .= $frm->label('<h4>' . htmlspecialchars(ucfirst($this->subSubject)) . '</h4>') . "</br>\n";
|
|
|
|
|
+ $fString .= $frm->label($instruction) . "\n";
|
|
|
$fString .= '</br>';
|
|
$fString .= '</br>';
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
foreach ($question['question'] as $seq) {
|
|
foreach ($question['question'] as $seq) {
|
|
|
$fString .= $frm->label($seq . " ") . "\n";
|
|
$fString .= $frm->label($seq . " ") . "\n";
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
$fString .= '</br></br>';
|
|
$fString .= '</br></br>';
|
|
|
$fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
|
|
$fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n";
|
|
|
- $fString .= $frm->hidden('checkerData', TkAnswerStore::issue($this->user, $question), ['id' => 'checkerData']);
|
|
|
|
|
|
|
+ $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']);
|
|
|
$fString .= $frm->hidden('subject', $this->subSubject, ['id' => 'subject']);
|
|
$fString .= $frm->hidden('subject', $this->subSubject, ['id' => 'subject']);
|
|
|
$fString .= '</br>' . "\n";
|
|
$fString .= '</br>' . "\n";
|
|
|
- $fString .= e107::getForm()->button('Answer', 'Check answer') . "\n";
|
|
|
|
|
- $fString .= e107::getForm()->close();
|
|
|
|
|
|
|
+ $fString .= $frm->button('Answer', 'Check answer') . "\n";
|
|
|
|
|
+ $fString .= $frm->close();
|
|
|
$fString .= '</br></br>';
|
|
$fString .= '</br></br>';
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return $fString;
|
|
return $fString;
|
|
|
-
|
|
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
}
|
|
}
|