/** * Process the PatternMatch. * * @return boolean|null A single boolean with a value of true if the sub-expression matches the pattern and false if it does not. If the sub-expression is NULL, the the operator results in NULL. * @throws \qtism\runtime\expressions\operators\OperatorProcessingException */ public function process() { $operands = $this->getOperands(); if ($operands->containsNull() === true) { return null; } if ($operands->exclusivelySingle() === false) { $msg = "The PatternMatch operator only accepts operands with a single cardinality."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_CARDINALITY); } if ($operands->exclusivelyString() === false) { $msg = "The PatternMatch operator only accepts operands with a string baseType."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_BASETYPE); } // XML schema always implicitly anchors the entire regular expression // because there is no carret (^) nor dollar ($) signs. // see http://www.regular-expressions.info/xml.html $rawPattern = $this->getExpression()->getPattern(); $pattern = OperatorUtils::escapeSymbols($rawPattern, array('$', '^')); $pattern = OperatorUtils::pregAddDelimiter('^' . $pattern . '$'); // XSD regexp always case-sensitive (nothing to do), dot matches white-spaces (use PCRE_DOTALL). $pattern .= 's'; $result = @preg_match($pattern, $operands[0]->getValue()); if ($result === 1) { return new Boolean(true); } elseif ($result === 0) { return new Boolean(false); } else { $error = preg_last_error(); $errorType = 'PCRE Engine compilation error'; switch ($error) { case PREG_INTERNAL_ERROR: $errorType = "PCRE Engine internal error"; break; case PREG_BACKTRACK_LIMIT_ERROR: $errorType = "PCRE Engine backtrack limit exceeded"; break; case PREG_RECURSION_LIMIT_ERROR: $errorType = "PCRE Engine recursion limit exceeded"; break; case PREG_BAD_UTF8_ERROR: $errorType = "PCRE Engine malformed UTF-8"; break; case PREG_BAD_UTF8_OFFSET_ERROR: $errorType = "PCRE Engine UTF-8 offset error"; break; } $msg = "An internal error occured while processing the regular expression '{$rawPattern}': {$errorType}."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::RUNTIME_ERROR); } }
/** * Create the OperatorProcessor relevant to the given $expression. * * @param \qtism\data\expressions\Expression $expression The Operator object you want to get the processor. * @param \qtism\runtime\expressions\operators\OperandsCollection $operands The operands to be involved in the Operator object. * @return \qtism\runtime\expressions\operators\OperatorProcessor An OperatorProcessor object ready to process $expression. * @throws \qtism\runtime\expressions\operators\OperatorProcessingException If the $operands count is not compliant with the Operator object to be processed. * @throws \InvalidArgumentException If $expression is not an Operator object. * @throws \RuntimeException If no relevant OperatorProcessor is found for the given $expression. */ public function createProcessor(QtiComponent $expression, OperandsCollection $operands = null) { if ($expression instanceof Operator) { if ($expression instanceof CustomOperator) { // QTI custom operator. Try to load an autoloaded class by using // its class attribute value. if ($expression->hasClass() === true) { $className = Utils::customOperatorClassToPhpClass($expression->getClass()); if (class_exists($className) === true) { return new $className($expression, $operands); } else { $msg = "No custom operator implementation found for class '" . $expression->getClass() . "'."; throw new RuntimeException($msg); } } else { $msg = "Only custom operators with a 'class' attribute value can be processed."; throw new RuntimeException($msg); } } else { // QTI built-in operator. $qtiClassName = ucfirst($expression->getQtiClassName()); $nsPackage = 'qtism\\runtime\\expressions\\operators\\'; $className = $nsPackage . $qtiClassName . 'Processor'; if (class_exists($className)) { if (is_null($operands) === true) { $operands = new OperandsCollection(); } return new $className($expression, $operands); } else { $msg = "No dedicated OperatorProcessor class found for QTI operator '{$qtiClassName}'."; throw new RuntimeException($msg); } } } else { $msg = "The OperatorProcessorFactory only accepts to create processors for Operator objects."; throw new InvalidArgumentException($msg); } }
/** * Process the Gcd operator. * * @return integer The integer value equal in value to the greatest common divisor of the sub-expressions. If any of the sub-expressions is NULL, the result is NULL. * @throws \qtism\runtime\expressions\operators\OperatorProcessingException */ public function process() { $operands = $this->getOperands(); if ($operands->containsNull() === true) { return null; } if ($operands->anythingButRecord() === false) { $msg = "The Gcd operator only accepts operands with a cardinality of single, multiple or ordered."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_CARDINALITY); } if ($operands->exclusivelyInteger() === false) { $msg = "The Gcd operator only accepts operands with an integer baseType."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_BASETYPE); } // Make a flat collection first. $flatCollection = new OperandsCollection(); $zeroCount = 0; $valueCount = 0; foreach ($operands as $operand) { if ($operand instanceof QtiScalar) { $valueCount++; if ($operand->getValue() !== 0) { $flatCollection[] = $operand; } else { $zeroCount++; } } elseif ($operand->contains(null)) { // Container with at least one null value inside. // -> If any of the sub-expressions is null or not numeric, returns null. return null; } else { // Container with no null values. foreach ($operand as $o) { $valueCount++; if ($o->getValue() !== 0) { $flatCollection[] = $o; } else { $zeroCount++; } } } } if ($zeroCount === $valueCount) { // All arguments of gcd() are 0. return new QtiInteger(0); } else { $g = $flatCollection[0]; $loopLimit = count($flatCollection) - 1; $i = 0; while ($i < $loopLimit) { $g = new QtiInteger(Utils::gcd($g->getValue(), $flatCollection[$i + 1]->getValue())); $i++; } return $g; } }
/** * Process the Repeat operator. * * Note: NULL values are simply ignored. If all sub-expressions are NULL, NULL is * returned. * * @return \qtism\runtime\common\OrderedContainer An ordered container filled sequentially by evaluating each sub-expressions, repeated a 'numberRepeats' of times. NULL is returned if all sub-expressions are NULL or numberRepeats < 1. * @throws \qtism\runtime\expressions\operators\OperatorProcessingException */ public function process() { $operands = $this->getOperands(); // get the value of numberRepeats $expression = $this->getExpression(); $numberRepeats = $expression->getNumberRepeats(); if (gettype($numberRepeats) === 'string') { // Variable reference found. $state = $this->getState(); $varName = Utils::sanitizeVariableRef($numberRepeats); $varValue = $state[$varName]; if (is_null($varValue) === true) { $msg = "The variable with name '{$varName}' could not be resolved."; throw new OperatorProcessingException($msg, $this); } elseif ($varValue instanceof Integer) { $msg = "The variable with name '{$varName}' is not an integer value."; throw new OperatorProcessingException($msg, $this); } $numberRepeats = $varValue->getValue(); } if ($numberRepeats < 1) { return null; } $result = null; for ($i = 0; $i < $numberRepeats; $i++) { $refType = null; foreach ($operands as $operand) { // If null, ignore if (is_null($operand) || $operand instanceof Container && $operand->isNull()) { continue; } // Check cardinality. if ($operand->getCardinality() !== Cardinality::SINGLE && $operand->getCardinality() !== Cardinality::ORDERED) { $msg = "The Repeat operator only accepts operands with a single or ordered cardinality."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_CARDINALITY); } // Check baseType. $currentType = RuntimeUtils::inferBaseType($operand); if ($refType !== null && $currentType !== $refType) { $msg = "The Repeat operator only accepts operands with the same baseType."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_BASETYPE); } elseif (is_null($result)) { $refType = $currentType; $result = new OrderedContainer($refType); } // Okay we are good... $operandCardinality = RuntimeUtils::inferCardinality($operand); if ($operandCardinality !== Cardinality::ORDERED) { $operand = new OrderedContainer($currentType, array($operand)); } foreach ($operand as $o) { $result[] = $o instanceof QtiDatatype ? clone $o : $o; } } } if (isset($result) && $result->isNull() !== true) { return $result; } else { return null; } }
/** * * @return null, \qtism\common\datatypes\Float */ protected function processPopSD() { $operands = $this->getOperands(); $operand = $operands[0]; $result = OperatorsUtils::standardDeviation(self::filterValues($operand->getArrayCopy()), false); return $result !== false ? new QtiFloat(floatval($result)) : null; }
/** * @dataProvider invalidCustomOperatorClassToPhpClassProvider * * @param string $customClass */ public function testInvalidCustomOperatorClassToPhpClass($customClass) { $this->assertFalse(OperatorsUtils::customOperatorClassToPhpClass($customClass)); }
/** * Process the RoundTo operator. * * An OperatorProcessingException will be thrown if: * * * The given operand is not a numeric value. * * The cardinality of the operand is not single. * * The value of the 'figures' attribute comes from a templateVariable which does not exist or is not numeric or null. * * @return null|float A single float with the value nearest to that of the expression's value or NULL if the sub-expression is NaN. * @throws \qtism\runtime\expressions\operators\OperatorProcessingException */ public function process() { $operands = $this->getOperands(); $state = $this->getState(); $operand = $operands[0]; // If the value is null, return null. if ($operands->containsNull()) { return null; } if (!$operands->exclusivelySingle()) { $msg = "The RoundTo operator accepts 1 operand with single cardinality."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_CARDINALITY); } // Accept only numerical operands. if (!$operands->exclusivelyNumeric()) { $msg = "The RoundTo operand accepts 1 operand with numerical baseType."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_BASETYPE); } // As per QTI 2.1 spec... if (is_nan($operand->getValue())) { return null; } elseif (is_infinite($operand->getValue())) { return $operand; } $roundingMode = $this->getExpression()->getRoundingMode(); $figures = $this->getExpression()->getFigures(); if (gettype($figures) === 'string') { // try to recover the value from the state. $figuresIdentifier = Utils::sanitizeVariableRef($figures); $figures = $state[$figuresIdentifier]; if (is_null($figures)) { $msg = "The variable '{$figuresIdentifier}' used to set up the 'figures' attribute is null or nonexisting."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::NONEXISTENT_VARIABLE); } elseif (!$figures instanceof Integer) { $msg = "The variable '{$figuresIdentifier}' used to set up the 'figures' attribute is not an integer."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::WRONG_VARIABLE_BASETYPE); } $figures = $figures->getValue(); } if ($roundingMode === RoundingMode::SIGNIFICANT_FIGURES) { if ($figures <= 0) { // As per QTI 2.1 spec. $msg = "The 'figures' attribute must be a non-zero positive integer when mode 'significantFigures' is used, '{$figures}' given."; throw new OperatorProcessingException($msg, $this, OperatorProcessingException::LOGIC_ERROR); } if ($operand->getValue() == 0) { return new Float(0.0); } $d = ceil(log10($operand->getValue() < 0 ? -$operand->getValue() : $operand->getValue())); $power = $figures - intval($d); $magnitude = pow(10, $power); $shifted = round($operand->getValue() * $magnitude); return new Float(floatval($shifted / $magnitude)); } else { // As per QTI 2.1 spec. if ($figures < 0) { $msg = "The 'figures' attribute must be a integer greater than or equal to zero when mode 'decimalPlaces' is used, '{$figures}' given."; throw new OperatorProcessingException($msg, $this); } return new Float(round($operand->getValue(), $figures)); } }