diff options
-rw-r--r-- | CHANGELOG.txt | 33 | ||||
-rw-r--r-- | INSTALL.txt | 48 | ||||
-rw-r--r-- | README.txt | 23 | ||||
-rw-r--r-- | backup/moodle1/lib.php | 79 | ||||
-rw-r--r-- | backup/moodle2/backup_qtype_algebra_plugin.class.php | 63 | ||||
-rw-r--r-- | backup/moodle2/restore_qtype_algebra_plugin.class.php | 106 | ||||
-rw-r--r-- | db/install.xml | 47 | ||||
-rw-r--r-- | db/upgrade.php | 59 | ||||
-rw-r--r-- | db/upgradelib.php | 71 | ||||
-rw-r--r-- | displayformula.php | 48 | ||||
-rw-r--r-- | edit_algebra_form.php | 378 | ||||
-rw-r--r-- | lang/en/qtype_algebra.php | 91 | ||||
-rw-r--r-- | lang/en/qtype_algebra_parser.php | 20 | ||||
-rw-r--r-- | lib.php | 38 | ||||
-rw-r--r-- | parser.php | 1721 | ||||
-rw-r--r-- | pix/icon.gif | bin | 0 -> 87 bytes | |||
-rw-r--r-- | question.php | 356 | ||||
-rw-r--r-- | questiontype.php | 551 | ||||
-rw-r--r-- | renderer.php | 162 | ||||
-rw-r--r-- | sage_server.py | 29 | ||||
-rw-r--r-- | settings.php | 11 | ||||
-rw-r--r-- | version.php | 5 | ||||
-rw-r--r-- | xmlrpc-utils.php | 269 |
23 files changed, 4208 insertions, 0 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..bb14cb4 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,33 @@ +Version 0.0.4 +Improvements + - Export and import to Moodle XML format + - Backup and restore functions added +Bug fixes + - Fixed parser problem with negative numbers +Version 0.0.3 +Improvements + - Added danish localizations based on forum feedback: mltiplication now + uses 'cdot' and decimal points are rendered as commas when Danish is + selected as a language +Bug fixes + - Operator priority, BODMAS, not quite implemented correctly. */ and +- not + implemented as equal priority - now fixed +Version 0.0.2 +Significant changes as a result of the first round of feedback! + - Renamed parser classes to conform to coding guidelines + - Moved all parser strings into a language pack + - Switched a lot of double quoted string to single as per guidelines + - added automatic formatted comments as required by coding guidelines + - changed treatment of variable names to help reduce confusion. Now + the first letter is treated as the name and the rest are subscripted. + Greek letter names are treated as a single character i.e. theta1 + becomes \theta_{1} in LaTeX. + - Added option to specify text which goes in front of response box + - Added support for specified variable names in the parser to improve + parsing in some situations e.g. 'xy' will now get treated as 'x * y' if + there are two variables 'x' and 'y' defined. +Bug fixes + - fixed bug when evaluating special constants in the parser + - fixed incorrect rendering of sqrt in LaTeX by the parser + - fixed incorrect sage-server.py file in the ZIP +Version 0.0.1 released diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..da45f38 --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,48 @@ +INSTALLATION INSTRUCTIONS + +Before installation: Please note that this is ALPHA quality software at +the moment. DO NOT install this on your production server - or at least +don't blame me if you do and it all goes horribly wrong! + + +REQUIREMENTS + +To install the algebra based question type you will need the following: + +1) Already installed copy of Moodle 2.1 or higher. It may work with other + Moodle versions but this is the only one I have tested it with. + +2) PHP5: The code uses exceptions and so requires PHP version 5 at a + minimum. I used this since Moodle 2.1 will require it so it should + not be an unusual requirement for long. I used PHP 5.3.5. + +3) If you want to use the SAGE XML-RPC server you will also need XML-RPC + support in PHP5. Most installations include this but the MAMP 1.7.1 + package does not. If you use MAMP you will need to download the MAMP + source code from the MAMP website and recompile PHP making sure you + call the initial configure script with the flag "--with-xmlrpc". + +4) If you want to use the SAGE XML-RPC server you will also need a copy of + SAGE which you can get from here: http://www.sagemath.org/. This should + be installed according to the instructions on their website. + + +INSTALLING + +1) Copy the contents of the "moodle" directory into your top level moodle + directory. Note that all the files are 'new' so there should be no files + overwritten. ('cp -iR' are good options to use). + +2) Go to Site Administration > Notifications and your database should be + configured. You are now ready to write algebra based questions which + use the 'Evaluate' and 'Equivalent' comarison methods. + +3) To run the SAGE XML-RPC server you will need an installed copy of SAGE + (http://www.sagemath.org/). Edit the first line of the 'sage-server.py' + file to point to your installed copy of the sage executable. Then simply + execute the sage-server.py script. It will run a very simple XML-RPC + server. If the machine you run your moodle server on is different from + the machine running your SAGE webserver you will need to edit line 191- + 193 of the file question/type/algebra/question.php to point to the + XML-RPC server. The same applies if you edit the network port. (This + mechanism obviously needs to be improved!) diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..539a0e4 --- /dev/null +++ b/README.txt @@ -0,0 +1,23 @@ +MOODLE ALGEBRA QUESTION TYPE +Updated to Moodle 2.0 by Stefan Raffeiner <stefan.raffeiner (at) gmail.com> +Updated to Moodle 2.1 by Jean-Michel Vedrine <vedrine (at) univ-st-etienne.fr> + +README BY ROGER MOORE: +MOODLE ALGEBRA QUESTION TYPE + +These files implement a algebra based question type for Moodle. +Installation instructions are found in the file INSTALL. + +The code has been tested and used for a large, introductory physics +course (~120 students) at the University of Alberta for several +terms now and is stable and suitable for use in a production +environment. + +The code is all released under the GPL V3. + +Please send any bugs, comments, suggestions for new features etc. +to me. + +Enjoy, + +Roger Moore <rwmoore (at) ualberta.ca> diff --git a/backup/moodle1/lib.php b/backup/moodle1/lib.php new file mode 100644 index 0000000..d9a6406 --- /dev/null +++ b/backup/moodle1/lib.php @@ -0,0 +1,79 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * @package qtype + * @subpackage algebra + * @copyright 2011 David Mudrak <david@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Multichoice question type conversion handler + */ +class moodle1_qtype_algebra_handler extends moodle1_qtype_handler { + + /** + * @return array + */ + public function get_question_subpaths() { + return array( + 'ANSWERS/ANSWER', + 'ALGEBRA', + 'ALGEBRA/VARLIST/VARIABLE' + ); + } + + /** + * Appends the algebra specific information to the question + */ + public function process_question(array $data, array $raw) { + // convert and write the answers first + if (isset($data['answers'])) { + $this->write_answers($data['answers'], $this->pluginname); + } + + // convert and write the algebra variables + if (isset($data['algebra'][0]['varlist']['variable'])) { + $variables = $data['algebra'][0]['varlist']['variable']; + } else { + $variables = array(); + } + $this->xmlwriter->begin_tag('algebra_variables'); + foreach ($variables as $variable) { + $this->xmlwriter->begin_tag('algebra_variable', array('id' => $this->converter->get_nextid())); + $this->xmlwriter->full_tag('name', $variable['name']); + $this->xmlwriter->full_tag('min', $variable['min']); + $this->xmlwriter->full_tag('max', $variable['max']); + $this->xmlwriter->end_tag('algebra_variable'); + } + $this->xmlwriter->end_tag('algebra_variables'); + + // and finally the algebra options + $options = $data['algebra'][0]; + $this->xmlwriter->begin_tag('algebra', array('id' => $this->converter->get_nextid())); + $this->xmlwriter->full_tag('compareby', $options['compareby']); + $this->xmlwriter->full_tag('nchecks', $options['nchecks']); + $this->xmlwriter->full_tag('tolerance', $options['tolerance']); + $this->xmlwriter->full_tag('disallow', $options['disallow']); + $this->xmlwriter->full_tag('allowedfuncs', $options['allowedfuncs']); + $this->xmlwriter->full_tag('answerprefix', $options['answerprefix']); + $this->xmlwriter->end_tag('algebra'); + } +} diff --git a/backup/moodle2/backup_qtype_algebra_plugin.class.php b/backup/moodle2/backup_qtype_algebra_plugin.class.php new file mode 100644 index 0000000..9e08467 --- /dev/null +++ b/backup/moodle2/backup_qtype_algebra_plugin.class.php @@ -0,0 +1,63 @@ +<?php + +/** + * Moodle algebra question type class. + * + * @copyright © 2010 Hon Wai, Lau + * @author Hon Wai, Lau <lau65536@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License version 3 + * @package questionbank + * @subpackage questiontypes + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * Provides the information to backup algebra questions + */ +class backup_qtype_algebra_plugin extends backup_qtype_plugin { + + /** + * Returns the qtype information to attach to question element + */ + protected function define_question_plugin_structure() { + + // Define the virtual plugin element with the condition to fulfill + $plugin = $this->get_plugin_element(null, '../../qtype', 'algebra'); + + // Create one standard named plugin element (the visible container) + $pluginwrapper = new backup_nested_element($this->get_recommended_name()); + + // connect the visible container ASAP + $plugin->add_child($pluginwrapper); + + // This qtype uses standard question_answers, add them here + // to the tree before any other information that will use them + $this->add_question_question_answers($pluginwrapper); + + // Now create the qtype own structures + + $algebravariables = new backup_nested_element('algebra_variables'); + + $algebravariable = new backup_nested_element('algebra_variable', array('id'), array( + 'name', 'min', 'max')); + + $algebra = new backup_nested_element('algebra', array('id'), array( + 'compareby', 'nchecks', 'tolerance', + 'disallow', 'allowedfuncs', 'answerprefix')); + + // Now the own qtype tree + $pluginwrapper->add_child($algebravariables); + $algebravariables->add_child($algebravariable); + $pluginwrapper->add_child($algebra); + + // set source to populate the data + $algebra->set_source_table('question_algebra', array('questionid' => backup::VAR_PARENTID)); + $algebravariable->set_source_table('question_algebra_variables', array('question' => backup::VAR_PARENTID)); + + // don't need to annotate ids nor files + + return $plugin; + } +} diff --git a/backup/moodle2/restore_qtype_algebra_plugin.class.php b/backup/moodle2/restore_qtype_algebra_plugin.class.php new file mode 100644 index 0000000..5c2d567 --- /dev/null +++ b/backup/moodle2/restore_qtype_algebra_plugin.class.php @@ -0,0 +1,106 @@ +<?php + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * restore plugin class that provides the necessary information + * needed to restore one algebra qtype plugin + */ +class restore_qtype_algebra_plugin extends restore_qtype_plugin { + + /** + * Returns the paths to be handled by the plugin at question level + */ + protected function define_question_plugin_structure() { + + $paths = array(); + + // This qtype uses question_answers, add them + $this->add_question_question_answers($paths); + + // Add own qtype stuff + $elename = 'algebravariable'; + $elepath = $this->get_pathfor('/algebra_variables/algebra_variable'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + $elename = 'algebra'; + $elepath = $this->get_pathfor('/algebra'); // we used get_recommended_name() so this works + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; // And we return the interesting paths + } + + /** + * Process the qtype/algebra element + */ + public function process_algebra($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + // Detect if the question is created or mapped + $oldquestionid = $this->get_old_parentid('question'); + $newquestionid = $this->get_new_parentid('question'); + $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; + + // If the question has been created by restore, we need to create its question_algebra too + if ($questioncreated) { + // Adjust some columns + $data->questionid = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_algebra', $data); + // Create mapping (needed for decoding links) + $this->set_mapping('question_algebra', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } + + /** + * Process the qtype/algebravariable element + */ + public function process_algebravariable($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Detect if the question is created or mapped + $oldquestionid = $this->get_old_parentid('question'); + $newquestionid = $this->get_new_parentid('question'); + $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; + + // If the question has been created by restore, we need to create its question_algebra_variables too + if ($questioncreated) { + // Adjust some columns + $data->question = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_algebra_variables', $data); + // Create mapping + $this->set_mapping('question_algebra_variable', $oldid, $newitemid); + } else { + // Nothing to remap if the question already existed + } + } +} diff --git a/db/install.xml b/db/install.xml new file mode 100644 index 0000000..51b8d85 --- /dev/null +++ b/db/install.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="question/type/symbollic/db" VERSION="20080516" COMMENT="XMLDB file for Moodle question/type/algebra"> + <TABLES> + <TABLE NAME="question_algebra" COMMENT="Options for algebra questions" NEXT="question_algebra_variables"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" + NEXT="questionid"/> + <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" + SEQUENCE="false" PREVIOUS="id" NEXT="compareby"/> + <FIELD NAME="compareby" TYPE="char" LENGTH="20" NOTNULL="true" UNSIGNED="false" DEFAULT="evaluated" + SEQUENCE="false" PREVIOUS="questionid" NEXT="nchecks"/> + <FIELD NAME="nchecks" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="10" + SEQUENCE="false" PREVIOUS="compareby" NEXT="tolerance"/> + <FIELD NAME="tolerance" TYPE="float" NOTNULL="true" UNSIGNED="false" + SEQUENCE="false" PREVIOUS="nchecks" NEXT="disallow"/> + <FIELD NAME="disallow" TYPE="text" LENGTH="small" NOTNULL="true" UNSIGNED="false" + SEQUENCE="false" PREVIOUS="tolerance" NEXT="allowedfuncs"/> + <FIELD NAME="allowedfuncs" TYPE="text" LENGTH="small" NOTNULL="true" UNSIGNED="false" + SEQUENCE="false" PREVIOUS="disallow" NEXT="answerprefix"/> + <FIELD NAME="answerprefix" TYPE="text" LENGTH="small" NOTNULL="true" UNSIGNED="false" + SEQUENCE="false" PREVIOUS="allowedfuncs"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="questionid"/> + <KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/> + </KEYS> + </TABLE> + <TABLE NAME="question_algebra_variables" COMMENT="Variables for algebra questions" PREVIOUS="question_algebra"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" + NEXT="question"/> + <FIELD NAME="question" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" + SEQUENCE="false" PREVIOUS="id" NEXT="name"/> + <FIELD NAME="name" TYPE="char" LENGTH="30" NOTNULL="true" UNSIGNED="false" + SEQUENCE="false" PREVIOUS="question" NEXT="min"/> + <FIELD NAME="min" TYPE="char" LENGTH="30" NOTNULL="true" UNSIGNED="false" DEFAULT="-" + SEQUENCE="false" PREVIOUS="name" NEXT="max"/> + <FIELD NAME="max" TYPE="char" LENGTH="30" NOTNULL="true" UNSIGNED="false" DEFAULT="-" + SEQUENCE="false" PREVIOUS="min"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="question"/> + <KEY NAME="question" TYPE="foreign" FIELDS="question" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..371150b --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,59 @@ +<?php // $Id: upgrade.php,v 1.1 2008/07/24 01:48:12 arborrow Exp $ + +// This file keeps track of upgrades to +// the algebra qtype plugin +// +// Sometimes, changes between versions involve +// alterations to database structures and other +// major things that may break installations. +// +// The upgrade function in this file will attempt +// to perform all the necessary actions to upgrade +// your older installtion to the current version. +// +// If there's something it cannot do itself, it +// will tell you what you need to do. +// +// The commands in here will all be database-neutral, +// using the functions defined in lib/ddllib.php + +function xmldb_qtype_algebra_upgrade($oldversion=0) { + + global $CFG, $THEME, $DB; + + $dbman = $DB->get_manager(); + +/// And upgrade begins here. For each one, you'll need one +/// block of code similar to the next one. Please, delete +/// this comment lines once this file start handling proper +/// upgrade code. + + // Add the field to store the string which is placed in front of the answer + // box when the question is displayed + if ($oldversion < 2008061500) { + $table = new xmldb_table('question_algebra'); + $field = new xmldb_field('answerprefix', XMLDB_TYPE_TEXT, 'small', null, XMLDB_NOTNULL, null, '', 'allowedfuncs'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_plugin_savepoint(true, 2008061500, 'qtype', 'algebra'); + } + + // Drop the answers and variables fields wich are totaly redundants + if ($oldversion < 2011072800) { + $table = new xmldb_table('question_algebra'); + $field = new xmldb_field('answers'); + + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + $field = new xmldb_field('variables'); + + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + upgrade_plugin_savepoint(true, 2011072800, 'qtype', 'algebra'); + } + return true; +} + diff --git a/db/upgradelib.php b/db/upgradelib.php new file mode 100644 index 0000000..2f05142 --- /dev/null +++ b/db/upgradelib.php @@ -0,0 +1,71 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Upgrade library code for the algebra question type. + * + * @package qtype + * @subpackage algebra + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Class for converting attempt data for algebra questions when upgrading + * attempts to the new question engine. + * + * This class is used by the code in question/engine/upgrade/upgradelib.php. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_algebra_qe2_attempt_updater extends question_qtype_attempt_updater { + public function right_answer() { + foreach ($this->question->options->answers as $ans) { + if ($ans->fraction > 0.999) { + return $ans->answer; + } + } + } + + public function was_answered($state) { + return !empty($state->answer); + } + + public function response_summary($state) { + if (!empty($state->answer)) { + return $state->answer; + } else { + return null; + } + } + + public function set_first_step_data_elements($state, &$data) { + } + + public function supply_missing_first_step_data(&$data) { + } + + public function set_data_elements_for_step($state, &$data) { + if (!empty($state->answer)) { + $data['answer'] = $state->answer; + } + } +} diff --git a/displayformula.php b/displayformula.php new file mode 100644 index 0000000..e5051e5 --- /dev/null +++ b/displayformula.php @@ -0,0 +1,48 @@ +<?php + +// Moodle algebra question type class +// Author: Roger Moore <rwmoore 'at' ualberta.ca> +// License: GNU Public License version 3 + +/** + * Script which converts the given formula text into LaTeX code and then + * displays the appropriate image file. It relies on the LaTeX filter to + * be present. + */ + +require_once('../../../config.php'); +require_once("$CFG->dirroot/question/type/algebra/parser.php"); +global $PAGE; + +$p = new qtype_algebra_parser; +try { + $query=urldecode($_SERVER['QUERY_STRING']); + $m=array(); + + if(!preg_match('/vars=([^&]*)&expr=(.*)$/A',$query,$m)) { + throw new Exception('Invalid query string received from http server!'); + } + $vars=explode(',',$m[1]); + if(empty($m[2])) { + $texexp=''; + } else { + $exp = $p->parse($m[2],$vars); + $texexp = '$$'.$exp->tex().'$$'; + } +} catch(Exception $e) { + $texexp = get_string('parseerror','qtype_algebra',$e->getMessage()); +} +$formatoptions = new stdClass; +$formatoptions->para = false; +$PAGE->set_context(get_context_instance(CONTEXT_SYSTEM)); +$text = format_text($texexp, FORMAT_MOODLE, $formatoptions); +?> +<html> + <head> + <title>Formula</title> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + </head> + <body bgcolor="#FFFFFF"> + <?php echo $text; ?> + </body> +</html> diff --git a/edit_algebra_form.php b/edit_algebra_form.php new file mode 100644 index 0000000..eae12bf --- /dev/null +++ b/edit_algebra_form.php @@ -0,0 +1,378 @@ +<?php +/** + * Defines the editing form for the algebra question type. + * + * @copyright © 2008 Roger Moore + * @author Roger Moore <rwmoore@ualberta.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package questionbank + * @subpackage questiontypes + */ + +require_once($CFG->dirroot . '/question/type/edit_question_form.php'); +require_once($CFG->dirroot . '/question/type/algebra/questiontype.php'); +require_once($CFG->dirroot . '/question/type/algebra/parser.php'); + +// Override the default number of answers and the number to add to avoid clutter. +// Algebra questions will likely not have huge number of different answers... +define("SYMB_QUESTION_NUMANS_START", 2); +define("SYMB_QUESTION_NUMANS_ADD", 1); + +// Override the default number of answers and the number to add to avoid clutter. +// algebra questions will likely not have huge number of different answers... +define("SYMB_QUESTION_NUMVAR_START", 2); +define("SYMB_QUESTION_NUMVAR_ADD", 1); + +/** + * symoblic editing form definition. + */ +class qtype_algebra_edit_form extends question_edit_form { + /** + * Add question-type specific form fields. + * + * @param MoodleQuickForm $mform the form being built. + */ + protected function definition_inner($mform) { + // Add the select control which will select the comparison type to use + $mform->addElement('select', 'compareby', get_string('compareby','qtype_algebra'), + array( "sage" => get_string('comparesage', 'qtype_algebra'), + "eval" => get_string('compareeval', 'qtype_algebra'), + "equiv" => get_string('compareequiv','qtype_algebra') + )); + $mform->addHelpButton('compareby', 'compareby', 'qtype_algebra'); + $mform->setDefault('compareby','eval'); + + + // Add the control to select the number of checks to perform + // First create an array with all the allowed values. We will then use this array + // with the array_combine function to create a single array where the keys are the + // same as the array values + $chk_array=array( '1', '2', '3', '5', '7', + '10', '20', '30', '50', '70', + '100', '200', '300', '500', '700', '1000'); + // Add the select element using the array_combine method discussed above + $mform->addElement('select', 'nchecks', get_string('nchecks','qtype_algebra'), + array_combine($chk_array,$chk_array)); + $mform->addHelpButton('nchecks', 'nchecks', 'qtype_algebra'); + // Set the default number of checks to perform + $mform->setDefault('nchecks','10'); + + + // Add the box to set the tolerance to use when performing evaluation checks + $mform->addElement('text', 'tolerance', get_string('tolerance','qtype_algebra')); + $mform->addHelpButton('tolerance', 'tolerance', 'qtype_algebra'); + $mform->setType('tolerance', PARAM_NUMBER); + $mform->setDefault('tolerance','0.001'); + + // Add an entry for the answer box prefix + $mform->addElement('text', 'answerprefix', get_string('answerprefix','qtype_algebra'),array('size'=>55)); + $mform->addHelpButton('answerprefix', 'answerprefix', 'qtype_algebra'); + $mform->setType('answerprefix', PARAM_RAW); + + // Add an entry for a disallowed expression + $mform->addElement('text', 'disallow', get_string('disallow','qtype_algebra'),array('size'=>55)); + $mform->addHelpButton('disallow', 'disallow', 'qtype_algebra'); + $mform->setType('disallow', PARAM_RAW); + + // Create an array which will store the function checkboxes + $func_group=array(); + // Create an array to add spacers between the boxes + $spacers=array('<br>'); + // Add the initial all functions box to the list of check boxes + $func_group[] =& $mform->createElement('checkbox','all','',get_string('allfunctions','qtype_algebra')); + // Create a checkbox element for each function understood by the parser + for($i=0;$i<count(qtype_algebra_parser::$functions);$i++) { + $func=qtype_algebra_parser::$functions[$i]; + $func_group[] =& $mform->createElement('checkbox',$func,'',$func); + if(($i % 6) == 5) { + $spacers[]='<br>'; + } else { + $spacers[]=str_repeat(' ',8-strlen($func)); + } + } + // Create and add the group of function controls to the form + $mform->addGroup($func_group,'allowedfuncs',get_string('allowedfuncs','qtype_algebra'),$spacers,true); + $mform->addHelpButton('allowedfuncs', 'allowedfuncs', 'qtype_algebra'); + $mform->disabledIf('allowedfuncs','allowedfuncs[all]','checked'); + $mform->setDefault('allowedfuncs[all]','checked'); + + $mform->addElement('static', 'variablesinstruct', + get_string('variables', 'qtype_algebra'), + get_string('filloutonevariable', 'qtype_algebra')); + $mform->closeHeaderBefore('variablesinstruct'); + // Create the array for the list of variables used in the question + $repeated=array(); + // Create the array for the list of repeated options used by the variable subforms + $repeatedoptions = array(); + + // Add the form elements to enter the variables + $repeated[] =& $mform->createElement('header','variablehdr',get_string('variableno','qtype_algebra','{no}')); + //$repeatedoptions['variablehdr']['helpbutton'] = array('variable',get_string('variable','qtype_algebra'), + // 'qtype_algebra'); + $repeated[] =& $mform->createElement('text','variable',get_string('variablename','qtype_algebra'),array('size'=>20)); + $mform->setType('variable', PARAM_RAW); + $repeated[] =& $mform->createElement('text','varmin',get_string('varmin','qtype_algebra'),array('size'=>20)); + $mform->setType('varmin', PARAM_RAW); + $repeatedoptions['varmin']['default'] = ''; + $repeated[] =& $mform->createElement('text','varmax',get_string('varmax','qtype_algebra'),array('size'=>20)); + $mform->setType('varmax', PARAM_RAW); + $repeatedoptions['varmax']['default'] = ''; + + // Get the current number of variables defined, if any + if (isset($this->question->options)) { + $countvars = count($this->question->options->variables); + } else { + $countvars = 0; + } + // Come up with the number of variable entries to add to the form at the start + if ($this->question->formoptions->repeatelements){ + $repeatsatstart = (SYMB_QUESTION_NUMVAR_START > ($countvars + SYMB_QUESTION_NUMVAR_ADD))? + SYMB_QUESTION_NUMVAR_START : ($countvars + SYMB_QUESTION_NUMVAR_ADD); + } else { + $repeatsatstart = $countvars; + } + $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 'novariables', 'addvariables', + SYMB_QUESTION_NUMVAR_ADD, get_string('addmorevariableblanks', 'qtype_algebra')); + + $mform->addElement('static', 'answersinstruct', + get_string('correctanswers', 'qtype_algebra'), + get_string('filloutoneanswer', 'qtype_algebra')); + $mform->closeHeaderBefore('answersinstruct'); + + $this->add_per_answer_fields($mform, get_string('answerno', 'qtype_algebra', '{no}'), + question_bank::fraction_options(), SYMB_QUESTION_NUMANS_START, SYMB_QUESTION_NUMANS_ADD); + + $this->add_interactive_settings(); + + } + + protected function data_preprocessing($question) { + $question = parent::data_preprocessing($question); + $question = $this->data_preprocessing_answers($question); + $question = $this->data_preprocessing_hints($question); + + if (!empty($question->options)) { + $question->compareby = $question->options->compareby; + $question->nchecks = $question->options->nchecks; + $question->tolerance = $question->options->tolerance; + $question->allowedfuncs = $question->options->allowedfuncs; + $question->disallow = $question->options->disallow; + $question->answerprefix = $question->options->answerprefix; + } + + return $question; + } + /** + * Sets the existing values into the form for the question specific data. + * + * This method copies the data from the existing database record into the form fields as default + * values for the various elements. + * + * @param $question the question object from the database being used to fill the form + */ + function set_data($question) { + // Check to see if there are any existing question options, if not then just call + // the base class set data method and exit + if (!isset($question->options)) { + return parent::set_data($question); + } + + // Now we do exactly the same for the variables... + $vars = $question->options->variables; + // If we found any variables then loop over them using a numerical key to provide an index + // to the arrays we need to access in the form + if (count($vars)) { + $key = 0; + foreach ($vars as $var) { + // For every variable set the default values + $default_values['variable['.$key.']'] = $var->name; + // Only set the min and max defaults if this variable has a range + if($var->min!='') { + $default_values['varmin['.$key.']'] = $var->min; + $default_values['varmax['.$key.']'] = $var->max; + } + $key++; + } + } + + // Add the default values for the allowed functions controls + // First check to see if there are any allowed functions defined + if(count($question->options->allowedfuncs)>0) { + // Clear the 'all functions' flag since functions are restricted + $default_values['allowedfuncs[all]']=0; + // Loop over all the functions which the parser understands + foreach(qtype_algebra_parser::$functions as $func) { + // For each function see if the function is in the allowed function + // list and if so set the check box otherwise remove the check box + if(in_array($func,$question->options->allowedfuncs)) { + $default_values['allowedfuncs['.$func.']']=1; + } else { + $default_values['allowedfuncs['.$func.']']=0; + } + } + } + // There are no allowed functions defined so all functions are allowed + else { + $default_values['allowedfuncs[all]']=1; + } + + // Add the default values to the question object in a form which the parent + // set data method will be able to use to find the default values + $question = (object)((array)$question + $default_values); + + // Finally call the parent set data method to handle everything else + parent::set_data($question); + } + + /** + * Validates the form data ensuring there are no obvious errors in the submitted data. + * + * This method performs some basic sanity checks on the form data before it gets converted + * into a database record. + * + * @param $data the data from the form which needs to be checked + * @param $files some files - I don't know what this is for! - files defined in the form?? + */ + public function validation($data, $files) { + // Call the base class validation method and keep any errors it generates + $errors = parent::validation($data, $files); + + // Regular expression string to match a number + $renumber='/([+-]*(([0-9]+\.[0-9]*)|([0-9]+)|(\.[0-9]+))|'. + '(([0-9]+\.[0-9]*)|([0-9]+)|(\.[0-9]+))E([-+]?\d+))/A'; + + // Perform sanity checks on the variables. + $vars = $data['variable']; + // Create an array of defined variables + $varlist=array(); + foreach ($vars as $key => $var) { + $trimvar = trim($var); + $trimmin = trim($data['varmin'][$key]); + $trimmax = trim($data['varmax'][$key]); + // Check that there is a valid variable name otherwise skip + if ($trimvar == '') { + continue; + } + // Check that this variable does not have the same name as a function + if(in_array($trimvar,qtype_algebra_parser::$functions) or in_array($trimvar,qtype_algebra_parser::$specials)) { + $errors['variable['.$key.']'] = get_string('illegalvarname','qtype_algebra',$trimvar); + } + // Check that this variable has not been defined before + if(in_array($trimvar,$varlist)) { + $errors['variable['.$key.']'] = get_string('duplicatevar','qtype_algebra'); + } else { + // Add the variable to the list of defined variables + $varlist[]=$trimvar; + } + // If the comparison algorithm selected is evaluate then ensure that each variable + // has a valid minimum and maximum defined. For the other types of comparison we can + // ignore the range + if($data['compareby']=='eval') { + // Check that a minimum has been defined + if ($trimmin == '') { + $errors['varmin['.$key.']'] = get_string('novarmin','qtype_algebra'); + } + // If there is one check that it is a number + else if(!preg_match($renumber,$trimmin)) { + $errors['varmin['.$key.']'] = get_string('notanumber','qtype_algebra'); + } + if ($trimmax == '') { + $errors['varmax['.$key.']'] = get_string('novarmax','qtype_algebra'); + } + // If there is one check that it is a number + else if(!preg_match($renumber,$trimmax)) { + $errors['varmax['.$key.']'] = get_string('notanumber','qtype_algebra'); + } + // Check that the minimum is less that the maximum! + if ((float)$trimmin > (float)$trimmax) { + $errors['varmin['.$key.']'] = get_string('varmingtmax','qtype_algebra'); + } + } // end check for eval type + } // end loop over variables + // Check that at least one variable is defined + if (count($varlist)==0) { + $errors['variable[0]'] = get_string('notenoughvars', 'qtype_algebra'); + } + + // Now perform the sanity checks on the answers + // Create a parser which we will use to check that the answers are understandable + $p = new qtype_algebra_parser; + $answers = $data['answer']; + $answercount = 0; + $maxgrade = false; + // Create an empty array to store the used variables + $ansvars=array(); + // Create an empty array to store the used functions + $ansfuncs=array(); + // Loop over all the answers in the form + foreach ($answers as $key => $answer) { + // Try to parse the answer string using the parser. If this fails it will + // throw an exception which we catch to generate the associated error string + // for the expression + try { + $expr=$p->parse($answer); + // Add any new variables to the list we are keeping. First we get the list + // of variables in this answer. Then we get the array of variables which are + // in this answer that are not in any previous answer (using array_diff). + // Finally we merge this difference array with the list of all variables so far + $tmpvars=$expr->get_variables(); + $ansvars=array_merge($ansvars,array_diff($tmpvars,$ansvars)); + // Check that all the variables in this answer have been declared + // Do this by looking for a non-empty array to be returned from the array_diff + // between the list of all declared variables and the variables in this answer + if($d=array_diff($tmpvars,$varlist)) { + $errors['answer['.$key.']'] = get_string('undefinedvar','qtype_algebra',"'".implode("', '",$d)."'"); + } + // Do the same for functions which we did for variables + $ansfuncs=array_merge($ansfuncs,array_diff($expr->get_functions(),$ansfuncs)); + // Check that this is not an empty answer + if (!is_a($expr,"qtype_algebra_parser_nullterm")) { + // Increase the number of answers + $answercount++; + // Check to see if the answer has the maximum grade + if ($data['fraction'][$key] == 1) { + $maxgrade = true; + } + } + } catch (Exception $e) { + $errors['answer['.$key.']']=$e->getMessage(); + // Return here because subsequent errors may be wrong due to not counting the answer + // which just failed to parse + return $errors; + } + } + // Check that we have at least one answer! + if ($answercount==0){ + $errors['answer[0]'] = get_string('notenoughanswers', 'quiz', 1); + } + // Check that at least one question has the maximum possible grade + if ($maxgrade == false) { + $errors['fraction[0]'] = get_string('fractionsnomax', 'question'); + } + + // Check for variables which are defined but never used. + // Do this by looking for a non-empty array to be returned from array_diff. + if($d=array_diff($varlist,$ansvars)) { + // Loop over all the variables in the form + foreach ($vars as $key => $var) { + $trimvar = trim($var); + // If the variable is in the unused array then add the error message to that variable + if(in_array($trimvar,$d)) { + $errors['variable['.$key.']'] = get_string('unusedvar','qtype_algebra'); + } + } + } + + // Check that the tolerance is greater than or equal to zero + if($data['tolerance']<0) { + $errors['tolerance']=get_string('toleranceltzero','qtype_algebra'); + } + + return $errors; + } + + public function qtype() { + return 'algebra'; + } +} + diff --git a/lang/en/qtype_algebra.php b/lang/en/qtype_algebra.php new file mode 100644 index 0000000..814aea8 --- /dev/null +++ b/lang/en/qtype_algebra.php @@ -0,0 +1,91 @@ +<?php +include('qtype_algebra_parser.php'); + +$string['answermustbegiven'] = 'You must enter an answer if there is a grade or feedback.'; +$string['answerno'] = 'Answer {$a}'; +$string['addmoreanswerblanks'] = 'Blanks for {no} More Answers'; +$string['addmorevariableblanks'] = 'Blanks for {no} More Variables'; +$string['allfunctions'] = 'All Functions'; +$string['allowedfuncs'] = 'Allowed Functions'; +$string['allowedfuncs_help'] = '**NOT YET IMPLEMENTED** + +These controls can be used to restrict the functions which the students +can use in their responses. If the "All" button is checked then +there are no restrictions on functions which the students may use in +their answers. This is the default case. To restrict the allowed functions +uncheck the "All" box and select the functions you wish to allow.'; +$string['allowedfunctions'] = 'Allowed Functions'; +$string['answer'] = 'Answer: {$a}'; +$string['answerboxprefix'] = 'String with which to prefix the answer box when displaying the question'; +$string['answerprefix_help'] = 'The text entered here will be placed in front of the input box where +students enter their answers. For example if a question is asking the form +of a function, f(x), then the string "f(x) = " could be entered in this +field.'; +$string['answerno'] = 'Answer {$a}'; +$string['answerprefix'] = 'Answer box prefix'; +$string['checktolerance'] = 'Check Tolerance'; +$string['compalgorithm'] = 'Comparison Algorithm'; +$string['compareby_help'] = 'This selects the method by which the students\' responses are compared +to all the questions answers. The different possibilities are: + +SAGE: uses the Open Source <a href="http://www.sagemath.org/">SAGE</a> +mathematics software to perform a full symbolic algebraic comparison. + +Evaluation: This method generates random numbers for +the question variables and then evaluates both the student response and the +question\'s answer for that set of values. + +Equivalence: +This is the simplest of all the methods. It will only perform the most basic of +comparisons between expressions.'; +$string['compareby'] = 'Comparison Algorithm'; +$string['comparesage'] = 'SAGE'; +$string['compareeval'] = 'Evaluation'; +$string['compareequiv'] = 'Equivalence'; +$string['correctanswers'] = 'Correct answers'; +$string['correctansweris'] = 'The correct answer is: {$a} giving '; +$string['disallow'] = 'Disallowed Answer'; +$string['disallow_help'] = 'contains an expression which will be disallowed as an answer. +Students entering an answers which matches this will be prevented from +receiving any grade for the question even if the response would match +a given answer for the question.'; +$string['disallowans'] = 'Disallowed Answer'; +$string['disallowanswer'] = 'Disallowed Answer'; +$string['editingalgebra'] = 'Editing an Algebra question'; +$string['evalchecks'] = 'Evaluation Checks'; +$string['filloutoneanswer'] = 'You must provide at least one possible answer. Answers left blank will not be used. \'*\' can be used as a wildcard to match any characters. The first matching answer will be used to determine the score and feedback. Only variables defined above are allowed'; +$string['filloutonevariable'] = 'You must provide at least one variable. All variables used by answers must be entered here. Minimum and a maximum values are only needed if the Evaluation comparison algorithm is used.'; +$string['illegalvarname'] = 'Illegal variable name \'{$a}\': same name as a parser function or special constant'; +$string['nchecks'] = 'Number of Evaluation Checks'; +$string['nchecks_help'] = 'Number of Evaluation Checks used in Evaluation Comparison Algorithm'; +$string['notanumber'] = 'Invalid value: a number is required'; +$string['notenoughvars'] = 'At least one variable is required for all algebra questions'; +$string['novarmax'] = 'No maximum bound specified for variable'; +$string['novarmin'] = 'No minimum bound specified for variable'; +$string['parseerror'] = 'Error parsing function: \'{$a}\''; +$string['restoreqdbfailed'] = 'Restoring algebra question failed: database write error'; +$string['restorevardbfailed'] = 'Restoring algebra question variable failed: database write error'; +$string['tolerance'] = 'Tolerance for Evaluation Checks'; +$string['tolerance_help'] = 'Determines the maximum difference between numerical +evaluations of the student response and question answers which will be +allowed to count as matching.'; +$string['toleranceltzero'] = 'Tolerance must be greater than or equal to zero'; +$string['undefinedvar'] = 'Undefined variable(s) {$a} used in one or more answers'; +$string['unusedvar'] = 'This variable is not used by any answer'; +$string['variable'] = 'Variable Name'; +$string['variablename'] = 'Variable Name'; +$string['variableno'] = 'Variable {$a}'; +$string['variables'] = 'Variables'; +$string['varmin'] = 'Minimum Value'; +$string['varmingtmax'] = 'The minimum value must be less than the maximum value'; +$string['varmax'] = 'Maximum Value'; + +$string['pluginnameadding'] = 'Adding an algebra question'; +$string['pluginnameediting'] = 'Editing an algebra question'; +$string['pluginname_link'] = 'question/type/algebra'; +$string['pluginname_help'] = 'Student enter a formula as response that include one or more variables. Correctness is evaluted using one of 3 differents methods'; +$string['pluginname'] = 'algebra'; +$string['pluginnamesummary'] = 'Student enter a formula that can include one or more variables. Correctness is evaluted using one of 3 differents methods.'; +$string['host'] = 'Host url of SAGE server'; +$string['port'] = 'Port of SAGE server'; +$string['uri'] = 'uri of SAGE server'; diff --git a/lang/en/qtype_algebra_parser.php b/lang/en/qtype_algebra_parser.php new file mode 100644 index 0000000..d4f7241 --- /dev/null +++ b/lang/en/qtype_algebra_parser.php @@ -0,0 +1,20 @@ +<?php +$string['badclosebracket'] = 'Invalid close bracket found'; +$string['badequivtype'] = 'Invalid type: can only compare parser terms with other parser terms'; +$string['badfuncargs'] = 'Invalid arguments for the function \'{$a}\''; +$string['decimal'] = '.'; +$string['illegalplusminus'] = 'Found a + or - in an invalid location'; +$string['mismatchedbracket'] = 'Mismatched brackets: Open and close bracket pair not of same type \'$a\''; +$string['mismatchedcloseb'] = 'Mismatched brackets: Close bracket without an open bracket found'; +$string['mismatchedopenb'] = 'Mismatched brackets: Open bracket without a close bracket found'; +$string['missingonearg'] = 'Syntax Error: Operator \'{$a}\' missing its argument'; +$string['missingtwoargs'] = 'Syntax Error: Operator \'{$a}\' requires two arguments'; +$string['morethantwoargs'] = 'Trying to compare a term with more than 2 arguments - no code to handle this case!'; +$string['multiply'] = '\\\\times'; +$string['nargswrong'] = 'Incorrect number of arguments for the term \'{$a}\''; +$string['noevaluate'] = 'The evaluate method for term \'{$a}\' has not been implemented'; +$string['notopterm'] = 'Syntax Error: Unable to condense to a single, top level operator'; +$string['undeclaredvar'] = 'Undeclared variable \'{$a}\' found'; +$string['undefinedfunction'] = 'Undefined function \'{$a}\''; +$string['undefinedvariable'] = 'Undefined variable \'{$a}\' found when numerically evaluating an expression'; +$string['unknownterm'] = 'Syntax Error: Unknown term found at \'{$a}\' in the expression'; @@ -0,0 +1,38 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Serve question type files + * + * @since 2.0 + * @package qtype + * @subpackage algebra + * @copyright Dongsheng Cai <dongsheng@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Checks file access for algebra questions. + */ +function qtype_algebra_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { + global $DB, $CFG; + require_once($CFG->libdir . '/questionlib.php'); + question_pluginfile($course, $context, 'qtype_algebra', $filearea, $args, $forcedownload, $options); +} diff --git a/parser.php b/parser.php new file mode 100644 index 0000000..263d951 --- /dev/null +++ b/parser.php @@ -0,0 +1,1721 @@ +<?php + +// Parser code for the Moodle Algebra question type +// Moodle algebra question type class +// Author: Roger Moore <rwmoore 'at' ualberta.ca> +// License: GNU Public License version 3 + + +// From the PHP manual: check for the existance of lcfirst and +// if not found create one. +if(!function_exists('lcfirst')) { + /** + * Make a string's first character lowercase + * + * @param string $str + * @return string the resulting string. + */ + function lcfirst( $str ) { + $str[0] = strtolower($str[0]); + return (string)$str; + } +} + +/** + * Helper function which will compare two strings using their length only. + * + * This function is intended for use in sorting arrays of strings by their string + * length. This is used to order arrays for regular expressions so that the longest + * expressions are checked first. + * + * @param $a first string to compare + * @param $b second string to compare + * @return -1 if $a is longer than $b, 0 if they are the same length and +1 if $a is shorter + */ +function qtype_algebra_parser_strlen_sort($a,$b) { + // Get the two string lengths once so we don't have to repeat the function call + $alen=strlen($a); + $blen=strlen($b); + // If the two lengths are equal return zero + if($alen==$blen) return 0; + // Otherwise return +1 if a>b or -1 if a<b + return ($alen>$blen) ? -1 : +1; +} + + +/** + * Class which represents a single term in an algebraic expression. + * + * A single algebraic term is considered to be either an operation, for example addition, + * subtraction, raising to a power etc. or something operated on, such as a number or + * variable. Each type of term implements a subclass of this base class. + */ +class qtype_algebra_parser_term { + + /** + * Constructor for the generic parser term. + * + * This method is called by all subclasses to initialize the base class for use. + * It initializes the number of arguments required, the format strings to use + * when converting the term in various strng formats, the parser text associated + * with the term and whether the term is one which commutes. + * + * @param $nargs number of arguments which this type of term requires + * @param $formats an array of the format strings for this term keyed by type + * @param $text the text from the expression associated with the array + * @param $commutes if set to true then this term commutes (only for 2 argument terms) + */ + function qtype_algebra_parser_term($nargs,$formats,$text='',$commutes=false) { + $this->_value=$text; + $this->_nargs=$nargs; + $this->_formats=$formats; + $this->_commutes=$commutes; + } + + /** + * Generates the list of arguments needed when converting the term into a string. + * + * This method returns an array with the arguments needed when converting the term + * into a string. The arrys can then be used with a format string to generate the + * string representation. The method is recursive because it needs to convert the + * arguments of the term into strings and so it will walk down the parse tree. + * + * @param $method name of method to call to convert arguments into strings + * @return array of the arguments that, with a format string, can be passed to sprintf + */ + function print_args($method) { + // Create an empty array to store the arguments in + $args=array(); + // Handle zero argument terms differently by making the + // first 'argument' the value of the term itself + if($this->_nargs==0) { + $args[]=$this->_value; + } else { + foreach($this->_arguments as $arg) { + $args[]=$arg->$method(); + } + } + // Return the array of arguments + return $args; + } + + /** + * Produces a 'prettified' string of the expression using the standard input syntax. + * + * This method will use the {@link print_args} method to convert the term and all its + * arguments into a string. + * + * @return input syntax format string of the expression + */ + function str() { + // First check to see if the class has been given all the arguments + $this->check_arguments(); + // Get an array of all the arguments except for the format string + $args=$this->print_args('str'); + // Insert the format string at the front of the argument array + array_unshift($args,$this->_formats['str']); + // Call sprintf using the argument array as the arguments + return call_user_func_array('sprintf',$args); + } + + /** + * Produces a LaTeX formatted string of the expression. + * + * This method will use the {@link print_args} method to convert the term and all its + * arguments into a LaTeX formatted string. This can then be given to the main Moodle + * engine, with TeX filter enabled, to produce a graphical representation of the + * expression. + * + * @return LaTeX format string of the expression + */ + function tex() { + // First check to see if the class has been given all the arguments + $this->check_arguments(); + // Get an array of all the arguments except for the format string + $args=$this->print_args('tex'); + // Insert the format string at the front of the argument array + array_unshift($args,$this->_formats['tex']); + // Call sprintf using the argument array as the arguments + return call_user_func_array('sprintf',$args); + } + + /** + * Produces a SAGE formatted string of the expression. + * + * This method will use the {@link print_args} method to convert the term and all its + * arguments into a SAGE formatted string. This can then be passed to SAGE via XML-RPC + * for symbolic comparisons. The format is very similar to the {@link str} method but + * has all multiplications made explicit with an asterix. + * + * @return SAGE format string of the expression + */ + function sage() { + // First check to see if the class has been given all the arguments + $this->check_arguments(); + // Get an array of all the arguments except for the format string + $args=$this->print_args('sage'); + // Insert the format string at the front of the argument array. First we + // check to see if there is a format element called 'sage' if not then we + // default to the standard string format + if(array_key_exists('sage',$this->_formats)) { + // Insert the sage format string at the front of the argument array + array_unshift($args,$this->_formats['sage']); + } else { + // Insert the normal format string at the front of the argument array + array_unshift($args,$this->_formats['str']); + } + // Call sprintf using the argument array as the arguments + return call_user_func_array('sprintf',$args); + } + + /** + * Returns the list of arguments for the term. + * + * This method provides access to the arguments of the term. Although this should + * ideally be private information it is needed in certain cases to determine + * how neighbouring terms should display themselves. + * + * @return array of arguments for this term + */ + function arguments() { + return $this->_arguments; + } + + /** + * Sets the arguments of the term to the values in the given array. + * + * The code here overrides the base class's method. The code uses this method to actually + * set the arguments in the given array but a second stage to choose the format of the + * multiplication operator is required. This is because a 'x' symbol is required when + * multiplying two numbers. However this can be omitted when multiplying two variables, + * a variable and a function etc. + * + * @param $args array to set the arguments of the term to + */ + function set_arguments($args) { + if (count($args)!=$this->_nargs) { + throw new Exception(get_string('nargswrong','qtype_algebra',$this->_value)); + } + $this->_arguments=$args; + } + + /** + * Checks to ensure that the correct number of arguments are defined. + * + * Note that this method just checks for the number or arguments it does not check + * whether they are valid arguments. If the parameter passed is true (default value) + * an exception will be thrown if the correct number of arguments are not present. Otherwise + * the function returns false. + * + * @param $exc if true then an exception will be thrown if the number of arguments is incorrect + * @return true if the correct number of arguments are present, false otherwise + */ + function check_arguments($exc=true) { + $retval=(count($this->_arguments)==$this->_nargs); + if($exc && !$retval) { + throw new Exception(get_string('nargswrong','qtype_algebra',$this->_value)); + } else { + return $retval; + } + } + + /** + * Returns a list of all the variable names found in the expression. + * + * This method uses the {@link collect} method to walk down the parse tree and collect + * a list of all the variables which the parser has found in the expression. The names + * of the variables are then returned. + * + * @return an array containing all the variables names in the expression + */ + function get_variables() { + $list=array(); + $this->collect($list,'qtype_algebra_parser_variable'); + return array_keys($list); + } + + /** + * Returns a list of all the function names found in the expression. + * + * This method uses the {@link collect} method to walk down the parse tree and collect + * a list of all the functions which the parser has found in the expression. The names + * of the functions are then returned. + * + * @return an array containing all the function names used in the expression + */ + function get_functions() { + $list=array(); + $this->collect($list,'qtype_algebra_parser_function'); + return array_keys($list); + } + + /** + * Collects all the terms of a given type with unique values in the parse tree + * + * This method walks recursively down the parse tree by calling itself for the arguments + * of the current term. The method simply adds the current term to the given imput array + * using a key set to the value of the term but only if the term matches the selected type. + * In this way terms only a single entry per term value is return which is the functionality + * required for the {@link get_variables} and {@link get_functions} methods. + * + * @param $list the array to add the term to if it matches the type + * @param $type the name of the type of term to collect. + * @return an array containing all the terms of the selected type keyed by their value + */ + function collect(&$list,$type) { + // Add this class to the list if of the correct type + if(is_a($this,$type)) { + // Add a key to the array with the value of the term, this means + // that multiple terms with the same value will overwrite each + // other so only one will remain. + $list[$this->_value]=0; + } + // Now loop over all the argument for this term (if any) and check them + foreach($this->_arguments as $arg) { + // Collect terms from the arguments as well + $arg->collect($list,$type); + } + } + + /** + * Checks to see if this term is equal to another term ignoring arguments. + * + * This method compares the current term to another term. The default method simply compares + * the class of each term. Terms which require more than this, for example comparing values + * too, override this method in theor own classes. + * + * @param $term the term to compare to the current one + * @return true if the terms match, false otherwise + */ + function equals($term) { + // Default method just checks to ensure that the Terms are both of the same type + return is_a($term,get_class($this)); + } + + /** + * Compares this term, including any arguments, with another term. + * + * This method uses the {@link equals} method to see if the current and given term match. + * It then looks at any arguments which the two terms have and, recursively, calls their + * compare methods to determine if they also match. For terms with two arguments which + * also commute the reverse ordering of the arguments is also tried if the first order + * fails to match. + * + * @param $expr top level term of an expression to compare against + * @return true if the expressions match, false otherwise + */ + function equivalent($expr) { + // Check that the argument is also a term + if(!is_a($expr,'qtype_algebra_parser_term')) { + throw new Exception(get_string('badequivtype','qtype_algebra')); + } + // Now check that this term is the same as the given term + if(!$this->equals($expr)) { + // Terms are not equal immediately return false since the two do not match + return false; + } + // Now compare the arguments recursively... + switch($this->_nargs) { + case 0: + // For zero arguments we already compared this class and found it the same so + // because there are no arguments to check we are equivalent! + return true; + case 1: + // For one argument we also need to compare the argument of each term + return $this->_arguments[0]->equivalent($expr->_arguments[0]); + case 2: + // Now it gets interesting. First we compare the two arguments in the same + // order and see what we get... + if($this->_arguments[0]->equivalent($expr->_arguments[0]) and + $this->_arguments[1]->equivalent($expr->_arguments[1])) { + // Both arguments are equivalent so we have a match + return true; + } + // Otherwise if the operator commutes we can see if the first argument matches + // the second argument and vice versa + else if($this->_commutes and $this->_arguments[0]->equivalent($expr->_arguments[1]) and + $this->_arguments[1]->equivalent($expr->_arguments[0])) { + return true; + } else { + return false; + } + default: + throw new Exception(get_string('morethantwoargs','qtype_algebra')); + } + } + + /** + * Returns the number of arguments required by the term. + * + * @return the number of arguments required by the term + */ + function n_args() { + return $this->_nargs; + } + + /** + * Evaluates the term numerically using the given variable values. + * + * The given parameter array is keyed by the name of the variable and the numerical + * value to assign it is stored in the array value. This method is an abstract one + * which must be implemented by all subclasses. Failure to do so will generate an + * exception when the method is called. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + throw new Exception(get_string('noevaluate','qtype_algebra',$this->_value)); + } + + /** + * Dumps the term and its arguments to standard out. + * + * This method will recursively call the entire parse tree attached to it and produce + * a nicely formatted dump of the term structure. This is mainly useful for debugging + * purposes. + * + * @param $indent string containing the indentation to use + * @param $params variable values to use if an evaluation is also desired + * @return a string indicating the type of the term + */ + function dump(&$params=array(),$indent='') { + echo "$indent<Term type '".get_class($this).'\' with value \''.$this->_value; + if(!empty($params)) { + echo ' eval=\''.$this->evaluate($params)."'>\n"; + } else { + echo "'>\n"; + } + foreach($this->_arguments as $arg) { + $arg->dump($params,$indent.' '); + } + } + + /** + * Special casting operator method to convert the term object to a string. + * + * This is primarily a debug method. It is called when the term object is cast into a + * string, such as happens when echoing or printing it. It simply returns a string + * indicating the type of the parser term. + * + * @return a string indicating the type of the term + */ + function __toString() { + return '<Algebraic parser term of type \''.get_class($this).'\'>'; + } + + // Member variables + var $_value; // String of the actual term itself + var $_arguments=array(); // Array of arguments in class form + var $_formats; // Array of format strings + var $_nargs; // Number of arguments for this term +} + +/** + * Class representing a null, or empty, term. + * + * This is the type of term returned when the parser is given an empty string to parse. + * It takes no arguments and will never be found in a parser tree. This term is solely + * to give a valid return type for an empty string condition and so avoids the need to + * throw an exception in such cases. + */ +class qtype_algebra_parser_nullterm extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a null term. + * + * Initializes a null term class. Since this class represents nothing no special + * initialization is required and no arguments are needed. + */ + function qtype_algebra_parser_nullterm() { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats,''); + } + + /** + * Returns the array of arguments needed to convert this class into a string. + * + * Since this class is represented by an empty string which has no formatting fields + * we override the base class method to return an empty array. + * + * @param $method name of method to call to convert arguments into strings + * @return array of the arguments that, with a format string, can be passed to sprintf + */ + function print_args($method) { + return array(); + } + + /** + * Evaluates the term numerically. + * + * Since this is an empty term we define the evaluation as zero regardless of the parameters. + * + * @param $params array of the variable values to use + */ + function evaluate($params) { + // Return something which is not a number + return acos(2.0); + } + + // Static class properties + const NARGS=0; + private static $formats=array('str' => '', + 'tex' => ''); +} + + +/** + * Class representing a number. + * + * All purely numerical quantities will be represented by this type of class. There are + * two basic types of numbers: non-exponential and exponential. Both types are handled by + * this single class. + */ +class qtype_algebra_parser_number extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a number term. + * + * This function initializes an instance of a number term using the string which + * matches the number's regular expression. + * + * @param $text string matching the number regular expression + */ + function qtype_algebra_parser_number($text='') { + // Unfortunately PHP maths will only support a '.' as a decimal point and will not support + // ',' as used in Danish, French etc. To allow for this we always convert any commas into + // decimal points before we parse the string + $text=str_replace(',','.',$text); + $this->_sign=''; + // Now determine whether this is in exponent form or just a plain number + if(preg_match('/([\.0-9]+)E([-+]?\d+)/',$text,$m)) { + $this->_base=$m[1]; + $this->_exp=$m[2]; + $eformats=array('str' => '%sE%s', + 'tex' => '%s '.get_string('multiply','qtype_algebra').' 10^{%s}'); + parent::qtype_algebra_parser_term(self::NARGS,$eformats,$text); + } else { + $this->_base=$text; + $this->_exp=''; + parent::qtype_algebra_parser_term(self::NARGS,self::$formats,$text); + } + } + + /** + * Sets this number to be negative. + * + * This method will convert the number into a nagetive one. It is called when + * the parser finds a subtraction operator in front of the number which does + * not have a variable or another number preceding it. + */ + function set_negative() { + // Prepend a minus sign to both the base and total value strings + $this->_base='-'.$this->_base; + $this->_value='-'.$this->_value; + $this->_sign='-'; + } + + /** + * Checks to see if this number is equal to another number. + * + * This is a two step process. First we use the base class equals method to ensure + * that we are comparing two numbers. Then we check that the two have the same value. + * + * @param $expt the term to compare to the current one + * @return true if the terms match, false otherwise + */ + function equals($expr) { + // Call the default method first to check type + if(parent::equals($expr)) { + return (float)$this->_value==(float)$expr->_value; + } else { + return false; + } + } + + /** + * Generates the list of arguments needed when converting the term into a string. + * + * For number terms there are two possible formats: those with an exponent and those + * without an exponent. This method determines which to use and then pushes the correct + * arguments into the array which is returned. + * + * @param $method name of method to call to convert arguments into strings + * @return array of the arguments that, with a format string, can be passed to sprintf + */ + function print_args($method) { + // When displaying the number we need to worry about whether to use a decimal point + // or a comma depending on the language currently selected/ Do this by replacing the + // decimal point (which we have to use internally because of the PHP math standard) + // with the correct string from the language pack + $base=str_replace('.',get_string('decimal','qtype_algebra'),$this->_base); + // Put the base part of the number into the argument array + $args=array($base); + // Check to see if we have an exponent... + if($this->_exp) { + // ...we do so add it to the argument array as well + $args[]=$this->_exp; + } + // Return the list of arguments + return $args; + } + + /** + * Evaluates the term numerically. + * + * All this method does is return the string representing the number cast as a double + * precision floating point variable. + * + * @param $params array of the variable values to use + */ + function evaluate($params) { + return doubleval($this->_value); + } + + // Static class properties + const NARGS=0; + private static $formats=array('str' => '%s', + 'tex' => '%s '); +} + +/** + * Class representing a variable term in an algebraic expression. + * + * When the parser finds a text string which does not correspond to a function it creates + * this type of term and puts the contents of that text into it. Variables with names + * corresponding to the names of the greek letters are replaced by those letters when + * rendering the term in LaTeX. Other variables display their first letter with all + * subsequent letters being lowercase. This reduces confusion when rendering expressions + * consisting of multiplication of two variables. + */ +class qtype_algebra_parser_variable extends qtype_algebra_parser_term { + // Define the list of variable names which will be replaced by greek letters + public static $greek = array ( + 'alpha', + 'beta', + 'gamma', + 'delta', + 'epsilon', + 'zeta', + 'eta', + 'theta', + 'iota', + 'kappa', + 'lambda', + 'mu', + 'nu', + 'xi', + 'omicron', + 'pi', + 'rho', + 'sigma', + 'tau', + 'upsilon', + 'phi', + 'chi', + 'psi', + 'omega' + ); + + /** + * Constructor for an algebraic term cass representing a variable. + * + * Initializes an instance of the variable term subclass. The method is given the text + * in the expression corresponding to the variable name. This is then parsed to get the + * variable name which is split into a base and subscript. If the start of the string + * matches the name of a greek letter this is taken as the base and the remainder as the + * subscript. Failing that either the subscript must be explicitly specified using an + * underscore character or the first character is taken as the base. + * + * @param $text text matching the variable name + */ + function qtype_algebra_parser_variable($text) { + // Create the array to store the regular expression matches in + $m=array(); + // Set the sign of the variable to be empty + $this->_sign=''; + // Try to match the text to a greek letter + if(preg_match('/('.implode('|',self::$greek).')/A',$text,$m)) { + // Take the base name of the variable to be the greek letter + $this->_base=$m[1]; + // Extract the remaining characters for use as the subscript + $this->_subscript=substr($text,strlen($m[1])); + // If the first letter of the subscript is an underscore then remove it + if($this->_subscript[0] == '_') { + $this->_subscript=substr($this->_subscript,1); + } + // Call the base class constructor with the variable text set to the combination of the + // base name and the subscript without an underscore between them + parent::qtype_algebra_parser_term(self::NARGS,self::$formats['greek'], + $this->_base.$this->_subscript); + } + // Otherwise we have a simple multi-letter variable name. Treat the fist letter as the base + // name and the rest as the subscript + else { + // Get the variable's base name + $this->_base=substr($text,0,1); + // Now set the subscript to the remaining letters + $this->_subscript=substr($text,1); + // If the first letter of the subscript is an underscore then remove it + if($this->_subscript[0] == '_') { + $this->_subscript=substr($this->_subscript,1); + } + // Call the base class constructor with the variable text set to the combination of the + // base name and the subscript without an underscore between them + parent::qtype_algebra_parser_term(self::NARGS,self::$formats['std'], + $this->_base.$this->_subscript); + } + } + + /** + * Sets this variable to be negative. + * + * This method will convert the number into a nagetive one. It is called when + * the parser finds a subtraction operator in front of the number which does + * not have a variable or another number preceding it. + */ + function set_negative() { + // Set the sign to be a '-' + $this->_sign='-'; + } + + /** + * Generates the list of arguments needed when converting the term into a string. + * + * The string of the variable depends solely on the name and subscript and hence these + * are the only two arguments returned in the array. + * + * @param $method name of method to call to convert arguments into strings + * @return array of the arguments that, with a format string, can be passed to sprintf + */ + function print_args($method) { + return array($this->_sign,$this->_base,$this->_subscript); + } + + /** + * Evaluates the number numerically. + * + * Overrides the base class method to simply return the numerical value of the number the + * class represents. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + if($this->_sign=='-') { + $mult=-1; + } else { + $mult=1; + } + if(array_key_exists($this->_value,$params)) { + return $mult*doubleval($params[$this->_value]); + } else { + // Found an indefined variable. Cannot evaluate numerically so throw exception + throw new Exception(get_string('undefinedvariable','qtype_algebra',$this->_value)); + } + } + + /** + * Checks to see if this variable is equal to another variable. + * + * This is a two step process. First we use the base class equals method to ensure + * that we are comparing two variables. Then we check that the two are the same variable. + * + * @param $expr the term to compare to the current one + * @return true if the terms match, false otherwise + */ + function equals($expr) { + // Call the default method first to check type + if(parent::equals($expr)) { + return $this->_value==$expr->_value and $this->_sign==$expr->_sign; + } else { + return false; + } + } + + // Static class properties + const NARGS=0; + private static $formats=array( + 'greek' => array('str' => '%s%s%s', + 'tex' => '%s\%s_{%s}'), + 'std' => array('str' => '%s%s%s', + 'tex' => '%s%s_{%s}') + ); +} + + +/** + * Class representing a power operation in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the power + * operator's syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. + */ +class qtype_algebra_parser_power extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a power operator term. + * + * This function initializes an instance of a power operator term using the string which + * matches the power operator expression. Since this is simply the character representing + * the operator it is not used except when producing a string representation of the term. + * + * @param $text string matching the term's regular expression + */ + function qtype_algebra_parser_power($text) { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats,$text); + } + + /** + * Evaluates the power operation numerically. + * + * Overrides the base class method to simply return the numerical value of the power + * operation. The method evaluates the two arguments of the term and then passes them to + * the 'pow' function from the maths library. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + $this->check_arguments(); + return pow(doubleval($this->_arguments[0]->evaluate($params)), + doubleval($this->_arguments[1]->evaluate($params))); + } + + // Static class properties + const NARGS=2; + private static $formats=array( + 'str' => '%s^%s', + 'tex' => '%s^{%s}' + ); +} + + +/** + * Class representing a divide operation in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the divide + * operator's syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. + */ +class qtype_algebra_parser_divide extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a divide operator term. + * + * This function initializes an instance of a divide operator term using the string which + * matches the divide operator expression. Since this is simply the character representing + * the operator it is not used except when producing a string representation of the term. + * + * @param $text string matching the term's regular expression + */ + function qtype_algebra_parser_divide($text) { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats,$text); + } + + /** + * Evaluates the divide operation numerically. + * + * Overrides the base class method to simply return the numerical value of the divide + * operation. The method evaluates the two arguments of the term and then simply divides + * them to get the return value. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + $this->check_arguments(); + // Get the value we are trying to divide by + $divby=$this->_arguments[1]->evaluate($params); + // Check to see if this is zero + if($divby==0) { + // Check the sign of the other argument and use to determine whether we return + // plus or minus infinity + return INF*$this->_arguments[0]->evaluate($params); + } else { + return $this->_arguments[0]->evaluate($params)/$divby; + } + } + + // Static class properties + const NARGS=2; + private static $formats=array( + 'str' => '%s/%s', + 'tex' => '\\frac{%s}{%s}' + ); +} + + +/** + * Class representing a multiplication operation in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the multiplication + * operator's syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. + */ +class qtype_algebra_parser_multiply extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a multiplication operator term. + * + * This function initializes an instance of a multiplication operator term using the string which + * matches the multiplication operator expression. Since this is simply the character representing + * the operator it is not used except when producing a string representation of the term. + * + * @param $text string matching the term's regular expression + */ + function qtype_algebra_parser_multiply($text) { + $this->mformats=array('*' => array('str' => '%s*%s', + 'tex' => '%s '.get_string('multiply','qtype_algebra').' %s'), + '.' => array('str' => '%s %s', + 'tex' => '%s %s', + 'sage'=> '%s*%s') + ); + parent::qtype_algebra_parser_term(self::NARGS,$this->mformats['*'],$text,true); + } + + /** + * Sets the arguments of the term to the values in the given array. + * + * This method sets the term's arguments to those in the given array. + * + * @param $args array to set the arguments of the term to + */ + function set_arguments($args) { + // First perform default argument setting method. This will generate + // an error if there is a problem with the number of arguments + parent::set_arguments($args); + // Set the default explicit format + $this->_formats=$this->mformats['*']; + // Only allow the implicit multipication if the second argument is either a + // special, variable, function or bracket and not negative. In all other cases the operator must be + // explicitly written + if(is_a($args[1],'qtype_algebra_parser_bracket') or + is_a($args[1],'qtype_algebra_parser_variable') or + is_a($args[1],'qtype_algebra_parser_special') or + is_a($args[1],'qtype_algebra_parser_function')) { + if(!method_exists($args[1],'set_negative') or $args[1]->_sign=='') { + $this->_formats=$this->mformats['.']; + } + } + // Check for one more special exemption: if the second argument is a power expression + // then we use the same criteria on the first argument of it + if(is_a($args[1],'qtype_algebra_parser_power')) { + // Get the arguments from the power term. Note we do not check these since + // power terms are parsed before multiplication ones and are required to + // have two arguments. + $powargs=$args[1]->arguments(); + // Allow the implicit multipication if the power's first argument is either a + // special, variable, function or bracket and not negative. + if(is_a($powargs[0],'qtype_algebra_parser_bracket') or + is_a($powargs[0],'qtype_algebra_parser_variable') or + is_a($powargs[0],'qtype_algebra_parser_special') or + is_a($powargs[0],'qtype_algebra_parser_function')) { + if(!method_exists($powargs[0],'set_negative') or $powargs[0]->_sign=='') { + $this->_formats=$this->mformats['.']; + } + } + } + } + + /** + * Evaluates the multiplication operation numerically. + * + * Overrides the base class method to simply return the numerical value of the multiplication + * operation. The method evaluates the two arguments of the term and then simply multiplies + * them to get the return value. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + $this->check_arguments(); + return $this->_arguments[0]->evaluate($params)* + $this->_arguments[1]->evaluate($params); + } + + // Static class properties + const NARGS=2; +} + + +/** + * Class representing a addition operation in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the addition + * operator's syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. + */ +class qtype_algebra_parser_add extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a addition operator term. + * + * This function initializes an instance of a addition operator term using the string which + * matches the addition operator expression. Since this is simply the character representing + * the operator it is not used except when producing a string representation of the term. + * + * @param $text string matching the term's regular expression + */ + function qtype_algebra_parser_add($text) { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats,$text,true); + } + + /** + * Evaluates the addition operation numerically. + * + * Overrides the base class method to simply return the numerical value of the addition + * operation. The method evaluates the two arguments of the term and then simply adds + * them to get the return value. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + $this->check_arguments(); + return $this->_arguments[0]->evaluate($params)+ + $this->_arguments[1]->evaluate($params); + } + + // Static class properties + const NARGS=2; + private static $formats=array( + 'str' => '%s+%s', + 'tex' => '%s + %s' + ); +} + + +/** + * Class representing a subtraction operation in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the subtraction + * operator's syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. + */ +class qtype_algebra_parser_subtract extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a subtraction operator term. + * + * This function initializes an instance of a subtraction operator term using the string which + * matches the subtraction operator expression. Since this is simply the character representing + * the operator it is not used except when producing a string representation of the term. + * + * @param $text string matching the term's regular expression + */ + function qtype_algebra_parser_subtract($text) { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats,$text); + } + + /** + * Evaluates the subtraction operation numerically. + * + * Overrides the base class method to simply return the numerical value of the subtraction + * operation. The method evaluates the two arguments of the term and then simply subtracts + * them to get the return value. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + $this->check_arguments(); + return $this->_arguments[0]->evaluate($params)- + $this->_arguments[1]->evaluate($params); + } + + // Static class properties + const NARGS=2; + private static $formats=array( + 'str' => '%s-%s', + 'tex' => '%s - %s' + ); +} + + +/** + * Class representing a special constant in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the a predefined + * special constant such as pi or 'e' (from natural logarithms). + */ +class qtype_algebra_parser_special extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a special constant term. + * + * This function initializes an instance of a special term using the string which + * matches the regular expression of a special constant. + * + * @param $text string matching a constant's regular expression + */ + function qtype_algebra_parser_special($text) { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats[$text],$text); + $this->_sign=''; + } + + /** + * Sets this special to be negative. + * + * This method will convert the number into a nagetive one. It is called when + * the parser finds a subtraction operator in front of the number which does + * not have a variable or another number preceding it. + */ + function set_negative() { + // Set the sign to be a '-' + $this->_sign='-'; + } + + /** + * Evaluates the special constant numerically. + * + * Overrides the base class method to simply return the numerical value of the special + * constant which is defined by an internal switch based on the constant's name. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + if($this->_sign=='-') { + $mult=-1; + } else { + $mult=1; + } + switch($this->_value) { + case 'pi': + return $mult*pi(); + case 'e': + return $mult*exp(1); + default: + return 0; + } + } + + /** + * Returns the array of arguments needed to convert this special term into a string. + * + * The special term generally has a fixed, predefined formatting already hard coded so + * the only remaining variable is the sign of the term and this is what this method + * returns. + * + * @param $method name of method to call to convert arguments into strings + * @return array of the arguments that, with a format string, can be passed to sprintf + */ + function print_args($method) { + return array($this->_sign); + } + + /** + * Checks to see if this constant is equal to another term. + * + * This is a two step process. First we use the base class equals method to ensure + * that we are comparing two variables. Then we check that the two are the same constant. + * + * @param $expr the term to compare to the current one + * @return true if the terms match, false otherwise + */ + function equals($expr) { + // Call the default method first to check type + if(parent::equals($expr)) { + return $this->_value==$expr->_value and $this->_sign==$this->_sign; + } else { + return false; + } + } + + // Static class properties + const NARGS=0; + private static $formats=array( + 'pi' => array( 'str' => '%spi', + 'tex' => '%s\\pi'), + 'e' => array( 'str' => '%se', + 'tex' => '%se') + ); +} + + +/** + * Class representing a function in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the function's + * syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. + */ +class qtype_algebra_parser_function extends qtype_algebra_parser_term { + + /** + * Constructs an instance of a function term. + * + * This function initializes an instance of a function term using the string which + * matches the name of a function. + * + * @param $text string matching the function's regular expression + */ + function qtype_algebra_parser_function($text) { + if(!function_exists($text) and !array_key_exists($text,self::$fnmap)) { + throw new Exception(get_string('undefinedfunction','qtype_algebra',$text)); + } + $formats=array( 'str' => '%s'.$text.'%s'); + if(array_key_exists($text,self::$texmap)) { + $formats['tex']='%s'.self::$texmap[$text].' %s'; + } else { + $formats['tex']='%s\\'.$text.' %s'; + } + $this->_sign=''; + parent::qtype_algebra_parser_term(self::NARGS,$formats,$text); + } + + /** + * Sets this function to be negative. + * + * This method will convert the function into a negative one. It is called when + * the parser finds a subtraction operator in front of the function which does + * not have a variable or another number preceding it e.g. 3*-sin(x) + */ + function set_negative() { + // Set the sign to be a '-' + $this->_sign='-'; + } + + /** + * Sets the arguments of the term to the values in the given array. + * + * The code here overrides the base class's method. The code uses this method to actually + * set the arguments in the given array but a second stage to insert brackets around the + * function's argument is required. + * + * @param $args array to set the arguments of the term to + */ + function set_arguments($args) { + if(count($args)!=$this->_nargs) { + throw new Exception(get_string('badfuncargs','qtype_algebra',$this->_value)); + } + if(!is_a($args[0],'qtype_algebra_parser_bracket')) { + // Check to see if this function requires a special bracket + if(in_array($this->_value,self::$bracketmap)) { + $b=new qtype_algebra_parser_bracket('<'); + } + // Does not require special brackets so create normal ones + else { + $b=new qtype_algebra_parser_bracket('('); + } + $b->set_arguments($args); + $this->_arguments=array($b); + } + // First term already a bracket + else { + // Check to see if we need a special bracket + if(in_array($this->_value,self::$bracketmap)) { + // Make the bracket special + $args[0]->make_special(); + } + // Set the arguments to the given type + $this->_arguments=$args; + } + } + + /** + * Generates the list of arguments needed when converting the term into a string. + * + * The string of the function depends solely on the function argument and the sign. + * The name has already been coded in at construction time. + * + * @param $method name of method to call to convert arguments into strings + * @return array of the arguments that, with a format string, can be passed to sprintf + */ + function print_args($method) { + // First ensure that there are the correct number of arguments + $this->check_arguments(); + return array($this->_sign,$this->_arguments[0]->$method()); + } + + /** + * Evaluates the function numerically. + * + * Overrides the base class method to simply return the numerical value of the function. + * Each function name is first checked against an internal map to determine the corresponding + * PHP math function to call. If the function is not in the map it is assumed to already be + * the correct name for a PHP math function. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + // First ensure that there are the correct number of arguments + $this->check_arguments(); + // Get the correct sign to multiply the value by + if($this->_sign=='-') { + $mult=-1; + } else { + $mult=1; + } + // Check to see if there is an entry to map the function name to a PHP function + if(array_key_exists($this->_value,self::$fnmap)) { + $func=self::$fnmap[$this->_value]; + return $mult*$func($this->_arguments[0]->evaluate($params)); + } + // No map entry so the function name must already be a PHP function... + else { + $tmp=$this->_value; + return $mult*$tmp($this->_arguments[0]->evaluate($params)); + } + } + + /** + * Checks to see if this function is equal to another term. + * + * This is a two step process. First we use the base class equals method to ensure + * that we are comparing two variables. Then we check that the two are the same constant. + * + * @param $expr the term to compare to the current one + * @return true if the terms match, false otherwise + */ + function equals($expr) { + // Call the default method first to check type + if(parent::equals($expr)) { + return $this->_value==$expr->_value and $this->_sign==$this->_sign; + } else { + return false; + } + } + + // Static class properties + const NARGS=1; + public static $fnmap = array ('ln' => 'log', + 'log' => 'log10' + ); + public static $texmap = array('asin' => '\\sin^{-1}', + 'acos' => '\\cos^{-1}', + 'atan' => '\\tan^{-1}', + 'sqrt' => '\\sqrt' + ); + // List of functions requiring special brackets + public static $bracketmap = array ('sqrt' + ); +} + + +/** + * Class representing a bracket operation in an algebraic expression. + * + * The parser creates an instance of this term when it finds a string matching the bracket + * operator's syntax. The string which corresponds to the term is passed to the constructor + * of this subclass. Note that a pair of brackets is treated as a single term. There are no + * separate open and close bracket operators. + */ +class qtype_algebra_parser_bracket extends qtype_algebra_parser_term { + + function qtype_algebra_parser_bracket($text) { + parent::qtype_algebra_parser_term(self::NARGS,self::$formats[$text],$text); + $this->_open=$text; + switch($this->_open) { + case '(': + $this->_close=')'; + break; + case '[': + $this->_close=']'; + break; + case '{': + $this->_close='}'; + break; + // Special kind of bracket. This behaves as normal brackets for a string but as invisible + // curly brackets '{}' with LaTeX. + case '<': + $this->_close='>'; + break; + } + } + + /** + * Evaluates the bracket operation numerically. + * + * Overrides the base class method to simply return the numerical value of the bracket + * operation. The method evaluates the argument of the term, i.e. what is inside the + * brackets, and then returns the value. + * + * @param $params array of values keyed by variable name + * @return the numerical value of the term given the provided values for the variables + */ + function evaluate($params) { + if(count($this->_arguments)!=$this->_nargs) { + return 0; + } + return $this->_arguments[0]->evaluate($params); + } + + /** + * Set the bracket type to 'special'. + * + * The method converts the bracket to the special type. The special type appears as a + * normal bracket in string mode but produces the invisible curly brackets for LaTeX. + */ + function make_special() { + $this->_open='<'; + $this->_close='>'; + // Call the base class constructor as if this were a new instance of the bracket + parent::qtype_algebra_parser_term(self::NARGS,self::$formats['<'],'<'); + } + + // Member variables + var $_open='('; + var $_close=')'; + + // Static class properties + const NARGS=1; + private static $formats=array( + '(' => array('str' => '(%s)', + 'tex' => '\\left( %s \\right)'), + '[' => array('str' => '[%s]', + 'tex' => '\\left[ %s \\right]'), + '{' => array('str' => '{%s}', + 'tex' => '\\left\\lbrace %s \\right\\rbrace'), + '<' => array('str' => '(%s)', + 'tex' => '{%s}') + ); +} + + + +/** + * The main parser class. + * + * This class implements the methods needed to parse an expression. It uses a series of + * regular expressions to indentify the different terms in the expression and then creates + * instances of the correct subclass to handle them. + */ +class qtype_algebra_parser { + // Special constants which the parser will understand + public static $specials = array ( + 'pi', + 'e' + ); + + // Functions which the parser will understand. These should all be standard PHP math functions. + public static $functions = array ('sqrt', + 'ln', + 'log', + 'cosh', + 'sinh', + 'sin', + 'cos', + 'tan', + 'asin', + 'acos', + 'atan' + ); + + // Array to define the priority of the different operations. The parser implements the standard BODMAS priority: + // brackets, order (power), division, mulitplication, addition, subtraction + private static $priority = array ( + array('qtype_algebra_parser_power'), + array('qtype_algebra_parser_function'), + array('qtype_algebra_parser_divide','qtype_algebra_parser_multiply'), + array('qtype_algebra_parser_add','qtype_algebra_parser_subtract') + ); + + // Regular experssion to match an open bracket + private static $OPENB = '/[\{\(\[]/A'; + // Regular experssion to match a close bracket + private static $CLOSEB = '/[\}\)\]]/A'; + // Regular expression to match a plain float or integer number without exponent + private static $PLAIN_NUMBER = '(([0-9]+(\.|,)[0-9]*)|([0-9]+)|((\.|,)[0-9]+))'; + // Regular expression to match a float or integer number with an exponent + private static $EXP_NUMBER = '(([0-9]+(\.|,)[0-9]*)|([0-9]+)|((\.|,)[0-9]+))E([-+]?\d+)'; + // Array to associate close brackets with the correct open bracket type + private static $BRACKET_MAP = array(')' => '(', ']' => '[', '}' => '{'); + + /** + * Constructor for the main parser class. + * + * This constructor initializes the token map of the main parser class. It constructs a map of + * regular expressions to class types. As it parses a string it uses these regular expressions to + * find tokens in the input string which are then fed to the corresponding term class for + * interpretation. + */ + function qtype_algebra_parser() { + $this->_tokens = array ( + array ('/(\^|\*\*)/A', 'qtype_algebra_parser_power' ), + array ('/('.implode('|',self::$functions).')/A', 'qtype_algebra_parser_function' ), + array ('/\//A', 'qtype_algebra_parser_divide' ), + array ('/\*/A', 'qtype_algebra_parser_multiply' ), + array ('/\+/A', 'qtype_algebra_parser_add' ), + array ('/-/A', 'qtype_algebra_parser_subtract' ), + array ('/('.implode('|',self::$specials).')/A', 'qtype_algebra_parser_special' ), + array ('/('.self::$EXP_NUMBER.'|'.self::$PLAIN_NUMBER.')/A', 'qtype_algebra_parser_number' ), + array ('/[A-Za-z][A-Za-z0-9_]*/A', 'qtype_algebra_parser_variable' ) + ); + } + + /** + * Parses a given string containing an algebric epxression and returns the corresponding parse tree. + * + * This method loops over the string using the regular expressions in the token map to break down the + * string into tokens. These tokens are arranged into a structured stack, taking account of the + * bracket structure. Finally then method calls the {@link interpret} method to convert the structured + * token strings into a fully parsed term structure. The method can optionally be passed a list of + * variables which are used in the expression. If such a list is passed then the parser will attempt + * to match the current position in the string with one of these given variables before any other + * token. When passing a variable list a third parameter allows a choice of whether to allow additional + * undeclared variables. This defaults to false when a list of variables is passed and is ignored otherwise. + * + * @param $text string containing the expression to parse + * @param $variables array containing known variable names + * @param $undecvars whether to allow (true) undeclared variable names + * @return top term of the parsed expression + */ + function parse($text,$variables=array(),$undecvars=false) { + // Create a regular expression to match the known variables if an array is specified + if(!empty($variables)) { + // Create an empty array to store the list of extra regular expressions to match + $reextra=array(); + // Loop over all the variable names we are given + foreach($variables as $var) { + // Create a temporary varible term using the current name + $tmpvar=new qtype_algebra_parser_variable($var); + // If the variable name has a subscript then create a new regular expression to + // search for which includes an underscore + if(!empty($tmpvar->_subscript)) { + $reextra[]=$tmpvar->_base.'_'.$tmpvar->_subscript; + } + } + // Merge the variable name array with the array of extra regular expressions to match + $variables=array_merge($variables,$reextra); + // Sort the array in order of increasing variable length in order to prevent 'x1' matching + // a variable 'x' before 'x1'. Do this using a helper function, which will compare two + // strings using their length only, and use this with the usort function. + usort($variables,'qtype_algebra_parser_strlen_sort'); + // Generate a single regular expression which will match both all known variables + $revar='/('.implode('|',$variables).')/A'; + } else { + $revar=''; + } + $i=0; + // Create an array to store the parse tree + $tree=array(); + // Create an array to act as a temporary storage stack. This stack is used to + // push higher levels of the parse tree as it is assembled from the expression + $stack=array(); + // Array used to store the match results from regular expression searches + $m=array(); + // Loop over the expression string moving along it using the offset variable $i while + // there are still characters left to parse + while($i<strlen($text)) { + // Match any white space at the start of the string and 'remove' it by advancing + // the pointer by the length of the string matching the regular expression white + // space pattern + if(preg_match('/\s+/A',substr($text,$i),$m)) { + $i+=strlen($m[0]); + // Return to the start of the loop in case this was white space characters at + // the end of the string + continue; + } + // Since we don't have any white space the first thing we look for (top priority) + // are open brackets + if(preg_match(self::$OPENB,substr($text,$i),$m)) { + // Check for a non-operator and if one is found assume implicit multiplication + if(count($tree)>0 and (is_array($tree[count($tree)-1]) or + (is_object($tree[count($tree)-1]) + and $tree[count($tree)-1]->n_args()==0))) { + // Make the implicit assumption explicit by adding an appropriate + // multiplication operator + array_push($tree,new qtype_algebra_parser_multiply('*')); + } + // Push the current parse tree onto the stack + array_push($stack,$tree); + // Create a new parse tree starting with a bracket term + $tree=array(new qtype_algebra_parser_bracket($m[0])); + // Increment the string pointer by the length of the string that was matched + $i+=strlen($m[0]); + // Return to the start of the loop + continue; + } + // Now see if we have a close bracket here + if(preg_match(self::$CLOSEB,substr($text,$i),$m)) { + // First check that the current parse tree has at least one term + if(count($tree)==0) { + throw new Exception(get_string('badclosebracket','qtype_algebra')); + } + // Now check that the current tree started with a bracket + if(!is_a($tree[0],'qtype_algebra_parser_bracket')) { + throw new Exception(get_string('mismatchedcloseb','qtype_algebra')); + } + // Check that the open and close bracket are of the same type + else if($tree[0]->_value != self::$BRACKET_MAP[$m[0]]) { + throw new Exception(get_string('mismatchedbracket','qtype_algebra',$tree[0]->_value.$m[0])); + } + // Append the current tree to the tree one level up on the stack + array_push($stack[count($stack)-1],$tree); + // The new tree is the lowest level tree on the stack so we + // pop the new tree off the stack + $tree=array_pop($stack); + $i+=strlen($m[0]); + continue; + } + // If a list of predefined variables was given to the method then check for them here + if(!empty($revar) and preg_match($revar,substr($text,$i),$m)) { + // Check for a zero argument term or brackets preceding the variable and if there is one then + // add the implicit multiplication operation + if(count($tree)>0 and (is_array($tree[count($tree)-1]) or $tree[count($tree)-1]->n_args()==0)) { + array_push($tree,new qtype_algebra_parser_multiply('*')); + } + // Increment the string index by the length of the variable's name + $i+=strlen($m[0]); + // Push a new variable term onto the parse tree + array_push($tree,new qtype_algebra_parser_variable($m[0])); + continue; + } + // Here we have not found any open or close brackets or known variables so we can + // parse the string for a normal token + foreach($this->_tokens as $token) { + //echo 'Looking for token ',$token[1],"\n"; + if(preg_match($token[0],substr($text,$i),$m)) { + //echo 'Found a ',$token[1],"!\n"; + // Check for a variable and throw an exception if undeclared variables are + // not allowed and a list of defined variables was passed + if(!empty($revar) and !$undecvars and $token[1]=='qtype_algebra_parser_variable') { + throw new Exception(get_string('undeclaredvar','qtype_algebra',$m[0])); + } + // Check for a zero argument term preceding a variable, function or special and then + // add the implicit multiplication + if(count($tree)>0 and ($token[1]=='qtype_algebra_parser_variable' or + $token[1]=='qtype_algebra_parser_function' or + $token[1]=='qtype_algebra_parser_special') + and (is_array($tree[count($tree)-1]) or + $tree[count($tree)-1]->n_args()==0)) { + array_push($tree,new qtype_algebra_parser_multiply('*')); + } + $i+=strlen($m[0]); + array_push($tree,new $token[1]($m[0])); + continue 2; + } + } + throw new Exception(get_string('unknownterm','qtype_algebra',substr($text,$i))); + } // end while loop over tokens + // If all the open brackets have been closed then the stack will be empty and the + // tree will contain the entire parsed expression + if(count($stack)>0) { + throw new Exception(get_string('mismatchedopenb','qtype_algebra')); + } + //print_r($tree); + //print_r($stack); + return $this->interpret($tree); + } + + /** + * Takes a structured token map and converts it into a parsed term structure. + * + * This is an internal method of the parser class and is called by the {@link parse} + * method. It performs the final stage of the parsing process and returns the fully + * parsed term structure. + * + * @param $tree structured token array + * @return top term of the fully parsed structure + */ + function interpret($tree) { + // First check to see if we are passed anything at all. If not then simply + // return a qtype_algebra_parser_nullterm + if(count($tree)==0) { + return new qtype_algebra_parser_nullterm(); + } + // Now we check to see if this tree is inside brackets. If so then + // we remove the bracket object from the tree and store it in a + // temporary variable. We will then parse the remainder of the tree + // and make the top level term the bracket's argument if applicable. + if(is_a($tree[0],'qtype_algebra_parser_bracket')) { + $bracket=array_splice($tree,0,1); + $bracket=$bracket[0]; + } else { + $bracket=''; + } + // Next we loop over the tree and look for arrays. These represent + // brackets inside our tree and so we need to process them first. + for($i=0;$i<count($tree);$i++) { + // Check for a list type if we find one then replace + // it with the interpreted term + if(is_array($tree[$i])) { + $tree[$i]=$this->interpret($tree[$i]); + } + } + // The next job is to check the subtraction operations to determine whether they are + // really subtraction operations or whether they are minus signs for negative numbers + $toremove=array(); + for($i=0;$i<count($tree);$i++) { + // Check that this element is an addition or subtraction operator + if(is_a($tree[$i],'qtype_algebra_parser_subtract') or is_a($tree[$i],'qtype_algebra_parser_add')) { + // Check whether the precedding argument (if there is one) is a number or + // a variable. In either case this is a addition/subtraction operation so we continue + if($i>0 and (is_a($tree[$i-1],'qtype_algebra_parser_variable') or + is_a($tree[$i-1],'qtype_algebra_parser_number') or + is_a($tree[$i-1],'qtype_algebra_parser_bracket'))) { + continue; + } + // Otherwise we have found a minus sign indicating a positive or negative quantity... + else { + // Check that we do have a number following otherwise generate an exception... + if($i==(count($tree)-1) or !method_exists($tree[$i+1],'set_negative')) { + throw new Exception(get_string('illegalplusminus','qtype_algebra')); + } + // If we have a subtract operation then we need to make the following number negative + if(is_a($tree[$i],'qtype_algebra_parser_subtract')) { + // Set the number to be negative + $tree[$i+1]->set_negative(); + } + // Add the term to the removal list + $toremove[$i]=1; + } + } + } + // Remove the elements from the tree who's keys are found in the removal list + $tree=array_diff_key($tree,$toremove); + // Re-key the tree array so that the keys are sequential + $tree=array_values($tree); + foreach(self::$priority as $ops) { + $i=0; + //echo 'Looking for ',$ops,"\n"; + while($i<count($tree)) { + if(in_array(get_class($tree[$i]),$ops)) { + //echo 'Found a ',get_class($tree[$i]),"\n"; + if($tree[$i]->n_args()==1) { + if(($i+1)<count($tree)) { + $tree[$i]->set_arguments(array_splice($tree,$i+1,1)); + $i++; + continue; + } else { + throw new Exception(get_string('missingonearg','qtype_algebra',$op)); + } + } elseif($tree[$i]->n_args() == 2) { + if($i>0 and $i<(count($tree)-1)) { + $tree[$i]->set_arguments(array($tree[$i-1], + $tree[$i+1])); + array_splice($tree,$i+1,1); + array_splice($tree,$i-1,1); + continue; + } else { + throw new Exception(get_string('missingtwoargs','qtype_algebra',$op)); + } + } + } else { + $i++; + } + } + } + // If there are no terms in the parse tree then we were passed an empty string + // in which case we create a null term and return it + if(count($tree)==0) { + return new qtype_algebra_parser_nullterm(); + } else if(count($tree)!=1) { + //print_r($tree); + throw new Exception(get_string('notopterm','qtype_algebra')); + } + if($bracket) { + $bracket->set_arguments(array($tree[0])); + return $bracket; + } else { + return $tree[0]; + } + } +} + +// Sort static arrays once here by inverse string length +usort(qtype_algebra_parser_variable::$greek,'qtype_algebra_parser_strlen_sort'); +usort(qtype_algebra_parser::$functions,'qtype_algebra_parser_strlen_sort'); + diff --git a/pix/icon.gif b/pix/icon.gif Binary files differnew file mode 100644 index 0000000..0668e33 --- /dev/null +++ b/pix/icon.gif diff --git a/question.php b/question.php new file mode 100644 index 0000000..12d27dd --- /dev/null +++ b/question.php @@ -0,0 +1,356 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * algebra answer question definition class. + * + * @package qtype + * @subpackage algebra + * @author Roger Moore <rwmoore 'at' ualberta.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/type/algebra/questiontype.php'); +require_once($CFG->dirroot . '/question/type/algebra/parser.php'); +require_once($CFG->dirroot . '/question/type/algebra/xmlrpc-utils.php'); + +/** + * Represents an algebra question. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_algebra_question extends question_graded_by_strategy + implements question_response_answer_comparer { + + /** @var array of question_answer. */ + public $answers = array(); + /** @var array of question_answer. */ + public $variables = array(); + public $compareby; + public $nchecks; + public $tolerance; + public $allowedfuncs; + public $disallow; + public $answerprefix; + + public function __construct() { + parent::__construct(new question_first_matching_answer_grading_strategy($this)); + } + + public function get_expected_data() { + return array('answer' => PARAM_RAW_TRIMMED); + } + + public function summarise_response(array $response) { + if (isset($response['answer'])) { + return $response['answer']; + } else { + return null; + } + } + + public function is_complete_response(array $response) { + return array_key_exists('answer', $response) && + ($response['answer'] || $response['answer'] === '0'); + } + + public function get_validation_error(array $response) { + if ($this->is_gradable_response($response)) { + return ''; + } + return get_string('pleaseenterananswer', 'qtype_algebra'); + } + + /** + * Parses the given expression with the parser if required. + * + * This method will check to see if the argument it is given is already a parsed + * expression and if not will attempt to parse it. + * + * @param $expr expression which will be parsed + * @return top term of the parse tree or a string if an exception is thrown + */ + public function parse_expression($expr) { + // Check to see if this is already a parsed expression + if(is_a($expr,'qtype_algebra_parser_term')) { + // It is a parsed expression so simply return it + return $expr; + } + + // Create an array of variable names for the parser from the question if defined + $varnames=array(); + if(isset($this->variables)) { + foreach($this->variables as $var) { + $varnames[]=$var->name; + } + } + // We now assume that we have a string to parse. Create a parser instance to + // to this and return the parser expression at the top of the parse tree + $p=new qtype_algebra_parser; + // Perform the actual parsing inside a try-catch block so that any exceptions + // can be caught and converted into errors + try { + return $p->parse($expr,$varnames); + } catch(Exception $e) { + // If the expression cannot be parsed then return a null term. This will + // make Moodle treat the answer as wrong. + // TODO: Would be nice to have support for 'invalid answer' in the quiz + // engine since an unparseable response is usually caused by a silly typo + return new qtype_algebra_parser_nullterm; + } + } + + /** + * Parses the given expression with the parser if required. + * + * This method will parse the expression and return a TeX string + * or empty string + * + * @param $expr expression which will be parsed + * @return top term of the parse tree or a string if an exception is thrown + */ + public function formated_expression($text) { + + // Create an array of variable names for the parser from the question if defined + $varnames=array(); + if(isset($this->variables)) { + foreach($this->variables as $var) { + $varnames[]=$var->name; + } + } + // We now assume that we have a string to parse. Create a parser instance to + // to this and return the parser expression at the top of the parse tree + $p=new qtype_algebra_parser; + // Perform the actual parsing inside a try-catch block so that any exceptions + // can be caught and converted into errors + try { + $exp = $p->parse($text, $varnames); + return '$$'.$exp->tex().'$$'; + } catch(Exception $e) { + return ''; + } + } + + public function is_same_response(array $prevresponse, array $newresponse) { + // Check that both states have valid responses + if (!isset($prevresponse['answer']) or !isset($newresponse['answer'])) { + // At last one of the states did not have a response set so return false by default + return false; + } + // Parse the previous response + $expr=$this->parse_expression($prevresponse['answer']); + // Parse the new response + $testexpr=$this->parse_expression($newresponse['answer']); + // The type of comparison done depends on the comparision algorithm selected by + // the question. Use the defined algorithm to select which comparison function + // to call... + if($this->compareby == 'sage') { + // Uses an XML-RPC server with SAGE to perform a full symbollic comparision + return self::test_response_by_sage($expr,$testexpr); + } else if($this->compareby == 'eval') { + // Tests the response by evaluating it for a certain range of each variable + return self::test_response_by_evaluation($expr,$testexpr); + } else { + // Tests the response by performing a simple parse tree equivalence algorithm + return self::test_response_by_equivalence($expr,$testexpr); + } + } + + public function get_answers() { + return $this->answers; + } + + public function compare_response_with_answer(array $response, question_answer $answer) { + // Deal with the match anything answer by returning true + if ($answer->answer == '*') { + return true; + } + $expr=$this->parse_expression($response['answer']); + // Check that there is a response and if not return false. We do this here + // because even an empty response should match a widlcard answer. + if(is_a($expr,'qtype_algebra_parser_nullterm')) { + return false; + } + + // Now parse the answer + $ansexpr=$this->parse_expression($answer->answer); + // The type of comparison done depends on the comparision algorithm selected by + // the question. Use the defined algorithm to select which comparison function + // to call... + if($this->compareby == 'sage') { + // Uses an XML-RPC server with SAGE to perform a full symbollic comparision + return self::test_response_by_sage($expr,$ansexpr); + } else if($this->compareby == 'eval') { + // Tests the response by evaluating it for a certain range of each variable + return self::test_response_by_evaluation($expr,$ansexpr); + } else { + // Tests the response by performing a simple parse tree equivalence algorithm + return self::test_response_by_equivalence($expr,$ansexpr); + } + } + + /** + * Checks whether a response matches a given answer using SAGE + * + * This method will compare the given response to the given answer using the SAGE + * open source algebra computation software. The software is run by a remote + * XML-RPC server which is called with both the asnwer and the response and told to + * compare the two algebraic expressions. + * + * @return boolean true if the response matches the answer, false otherwise + */ + function test_response_by_sage($response, $answer) { + // TODO: Store server information in the Moodle configuration + $request=array( + 'host' => 'localhost', + 'port' => 7777, + 'uri' => '' + ); + // Sets the name of the method to call to full_symbolic_compare + $request['method']='full_symbolic_compare'; + // Get a list of all the variables to declare + $vars=$response->get_variables(); + $vars=array_merge($vars,array_diff($vars,$answer->get_variables())); + // Sets the arguments to the sage string of the response and the list of variables + $request['args']=array($answer->sage(),$response->sage(),$vars); + // Calls the XML-RPC method on the server and returns the response + return xu_rpc_http_concise($request)==0; + } + + /** + * Checks whether a response matches a given answer using an evaluation method + * + * This method will compare the given response to the given answer by evaluating both + * for given values of the variables. Each variable must have a predefined range over + * which it can be checked and then both expressions will be evalutated several times + * using values randomly chosen to be within the range. + * + * @return boolean true if the response matches the answer, false otherwise + */ + function test_response_by_evaluation($response, $answer) { + // Flag used to denote mismatch in response and answer + $same=true; + // Run the evaluation loop 10 times with different random variables... + for($i=0;$i<$this->nchecks;$i++) { + // Create an array to store the values of all the variables + $values=array(); + // Loop over all the variables in the question + foreach($this->variables as $var) { + // Set the value of the variable to a random number between the min and max + $values[$var->name]=$var->min+lcg_value()*abs($var->max-$var->min); + } + $resp_value=$response->evaluate($values); + $ans_value=$answer->evaluate($values); + // Return false if only one of the reponse or answer gives NaN + if(is_nan($resp_value) xor is_nan($ans_value)) { + return false; + } + // Return false if only one of the reponse or answer is infinite + if(is_infinite($resp_value) xor is_infinite($ans_value)) { + return false; + } + // Use the fractional difference method if the answer has a value + // which is clearly distinguishable from zero + if(abs($ans_value)>1e-300) { + // Get the difference between the response and answer evaluations + $diff=abs(($resp_value-$ans_value)/$ans_value); + } + // Otherwise use an arbitrary minimum value + else { + // Get the difference between the response and answer evaluations + $diff=abs(($resp_value-$ans_value)/1e-300); + } + // Check to see if the difference is greater than tolerance + if($diff > $this->tolerance) { + // Return false since the formulae have been shown not to be the same + return false; + } + } + // We made it through the loop so now return true + return true; + } + + /** + * Checks whether a response matches a given answer using a simple equivalence algorithm + * + * This method will compare the given response to the given answer by simply checking to + * see if the two parse trees are equivalent. This allows for a slightly more sophisticated + * check than a simple text compare but will not, neccessarily, catch two equivalent but + * different algebraic expressions. + * + * @return boolean true if the response matches the answer, false otherwise + */ + public function test_response_by_equivalence($response, $answer) { + // Use the parser's equivalent method to see if the response is the same as the answer + return $response->equivalent($answer); + } + + public function check_file_access($qa, $options, $component, $filearea, + $args, $forcedownload) { + if ($component == 'question' && $filearea == 'answerfeedback') { + $currentanswer = $qa->get_last_qt_var('answer'); + $answer = $qa->get_question()->get_matching_answer(array('answer' => $currentanswer)); + $answerid = reset($args); // itemid is answer id. + return $options->feedback && $answerid == $answer->id; + + } else if ($component == 'question' && $filearea == 'hint') { + return $this->check_hint_file_access($qa, $options, $args); + + } else { + return parent::check_file_access($qa, $options, $component, $filearea, + $args, $forcedownload); + } + } +} + +/** + * Class to represent an algebra question variable, loaded from the question_algebra_variables table + * in the database. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_variable { + /** @var integer the answer id. */ + public $id; + + /** @var string the name. */ + public $name; + + /** @var string minimum value. */ + public $min = '-'; + + /** @var string maximum value. */ + public $max = '-'; + + /** + * Constructor. + * @param int $id the variable. + * @param string $name the name. + * @param string $min the minimum value. + * @param string $maximum value. + */ + public function __construct($id, $name, $min, $max) { + $this->id = $id; + $this->name = $name; + $this->min = $min; + $this->max = $max; + } +} diff --git a/questiontype.php b/questiontype.php new file mode 100644 index 0000000..dfd9f3b --- /dev/null +++ b/questiontype.php @@ -0,0 +1,551 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Question type class for the algebra question type. + * + * @package qtype + * @subpackage algebra + * @author Roger Moore <rwmoore 'at' ualberta.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/type/algebra/question.php'); +require_once($CFG->dirroot . '/question/type/algebra/parser.php'); +require_once($CFG->dirroot . '/question/type/algebra/xmlrpc-utils.php'); + +/** + * ALGEBRA QUESTION TYPE CLASS + * + * @package questionbank + * @subpackage questiontypes + */ + +class qtype_algebra extends question_type { + + /** + * Defines the table which extends the question table. This allows the base questiontype + * to automatically save, backup and restore the extra fields. + * + * @return an array with the table name (first) and then the column names (apart from id and questionid) + */ + public function extra_question_fields() { + return array('question_algebra', + 'compareby', // Name of comparison algorithm to use + 'nchecks', // Number of evaluate checks to make when comparing by evaluation + 'tolerance', // Max. fractional difference allowed for evaluation checks + 'allowedfuncs', // Comma separated list of functions allowed in responses + 'disallow', // Response which may be correct but which is not allowed + 'answerprefix' // String which is placed in front of the asnwer box + ); + } + + public function questionid_column_name() { + return 'questionid'; + } + + public function move_files($questionid, $oldcontextid, $newcontextid) { + parent::move_files($questionid, $oldcontextid, $newcontextid); + $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid); + } + + protected function delete_files($questionid, $contextid) { + parent::delete_files($questionid, $contextid); + $this->delete_files_in_answers($questionid, $contextid); + } + + public function delete_question($questionid, $contextid) { + global $DB; + $DB->delete_records('question_algebra', array('questionid' => $questionid)); + $DB->delete_records('question_algebra_variables', array('question' => $questionid)); + + parent::delete_question($questionid, $contextid); + } + + /** + * Saves the questions variables to the database + * + * This is called by {@link save_question_options()} to save the variables of the question to + * the database from the data in the submitted form. The method returns an array of the variables + * IDs written to the database or, in the event of an error, throws an exception. + * + * @param object $question This holds the information from the editing form, + * it is not a standard question object. + * @return array of variable object IDs + */ + public function save_question_variables($question) { + global $DB; + // Create the results class + $result = new stdClass; + // Get all the old answers from the database as an array + if (!$oldvars = $DB->get_records('question_algebra_variables', array('question' => $question->id), 'id ASC')) { + $oldvars = array(); + } + // Create an array of the variable IDs for the question + $variables = array(); + + // Loop over all the answers in the question form and write them to the database + foreach ($question->variable as $key => $varname) { + // Check to see that there is a variable and skip any which are empty + if ($varname == '') { + continue; + } + + // Get the old variable from the array and overwrite what is required, if there + // is no old variable then we skip to the 'else' clause + if ($oldvar = array_shift($oldvars)) { // Existing variable, so reuse it + $var = $oldvar; + $var->name = trim($varname); + $var->min = trim($question->varmin[$key]); + $var->max = trim($question->varmax[$key]); + // Update the record in the database to denote this change. + if (!$DB->update_record('question_algebra_variables', $var)) { + throw new Exception("Could not update algebra question variable (id=$var->id)"); + } + } + // This is a completely new variable so we have to create a new record + else { + $var = new stdClass; + $var->name = trim($varname); + $var->question = $question->id; + $var->min = trim($question->varmin[$key]); + $var->max = trim($question->varmax[$key]); + // Insert a new record into the database table + if (!$var->id = $DB->insert_record('question_algebra_variables', $var)) { + throw new Exception("Could not insert algebra question variable '$varname'!"); + } + } + // Add the variable ID to the array of IDs + $variables[] = $var->id; + } // end loop over variables + + // Delete any left over old variables records. + foreach ($oldvars as $oldvar) { + $DB->delete_records('question_algebra_variables', array('id' => $oldvar->id)); + } + // Finally we are all done so return the result! + return $variables; + } + + /** + * Saves the questions answers to the database + * + * This is called by {@link save_question_options()} to save the answers to the question to + * the database from the data in the submitted form. This method should probably be in the + * questin base class rather than in the algebra subclass since the code is common to multiple + * question types and originally comes from the shortanswer question type. The method returns + * a list of the answer ID written to the database or throws an exception if an error is detected. + * + * @param object $question This holds the information from the editing form, + * it is not a standard question object. + * @return array of answer IDs which were written to the database + */ + public function save_question_answers($question) { + global $CFG, $DB; + + $context = $question->context; + // Create the results class + $result = new stdClass; + + // Get all the old answers from the database as an array + if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) { //'question', $question->'id ASC')) { + $oldanswers = array(); + } + // Create an array of the answer IDs for the question + $answers = array(); + // Set the maximum answer fraction to be -1. We will check this at the end of our + // loop over the questions and if it is not 100% (=1.0) then we will flag an error + $maxfraction = -1; + + // Loop over all the answers in the question form and write them to the database + foreach ($question->answer as $key => $answerdata) { + // Check for, and ignore, completely blank answer from the form. + if (trim($answerdata) == '' && $question->fraction[$key] == 0 && + html_is_blank($question->feedback[$key]['text'])) { + continue; + } + // Update an existing answer if possible. + $answer = array_shift($oldanswers); + if (!$answer) { + $answer = new stdClass(); + $answer->question = $question->id; + $answer->answer = ''; + $answer->feedback = ''; + $answer->feedbackformat = FORMAT_HTML; + if (!$answer->id = $DB->insert_record('question_answers', $answer)) { + throw new Exception("Could not create new algebra question answer"); + } + } + + $answer->answer = trim($answerdata); + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $this->import_or_save_files($question->feedback[$key], + $context, 'question', 'answerfeedback', $answer->id); + $answer->feedbackformat = $question->feedback[$key]['format']; + if (!$DB->update_record('question_answers', $answer)) { + throw new Exception("Could not update algebra question answer (id=$answer->id)"); + } + + + $answers[] = $answer->id; + if ($question->fraction[$key] > $maxfraction) { + $maxfraction = $question->fraction[$key]; + } + } // end loop over answers + + // Perform sanity check on the maximum fractional grade which should be 100% + if ($maxfraction != 1) { + $maxfraction = $maxfraction * 100; + throw new Exception(get_string('fractionsnomax', 'quiz', $maxfraction)); + } + + // Delete any left over old answer records. + $fs = get_file_storage(); + foreach ($oldanswers as $oldanswer) { + $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id); + $DB->delete_records('question_answers', array('id' => $oldanswer->id)); + } + + // Finally we are all done so return the result! + return $answers; + } + + /** + * Saves question-type specific options + * + * This is called by {@link save_question()} to save the question-type specific data from a + * submitted form. This method takes the form data and formats into the correct format for + * writing to the database. It then calls the parent method to actually write the data. + * + * @param object $question This holds the information from the editing form, + * it is not a standard question object. + * @return object $result->error or $result->noticeyesno or $result->notice + */ + public function save_question_options($question) { + // Start a try block to catch any exceptions generated when we attempt to parse and + // then add the answers and variables to the database + try { + // First write out all the variables associated with the question + $variables=$this->save_question_variables($question); + + // Loop over all the answers in the question form and parse them to generate + // a parser string. This ensures a constant formatting is stored in the database + foreach ($question->answer as &$answer) { + $expr=$this->parse_expression($answer); + // TODO detect invalid answer and issue a warning + $answer=$expr->sage(); + } + + // Now we need to write out all the answers to the question to the database + $answers=$this->save_question_answers($question); + + } catch (Exception $e) { + // Error when adding answers or variables to the database so create a result class + // and put the error string in the error member funtion and then return the class + // This keeps us compatible with the existing save_question_options methods. + $result=new stdClass; + $result->error=$e->getMessage(); + return $result; + } + + // Process the allowed functions field. This code just sets up the variable, it is saved + // in the parent class' save_question_options method called at the end of this method + // Look for the 'all' option. If we find it then set the string to an empty value + if(array_key_exists('all',$question->allowedfuncs)) { + $question->allowedfuncs=''; + } + // Not all functions are allowed so set allowed functions to those which are + else { + // Create a comma separated string of the function names which are stored in the + // keys of the array + $question->allowedfuncs=implode(',',array_keys($question->allowedfuncs)); + } + + // Call the parent method to write the extensions fields to the database. This either returns null + // or an error object so if we get anything then return it otherwise return our existing + $parentresult = parent::save_question_options($question); + if ($parentresult !== null) { + // Parent function returns null if all is OK + return $parentresult; + } + // Otherwise just return true - this mimics the shortanswer return format + else { + return true; + } + } + + /** + * Loads the question type specific options for the question. + * + * This function loads the compare algorithm type, disallowed strings and variables + * into the class from the database table in which they are stored. It first uses the + * parent class method to get the database information. + * + * @param object $question The question object for the question. This object + * should be updated to include the question type + * specific information (it is passed by reference). + * @return bool Indicates success or failure. + */ + public function get_question_options($question) { + // Get the information from the database table. If this fails then immediately bail. + // Note unlike the save_question_options base class method this method DOES get the question's + // answers along with any answer extensions + global $DB; + if(!parent::get_question_options($question)) { + return false; + } + // Check that we have answers and if not then bail since this question type requires answers + if(count($question->options->answers)==0) { + notify('Failed to load question answers from the table question_answers for questionid ' . + $question->id); + return false; + } + // Now get the variables from the database as well + $question->options->variables = $DB->get_records('question_algebra_variables', array('question' => $question->id)); + + + + //, 'id ASC'); + // Check that we have variables and if not then bail since this question type requires variables + + if(count($question->options->variables)==0) { + notify('Failed to load question variables from the table question_algebra_variables '. + "for questionid $question->id"); + return false; + } + + + // Check to see if there are any allowed functions + if($question->options->allowedfuncs!='') { + // Extract the allowed functions as an array + $question->options->allowedfuncs=explode(',',$question->options->allowedfuncs); + } + // Otherwise just create an empty array + else { + $question->options->allowedfuncs=array(); + } + + // Everything worked so return true + return true; + } + + /** + * Imports the question from Moodle XML format. + * + * This method is called by the format class when importing an algebra question from the + * Moodle XML format. + * + * @param $data structure containing the XML data + * @param $question question object to fill: ignored by this function (assumed to be null) + * @param $format format class importing the question + * @param $extra extra information (not required for importing this question in this format) + * @return text string containing the question data in XML format + */ + public function import_from_xml($data, $question, qformat_xml $format, $extra=null) { + if (!array_key_exists('@', $data)) { + return false; + } + if (!array_key_exists('type', $data['@'])) { + return false; + } + if ($data['@']['type'] == 'algebra') { + // Import the common question headers + $qo = $format->import_headers($data); + // Set the question type + $qo->qtype='algebra'; + + $qo->compareby = $format->getpath($data, array('#','compareby',0,'#'),'eval'); + $qo->tolerance = $format->getpath($data, array('#','tolerance',0,'#'),'0'); + $qo->nchecks = $format->getpath($data, array('#','nchecks',0,'#'),'10'); + $qo->disallow = $format->getpath($data, array('#','disallow',0,'#','text',0,'#'),'',true); + $allowedfuncs = $format->getpath($data, array('#','allowedfuncs',0,'#'), ''); + if($allowedfuncs=='') { + $qo->allowedfuncs=array('all' => 1); + } + // Need to separate the allowed functions into an array of strings and then + // flip the values of this array into the keys because this is what the + // save options method requires + else { + $qo->allowedfuncs=array_flip(explode(',',$allowedfuncs)); + } + $qo->answerprefix = $format->getpath($data, array('#','answerprefix',0,'#','text',0,'#'),'',true); + + // Import all the answers + $answers = $data['#']['answer']; + $a_count = 0; + // Loop over each answer block found in the XML + foreach($answers as $answer) { + // Use the common answer import function in the format class to load the data + $ans = $format->import_answer($answer); + $qo->answer[$a_count] = $ans->answer['text']; + $qo->fraction[$a_count] = $ans->fraction; + $qo->feedback[$a_count] = $ans->feedback; + ++$a_count; + } + + // Import all the variables + $vars = $data['#']['variable']; + $v_count = 0; + // Loop over each answer block found in the XML + foreach($vars as $var) { + $qo->variable[$v_count] = $format->getpath($var, array('@','name'),0); + $qo->varmin[$v_count] = $format->getpath($var, array('#','min',0,'#'),'0',false,get_string('novarmin','qtype_algebra')); + $qo->varmax[$v_count] = $format->getpath($var, array('#','max',0,'#'),'0',false,get_string('novarmax','qtype_algebra')); + ++$v_count; + } + + $format->import_hints($qo, $data); + + return $qo; + } + return false; + } + + + /** + * Exports the question to Moodle XML format. + * + * This method is called by the format class when exporting an algebra question into then + * Moodle XML format. + * + * @param $question question to be exported into XML format + * @param $format format class exporting the question + * @param $extra extra information (not required for exporting this question in this format) + * @return text string containing the question data in XML format + */ + public function export_to_xml($question, qformat_xml $format, $extra=null) { + $expout=''; + // Create a text string of the allowed functions from the array + $allowedfuncs=implode(',',$question->options->allowedfuncs); + // Write out all the extra fields belonging to the algebra question type + $expout .= " <compareby>{$question->options->compareby}</compareby>\n"; + $expout .= " <tolerance>{$question->options->tolerance}</tolerance>\n"; + $expout .= " <nchecks>{$question->options->nchecks}</nchecks>\n"; + $expout .= " <disallow>".$format->writetext($question->options->disallow,1,true)."</disallow>\n"; + $expout .= " <allowedfuncs>$allowedfuncs</allowedfuncs>\n"; + $expout .= " <answerprefix>".$format->writetext($question->options->answerprefix,1,true). + "</answerprefix>\n"; + // Write out all the answers + $expout .= $format->write_answers($question->options->answers); + // Loop over all the variables for the question and write out all their details + foreach ($question->options->variables as $var) { + $expout .= "<variable name=\"{$var->name}\">\n"; + $expout .= " <min>{$var->min}</min>\n"; + $expout .= " <max>{$var->max}</max>\n"; + $expout .= "</variable>\n"; + } + return $expout; + } + + // Gets all the question responses + public function get_all_responses(&$question, &$state) { + $result = new stdClass; + $answers = array(); + // Loop over all the answers + if (is_array($question->options->answers)) { + foreach ($question->options->answers as $aid=>$answer) { + $r = new stdClass; + $r->answer = $answer->answer; + $r->credit = $answer->fraction; + $answers[$aid] = $r; + } + } + $result->id = $question->id; + $result->responses = $answers; + return $result; + } + + /** + * Parses the given expression with the parser if required. + * + * This method will check to see if the argument it is given is already a parsed + * expression and if not will attempt to parse it. + * + * @param $expr expression which will be parsed + * @param $question question containing the expression or null if none + * @return top term of the parse tree or a string if an exception is thrown + */ + function parse_expression($expr) { + // Check to see if this is already a parsed expression + if(is_a($expr, 'qtype_algebra_parser_term')) { + // It is a parsed expression so simply return it + return $expr; + } + // Check whether we have a state object or a simple string. If a state + // then replace it with the response string + if(isset($expr->responses[''])) { + $expr=$expr->responses['']; + } + // Create an empty array of variable names for the parser (no variable checking here as it is done in the form validation + // TODO see in case of import + $varnames=array(); + + // We now assume that we have a string to parse. Create a parser instance to + // to this and return the parser expression at the top of the parse tree + $p=new qtype_algebra_parser; + // Perform the actual parsing inside a try-catch block so that any exceptions + // can be caught and converted into errors + try { + return $p->parse($expr,$varnames); + } catch(Exception $e) { + // If the expression cannot be parsed then return a null term. This will + // make Moodle treat the answer as wrong. + // TODO: Would be nice to have support for 'invalid answer' in the quiz + // engine since an unparseable response is usually caused by a silly typo + return new qtype_algebra_parser_nullterm; + } + } + + protected function initialise_question_instance(question_definition $question, $questiondata) { + parent::initialise_question_instance($question, $questiondata); + $question->variables = array(); + if (!empty($questiondata->options->variables)) { + foreach ($questiondata->options->variables as $v) { + $question->variables[$v->id] = new question_variable($v->id, $v->name, $v->min, $v->max); + } + } + $question->compareby = $questiondata->options->compareby; + $question->nchecks = $questiondata->options->nchecks; + $question->tolerance = $questiondata->options->tolerance; + $question->allowedfuncs = $questiondata->options->allowedfuncs; + $question->disallow = $questiondata->options->disallow; + $question->answerprefix = $questiondata->options->answerprefix; + $this->initialise_question_answers($question, $questiondata); + } + + public function get_random_guess_score($questiondata) { + foreach ($questiondata->options->answers as $aid => $answer) { + if ('*' == trim($answer->answer)) { + return $answer->fraction; + } + } + return 0; + } + + public function get_possible_responses($questiondata) { + $responses = array(); + + foreach ($questiondata->options->answers as $aid => $answer) { + $responses[$aid] = new question_possible_response($answer->answer, + $answer->fraction); + } + $responses[null] = question_possible_response::no_response(); + + return array($questiondata->id => $responses); + } +}
\ No newline at end of file diff --git a/renderer.php b/renderer.php new file mode 100644 index 0000000..4ffe674 --- /dev/null +++ b/renderer.php @@ -0,0 +1,162 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Algebra question renderer class. + * + * @package qtype + * @subpackage algebra + * @author Roger Moore <rwmoore 'at' ualberta.ca> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Generates the output for algebra questions. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_algebra_renderer extends qtype_renderer { + public function formulation_and_controls(question_attempt $qa, + question_display_options $options) { + global $CFG; + + $question = $qa->get_question(); + + $currentanswer = $qa->get_last_qt_var('answer'); + + $inputname = $qa->get_qt_field_name('answer'); + + $nameprefix = str_replace(':', '_', $inputname); // valid javascript name + $inputattributes = array( + 'type' => 'text', + 'name' => $inputname, + 'value' => $currentanswer, + 'id' => $inputname, + 'size' => 80, + ); + + if ($options->readonly) { + $inputattributes['readonly'] = 'readonly'; + } + + $feedbackimg = ''; + if ($options->correctness) { + $answer = $question->get_matching_answer(array('answer' => $currentanswer)); + if ($answer) { + $fraction = $answer->fraction; + } else { + $fraction = 0; + } + $inputattributes['class'] = $this->feedback_class($fraction); + $feedbackimg = $this->feedback_image($fraction); + } + + $iframename = $nameprefix.'_if'; + // Name of the javascript function which causes the entered formula to be rendered + $df_name = $nameprefix.'_display'; + // Create an array of variable names to use when displaying the function entered + $varnames=array(); + if($question and isset($question->variables)) { + $variables = $question->variables; + foreach($question->variables as $var) { + $varnames[]=$var->name; + } + } + + $varnames=implode(',',$varnames); + // Javascript function which the button uses to display the rendering + // This function sents the source of the iframe to the 'displayformula.php' script giving + // it an argument of the formula entered by the student. + $displayfunction = + 'function '.$df_name."() {\n". + ' var text="vars='.$varnames.'&expr="+escape(document.getElementsByName("'.$inputname.'")[0].value);'."\n". + " if(text.length != 0) {\n". + ' document.getElementsByName("'.$iframename.'")[0].src="'. + $CFG->wwwroot.'/question/type/algebra/displayformula.php?"+'. + 'text.replace(/\+/g,"%2b")'."\n". + " }\n". + " }\n"; + + $questiontext = $question->format_questiontext($qa); + + $input = html_writer::empty_tag('input', $inputattributes) . $feedbackimg; + + + $result = html_writer::tag('div', $questiontext, array('class' => 'qtext')); + + $result .= html_writer::tag('script', $displayfunction, array('type'=>'text/javascript')); + + $result .= html_writer::start_tag('div', array('class' => 'ablock')); + $result .= html_writer::start_tag('div', array('class' => 'prompt', 'style' => 'vertical-align: top')); + if(isset($question->answerprefix) and !empty($question->answerprefix)) { + $opts=new StdClass; + $opts->para=false; + $result .= html_writer::tag('div', format_text($question->answerprefix,FORMAT_MOODLE,$opts).$input, array('class' => 'answer')); + } else { + $result .= get_string('answer', 'qtype_algebra', + html_writer::tag('div', $input, array('class' => 'answer'))); + } + $result .= html_writer::end_tag('div'); + + $result .= html_writer::end_tag('div'); + + + if ($qa->get_state() == question_state::$invalid) { + $result .= html_writer::nonempty_tag('div', + $question->get_validation_error(array('answer' => $currentanswer)), + array('class' => 'validationerror')); + } + $result .= html_writer::start_tag('div', array('class' => 'dispresponse')); + $result .= html_writer::empty_tag('input', array('type'=>'button', 'value'=>'Display Response', 'onclick'=>$df_name.'()')); + $result .= html_writer::start_tag('iframe', array('name'=>$iframename, 'width'=>'60%', 'height'=>60, 'align'=>'middle', 'src'=>'')); + $result .= html_writer::end_tag('iframe'); + $result .= html_writer::tag('script', $df_name.'();', array('type'=>'text/javascript')); + $result .= html_writer::end_tag('div'); + + return $result; + } + + public function specific_feedback(question_attempt $qa) { + $question = $qa->get_question(); + + $answer = $question->get_matching_answer(array('answer' => $qa->get_last_qt_var('answer'))); + if (!$answer || !$answer->feedback) { + return ''; + } + + return $question->format_text($answer->feedback, $answer->feedbackformat, + $qa, 'question', 'answerfeedback', $answer->id); + } + + public function correct_response(question_attempt $qa) { + $question = $qa->get_question(); + + $answer = $question->get_matching_answer($question->get_correct_response()); + if (!$answer) { + return ''; + } + $formatoptions = new stdClass; + $formatoptions->para = false; + $formatoptions->clean = false; + $formattedanswer = format_text($question->formated_expression($answer->answer), FORMAT_MOODLE, $formatoptions); + return get_string('correctansweris', 'qtype_algebra', s($answer->answer)). $formattedanswer; + } +}
\ No newline at end of file diff --git a/sage_server.py b/sage_server.py new file mode 100644 index 0000000..8b41bf7 --- /dev/null +++ b/sage_server.py @@ -0,0 +1,29 @@ +#!/usr/bin/env sage -python + +from SimpleXMLRPCServer import SimpleXMLRPCServer + +from sage.all import Sage + +s=Sage() + +server=SimpleXMLRPCServer(("localhost",7777)) +server.register_introspection_functions() + +def full_symbolic_compare(expr1,expr2,vars): + varstr=",".join(vars) + print "Comparing %s to %s with variables %s" % (expr1,expr2,varstr) + s.eval('%s=var("%s")' % (varstr,varstr)) + s.eval("_func=(%s)-(%s)" % (expr1,expr2)) + result=s.eval("_func.simplify_full()") + if result=='0': + print "Equal" + return 0 + else: + print "Not equal" + return 1 + +server.register_function(full_symbolic_compare) + +print full_symbolic_compare('x^2+2*x+1', 'x^2+2*x+1', ['x']) + +server.serve_forever() diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..a0f5423 --- /dev/null +++ b/settings.php @@ -0,0 +1,11 @@ +<?php
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+ //host
+ $settings->add(new admin_setting_configtext('qtype_algebra_host', get_string('host', 'qtype_algebra'), '', 'localhost', PARAM_TEXT));
+ //port
+ $settings->add(new admin_setting_configtext('qtype_algebra_port', get_string('port', 'qtype_algebra'), '', 7777, PARAM_INT));
+ //host
+ $settings->add(new admin_setting_configtext('qtype_algebra_uri', get_string('uri', 'qtype_algebra'), '', '', PARAM_TEXT));
+}
diff --git a/version.php b/version.php new file mode 100644 index 0000000..81af712 --- /dev/null +++ b/version.php @@ -0,0 +1,5 @@ +<?php +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2011072800; +$plugin->requires = 2011060313; diff --git a/xmlrpc-utils.php b/xmlrpc-utils.php new file mode 100644 index 0000000..a671d75 --- /dev/null +++ b/xmlrpc-utils.php @@ -0,0 +1,269 @@ +<?php + +/* + This file is part of, or distributed with, libXMLRPC - a C library for + xml-encoded function calls. + + Author: Dan Libby (dan@libby.com) + Epinions.com may be contacted at feedback@epinions-inc.com +*/ + +/* + Copyright 2001 Epinions, Inc. + + Subject to the following 3 conditions, Epinions, Inc. permits you, free + of charge, to (a) use, copy, distribute, modify, perform and display this + software and associated documentation files (the "Software"), and (b) + permit others to whom the Software is furnished to do so as well. + + 1) The above copyright notice and this permission notice shall be included + without modification in all copies or substantial portions of the + Software. + + 2) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT ANY WARRANTY OR CONDITION OF + ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING WITHOUT LIMITATION ANY + IMPLIED WARRANTIES OF ACCURACY, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE OR NONINFRINGEMENT. + + 3) IN NO EVENT SHALL EPINIONS, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, + SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES OR LOST PROFITS ARISING OUT + OF OR IN CONNECTION WITH THE SOFTWARE (HOWEVER ARISING, INCLUDING + NEGLIGENCE), EVEN IF EPINIONS, INC. IS AWARE OF THE POSSIBILITY OF SUCH + DAMAGES. + +*/ + + +/* xmlrpc utilities (xu) + * author: Dan Libby (dan@libby.com) + */ + +// ensure extension is loaded. +xu_load_extension(); + +// a function to ensure the xmlrpc extension is loaded. +// xmlrpc_epi_dir = directory where libxmlrpc.so.0 is located +// xmlrpc_php_dir = directory where xmlrpc-epi-php.so is located +function xu_load_extension($xmlrpc_php_dir="") { + $bSuccess=false; + if(!extension_loaded('xmlrpc')) { + $bSuccess = true; + putenv("LD_LIBRARY_PATH=/usr/lib/php4/apache/xmlrpc/"); + if ($xmlrpc_php_dir) { + $xmlrpc_php_dir .= '/'; + } + if (!extension_loaded("xmlrpc")) { + $bSuccess = dl($xmlrpc_php_dir . "xmlrpc-epi-php.so"); + } + } + return $bSuccess; +} + +/* generic function to call an http server with post method */ +function xu_query_http_post($request, $host, $uri, $port, $debug, + $timeout, $user, $pass, $secure=false) { + $response_buf = ""; + if ($host && $uri && $port) { + $content_len = strlen($request); + + $fsockopen = $secure ? "fsockopen_ssl" : "fsockopen"; + + dbg1("opening socket to host: $host, port: $port, uri: $uri", $debug); + $query_fd = $fsockopen($host, $port, $errno, $errstr, 10); + + if ($query_fd) { + + $auth = ""; + if ($user) { + $auth = "Authorization: Basic " . + base64_encode($user . ":" . $pass) . "\r\n"; + } + + $http_request = + "POST $uri HTTP/1.0\r\n" . + "User-Agent: xmlrpc-epi-php/0.2 (PHP)\r\n" . + "Host: $host:$port\r\n" . + $auth . + "Content-Type: text/xml\r\n" . + "Content-Length: $content_len\r\n" . + "\r\n" . + $request; + + dbg1("sending http request:</h3> <xmp>\n$http_request\n</xmp>", $debug); + + fputs($query_fd, $http_request, strlen($http_request)); + + dbg1("receiving response...", $debug); + + $header_parsed = false; + + $line = fgets($query_fd, 4096); + while ($line) { + if (!$header_parsed) { + if ($line === "\r\n" || $line === "\n") { + $header_parsed = 1; + } + dbg2("got header - $line", $debug); + } + else { + $response_buf .= $line; + } + $line = fgets($query_fd, 4096); + } + + fclose($query_fd); + } + else { + dbg1("socket open failed", $debug); + } + } + else { + dbg1("missing param(s)", $debug); + } + + dbg1("got response:</h3>. <xmp>\n$response_buf\n</xmp>\n", $debug); + + return $response_buf; +} + +function xu_fault_code($code, $string) { + return array('faultCode' => $code, + 'faultString' => $string); +} + + +function find_and_decode_xml($buf, $debug) { + if (strlen($buf)) { + $xml_begin = substr($buf, strpos($buf, "<?xml")); + if (strlen($xml_begin)) { + + $retval = xmlrpc_decode($xml_begin); + } + else { + dbg1("xml start token not found", $debug); + } + } + else { + dbg1("no data", $debug); + } + return $retval; +} + + +/** + * @param params a struct containing 3 or more of these key/val pairs: + * @param host remote host (required) + * @param uri remote uri (required) + * @param port remote port (required) + * @param method name of method to call + * @param args arguments to send (parameters to remote xmlrpc server) + * @param debug debug level (0 none, 1, some, 2 more) + * @param timeout timeout in secs. (0 = never) + * @param user user name for authentication. + * @param pass password for authentication + * @param secure secure. wether to use fsockopen_ssl. (requires special php build). + * @param output array. xml output options. can be null. details below: + * + * output_type: return data as either php native data types or xml + * encoded. ifphp is used, then the other values are ignored. default = xml + * verbosity: determine compactness of generated xml. options are + * no_white_space, newlines_only, and pretty. default = pretty + * escaping: determine how/whether to escape certain characters. 1 or + * more values are allowed. If multiple, they need to be specified as + * a sub-array. options are: cdata, non-ascii, non-print, and + * markup. default = non-ascii | non-print | markup + * version: version of xml vocabulary to use. currently, three are + * supported: xmlrpc, soap 1.1, and simple. The keyword auto is also + * recognized to mean respond in whichever version the request came + * in. default = auto (when applicable), xmlrpc + * encoding: the encoding that the data is in. Since PHP defaults to + * iso-8859-1 you will usually want to use that. Change it if you know + * what you are doing. default=iso-8859-1 + * + * example usage + * + * $output_options = array('output_type' => 'xml', + * 'verbosity' => 'pretty', + * 'escaping' => array('markup', 'non-ascii', 'non-print'), + * 'version' => 'xmlrpc', + * 'encoding' => 'utf-8' + * ); + * or + * + * $output_options = array('output_type' => 'php'); + */ +function xu_rpc_http_concise($params) { + $host = $uri = $port = $method = $args = $debug = null; + $timeout = $user = $pass = $secure = $debug = null; + + extract($params); + + // default values + if(!$port) { + $port = 80; + } + if(!$uri) { + $uri = '/'; + } + if(!isset($output)) { + $output = array('version' => 'xmlrpc'); + } + + $response_buf = ""; + if ($host && $uri && $port) { + $request_xml = xmlrpc_encode_request($method, $args, $output); + $response_buf = xu_query_http_post($request_xml, $host, $uri, $port, $debug, + $timeout, $user, $pass, $secure); + + $retval = find_and_decode_xml($response_buf, $debug); + } + return $retval; +} + +/* call an xmlrpc method on a remote http server. legacy support. */ +function xu_rpc_http($method, $args, $host, $uri="/", $port=80, $debug=false, + $timeout=0, $user=false, $pass=false, $secure=false) { + return xu_rpc_http_concise( + array( + method => $method, + args => $args, + host => $host, + uri => $uri, + port => $port, + debug => $debug, + timeout => $timeout, + user => $user, + pass => $pass, + secure => $secure + )); +} + + + +function xu_is_fault($arg) { + // xmlrpc extension finally supports this. + return is_array($arg) ? xmlrpc_is_fault($arg) : false; +} + +/* sets some http headers and prints xml */ +function xu_server_send_http_response($xml) { + header("Content-type: text/xml"); + header("Content-length: " . strlen($xml) ); + echo $xml; +} + + +function dbg($msg) { + echo "<h3>$msg</h3>"; flush(); +} +function dbg1($msg, $debug_level) { + if ($debug_level >= 1) { + dbg($msg); + } +} +function dbg2($msg, $debug_level) { + if ($debug_level >= 2) { + dbg($msg); + } +} + |