@@ -83,6 +83,9 @@ class formulas_part {
8383 /** @var int whether there are multiple possible answers */
8484 public int $ answernotunique ;
8585
86+ /** @var int whether students can leave one or more fields empty */
87+ public int $ emptyallowed ;
88+
8689 /** @var string definition of the grading criterion */
8790 public string $ correctness ;
8891
@@ -418,6 +421,11 @@ public function normalize_response(array $response): array {
418421 * @return bool
419422 */
420423 public function is_gradable_response (array $ response ): bool {
424+ // If the part allows empty fields, we do not have to check anything; the response would be
425+ // gradable even if all fields were empty.
426+ if ($ this ->emptyallowed ) {
427+ return true ;
428+ }
421429 return !$ this ->is_unanswered ($ response );
422430 }
423431
@@ -431,6 +439,11 @@ public function is_gradable_response(array $response): bool {
431439 * @return bool
432440 */
433441 public function is_complete_response (array $ response ): bool {
442+ // If the part allows empty fields, we do not have to check anything; the response can be
443+ // considered complete even if all fields are empty.
444+ if ($ this ->emptyallowed ) {
445+ return true ;
446+ }
434447 // First, we check if there is a combined unit field. In that case, there will
435448 // be only one field to verify.
436449 if ($ this ->has_combined_unit_field ()) {
@@ -463,6 +476,9 @@ public function is_complete_response(array $response): bool {
463476 * @return bool
464477 */
465478 public function is_unanswered (array $ response ): bool {
479+ if (array_key_exists ('_seed ' , $ response )) {
480+ return true ;
481+ }
466482 if (!array_key_exists ('normalized ' , $ response )) {
467483 $ response = $ this ->normalize_response ($ response );
468484 }
@@ -528,6 +544,10 @@ public function get_evaluated_answers(): array {
528544 // their numerical value.
529545 if ($ isalgebraic ) {
530546 foreach ($ this ->evaluatedanswers as &$ answer ) {
547+ // If the answer is $EMPTY, there is nothing to do.
548+ if ($ answer === '$EMPTY ' ) {
549+ continue ;
550+ }
531551 $ answer = $ this ->evaluator ->substitute_variables_in_algebraic_formula ($ answer );
532552 }
533553 // In case we later write to $answer, this would alter the last entry of the $modelanswers
@@ -547,6 +567,11 @@ public function get_evaluated_answers(): array {
547567 */
548568 private static function wrap_algebraic_formulas_in_quotes (array $ formulas ): array {
549569 foreach ($ formulas as &$ formula ) {
570+ // We do not have to wrap the $EMPTY token in quotes.
571+ if ($ formula === '$EMPTY ' ) {
572+ continue ;
573+ }
574+
550575 // If the formula is aready wrapped in quotes, we throw an Exception, because that
551576 // should not happen. It will happen, if the student puts quotes around their response, but
552577 // we want that to be graded wrong. The exception will be caught and dealt with upstream,
@@ -631,7 +656,7 @@ public function add_special_variables(array $studentanswers, float $conversionfa
631656 foreach ($ studentanswers as $ i => &$ studentanswer ) {
632657 // We only do the calculation if the answer type is not algebraic. For algebraic
633658 // answers, we don't do anything, because quotes have already been added.
634- if (!$ isalgebraic ) {
659+ if (!$ isalgebraic && $ studentanswer !== ' $EMPTY ' ) {
635660 $ studentanswer = $ conversionfactor * $ studentanswer ;
636661 $ ssqstudentanswer += $ studentanswer ** 2 ;
637662 }
@@ -643,16 +668,19 @@ public function add_special_variables(array $studentanswers, float $conversionfa
643668 // The variable _d will contain the absolute differences between the model answer
644669 // and the student's response. Using the parser's diff() function will make sure
645670 // that algebraic answers are correctly evaluated.
671+ // Note: We *must* send the model answer first, because the function has a special check for the
672+ // EMPTY token.
646673 $ command .= '_d = diff(_a, _r); ' ;
647-
648- // Prepare the variable _err which is the root of the sum of squared differences.
649674 $ command .= "_err = sqrt(sum(map('*', _d, _d))); " ;
650675
651676 // Finally, calculate the relative error, unless the question uses an algebraic answer.
652677 if (!$ isalgebraic ) {
653678 // We calculate the sum of squares of all model answers.
654679 $ ssqmodelanswer = 0 ;
655680 foreach ($ this ->get_evaluated_answers () as $ answer ) {
681+ if ($ answer === '$EMPTY ' ) {
682+ continue ;
683+ }
656684 $ ssqmodelanswer += $ answer ** 2 ;
657685 }
658686 // If the sum of squares is 0 (i.e. all answers are 0), then either the student
@@ -731,16 +759,20 @@ public function grade(array $response, bool $finalsubmit = false): array {
731759 // Check whether the answer is valid for the given answer type. If it is not,
732760 // we just throw an exception to make use of the catch block. Note that if the
733761 // student's answer was empty, it will fail in this check.
734- if (!$ parser ->is_acceptable_for_answertype ($ this ->answertype )) {
762+ if (!$ parser ->is_acceptable_for_answertype ($ this ->answertype , $ this -> emptyallowed )) {
735763 throw new Exception ();
736764 }
737765
738766 // Make sure the stack is empty, as there might be left-overs from a previous
739767 // failed evaluation, e.g. caused by an invalid answer.
740768 $ this ->evaluator ->clear_stack ();
741769
742- $ evaluated = $ this ->evaluator ->evaluate ($ parser ->get_statements ())[0 ];
743- $ evaluatedresponse [] = token::unpack ($ evaluated );
770+ // Evaluate. If the answer was empty (an empty string or the '$EMPTY'), the parser
771+ // will create an appropriate evaluable statement or return an empty array. The evaluator,
772+ // on the other hand, will know how to deal with the "false" return value from reset()
773+ // and return the $EMPTY token.
774+ $ statements = $ parser ->get_statements ();
775+ $ evaluatedresponse [] = token::unpack ($ this ->evaluator ->evaluate (reset ($ statements )));
744776 } catch (Throwable $ t ) {
745777 // TODO: convert to non-capturing catch
746778 // If parsing, validity check or evaluation fails, we consider the answer as wrong.
@@ -828,8 +860,12 @@ public function get_correct_response(bool $forfeedback = false): array {
828860 $ answers = $ this ->get_evaluated_answers ();
829861
830862 // Numeric answers should be localized, if that functionality is enabled.
863+ // Empty answers should be just the empty string; a more user-friendly
864+ // output will be created in the renderer.
831865 foreach ($ answers as &$ answer ) {
832- if (is_numeric ($ answer )) {
866+ if ($ answer === '$EMPTY ' ) {
867+ $ answer = '' ;
868+ } else if (is_numeric ($ answer )) {
833869 $ answer = qtype_formulas::format_float ($ answer );
834870 }
835871 }
0 commit comments