Skip to content
Snippets Groups Projects
Commit 62276b73 authored by Chris Sangwin's avatar Chris Sangwin
Browse files

Fix varmatrix input for decimal option.

parent 26136cfd
No related branches found
No related tags found
No related merge requests found
......@@ -12,15 +12,7 @@ past development history is documented on [Development history](Development_hist
2. Shape of brackets surrounding matrix/var matrix input types now matches question level option for matrix parentheses. (TODO: possible option to change shape at the input level?)
3. Allow users to [systematically deploy](../CAS/Systematic_deployment.md) all variants of a question in a simple manner.
4. Tag inputs with 'aria-live' is 'assertive' for better screen reader support.
Support the use of a [comma as the decimal separator](Syntax_numbers.md)
1. (Done) Mechanism for Maxima to output LaTeX.
2. (Done) Mechanism to output expressions as they should be typed.
3. (Done) Question wide option for decimal separator added. TODO: how to give access to this?
4. Core input parsing mechanism for strict syntax.
5. Input parsing mechanism for weak syntax (but nobody has actually asked for this yet...).
6. Refactor matrix of variable size.
5. Add an option to support the use of a [comma as the decimal separator](Syntax_numbers.md).
TODO:
......
......@@ -2,7 +2,7 @@
Note, where the feature is listed as "(done)" means we have prototype code in the testing phase.
## Features to add for STACK 4.5 or later ##
## Features to add later ##
### Units inputs ###
......@@ -34,6 +34,7 @@ Note, where the feature is listed as "(done)" means we have prototype code in th
### Other ideas ###
* How can we _easily_ allow teachers to set/override this option for imported materials?
* Document ways of using JSXGraph `http://jsxgraph.org` for better support of graphics.
* Better options for automatically generated plots. (Aalto use of tikzpicture?) (Draw package?)
* 3D Graphics. Can we use: https://threejs.org/
......
......@@ -82,8 +82,6 @@ We have always worked on the basis of being as forgiving as possible, and accept
To be decided.
* To what extent do continental teachers accept use of '`.`' as a decimal separator? If _never_ then we probably don't need the "weak" options proposed above.
* Can teachers set this option at the question level or should it respect a site-wide option? (It's a shame Moodle can't set plugin options at the course level, otherwise we'd return to the cascading options which were available in STACK 1.0 some twenty years ago...)
* How can we _easily_ allow teachers to set/override this option for imported materials?
## Practical implementation in STACK
......@@ -91,10 +89,7 @@ Students do not type in expression termination symbols `;`, freeing up this symb
Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima syntax, so that numbers are typed in base 10, and the decimal point (`.`) must be used by teachers as the decimal separator._ This simplifies the problem considerably, as input parsing is only required for students' answers.
1. Mechanism for Maxima to output LaTeX. (Done - but more work needed on testing and question-wide options)
2. Mechanism to output expressions as they should be typed. E.g. "The teacher's answer is \(???\) which can be typed as `???`". (Done - but more work needed on testing and question-wide options)
3. Input parsing mechanism for _students' answers only_.
Currently the only option available is "strict".
## Practial implementation in other software
......
......@@ -23,15 +23,6 @@
*/
class stack_varmatrix_input extends stack_input {
private static $tostringparams = array('inputform' => true,
'qmchar' => true,
'pmchar' => 0,
'nosemicolon' => true,
'dealias' => false, // This is needed to stop pi->%pi etc.
'nounify' => true,
'varmatrix' => true
);
protected $extraoptions = array(
'hideanswer' => false,
'allowempty' => false,
......@@ -42,6 +33,21 @@ class stack_varmatrix_input extends stack_input {
'validator' => false
);
protected function is_blank_response($contents) {
if ($contents == array('EMPTYANSWER')) {
return true;
}
$allblank = true;
foreach ($contents as $row) {
foreach ($row as $val) {
if (!('' == trim($val) || '?' == $val || 'null' == $val)) {
$allblank = false;
}
}
}
return $allblank;
}
public function render(stack_input_state $state, $fieldname, $readonly, $tavalue) {
// Note that at the moment, $this->boxHeight and $this->boxWidth are only
// used as minimums. If the current input is bigger, the box is expanded.
......@@ -70,10 +76,7 @@ class stack_varmatrix_input extends stack_input {
} else {
$current = array();
foreach ($state->contents as $row) {
$cs = stack_ast_container::make_from_teacher_source($row);
if ($cs->get_valid()) {
$current[] = $cs->ast_to_string(null, self::$tostringparams);
}
$current[] = implode(" ", $row);
}
$current = implode("\n", $current);
}
......@@ -125,7 +128,7 @@ class stack_varmatrix_input extends stack_input {
* Most return the same as went in.
*
* @param array|string $in
* @return string
* @return array
*/
protected function response_to_contents($response) {
$contents = array();
......@@ -149,10 +152,11 @@ class stack_varmatrix_input extends stack_input {
foreach ($contents as $key => $row) {
// Pad out short rows.
$padrow = array();
for ($i = 0; $i < ($maxlen - count($row)); $i++) {
$row[] = '?';
$padrow[] = '?';
}
$contents[$key] = '[' . implode(',', $row) . ']';
$contents[$key] = array_merge($row, $padrow);
}
if ($contents == array() && $this->get_extra_option('allowempty')) {
$contents = array('EMPTYANSWER');
......@@ -184,7 +188,77 @@ class stack_varmatrix_input extends stack_input {
* @return string
*/
public function contents_to_maxima($contents) {
return 'matrix('.implode(',', $contents).')';
if ($contents == array('EMPTYANSWER')) {
return 'matrix(EMPTYCHAR)';
}
$matrix = array();
foreach ($contents as $row) {
$matrix[] = '['.implode(',', $row).']';
}
return 'matrix('.implode(',', $matrix).')';
}
/**
* @param array $contents the content array of the student's input.
* @return array of the validity, errors strings and modified contents.
*/
protected function validate_contents($contents, $basesecurity, $localoptions) {
$errors = array();
$notes = array();
$valid = true;
list ($secrules, $filterstoapply) = $this->validate_contents_filters($basesecurity);
// Now validate the input as CAS code.
$modifiedcontents = array();
if ($contents == array('EMPTYANSWER')) {
$modifiedcontents = $contents;
} else {
foreach ($contents as $row) {
$modifiedrow = array();
foreach ($row as $val) {
$answer = stack_ast_container::make_from_student_source($val, '', $secrules, $filterstoapply,
array(), 'Root', $localoptions->get_option('decimals'));
if ($answer->get_valid()) {
$modifiedrow[] = $answer->get_inputform();
} else {
$modifiedrow[] = 'EMPTYCHAR';
}
$valid = $valid && $answer->get_valid();
$errors[] = $answer->get_errors();
$note = $answer->get_answernote(true);
if ($note) {
foreach ($note as $n) {
$notes[$n] = true;
}
}
}
$modifiedcontents[] = $modifiedrow;
}
}
// Construct one final "answer" as a single maxima object.
// In the case of matrices (where $caslines are empty) create the object directly here.
// As this will create a matrix we need to check that 'matrix' is not a forbidden word.
// Should it be a forbidden word it gets still applied to the cells.
if (isset(stack_cas_security::list_to_map($this->get_parameter('forbidWords', ''))['matrix'])) {
$modifiedforbid = str_replace('\,', 'COMMA_TAG', $this->get_parameter('forbidWords', ''));
$modifiedforbid = explode(',', $modifiedforbid);
array_map('trim', $modifiedforbid);
unset($modifiedforbid[array_search('matrix', $modifiedforbid)]);
$modifiedforbid = implode(',', $modifiedforbid);
$modifiedforbid = str_replace('COMMA_TAG', '\,', $modifiedforbid);
$secrules->set_forbiddenwords($modifiedforbid);
// Cumbersome, and cannot deal with matrix being within an alias...
// But first iteration and so on.
}
$value = $this->contents_to_maxima($modifiedcontents);
// Sanitised above.
$answer = stack_ast_container::make_from_teacher_source($value, '', $secrules);
$answer->get_valid();
$caslines = array();
return array($valid, $errors, $notes, $answer, $caslines);
}
/**
......@@ -194,8 +268,54 @@ class stack_varmatrix_input extends stack_input {
* @return string
*/
private function maxima_to_raw_input($in) {
$decimal = '.';
$listsep = ',';
if ($this->options->get_option('decimals') === ',') {
$decimal = ',';
$listsep = ';';
}
$tostringparams = array('inputform' => true,
'qmchar' => true,
'pmchar' => 0,
'nosemicolon' => true,
'dealias' => false, // This is needed to stop pi->%pi etc.
'nounify' => true,
'nontuples' => false,
'varmatrix' => true,
'decimal' => $decimal,
'listsep' => $listsep
);
$cs = stack_ast_container::make_from_teacher_source($in);
return $cs->ast_to_string(null, self::$tostringparams);
return $cs->ast_to_string(null, $tostringparams);
}
public function get_correct_response($value) {
if (trim($value) == 'EMPTYANSWER' || $value === null) {
$value = '';
}
// TODO: refactor this ast creation away.
$cs = stack_ast_container::make_from_teacher_source($value, '', new stack_cas_security(), array());
$cs->set_nounify(0);
// Hard-wire to strict Maxima syntax.
$decimal = '.';
$listsep = ',';
$params = array('checkinggroup' => true,
'qmchar' => false,
'pmchar' => 1,
'nosemicolon' => true,
'keyless' => true,
'dealias' => false, // This is needed to stop pi->%pi etc.
'nounify' => 0,
'nontuples' => false,
'decimal' => $decimal,
'listsep' => $listsep
);
if ($cs->get_valid()) {
$value = $cs->ast_to_string(null, $params);
}
return $this->maxima_to_response_array($value);
}
protected function ajax_to_response_array($in) {
......
......@@ -1770,4 +1770,20 @@ class input_algebraic_test extends qtype_stack_testcase {
$this->assertEquals('\[ \left \{3,1415 ; 2,7100 \right \} \]', $state->contentsdisplayed);
$this->assertEquals('', $state->errors);
}
public function test_decimal_output_3() {
$options = new stack_options();
$options->set_option('decimals', ',');
// Teacher must use correct syntax.
$el = stack_input_factory::make('algebraic', 'state', 'matrix([3.1415,2.71])', $options);
$el->set_parameter('forbidFloats', false);
// Student uses commas and semicolons.
$state = $el->validate_student_response(array('state' => 'matrix([3,1415;2,71])'), $options,
'matrix([3.1415,2.71])', new stack_cas_security());
$this->assertEquals(stack_input::VALID, $state->status);
$this->assertEquals('matrix([3.1415,2.71])', $state->contentsmodified);
$this->assertEquals('\[ \left[\begin{array}{cc} 3,1415 & 2,7100 \end{array}\right] \]', $state->contentsdisplayed);
$this->assertEquals('', $state->errors);
}
}
......@@ -143,6 +143,21 @@ class input_varmatrix_test extends qtype_stack_testcase {
$this->assertEquals('', $state->lvars);
}
public function test_validate_student_response_invalid_two_blank() {
$options = new stack_options();
$el = stack_input_factory::make('varmatrix', 'ans1', 'M');
$inputvals = array(
'ans1' => "1 2 3\n4",
);
$state = $el->validate_student_response($inputvals, $options, 'matrix([1,2,3],[3,4,5])', new stack_cas_security());
$this->assertEquals(stack_input::INVALID, $state->status);
$this->assertEquals('', $state->note);
$this->assertEquals('matrix([1,2,3],[4,QMCHAR,QMCHAR])', $state->contentsmodified);
$this->assertEquals('\[ \left[\begin{array}{ccc} 1 & 2 & 3 \\\\ 4 & \color{red}{?} & \color{red}{?} \end{array}\right] \]',
$state->contentsdisplayed);
$this->assertEquals('', $state->lvars);
}
public function test_validate_student_response_invalid() {
$options = new stack_options();
$el = stack_input_factory::make('varmatrix', 'ans1', 'M');
......@@ -152,7 +167,7 @@ class input_varmatrix_test extends qtype_stack_testcase {
$state = $el->validate_student_response($inputvals, $options, 'matrix([1,2,3],[3,4,5])', new stack_cas_security());
$this->assertEquals(stack_input::INVALID, $state->status);
$this->assertEquals('missing_stars', $state->note);
$this->assertEquals('matrix(EMPTYCHAR,[4,5,6])', $state->contentsmodified);
$this->assertEquals('matrix([1,EMPTYCHAR,3],[4,5,6])', $state->contentsmodified);
$this->assertEquals('<span class="stacksyntaxexample">matrix([1,2x,3],[4,5,6])</span>',
$state->contentsdisplayed);
$this->assertEquals('', $state->lvars);
......@@ -167,11 +182,11 @@ class input_varmatrix_test extends qtype_stack_testcase {
$state = $el->validate_student_response($inputvals, $options, 'matrix([1,2,3],[3,4,5])', new stack_cas_security());
$this->assertEquals(stack_input::INVALID, $state->status);
$this->assertEquals('missing_stars | missingLeftBracket', $state->note);
$this->assertEquals('matrix(EMPTYCHAR,[4,5,6])', $state->contentsmodified);
$this->assertEquals('matrix([1,EMPTYCHAR,3],[4,5,6])', $state->contentsmodified);
$this->assertEquals('<span class="stacksyntaxexample">matrix([1,2x),3],[4,5,6])</span>',
$state->contentsdisplayed);
$this->assertEquals('You have a missing left bracket <span class="stacksyntaxexample">(</span> in the expression: ' .
'<span class="stacksyntaxexample">[1,2*x),3]</span>.', $state->errors);
'<span class="stacksyntaxexample">2*x)</span>.', $state->errors);
$this->assertEquals('', $state->lvars);
}
......@@ -184,13 +199,13 @@ class input_varmatrix_test extends qtype_stack_testcase {
$state = $el->validate_student_response($inputvals, $options, 'matrix([1,2,3],[3,4,5])', new stack_cas_security());
$this->assertEquals(stack_input::INVALID, $state->status);
$this->assertEquals('missing_stars | missingLeftBracket', $state->note);
$this->assertEquals('matrix(EMPTYCHAR,EMPTYCHAR)', $state->contentsmodified);
$this->assertEquals('matrix([1,EMPTYCHAR,3],[4,5,EMPTYCHAR])', $state->contentsmodified);
$this->assertEquals('<span class="stacksyntaxexample">matrix([1,2x),3],[4,5,6a])</span>',
$state->contentsdisplayed);
$this->assertEquals('You have a missing left bracket <span class="stacksyntaxexample">(</span> in the expression: ' .
'<span class="stacksyntaxexample">[1,2*x),3]</span>. ' .
'<span class="stacksyntaxexample">2*x)</span>. ' .
'You seem to be missing * characters. Perhaps you meant to type ' .
'<span class="stacksyntaxexample">[4,5,6<span class="stacksyntaxexamplehighlight">*</span>a]</span>.',
'<span class="stacksyntaxexample">6<span class="stacksyntaxexamplehighlight">*</span>a</span>.',
$state->errors);
$this->assertEquals('', $state->lvars);
}
......@@ -271,4 +286,38 @@ class input_varmatrix_test extends qtype_stack_testcase {
'\ln ^2\left(9\right)\cdot x^2\cdot 9^{x\cdot y} \end{array}\right] \]',
$state->contentsdisplayed);
}
public function test_validate_student_response_decimals_dot() {
$options = new stack_options();
$options->set_option('decimals', '.');
$el = stack_input_factory::make('varmatrix', 'ans1', 'M');
$el->set_parameter('forbidFloats', false);
$inputvals = array(
'ans1' => "x 2.7\n sqrt(2) 3.14",
);
$state = $el->validate_student_response($inputvals, $options, 'matrix([a,b],[c,d])', new stack_cas_security());
$this->assertEquals(stack_input::VALID, $state->status);
$this->assertEquals('', $state->note);
$this->assertEquals('matrix([x,2.7],[sqrt(2),3.14])', $state->contentsmodified);
$this->assertEquals('\[ \left[\begin{array}{cc} x & 2.7 \\\\ \sqrt{2} & 3.1 \end{array}\right] \]',
$state->contentsdisplayed);
$this->assertEquals('\( \left[ x \right]\) ', $state->lvars);
}
public function test_validate_student_response_decimals_continental() {
$options = new stack_options();
$options->set_option('decimals', ',');
$el = stack_input_factory::make('varmatrix', 'ans1', 'M');
$el->set_parameter('forbidFloats', false);
$inputvals = array(
'ans1' => "x 2,7\n sqrt(2) 3,14",
);
$state = $el->validate_student_response($inputvals, $options, 'matrix([a,b],[c,d])', new stack_cas_security());
$this->assertEquals(stack_input::VALID, $state->status);
$this->assertEquals('', $state->note);
$this->assertEquals('matrix([x,2.7],[sqrt(2),3.14])', $state->contentsmodified);
$this->assertEquals('\[ \left[\begin{array}{cc} x & 2,7 \\\\ \sqrt{2} & 3,1 \end{array}\right] \]',
$state->contentsdisplayed);
$this->assertEquals('\( \left[ x \right]\) ', $state->lvars);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment