diff --git a/qtism/data/storage/xml/marshalling/MapEntryMarshaller.php b/qtism/data/storage/xml/marshalling/MapEntryMarshaller.php index 377cfaefe..94c2d5d5c 100644 --- a/qtism/data/storage/xml/marshalling/MapEntryMarshaller.php +++ b/qtism/data/storage/xml/marshalling/MapEntryMarshaller.php @@ -4,145 +4,142 @@ * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * Copyright (c) 2013 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); - * - * @author Jérôme Bogaerts, + * + * Copyright (c) 2013-2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * + * @author Jérôme Bogaerts * @license GPLv2 - * @package */ - namespace qtism\data\storage\xml\marshalling; use qtism\data\QtiComponent; -use qtism\common\enums\BaseType; use qtism\data\storage\Utils; use qtism\data\state\MapEntry; +use qtism\common\enums\BaseType; +use qtism\common\utils\Version; use \DOMElement; use \InvalidArgumentException; use \UnexpectedValueException; /** * Marshalling/Unmarshalling implementation for mapEntry. - * - * This marshaller is a parametric one. It allows you to know + * + * This marshaller is a parametric one. It allows you to know * the baseType of the 'mapKey' attribute while unmarshalling * it. The value of the given baseType is found in the related * responseDeclaration element. - * + * * @author Jérôme Bogaerts * */ -class MapEntryMarshaller extends Marshaller { - - private $baseType; - - /** +class MapEntryMarshaller extends Marshaller +{ + private $baseType; + + /** * Set a baseType to this marshaller implementation in order * to force the datatype used for the unserialization of the * 'mapKey' attribute. - * + * * @param int $baseType A baseType from the BaseType enumeration. - * @throws InvalidArgumentException If $baseType is not a value from the BaseType enumeration. + * @throws \InvalidArgumentException If $baseType is not a value from the BaseType enumeration. */ - protected function setBaseType($baseType) { - if (in_array($baseType, BaseType::asArray())) { - $this->baseType = $baseType; - } - else { - $msg = 'The baseType argument must be a valid QTI baseType value from the BaseType enumeration.'; - throw new InvalidArgumentException($msg); - } - } - - /** - * Get the baseType that is used to force the unserialization of + protected function setBaseType($baseType) + { + if (in_array($baseType, BaseType::asArray())) { + $this->baseType = $baseType; + } else { + $msg = 'The baseType argument must be a valid QTI baseType value from the BaseType enumeration.'; + throw new InvalidArgumentException($msg); + } + } + + /** + * Get the baseType that is used to force the unserialization of * the 'mapKey' attribute. - * + * * @return int A baseType from the BaseType enumeration. */ - public function getBaseType() { - return $this->baseType; - } - - /** + public function getBaseType() + { + return $this->baseType; + } + + /** * Create a new instance of ValueMarshaller. - * + * * @param int $baseType A value from the BaseType enumeration. - * @throws InvalidArgumentException if $baseType is not a value from the BaseType enumeration. + * @throws \InvalidArgumentException if $baseType is not a value from the BaseType enumeration. */ - public function __construct($baseType) { - $this->setBaseType($baseType); - } - - /** + public function __construct($baseType) + { + $this->setBaseType($baseType); + } + + /** * Marshall a MapEntry object into a DOMElement object. - * - * @param QtiComponent $component A MapEntry object. - * @return DOMElement The according DOMElement object. + * + * @param \qtism\data\QtiComponent $component A MapEntry object. + * @return \DOMElement The according DOMElement object. */ - protected function marshall(QtiComponent $component) { - $element = static::getDOMCradle()->createElement($component->getQtiClassName()); - - self::setDOMElementAttribute($element, 'mapKey', $component->getMapKey()); - self::setDOMElementAttribute($element, 'mappedValue', $component->getMappedValue()); - self::setDOMElementAttribute($element, 'caseSensitive', $component->isCaseSensitive()); - - return $element; - } - - /** + protected function marshall(QtiComponent $component) + { + $element = static::getDOMCradle()->createElement($component->getQtiClassName()); + + self::setDOMElementAttribute($element, 'mapKey', $component->getMapKey()); + self::setDOMElementAttribute($element, 'mappedValue', $component->getMappedValue()); + self::setDOMElementAttribute($element, 'caseSensitive', $component->isCaseSensitive()); + + return $element; + } + + /** * Unmarshall a DOMElement object corresponding to a QTI mapEntry element. - * - * @param DOMElement $element A DOMElement object. - * @return QtiComponent A MapEntry object. - * @throws UnmarshallingException + * + * @param \DOMElement $element A DOMElement object. + * @return \qtism\data\QtiComponent A MapEntry object. + * @throws \qtism\data\storage\xml\marshalling\UnmarshallingException + */ + protected function unmarshall(DOMElement $element) + { + try { + $mapKey = static::getDOMElementAttributeAs($element, 'mapKey'); + $mapKey = Utils::stringToDatatype($mapKey, $this->getBaseType()); + + if (($mappedValue = static::getDOMElementAttributeAs($element, 'mappedValue', 'float')) !== null) { + + $object = new MapEntry($mapKey, $mappedValue); + + if (($caseSensitive = static::getDOMElementAttributeAs($element, 'caseSensitive', 'boolean')) !== null) { + $object->setCaseSensitive($caseSensitive); + } + + return $object; + } else { + $msg = "The mandatory 'mappedValue' attribute is missing from element '" . $element->nodeName . "'."; + throw new UnmarshallingException($msg, $element); + } + } catch (UnexpectedValueException $e) { + $msg = "The value '${mapKey}' of the 'mapKey' attribute could not be converted to a '" . BaseType::getNameByConstant($this->getBaseType()) . "' value."; + throw new UnmarshallingException($msg, $element, $e); + } + } + + /** + * @see \qtism\data\storage\xml\marshalling\Marshaller::getExpectedQtiClassName() */ - protected function unmarshall(DOMElement $element) { - - if (($mapKey = static::getDOMElementAttributeAs($element, 'mapKey')) !== null) { - - try { - $mapKey = Utils::stringToDatatype($mapKey, $this->getBaseType()); - - if (($mappedValue = static::getDOMElementAttributeAs($element, 'mappedValue', 'float')) !== null) { - - $object = new MapEntry($mapKey, $mappedValue); - - if (($caseSensitive = static::getDOMElementAttributeAs($element, 'caseSensitive', 'boolean')) !== null) { - $object->setCaseSensitive($caseSensitive); - } - - return $object; - } - else { - $msg = "The mandatory 'mappedValue' attribute is missing from element '" . $element->nodName . "'."; - throw new UnmarshallingException($msg, $element); - } - } - catch (UnexpectedValueException $e) { - $msg = "The value of the 'mapKey' attribute '${mapKey}' could not be converted to qti:valueType."; - throw new UnmarshallingException($msg, $element, $e); - } - - } - else { - $msg = "The mandatory 'mapKey' attribute is missing from the '" . $element->localName . "' element"; - throw new UnmarshallingException($msg, $element); - } - } - - public function getExpectedQtiClassName() { - return 'mapEntry'; - } + public function getExpectedQtiClassName() + { + return 'mapEntry'; + } } diff --git a/qtism/runtime/expressions/MapResponseProcessor.php b/qtism/runtime/expressions/MapResponseProcessor.php index 736e40750..4ee650f6d 100644 --- a/qtism/runtime/expressions/MapResponseProcessor.php +++ b/qtism/runtime/expressions/MapResponseProcessor.php @@ -14,176 +14,162 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2013 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * Copyright (c) 2013-2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); * - * @author Jérôme Bogaerts, + * @author Jérôme Bogaerts * @license GPLv2 - * @package qtism - * * */ + namespace qtism\runtime\expressions; -use qtism\common\datatypes\Scalar; +use qtism\common\enums\BaseType; use qtism\common\datatypes\String; use qtism\common\datatypes\Float; use qtism\common\Comparable; use qtism\runtime\common\ResponseVariable; use qtism\data\expressions\Expression; use qtism\data\expressions\MapResponse; -use \InvalidArgumentException; /** * The MapResponseProcessor class aims at processing MapResponse Expression objects. - * + * * FROM IMS QTI: - * - * This expression looks up the value of a response variable and then transforms it using the - * associated mapping, which must have been declared. The result is a single float. If the - * response variable has single cardinality then the value returned is simply the mapped - * target value from the map. If the response variable has multiple or ordered cardinality - * then the value returned is the sum of the mapped target values. This expression cannot + * + * This expression looks up the value of a response variable and then transforms it using the + * associated mapping, which must have been declared. The result is a single float. If the + * response variable has single cardinality then the value returned is simply the mapped + * target value from the map. If the response variable has multiple or ordered cardinality + * then the value returned is the sum of the mapped target values. This expression cannot * be applied to variables of record cardinality. - * - * For example, if a mapping associates the identifiers {A,B,C,D} with the values {0,1,0.5,0} - * respectively then mapResponse will map the single value 'C' to the numeric value 0.5 and + * + * For example, if a mapping associates the identifiers {A,B,C,D} with the values {0,1,0.5,0} + * respectively then mapResponse will map the single value 'C' to the numeric value 0.5 and * the set of values {C,B} to the value 1.5. - * - * If a container contains multiple instances of the same value then that value is counted + * + * If a container contains multiple instances of the same value then that value is counted * once only. To continue the example above {B,B,C} would still map to 1.5 and not 2.5. - * + * * @author Jérôme Bogaerts * */ -class MapResponseProcessor extends ExpressionProcessor { - - public function setExpression(Expression $expression) { - if ($expression instanceof MapResponse) { - parent::setExpression($expression); - } - else { - $msg = "The MapResponseProcessor only accepts MapResponse Expression objects to be processed."; - throw new InvalidArgumentException($msg); - } - } - - /** +class MapResponseProcessor extends ExpressionProcessor +{ + /** * Process the MapResponse expression. - * + * * * An ExpressionProcessingException is thrown if the variable is not defined. * * An ExpressionProcessingException is thrown if the variable has no mapping defined. * * An ExpressionProcessingException is thrown if the variable is not a ResponseVariable. * * An ExpressionProcessingException is thrown if the cardinality of the variable is RECORD. - * + * * @return a QTI float value. - * @throws ExpressionProcessingException + * @throws \qtism\runtime\expressions\ExpressionProcessingException */ - public function process() { - $expr = $this->getExpression(); - $state = $this->getState(); - $identifier = $expr->getIdentifier(); - $variable = $state->getVariable($identifier); - - if (!is_null($variable)) { - - if ($variable instanceof ResponseVariable) { - - $mapping = $variable->getMapping(); - - if (is_null($mapping)) { + public function process() + { + $expr = $this->getExpression(); + $state = $this->getState(); + $identifier = $expr->getIdentifier(); + $variable = $state->getVariable($identifier); + + if (!is_null($variable)) { + + if ($variable instanceof ResponseVariable) { + + $mapping = $variable->getMapping(); + + if (is_null($mapping)) { return new Float(0.0); } - + + // Single cardinality behaviour. if ($variable->isSingle()) { - + foreach ($mapping->getMapEntries() as $mapEntry) { - + $val = $state[$identifier]; $mapKey = $mapEntry->getMapKey(); - + if ($val instanceof String && $mapEntry->isCaseSensitive() === false) { $val = mb_strtolower($val->getValue(), 'UTF-8'); $mapKey = mb_strtolower($mapKey, 'UTF-8'); } if ($val instanceof Comparable && $val->equals($mapKey) || $val === $mapKey) { - // relevant mapping found. - $mappedValue = $mapEntry->getMappedValue(); - return new Float($mappedValue); + return new Float($mapEntry->getMappedValue()); + } elseif ($variable->getBaseType() === BaseType::STRING && $val === null && $mapKey === '') { + return new Float($mapEntry->getMappedValue()); } } - + // No relevant mapping found, return mapping default. return new Float($mapping->getDefaultValue()); - } - else if ($variable->isMultiple()) { + // Multiple cardinality behaviour. + } elseif ($variable->isMultiple()) { + $result = 0.0; - - if (!is_null($variable->getValue())) { - $mapped = array(); // already mapped keys. - $mapEntries = $mapping->getMapEntries(); - - foreach ($variable->getValue() as $val) { - - for ($i = 0; $i < count($mapEntries); $i++) { - - $mapKey = $rawMapKey = $mapEntries[$i]->getMapKey(); - - $processedVal = null; - if ($val instanceof String && $mapEntries[$i]->isCaseSensitive() === false) { - $processedVal = mb_strtolower($val->getValue(), 'UTF-8'); - $mapKey = mb_strtolower($mapKey, 'UTF-8'); - } - - if (($val instanceof Comparable && $val->equals($mapKey) === true) || $processedVal === $mapKey) { - if (in_array($rawMapKey, $mapped, true) === false) { - $result += $mapEntries[$i]->getMappedValue(); - $mapped[] = $rawMapKey; - - } - // else... - // This value has already been mapped. - - break; - } + $variableValue = (count($variable->getValue()) === 0) ? array(null) : $variable->getValue(); + + $mapped = array(); // already mapped keys. + $mapEntries = $mapping->getMapEntries(); + + foreach ($variableValue as $val) { + + for ($i = 0; $i < count($mapEntries); $i++) { + + $mapKey = $rawMapKey = $mapEntries[$i]->getMapKey(); + if ($val instanceof String && $mapEntries[$i]->isCaseSensitive() === false) { + $val = new String(mb_strtolower($val->getValue(), 'UTF-8')); + $mapKey = mb_strtolower($mapKey, 'UTF-8'); } - - if ($i >= count($mapEntries)) { - // No explicit mapping found for source value $val. - $result += $mapping->getDefaultValue(); + + if (($val instanceof Comparable && $val->equals($mapKey) === true) || ($variable->getBaseType() === BaseType::STRING && $val === null && $mapKey === '')) { + if (in_array($rawMapKey, $mapped, true) === false) { + $result += $mapEntries[$i]->getMappedValue(); + $mapped[] = $rawMapKey; + } + // else... + // This value has already been mapped. + break; } } - - // When mapping a container, try to apply lower or upper bound. - if ($mapping->hasLowerBound() && $result < $mapping->getLowerBound()) { - return new Float($mapping->getLowerBound()); - } - else if ($mapping->hasUpperBound() && $result > $mapping->getUpperBound()) { - return new Float($mapping->getUpperBound()); - } - else { - return new Float($result); + + if ($i >= count($mapEntries)) { + // No explicit mapping found for source value $val. + $result += $mapping->getDefaultValue(); } } - else { - // Returns a 0.0 result. + + // When mapping a container, try to apply lower or upper bound. + if ($mapping->hasLowerBound() && $result < $mapping->getLowerBound()) { + return new Float($mapping->getLowerBound()); + } elseif ($mapping->hasUpperBound() && $result > $mapping->getUpperBound()) { + return new Float($mapping->getUpperBound()); + } else { return new Float($result); } - } - else { - $msg = "MapResponse cannot be applied on a RECORD container."; + } else { + $msg = "MapResponse cannot be applied on a Record container."; throw new ExpressionProcessingException($msg, $this, ExpressionProcessingException::WRONG_VARIABLE_BASETYPE); } - } - else { - $msg = "The target variable must be a ResponseVariable, OutcomeVariable given while processing MapResponse."; - throw new ExpressionProcessingException($msg, $this, ExpressionProcessingException::WRONG_VARIABLE_TYPE); - } - } - else { - $msg = "No variable with identifier '${identifier}' could be found while processing MapResponse."; - throw new ExpressionProcessingException($msg, $this, ExpressionProcessingException::NONEXISTENT_VARIABLE); - } - } + + } else { + $msg = "The target variable of a MapResponse expression must be a ResponseVariable."; + throw new ExpressionProcessingException($msg, $this, ExpressionProcessingException::WRONG_VARIABLE_TYPE); + } + } else { + $msg = "No variable with identifier '${identifier}' could be found while processing MapResponse."; + throw new ExpressionProcessingException($msg, $this, ExpressionProcessingException::NONEXISTENT_VARIABLE); + } + } + + /** + * @see \qtism\runtime\expressions\ExpressionProcessor::getExpressionType() + */ + protected function getExpressionType() + { + return 'qtism\\data\\expressions\\MapResponse'; + } } diff --git a/test/qtism/runtime/expressions/MapResponseProcessorTest.php b/test/qtism/runtime/expressions/MapResponseProcessorTest.php index 9cc32dd8a..4fd952d70 100644 --- a/test/qtism/runtime/expressions/MapResponseProcessorTest.php +++ b/test/qtism/runtime/expressions/MapResponseProcessorTest.php @@ -1,9 +1,9 @@ process(); $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); - $this->assertEquals(0.0, $result->getValue()); + $this->assertEquals(1.0, $result->getValue()); $state['response1'] = new MultipleContainer(BaseType::PAIR, array(new Pair('A', 'B'))); $result = $mapResponseProcessor->process(); @@ -93,7 +96,7 @@ public function testMultipleComplexTyping() { $this->assertEquals(4, $result->getValue()); // 2.5 taken into account only once! } - public function testIndentifier() { + public function testIdentifier() { $variableDeclaration = $this->createComponentFromXml(' @@ -116,13 +119,19 @@ public function testIndentifier() { } public function testVariableNotDefined() { - $this->setExpectedException("qtism\\runtime\\expressions\\ExpressionProcessingException"); + $this->setExpectedException( + "qtism\\runtime\\expressions\\ExpressionProcessingException", + "No variable with identifier 'INVALID' could be found while processing MapResponse.", + ExpressionProcessingException::NONEXISTENT_VARIABLE + ); + $mapResponseExpr = $this->createComponentFromXml(''); $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); $mapResponseProcessor->process(); } public function testNoMapping() { + // When no mapping is set. We consider a "fake" mapping with a default value of 0. $variableDeclaration = $this->createComponentFromXml(''); $variable = ResponseVariable::createFromDataModel($variableDeclaration); $mapResponseExpr = $this->createComponentFromXml(''); @@ -131,6 +140,17 @@ public function testNoMapping() { $mapResponseProcessor->setState(new State(array($variable))); $result = $mapResponseProcessor->process(); $this->assertEquals(0.0, $result->getValue()); + + $variableDeclaration = $this->createComponentFromXml(''); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + + $state = new State(array($variable)); + $state['response1'] = new Identifier('correct_identifier'); + $mapResponseProcessor->setState($state); + $result = $mapResponseProcessor->process(); + $this->assertEquals(0.0, $result->getValue()); } public function testMultipleCardinalityIdentifierToFloat() { @@ -197,12 +217,6 @@ public function testMultipleCardinalityIdentifierToFloat() { $state['RESPONSE'] = new MultipleContainer(BaseType::IDENTIFIER, array(new Identifier('choice7'), new Identifier('identifierX'))); $result = $mapResponseProcessor->process(); $this->assertEquals(-21.0, $result->getValue()); - - // Response is 'choice1', 'choice7'. As entries 'Choice1' and 'Choice7' are marked - // as case insensitive, they will be matched. - $state['RESPONSE'] = new MultipleContainer(BaseType::IDENTIFIER, array(new Identifier('choice7'), new Identifier('choice1'))); - $result = $mapResponseProcessor->process(); - $this->assertEquals(-18.0, $result->getValue()); // Empty state. // An exception is raised because no RESPONSE variable found. @@ -227,4 +241,425 @@ public function testOutcomeDeclaration() { $mapResponseProcessor->setState(new State(array($variable))); $mapResponseProcessor->process(); } + + public function testEmptyMapEntryForStringSingleCardinality() { + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // No response provided, so the null value is equal to empty string... + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Empty string response provided. Expected is the same result as above... + $state['response1'] = new String(''); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Non empty string (with match). Expected is 1. + $state['response1'] = new String('Correct'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1, $result->getValue()); + + // Non empty string (without match). Expected is 1. + $state['response1'] = new String('Incorrect'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(0, $result->getValue()); + } + + public function testEmptyMapEntryForStringMultipleCardinality() { + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // No response provided, so the null value is equal to empty string... + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Empty string provided, so we expect the same result as with null. + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String(''))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Null provided as the value of the only value of the container, we expect the same result as above. + $state['response1'] = new MultipleContainer(BaseType::STRING, array(null)); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Empty container provided, as it is in QTI treated as null, we expect the same result as above. + $state['response1'] = new MultipleContainer(BaseType::STRING); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // mapKeys are matched a single time. + $state['response1'] = new MultipleContainer(BaseType::STRING, array(null, null)); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String(''), new String(''))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String('Correct'), new String('Correct'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String('Correct'), new String(''))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(0, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String(''), new String('Correct'), null)); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(0, $result->getValue()); + } + + public function testEmptyMapEntryForStringMultipleCardinalityCaseInsensitive() { + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // No response provided, so the null value is equal to empty string... + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Empty string provided, so we expect the same result as with null. + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String(''))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Null provided as the value of the only value of the container, we expect the same result as above. + $state['response1'] = new MultipleContainer(BaseType::STRING, array(null)); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // Empty container provided, as it is in QTI treated as null, we expect the same result as above. + $state['response1'] = new MultipleContainer(BaseType::STRING); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + // mapKeys are matched a single time. + $state['response1'] = new MultipleContainer(BaseType::STRING, array(null, null)); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String(''), new String(''))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String('Correct'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String('correct'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String('Correct'), new String('correct'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String('Correct'), new String(''))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(0, $result->getValue()); + + $state['response1'] = new MultipleContainer(BaseType::STRING, array(new String(''), new String('correct'), null)); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(0, $result->getValue()); + } + + public function testLowerBoundSingleCardinality() { + // In case of using a single cardinality variablen lower bound is ignored! + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // Should just "touch" the lower bound... + $state['RESPONSE'] = new String('incorrect_1'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1.0, $result->getValue()); + + // Should go below the lower bound because when using a single cardinality value, the lower bound is ignored (only used with container mapping)... + $state['RESPONSE'] = new String('incorrect_2'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-2.0, $result->getValue()); + } + + public function testLowerBoundIgnoredWithSingleCardinality() { + // In case of using a single cardinality variable lower bound is ignored! + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // Should just "touch" the lower bound... + $state['RESPONSE'] = new String('incorrect_1'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1.0, $result->getValue()); + + // Should go below the lower bound because when using a single cardinality value, the lower bound is ignored (only used with container mapping)... + $state['RESPONSE'] = new String('incorrect_2'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-2.0, $result->getValue()); + } + + public function testLowerBoundWithMultipleCardinality() { + // In case of using a single cardinality variable lower bound is ignored! + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // Should just "touch" the lower bound... + $state['RESPONSE'] = new MultipleContainer(BaseType::STRING, array(new String('incorrect_1'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1.0, $result->getValue()); + + // Lower bound is the limit as we are mapping a container (multiple cardinality string). + $state['RESPONSE'] = new MultipleContainer(BaseType::STRING, array(new String('incorrect_2'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(-1.0, $result->getValue()); + } + + public function testUpperBoundIgnoredWithSingleCardinality() { + // In case of using a single cardinality variable lower bound is ignored! + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // Should just "touch" the upperBound... + $state['RESPONSE'] = new String('correct_1'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1.0, $result->getValue()); + + // Should go above the upper bound because when using a single cardinality value, the upper bound is ignored (only used with container mapping)... + $state['RESPONSE'] = new String('correct_2'); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(2.0, $result->getValue()); + } + + public function testUpperBoundWithMultipleCardinality() { + // In case of using a single cardinality variable lower bound is ignored! + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + $mapResponseProcessor->setState($state); + + // Should just "touch" the upper bound... + $state['RESPONSE'] = new MultipleContainer(BaseType::STRING, array(new String('correct_1'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1.0, $result->getValue()); + + // Upper bound is the limit as we are mapping a container (multiple cardinality string). + $state['RESPONSE'] = new MultipleContainer(BaseType::STRING, array(new String('correct_2'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1.0, $result->getValue()); + } + + public function testOrderedContainer() { + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor->setState($state); + + $state['response1'] = new OrderedContainer(BaseType::PAIR, array(new Pair('A', 'B'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1.5, $result->getValue()); + + $state['response1'] = new OrderedContainer(BaseType::PAIR, array(new Pair('A', 'B'), new Pair('C', 'D'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(4.0, $result->getValue()); + + $state['response1'] = new OrderedContainer(BaseType::PAIR, array(new Pair('C', 'D'), new Pair('A', 'B'))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(4.0, $result->getValue()); + } + + public function testDefaultValue() { + $variableDeclaration = $this->createComponentFromXml(' + + + + + + + '); + $variable = ResponseVariable::createFromDataModel($variableDeclaration); + $mapResponseExpr = $this->createComponentFromXml(''); + $mapResponseProcessor = new MapResponseProcessor($mapResponseExpr); + + $state = new State(); + $state->setVariable($variable); + $mapResponseProcessor->setState($state); + + // No response, should have 1. + $state['response1'] = null; + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1.0, $result->getValue()); + + // No match, should have 1. + $state['response1'] = new OrderedContainer(BaseType::POINT, array(new Point(-2, 2))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(1.0, $result->getValue()); + + // No match, two times, should have 2. + $state['response1'] = new OrderedContainer(BaseType::POINT, array(new Point(-2, 2), new Point(100, 100))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(2.0, $result->getValue()); + + // One is not matched, the other is. Should have 2.5. + $state['response1'] = new OrderedContainer(BaseType::POINT, array(new Point(-2, 2), new Point(0, 0))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(2.5, $result->getValue()); + + // Both matched but there is an upperBound. Should have 3.0. + $state['response1'] = new OrderedContainer(BaseType::POINT, array(new Point(10, 10), new Point(0, 0))); + $result = $mapResponseProcessor->process(); + $this->assertInstanceOf('qtism\\common\\datatypes\\Float', $result); + $this->assertEquals(3.0, $result->getValue()); + } } diff --git a/test/samples/custom/items/extended_text_empty_mapentry.xml b/test/samples/custom/items/extended_text_empty_mapentry.xml new file mode 100644 index 000000000..1fa0c56ee --- /dev/null +++ b/test/samples/custom/items/extended_text_empty_mapentry.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + What is the first name of Mr Einstein? + + + +