From 8f5bf196ee5ba54bb0b7fdce477394c920bccde5 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Sat, 30 Apr 2022 11:51:50 +0100 Subject: [PATCH 01/41] Proof of concept for adding a comma in tex output as the decimal separator. Issue #789. --- doc/en/CAS/Numbers.md | 6 ++++ stack/maxima/print-comma.lisp | 66 +++++++++++++++++++++++++++++++++++ stack/maxima/stackmaxima.mac | 9 +++-- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 stack/maxima/print-comma.lisp diff --git a/doc/en/CAS/Numbers.md b/doc/en/CAS/Numbers.md index ef7c6a6ad64..6b7d365ada3 100644 --- a/doc/en/CAS/Numbers.md +++ b/doc/en/CAS/Numbers.md @@ -101,6 +101,12 @@ Maxima has a separate system for controlling the number of decimal digits used i fpprec:20, /* Work with 20 digits. */ fpprintprec:12, /* Print only 12 digits. */ +## Changing the decimal separator, e.g. using a comma for separating decimals ## + +STACK now supports a mechanism for changing the decimal separator and using a comma for separating decimals. Using the following will print floating point numbers with a comma instead of the decimal point. + + stackfltfmt:"comma"; + ## Notes about numerical rounding ## There are two ways to round numbers ending in a digit \(5\). diff --git a/stack/maxima/print-comma.lisp b/stack/maxima/print-comma.lisp new file mode 100644 index 00000000000..9f1bdd2ffb6 --- /dev/null +++ b/stack/maxima/print-comma.lisp @@ -0,0 +1,66 @@ +(in-package :cl-user) + +(defun inject-comma (string comma-char comma-interval) + (let* ((len (length string)) + (offset (mod len comma-interval))) + (with-output-to-string (out) + (write-string string out :start 0 :end offset) + (do ((i offset (+ i comma-interval))) + ((>= i len)) + (unless (zerop i) + (write-char comma-char out)) + (write-string string out :start i :end (+ i comma-interval)))))) + + +(defun print-float (stream arg colonp atp + &optional + (point-char #\.) + (comma-char #\,) + (comma-interval 3)) + "A function for printing floating point numbers, with an interface +suitable for use with the tilde-slash FORMAT directive. The full form +is + + ~point-char,comma-char,comma-interval/print-float/ + +The point-char is used in place of the decimal point, and defaults to +#\\. If : is specified, then the whole part of the number will be +grouped in the same manner as ~D, using COMMA-CHAR and COMMA-INTERVAL. +If @ is specified, then the sign is always printed." + (let* ((sign (if (minusp arg) "-" (if (and atp (plusp arg)) "+" ""))) + (output (format nil "~F" arg)) + (point (position #\. output :test 'char=)) + (whole (subseq output (if (minusp arg) 1 0) point)) + (fractional (subseq output (1+ point)))) + (when colonp + (setf whole (inject-comma whole comma-char comma-interval))) + (format stream "~A~A~C~A" + sign whole point-char fractional))) + +;; Basic usage examples. +;; colonp decides if we group digits or not. +;; atp controls if we print an initial + sign +;; The next arguments are point-char, comma-char and comma interval. +;; printf_float(false, %pi*10^6, true, false, ",", " ", 3); +;; printf_float(false, -%pi*10^6, true, false, ",", ".", 3); + +(defun maxima::$printf_float (stream arg &optional + (colonp t) (atp t) + (point-char #\.) + (comma-char #\,) + (comma-interval 3)) + (flet ((coerce-to-char (s) + (cond ((characterp s) s) + ((and (stringp s) (equal s "")) + (code-char 0)) + ((stringp s) + (car (coerce s 'list))) + ((symbolp s) + (cadr (coerce (format nil "~a" s) 'list))) + (t + ;; fix me + (error "Input needs to be a character or string, found ~a." s))))) + (let ((point-char (coerce-to-char point-char)) + (comma-char (coerce-to-char comma-char)) + (arg (maxima::$float arg))) + (print-float stream arg colonp atp point-char comma-char comma-interval)))) diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac index 33da1b1c740..07657b0f1ae 100644 --- a/stack/maxima/stackmaxima.mac +++ b/stack/maxima/stackmaxima.mac @@ -152,6 +152,7 @@ load("inequalities.mac"); load("intervals.mac"); load("stackunits.mac"); load("stacktex.lisp"); +load("print-comma.lisp"); load("stackstrings.mac"); /* This file is a modified core Maxima function with local variable name clashes fixed. */ @@ -677,8 +678,12 @@ stack_disp_strip_dollars(ex) := block( /* Display of numbers. Thanks to Robert Dodier. */ stackintfmt: "~d"; stackfltfmt: "~a"; -?texnumformat(x) := if ev(floatnump(x),simp) then - ev(printf(false, stackfltfmt, x), simp) else if ev(integerp(x),simp) then ( +?texnumformat(x) := if ev(floatnump(x),simp) then ( + if stackfltfmt = "comma" then + ev(printf_float(false, x, false, false, ",", ".", 3), simp) + else + ev(printf(false, stackfltfmt, x), simp) + ) else if ev(integerp(x),simp) then ( if (is(stackintfmt="~r") or is(stackintfmt="~:r")) then sconcat("\\mbox{",ev(printf(false, stackintfmt, x), simp),"}") else From 2e1ec83ba6177f3d55b947bf9ec95cdf06fa0f24 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Fri, 7 Jul 2023 15:03:58 +0100 Subject: [PATCH 02/41] WIP: separate list, set and function arguments with semicolon. Issue #789. --- doc/en/CAS/Numbers.md | 2 ++ stack/maxima/stacktex.lisp | 26 ++++++++++++++++++++++++++ tests/castext_test.php | 27 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/doc/en/CAS/Numbers.md b/doc/en/CAS/Numbers.md index 24a89f8a450..de656843dc1 100644 --- a/doc/en/CAS/Numbers.md +++ b/doc/en/CAS/Numbers.md @@ -127,6 +127,8 @@ STACK now supports a mechanism for changing the decimal separator and using a co stackfltfmt:"comma"; +If you use this option then items in sets, lists and as arguments to functions will no longer be separated by a comma. To avoid conflicting notation, items will be separated by a semicolon (`;`). + ## Notes about numerical rounding ## There are two ways to round numbers ending in a digit \(5\). diff --git a/stack/maxima/stacktex.lisp b/stack/maxima/stacktex.lisp index 703e56a3517..2897aee96b5 100644 --- a/stack/maxima/stacktex.lisp +++ b/stack/maxima/stacktex.lisp @@ -492,3 +492,29 @@ (defmfun $get_texword (x) (or (get x 'texword) (get (get x 'reversealias) 'texword))) (defmfun $get_texsym (x) (car (or (get x 'texsym) (get x 'strsym) (get x 'dissym) (stripdollar x)))) + +;; ************************************************************************************************* +;; Added 20 Feb 2022. +;; +;; Change the list separation on tex output when commas are used for decimal separators. +;; +;; Code below makes the list separator a normal "texput" concern. +;; E.g. in maxima: texput(stacklistsep, " ; "); +;; (defprop $stacklistsep " , " texword) +;; +;;(defun tex-matchfix (x l r) +;; (setq l (append l (car (texsym (caar x)))) +;; ;; car of texsym of a matchfix operator is the lead op +;; r (append (list (nth 1 (texsym (caar x)))) r) +;; ;; cdr is the trailing op +;; x (tex-list (cdr x) nil r (or (nth 2 (texsym (caar x))) (get '$stacklistsep 'texword)))) +;; (append l x)) + +(defun tex-matchfix (x l r) + (setq l (append l (car (texsym (caar x)))) + ;; car of texsym of a matchfix operator is the lead op + r (append (list (nth 1 (texsym (caar x)))) r) + ;; cdr is the trailing op + x (tex-list (cdr x) nil r (or (nth 2 (texsym (caar x))) (if (string= $stackfltfmt '"comma") '" ; " '" , ")))) + (append l x)) + diff --git a/tests/castext_test.php b/tests/castext_test.php index e2d2eadbcb0..6d3a1d73395 100644 --- a/tests/castext_test.php +++ b/tests/castext_test.php @@ -1314,6 +1314,33 @@ public function test_numerical_display_roman() { $at2->get_rendered()); } + /** + * @covers \qtype_stack\stack_cas_castext2_latex + */ + public function test_numerical_display_commas() { + $st = 'The number {@3.1415@} is written with commas. '; + $st .= 'Sets {@{1.2, 4, 5, 3.123}@} and lists {@[1.2, 4, 5, 3.123]@}'; + + $a2 = array('stackfltfmt:"comma"'); + $s2 = array(); + foreach ($a2 as $s) { + $s2[] = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), array()); + } + $cs2 = new stack_cas_session2($s2, null, 0); + + $at2 = castext2_evaluatable::make_from_source($st, 'test-case'); + + $this->assertTrue($at2->get_valid()); + $cs2->add_statement($at2); + $cs2->instantiate(); + + $this->assertEquals( + 'The number \({3,1415}\) is written with commas. ' . + 'Sets \({\left \{1,2 ; 3,123 ; 4 ; 5 \right \}}\) ' . + 'and lists \({\left[ 1,2 ; 4 ; 5 ; 3,123 \right]}\)', + $at2->get_rendered()); + } + /** * @covers \qtype_stack\stack_cas_castext2_latex */ From 8317ddec7335f58369815762dc92c034c9d07ad3 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Mon, 10 Jul 2023 09:19:25 +0100 Subject: [PATCH 03/41] Proposal for the decimal separator in issue #789. Comment needed. --- doc/en/CAS/Numbers.md | 15 +- doc/en/Developer/Development_track.md | 5 + doc/en/Developer/Syntax_Future.md | 1 + doc/en/Developer/Syntax_numbers.md | 97 +++++++++ doc/meta_en.json | 5 + stack/cas/security-map.json | 4 + stack/maxima/assessment.mac | 18 +- stack/maxima/print-comma.lisp | 2 + stack/maxima/stackmaxima.mac | 25 ++- stack/maxima/stacktex.lisp | 2 +- tests/cassession2_test.php | 299 +++++++++++++++++--------- tests/castext_test.php | 2 +- 12 files changed, 354 insertions(+), 121 deletions(-) create mode 100644 doc/en/Developer/Syntax_numbers.md diff --git a/doc/en/CAS/Numbers.md b/doc/en/CAS/Numbers.md index de656843dc1..de8b33de8b5 100644 --- a/doc/en/CAS/Numbers.md +++ b/doc/en/CAS/Numbers.md @@ -123,11 +123,20 @@ Maxima has a separate system for controlling the number of decimal digits used i ## Changing the decimal separator, e.g. using a comma for separating decimals ## -STACK now supports a mechanism for changing the decimal separator and using a comma for separating decimals. Using the following will print floating point numbers with a comma instead of the decimal point. +STACK now supports a mechanism for changing the decimal separator and using a comma for separating decimals. Using the following in the question variables will print floating point numbers with a comma instead of the decimal point throughout the question, including students' inputs - stackfltfmt:"comma"; + texput_decimal(","); + +For finer control in other parts of the question, just set the variable + + stackfltsep:","; + +The global variables `stackfltfmt` and `stackfltsep` should have independent effects. + +If you use the option for a comma then items in sets, lists and as arguments to functions will no longer be separated by a comma. To avoid conflicting notation, items will be separated by a semicolon (`;`). + +If you separate decimal groups of digits with commas, e.g. if `stackfltfmt:"~:d"`, then these commas are replaced by spaces to avoid ambiguity. The replacement of commas occurs in integers as well as floats to make sure commas in integers cause no confusion. -If you use this option then items in sets, lists and as arguments to functions will no longer be separated by a comma. To avoid conflicting notation, items will be separated by a semicolon (`;`). ## Notes about numerical rounding ## diff --git a/doc/en/Developer/Development_track.md b/doc/en/Developer/Development_track.md index c2af209bfcd..e35915b006f 100644 --- a/doc/en/Developer/Development_track.md +++ b/doc/en/Developer/Development_track.md @@ -15,6 +15,11 @@ DONE 5. Caschat page now saves question variables and general feedback back into the question. Fixes issue #984. 6. Confirm support for Maxima 5.46.0 and 5.47.0. +Support the use of a [comma as the decimal separator](Syntax_numbers.md) +1. (Done) Mechanism for Maxima to output LaTeX. +2. Mechanism to output expressions as they should be typed. +3. Input parsing mechanism. + TODO: 1. Support for PHP 8.2. See issue #986. diff --git a/doc/en/Developer/Syntax_Future.md b/doc/en/Developer/Syntax_Future.md index 4928e7403b8..9e207f0d98b 100644 --- a/doc/en/Developer/Syntax_Future.md +++ b/doc/en/Developer/Syntax_Future.md @@ -55,6 +55,7 @@ Therefore, a value of `3` both inserts `*`s for implied multiplication and allow Support other issues in context, at the parsing stage. +* Support the use of a [comma as the decimal separator](Syntax_numbers.md) * Base M numbers * Allow entry of unicode? * Spot order of precedence problems: `x = 1 or 2` (normally used to mean `x=1 or x=2`). diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md new file mode 100644 index 00000000000..588aa1178fb --- /dev/null +++ b/doc/en/Developer/Syntax_numbers.md @@ -0,0 +1,97 @@ +# Entry of numbers in STACK + +___This is a proposal for discussion as of 10 July 2023.___ + +This document discusses entry of numbers into STACK. This discussion will also be relevant to other online assessment systems and technology more generally. When we type in a string of symbols into a computer there is a context and assumptions which arise from that context. For example is `e` the base of the natural logarithms or is `e` to be interpreted as part of a floating point number, e.g. `6.6263−34`? There are two related issues. + +* Which symbol to use as the decimal separator, '`,`' or '`.`'? +* Support for number bases (other than decimal). + +We start by designing the input mechanism for decimal separators. + +## Standards + +[ISO 80000-1:2022](https://www.iso.org/standard/76921.html) _Quantities and units — Part 1: General_ gives "general information and definitions concerning quantities, systems of quantities, units, quantity and unit symbols, and coherent unit systems, especially the International System of Quantities (ISQ)." Section 7.2.2 covers the decimal sign: "The decimal sign is either a comma or a point on the line. The same decimal sign should be used consistently within a document." + +Further, in section _7.2 Numbers_: "To facilitate the reading of numbers with many digits, these may be separated into groups of three, counting from the decimal sign towards the left and the right. No group shall contain more than three digits. Where such separation into groups of three is used, the groups shall be separated by a small space and not by a point or a comma or by any other means." + +It goes on to say "The General Conference on Weights and Measures (Fr: Conférence Générale des Poids et Mesures) at +its meeting in 2003 passed unanimously the following resolution: _"The decimal marker shall be either a point on the line or a comma on the line."_ In practice, the choice between these alternatives depends on customary use in the language concerned". + +The older [ISO 6093:1985](https://www.iso.org/standard/12285.html) _Specification for Representation of numerical values in character strings for information interchange_ +also allows for either a comma or point for the decimal separator. + +These standards to not provide advice on how to separate items, e.g. in lists, and so how to interpret expressions such as `{1,2}`. There are two options for interpreting `{1,2}`: + +1. A set containing the single number six fifths, \(\frac{6}{5}\). +2. A set containing the two integers one and two. + +## Design of syntax for decimal separators + +The only opportunity for ambiguity arises in the use of a comma '`,`' in an expression, which could be a decimal separator or a list separator. + +1. We assume we are only parsing a single expression. Hence expressions are _not_ separated by a semicolon '`;`'. However, a single expression might contain more than one number, e.g. coefficients in a polynomial, members of a set/list, and arguments to a function (e.g. \(\max(a, b)\) or \(\max(a; b)\)). +2. The symbol '`.`' must be a decimal separator. +3. The symbol '`;`' must separate items in a list, set, function, etc. + +It is reasonable to expect students to be consistent in their use of the '`,`' within a particular expression. This follows the advice in ISO 80000-1:2022. +Therefore students cannot use all of '`.`', '`,`' and '`;`' in a single expression without inconsistency. + +In the current STACK design student input of `1,23` would be invalid and generate an error: "A comma in your expression appears in a strange way." Many users will wish to retain this behaviour. Therefore although this expression is not ambiguous, in a British context it does not follow common usage and could well indicate a misunderstanding about how to type in sets, lists, coordinates functions etc. +A similar problem occurs in a continental context where `1;23` contains an unencapsulated list separation. This expression is not ambiguous and a similar error message such as "A in your expression appears in a strange way." would be similarly helpful. + +Examples. + +| Typed expression | '`.`' | '`,`' | '`;`' | Ambiguity? | Comments | +|------------------|-------|-------|-------|------------|-------------------------------------------------------------| +|`123` | . | . | . | No | | +|`1.23` | Y | . | . | No | Single decimal number. | +|`1,23` | . | Y | . | No/error | Single decimal number or an unencapsulated list. | +|`1.2+2,3*x` | Y | Y | . | Error | Inconsistent decimal separators used. | +|`1;23` | . | . | Y | Error | This expression contains an unencapsulated list. | +|`{123}` | . | . | . | No | Set of one integer. | +|`{1.23}` | Y | . | . | No | Set of one float. | +|`{1,23}` | . | Y | . | Yes | Option needed to interpret the role of '`,`'. | +|`{1.2,3}` | Y | Y | . | No | '`.`' used, '`;`' not used, so '`,`' must separate lists. | +|`{1;23}` | . | . | Y | No | Set of two integers. | +|`{1.2;3}` | Y | . | Y | No | '`.`' used, and no '`,`' | +|`{1,2;3}` | . | Y | Y | No | '`;`' used, no '`.`', so '`,`' is a decimal separator. | +|`{1,2;3;4.1}` | Y | Y | Y | Error | Inconsistent decimal/list separators used. | + + +## Proposal for options in STACK + +We need a new question-level option in STACK for decimal separators. This option distinguishes between "British" '`.`' and "contiential" '`,`' decimal separators. Output, e.g. LaTeX generated by Maxima, will respect this useage throughout the question. Hence the need for a question-level option. + +1. Strict continential. Reject any use of '`.`' as a decimal separator. I.e. reject any use of '`.`'. +2. Strict British. Reject any use of '`,`' as decimal separator. Warn for unencapsulated lists with '`,`' and reject any use of '`;`'. (Current STACK behaviour) +3. Weak continential. When ambiguity arises, assume '`,`' should be a decimal separator. +4. Weak British. When ambiguity arises, assume '`,`' should be a list separator. + +Wherever '`;`' is permitted (all but Strict British) we should warn for unencapsulated lists with '`;`' as we currently do for '`,`' in STACK. + +We have always worked on the basis of being as forgiving as possible, and accepting expessions where no ambiguity arises. E.g. `2x` must always mean `2*x` under any reasonable interpretation, and if we choose to reject it in STACK we do so with explicit validation feedback explaining where to put the `*` symbol. Therefore, we should try to do the same when supporting input sytax for decimal seprators. Forgiving inference rules + +1. If a student's expression contains neither dots '`.`' or semicolon '`;`' then a question-level (continential/British) option is used to determine the meaning of the '`,`'. +2. If the student's expression contains a '`;`' then any commas are interpreted as decimal separators. +3. If the student's expression contains a '`.`' then any commas are interpreted as list separators. +4. If a student's expression contains both dots '`.`' and semicolon '`;`' then a student cannot use '`,`' without ambiguity. A question-level option is needed to determine the probable meaning. + +To be decided. + +* To what extent do continental teachers accept use of '`.`' as a decimal separator? If _never_ then we probably don't need the "weak" options proposed above. +* Can teachers set this option at the question level or should it respect a site-wide option? (It's a shame Moodle can't set plugin options at the course level, otherwise we'd return to the cascading options which were available in STACK 1.0 some twenty years ago...) +* How can we _easily_ allow teachers to set/override this option for imported materials? + +## Practical implementation in STACK + +Students do not type in expression termination symbols `;`, freeing up this symbol for use in students' input for separating list items. + +Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima syntax, so that numbers are typed in base 10, and the decimal point (`.`) must be used by teachers as the decimal separator._ This simplifies the problem considerably, as input parsing is only required for students' answers. + +1. Mechanism for Maxima to output LaTeX. (Done - but more work needed on testing and question-wide options) +2. Mechanism to output expressions as they should be typed. E.g. "The teacher's answer is \(???\) which can be typed as `???`". +3. Input parsing mechanism for _students' answers only_. + +(Note to self, strings may contain harmless punctuation characters which should not be changed...) + diff --git a/doc/meta_en.json b/doc/meta_en.json index a0c5d56373c..5c4e67a50a3 100644 --- a/doc/meta_en.json +++ b/doc/meta_en.json @@ -795,6 +795,11 @@ "description":"Discussion of future plans for dealing with syntax of answers and STACK." }, { + "file":"Syntax_numbers.md", + "title":"Discussion of support for using comma as the decimal separator", + "description":"Discussion of support for using comma as the decimal separator." + }, + { "file":"Unit_tests.md", "title":"Unit Tests - STACK Documentation", "description":"Information on the three types of unit tests supported by STACK; PHP Unit tests, Maxima unit tests and tests scripts exposed to the question author." diff --git a/stack/cas/security-map.json b/stack/cas/security-map.json index 948a130283b..e64fb03910d 100644 --- a/stack/cas/security-map.json +++ b/stack/cas/security-map.json @@ -7738,6 +7738,10 @@ "contextvariable": "true", "built-in": true }, + "texput_decimal": { + "function": "t", + "contextvariable": "true" + }, "texsub": { "function": "s", "built-in": true diff --git a/stack/maxima/assessment.mac b/stack/maxima/assessment.mac index 309a267fa33..233507012f7 100644 --- a/stack/maxima/assessment.mac +++ b/stack/maxima/assessment.mac @@ -655,11 +655,16 @@ remove_disp(ex) := ev(ex, disp_parens=lambda([ex2], ex2), disp_select=lambda([ex /* Note, displaydp does not do any rounding, it is only display. Use significantfigures. */ /* To print out *values* with trailing decimal places use this function. */ -displaydptex(ex):=block([ss, n, dp], +displaydptex(ex):=block([ss, n, dp, tx], [n, dp]:args(ex), ss:sconcat("~,", string(dp), "f"), if is(equal(dp,0)) then ss:"~d", - ev(printf(false, ss, ev(float(n))), simp) + tx:ev(printf(false, ss, ev(float(n))), simp), + if is(stackfltsep = ",") then ( + tx:ssubst("\\ ", ",", tx), + tx:ssubst(",", ".", tx) + ), + tx ); texput(displaydp, displaydptex); @@ -783,11 +788,16 @@ scientific_notation([a]) := block([oldsimp, x, ex, ex2, ex3, exn], )$ /* displysci is an inert internal function of three arguments. */ -displayscitex(ex):=block([ss, n, dp], +displayscitex(ex):=block([ss, n, dp, tx], [n, dp, expo]:args(ex), ss:sconcat("~,", string(dp), "f \\times 10^{~a}"), if is(equal(dp, 0)) then ss:"~d \\times 10^{~a}", - ev(printf(false, ss, ev(float(n)), expo), simp) + tx:ev(printf(false, ss, ev(float(n)), expo), simp), + if is(stackfltsep = ",") then ( + tx:ssubst("\\ ", ",", tx), + tx:ssubst(",", ".", tx) + ), + tx )$ texput(displaysci, displayscitex)$ diff --git a/stack/maxima/print-comma.lisp b/stack/maxima/print-comma.lisp index 9f1bdd2ffb6..f83dbc98061 100644 --- a/stack/maxima/print-comma.lisp +++ b/stack/maxima/print-comma.lisp @@ -1,5 +1,7 @@ (in-package :cl-user) +(setq stackdecimalsep #\,) + (defun inject-comma (string comma-char comma-interval) (let* ((len (length string)) (offset (mod len comma-interval))) diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac index f2a17c79a07..313fe556a96 100644 --- a/stack/maxima/stackmaxima.mac +++ b/stack/maxima/stackmaxima.mac @@ -160,7 +160,6 @@ load("inequalities.mac"); load("intervals.mac"); load("stackunits.mac"); load("stacktex.lisp"); -load("print-comma.lisp"); load("stackstrings.mac"); load("fboundp.mac"); @@ -749,18 +748,26 @@ stack_disp_strip_dollars(ex) := block( /* Display of numbers. Thanks to Robert Dodier. */ stackintfmt: "~d"; stackfltfmt: "~a"; -?texnumformat(x) := if ev(floatnump(x),simp) then ( - if stackfltfmt = "comma" then - ev(printf_float(false, x, false, false, ",", ".", 3), simp) - else - ev(printf(false, stackfltfmt, x), simp) +stackfltsep: "."; +texput_decimal(ex):= stackfltsep:ex$ + +?texnumformat(x) := block([tx], + if ev(floatnump(x), simp) then block( + tx:ev(printf(false, stackfltfmt, x), simp) ) else if ev(integerp(x),simp) then ( if (is(stackintfmt="~r") or is(stackintfmt="~:r")) then - sconcat("\\mbox{",ev(printf(false, stackintfmt, x), simp),"}") + tx:sconcat("\\mbox{",ev(printf(false, stackintfmt, x), simp),"}") else - ev(printf(false, stackintfmt, x), simp) + tx:ev(printf(false, stackintfmt, x), simp) ) else - string(x); + tx:string(x), + /* We need this separation because validation displays trailing zeros and this is controlled by stackfltfmt. */ + if is(stackfltsep = ",") then ( + tx:ssubst("\\ ", ",", tx), + tx:ssubst(",", ".", tx) + ), + tx +)$ /* Some systems are throwing an error here, which is spurious. */ errcatch(compile(?texnumformat)); diff --git a/stack/maxima/stacktex.lisp b/stack/maxima/stacktex.lisp index 2897aee96b5..ee1cdc9025d 100644 --- a/stack/maxima/stacktex.lisp +++ b/stack/maxima/stacktex.lisp @@ -515,6 +515,6 @@ ;; car of texsym of a matchfix operator is the lead op r (append (list (nth 1 (texsym (caar x)))) r) ;; cdr is the trailing op - x (tex-list (cdr x) nil r (or (nth 2 (texsym (caar x))) (if (string= $stackfltfmt '"comma") '" ; " '" , ")))) + x (tex-list (cdr x) nil r (or (nth 2 (texsym (caar x))) (if (string= $stackfltsep '",") '" ; " '" , ")))) (append l x)) diff --git a/tests/cassession2_test.php b/tests/cassession2_test.php index 00d3f739482..1951c2a1628 100644 --- a/tests/cassession2_test.php +++ b/tests/cassession2_test.php @@ -1213,20 +1213,21 @@ public function test_dispdp() { // Tests in the following form. // 0. Input string. // 1. Number of decimal places. - // 2. Displayed form in LaTeX. - // 3. Value form after rounding. + // 2. Latex displayed form. + // 3. Latex displayed form, contiental comma. + // 4. Value form after rounding. // E.g. dispdp(3.14159,2) -> displaydp(3.14,2). // @codingStandardsIgnoreEnd $tests = array( - array('3.14159', '2', '3.14', '3.14', 'displaydp(3.14,2)'), - array('100', '1', '100.0', '100.0', 'displaydp(100.0,1)', ''), - array('100', '2', '100.00', '100.00', 'displaydp(100.0,2)'), - array('100', '3', '100.000', '100.000', 'displaydp(100.0,3)'), - array('100', '4', '100.0000', '100.0000', 'displaydp(100.0,4)'), - array('100', '5', '100.00000', '100.00000', 'displaydp(100.0,5)'), - array('0.99', '1', '1.0', '1.0', 'displaydp(1.0,1)'), + array('3.14159', '2', '3.14', '3,14', '3.14', 'displaydp(3.14,2)'), + array('100', '1', '100.0', '100,0', '100.0', 'displaydp(100.0,1)', ''), + array('100', '2', '100.00', '100,00', '100.00', 'displaydp(100.0,2)'), + array('100', '3', '100.000', '100,000', '100.000', 'displaydp(100.0,3)'), + array('100', '4', '100.0000', '100,0000', '100.0000', 'displaydp(100.0,4)'), + array('100', '5', '100.00000', '100,00000', '100.00000', 'displaydp(100.0,5)'), + array('0.99', '1', '1.0', '1,0', '1.0', 'displaydp(1.0,1)'), ); foreach ($tests as $key => $c) { @@ -1241,8 +1242,27 @@ public function test_dispdp() { foreach ($tests as $key => $c) { $cs = $at1->get_by_key('p'.$key); $this->assertEquals($c[2], $cs->get_display()); - $this->assertEquals($c[3], $cs->get_dispvalue()); - $this->assertEquals($c[4], $cs->get_value()); + $this->assertEquals($c[4], $cs->get_dispvalue()); + $this->assertEquals($c[5], $cs->get_value()); + } + + // Now check comma output as well. + $s1 = array(stack_ast_container::make_from_teacher_source('stackfltsep:","', '', + new stack_cas_security(), array())); + foreach ($tests as $key => $c) { + $s1[] = stack_ast_container::make_from_teacher_source("p{$key}:dispdp({$c[0]},{$c[1]})", + '', new stack_cas_security(), array()); + } + + $options = new stack_options(); + $options->set_option('simplify', false); + $at1 = new stack_cas_session2($s1, $options, 0); + $at1->instantiate(); + foreach ($tests as $key => $c) { + $cs = $at1->get_by_key('p'.$key); + $this->assertEquals($c[3], $cs->get_display()); + $this->assertEquals($c[4], $cs->get_dispvalue()); + $this->assertEquals($c[5], $cs->get_value()); } } @@ -1407,16 +1427,17 @@ public function test_sf() { // Tests in the following form. // 0. Input string. // 1. Number of significant figures. - // 2. Displayed form. + // 2. Latex displayed form. + // 3. Latex displayed form, contiental comma. // E.g. significantfigures(3.14159,2) -> 3.1. // @codingStandardsIgnoreEnd $tests = array( - array('lg(19)', '4', '1.279'), - array('pi', '4', '3.142'), - array('sqrt(27)', '8', '5.1961524'), - array('-5.985', '3', '-5.99'), + array('lg(19)', '4', '1.279', '1,279'), + array('pi', '4', '3.142', '3,142'), + array('sqrt(27)', '8', '5.1961524', '5,1961524'), + array('-5.985', '3', '-5.99', '-5,99'), ); foreach ($tests as $key => $c) { @@ -1430,9 +1451,25 @@ public function test_sf() { $at1->instantiate(); foreach ($tests as $key => $c) { - $sk = "p{$key}"; $this->assertEquals($c[2], $s1[$key]->get_display()); } + + // Now check comma output as well. + $s1 = array(stack_ast_container::make_from_teacher_source('stackfltsep:","', '', + new stack_cas_security(), array())); + foreach ($tests as $key => $c) { + $s = "p{$key}:significantfigures({$c[0]},{$c[1]})"; + $s1[] = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), array()); + } + + $options = new stack_options(); + $options->set_option('simplify', false); + $at1 = new stack_cas_session2($s1, $options, 0); + $at1->instantiate(); + + foreach ($tests as $key => $c) { + $this->assertEquals($c[3], $s1[$key + 1]->get_display()); + } } public function test_scientific_notation() { @@ -1441,84 +1478,103 @@ public function test_scientific_notation() { // Tests in the following form. // 0. Input string. // 1. Number of significant figures. - // 2. Displayed form. + // 2. Latex displayed form. + // 3. Latex displayed form, contiental comma. // E.g. scientific_notation(314.159,2) -> 3.1\times 10^2. - // 3. Dispvalue form, that is how it should be typed in. - // 4. Value. - // 5. Optional: what happens to the display with simp:true. - // 6. Optional: what happens to the dispvalue form with simp:true. - // 7. Optional: what happens to the value form with simp:true. + // 4. Dispvalue form, that is how it should be typed in. + // 5. Value. + // 6. Optional: what happens to the display with simp:true. + // 7. Optional: what happens to the dispvalue form with simp:true. + // 8. Optional: what happens to the value form with simp:true. // @codingStandardsIgnoreEnd $tests = array( - array('2.998e8', '2', '3.00 \times 10^{8}', '3.00E8', 'displaysci(3,2,8)'), - array('-2.998e8', '2', '-3.00 \times 10^{8}', '-3.00E8', 'displaysci(-3,2,8)'), - array('6.626e-34', '2', '6.63 \times 10^{-34}', '6.63E-34', 'displaysci(6.63,2,-34)'), - array('-6.626e-34', '2', '-6.63 \times 10^{-34}', '-6.63E-34', 'displaysci(-6.63,2,-34)'), - array('6.022e23', '2', '6.02 \times 10^{23}', '6.02E23', 'displaysci(6.02,2,23)'), - array('5.985e30', '2', '5.99 \times 10^{30}', '5.99E30', 'displaysci(5.99,2,30)'), - array('-5.985e30', '2', '-5.99 \times 10^{30}', '-5.99E30', 'displaysci(-5.99,2,30)'), - array('1.6726e-27', '2', '1.67 \times 10^{-27}', '1.67E-27', 'displaysci(1.67,2,-27)'), - array('1e5', '2', '1.00 \times 10^{5}', '1.00E5', 'displaysci(1,2,5)'), - array('1.9e5', '2', '1.90 \times 10^{5}', '1.90E5', 'displaysci(1.9,2,5)'), - array('1.0e9', '2', '1.00 \times 10^{9}', '1.00E9', 'displaysci(1,2,9)'), - array('100000', '2', '1.00 \times 10^{5}', '1.00E5', 'displaysci(1,2,5)'), - array('110000', '2', '1.10 \times 10^{5}', '1.10E5', 'displaysci(1.1,2,5)'), - array('54e3', '2', '5.40 \times 10^{4}', '5.40E4', 'displaysci(5.4,2,4)'), - - array('0.00000000000067452', '2', '6.75 \times 10^{-13}', '6.75E-13', 'displaysci(6.75,2,-13)'), - array('-0.00000000000067452', '2', '-6.75 \times 10^{-13}', '-6.75E-13', 'displaysci(-6.75,2,-13)'), - array('-0.0000000000006', '2', '-6.00 \times 10^{-13}', '-6.00E-13', 'displaysci(-6,2,-13)'), - array('0.0000000000000000000005555', '2', '5.56 \times 10^{-22}', '5.56E-22', 'displaysci(5.56,2,-22)'), - array('0.00000000000000000000055', '2', '5.50 \times 10^{-22}', '5.50E-22', 'displaysci(5.5,2,-22)'), - array('-0.0000000000000000000005555', '2', '-5.56 \times 10^{-22}', '-5.56E-22', 'displaysci(-5.56,2,-22)'), - array('67260000000000000000000000', '2', '6.73 \times 10^{25}', '6.73E25', 'displaysci(6.73,2,25)'), - array('67000000000000000000000000', '2', '6.70 \times 10^{25}', '6.70E25', 'displaysci(6.7,2,25)'), - array('-67260000000000000000000000', '2', '-6.73 \times 10^{25}', '-6.73E25', 'displaysci(-6.73,2,25)'), - array('-67000000000000000000000000', '2', '-6.70 \times 10^{25}', '-6.70E25', 'displaysci(-6.7,2,25)'), - array('0.001', '2', '1.00 \times 10^{-3}', '1.00E-3', 'displaysci(1,2,-3)'), - array('-0.001', '2', '-1.00 \times 10^{-3}', '-1.00E-3', 'displaysci(-1,2,-3)'), - array('10', '2', '1.00 \times 10^{1}', '1.00E1', 'displaysci(1,2,1)'), - array('2', '0', '2 \times 10^{0}', '2E0', 'displaysci(2,0,0)'), - array('300', '0', '3 \times 10^{2}', '3E2', 'displaysci(3,0,2)'), - array('4321.768', '3', '4.322 \times 10^{3}', '4.322E3', 'displaysci(4.322,3,3)'), - array('-53000', '2', '-5.30 \times 10^{4}', '-5.30E4', 'displaysci(-5.3,2,4)'), - array('6720000000', '3', '6.720 \times 10^{9}', '6.720E9', 'displaysci(6.72,3,9)'), - array('6.0221409e23', '4', '6.0221 \times 10^{23}', '6.0221E23', 'displaysci(6.0221,4,23)'), - array('1.6022e-19', '4', '1.6022 \times 10^{-19}', '1.6022E-19', 'displaysci(1.6022,4,-19)'), - array('1.55E8', '2', '1.55 \times 10^{8}', '1.55E8', 'displaysci(1.55,2,8)'), - array('-0.01', '1', '-1.0 \times 10^{-2}', '-1.0E-2', 'displaysci(-1,1,-2)'), - array('-0.00000001', '3', '-1.000 \times 10^{-8}', '-1.000E-8', 'displaysci(-1,3,-8)'), - array('-0.00000001', '1', '-1.0 \times 10^{-8}', '-1.0E-8', 'displaysci(-1,1,-8)'), - array('-0.00000001', '0', '-1 \times 10^{-8}', '-1E-8', 'displaysci(-1,0,-8)'), - array('-1000', '2', '-1.00 \times 10^{3}', '-1.00E3', 'displaysci(-1,2,3)'), - array('31415.927', '3', '3.142 \times 10^{4}', '3.142E4', 'displaysci(3.142,3,4)'), - array('-31415.927', '3', '-3.142 \times 10^{4}', '-3.142E4', 'displaysci(-3.142,3,4)'), - array('155.5', '2', '1.56 \times 10^{2}', '1.56E2', 'displaysci(1.56,2,2)'), - array('15.55', '2', '1.56 \times 10^{1}', '1.56E1', 'displaysci(1.56,2,1)'), - array('777.7', '2', '7.78 \times 10^{2}', '7.78E2', 'displaysci(7.78,2,2)'), - array('775.5', '2', '7.76 \times 10^{2}', '7.76E2', 'displaysci(7.76,2,2)'), - array('775.55', '2', '7.76 \times 10^{2}', '7.76E2', 'displaysci(7.76,2,2)'), - array('0.5555', '2', '5.56 \times 10^{-1}', '5.56E-1', 'displaysci(5.56,2,-1)'), - array('0.05555', '2', '5.56 \times 10^{-2}', '5.56E-2', 'displaysci(5.56,2,-2)'), - array('cos(23*pi/180)', '3', '9.205 \times 10^{-1}', '9.205E-1', 'displaysci(9.205,3,-1)'), - array('9000', '1', '9.0 \times 10^{3}', '9.0E3', 'displaysci(9,1,3)'), - array('8000', '0', '8 \times 10^{3}', '8E3', 'displaysci(8,0,3)'), + array('2.998e8', '2', '3.00 \times 10^{8}', '3,00 \times 10^{8}', '3.00E8', 'displaysci(3,2,8)'), + array('-2.998e8', '2', '-3.00 \times 10^{8}', '-3,00 \times 10^{8}', '-3.00E8', 'displaysci(-3,2,8)'), + array('6.626e-34', '2', '6.63 \times 10^{-34}', '6,63 \times 10^{-34}', '6.63E-34', 'displaysci(6.63,2,-34)'), + array('-6.626e-34', '2', '-6.63 \times 10^{-34}', '-6,63 \times 10^{-34}', '-6.63E-34', + 'displaysci(-6.63,2,-34)'), + array('6.022e23', '2', '6.02 \times 10^{23}', '6,02 \times 10^{23}', '6.02E23', 'displaysci(6.02,2,23)'), + array('5.985e30', '2', '5.99 \times 10^{30}', '5,99 \times 10^{30}', '5.99E30', 'displaysci(5.99,2,30)'), + array('-5.985e30', '2', '-5.99 \times 10^{30}', '-5,99 \times 10^{30}', '-5.99E30', 'displaysci(-5.99,2,30)'), + array('1.6726e-27', '2', '1.67 \times 10^{-27}', '1,67 \times 10^{-27}', '1.67E-27', 'displaysci(1.67,2,-27)'), + array('1e5', '2', '1.00 \times 10^{5}', '1,00 \times 10^{5}', '1.00E5', 'displaysci(1,2,5)'), + array('1.9e5', '2', '1.90 \times 10^{5}', '1,90 \times 10^{5}', '1.90E5', 'displaysci(1.9,2,5)'), + array('1.0e9', '2', '1.00 \times 10^{9}', '1,00 \times 10^{9}', '1.00E9', 'displaysci(1,2,9)'), + array('100000', '2', '1.00 \times 10^{5}', '1,00 \times 10^{5}', '1.00E5', 'displaysci(1,2,5)'), + array('110000', '2', '1.10 \times 10^{5}', '1,10 \times 10^{5}', '1.10E5', 'displaysci(1.1,2,5)'), + array('54e3', '2', '5.40 \times 10^{4}', '5,40 \times 10^{4}', '5.40E4', 'displaysci(5.4,2,4)'), + + array('0.00000000000067452', '2', '6.75 \times 10^{-13}', '6,75 \times 10^{-13}', '6.75E-13', + 'displaysci(6.75,2,-13)'), + array('-0.00000000000067452', '2', '-6.75 \times 10^{-13}', '-6,75 \times 10^{-13}', '-6.75E-13', + 'displaysci(-6.75,2,-13)'), + array('-0.0000000000006', '2', '-6.00 \times 10^{-13}', '-6,00 \times 10^{-13}', '-6.00E-13', + 'displaysci(-6,2,-13)'), + array('0.0000000000000000000005555', '2', '5.56 \times 10^{-22}', '5,56 \times 10^{-22}', '5.56E-22', + 'displaysci(5.56,2,-22)'), + array('0.00000000000000000000055', '2', '5.50 \times 10^{-22}', '5,50 \times 10^{-22}', '5.50E-22', + 'displaysci(5.5,2,-22)'), + array('-0.0000000000000000000005555', '2', '-5.56 \times 10^{-22}', '-5,56 \times 10^{-22}', '-5.56E-22', + 'displaysci(-5.56,2,-22)'), + array('67260000000000000000000000', '2', '6.73 \times 10^{25}', '6,73 \times 10^{25}', '6.73E25', + 'displaysci(6.73,2,25)'), + array('67000000000000000000000000', '2', '6.70 \times 10^{25}', '6,70 \times 10^{25}', '6.70E25', + 'displaysci(6.7,2,25)'), + array('-67260000000000000000000000', '2', '-6.73 \times 10^{25}', '-6,73 \times 10^{25}', '-6.73E25', + 'displaysci(-6.73,2,25)'), + array('-67000000000000000000000000', '2', '-6.70 \times 10^{25}', '-6,70 \times 10^{25}', '-6.70E25', + 'displaysci(-6.7,2,25)'), + + array('0.001', '2', '1.00 \times 10^{-3}', '1,00 \times 10^{-3}', '1.00E-3', 'displaysci(1,2,-3)'), + array('-0.001', '2', '-1.00 \times 10^{-3}', '-1,00 \times 10^{-3}', '-1.00E-3', 'displaysci(-1,2,-3)'), + array('10', '2', '1.00 \times 10^{1}', '1,00 \times 10^{1}', '1.00E1', 'displaysci(1,2,1)'), + array('2', '0', '2 \times 10^{0}', '2 \times 10^{0}', '2E0', 'displaysci(2,0,0)'), + array('300', '0', '3 \times 10^{2}', '3 \times 10^{2}', '3E2', 'displaysci(3,0,2)'), + array('4321.768', '3', '4.322 \times 10^{3}', '4,322 \times 10^{3}', '4.322E3', 'displaysci(4.322,3,3)'), + array('-53000', '2', '-5.30 \times 10^{4}', '-5,30 \times 10^{4}', '-5.30E4', 'displaysci(-5.3,2,4)'), + array('6720000000', '3', '6.720 \times 10^{9}', '6,720 \times 10^{9}', '6.720E9', 'displaysci(6.72,3,9)'), + array('6.0221409e23', '4', '6.0221 \times 10^{23}', '6,0221 \times 10^{23}', '6.0221E23', + 'displaysci(6.0221,4,23)'), + array('1.6022e-19', '4', '1.6022 \times 10^{-19}', '1,6022 \times 10^{-19}', '1.6022E-19', + 'displaysci(1.6022,4,-19)'), + array('1.55E8', '2', '1.55 \times 10^{8}', '1,55 \times 10^{8}', '1.55E8', 'displaysci(1.55,2,8)'), + + array('-0.01', '1', '-1.0 \times 10^{-2}', '-1,0 \times 10^{-2}', '-1.0E-2', 'displaysci(-1,1,-2)'), + array('-0.00000001', '3', '-1.000 \times 10^{-8}', '-1,000 \times 10^{-8}', '-1.000E-8', + 'displaysci(-1,3,-8)'), + array('-0.00000001', '1', '-1.0 \times 10^{-8}', '-1,0 \times 10^{-8}', '-1.0E-8', 'displaysci(-1,1,-8)'), + array('-0.00000001', '0', '-1 \times 10^{-8}', '-1 \times 10^{-8}', '-1E-8', 'displaysci(-1,0,-8)'), + array('-1000', '2', '-1.00 \times 10^{3}', '-1,00 \times 10^{3}', '-1.00E3', 'displaysci(-1,2,3)'), + array('31415.927', '3', '3.142 \times 10^{4}', '3,142 \times 10^{4}', '3.142E4', 'displaysci(3.142,3,4)'), + array('-31415.927', '3', '-3.142 \times 10^{4}', '-3,142 \times 10^{4}', '-3.142E4', 'displaysci(-3.142,3,4)'), + array('155.5', '2', '1.56 \times 10^{2}', '1,56 \times 10^{2}', '1.56E2', 'displaysci(1.56,2,2)'), + array('15.55', '2', '1.56 \times 10^{1}', '1,56 \times 10^{1}', '1.56E1', 'displaysci(1.56,2,1)'), + array('777.7', '2', '7.78 \times 10^{2}', '7,78 \times 10^{2}', '7.78E2', 'displaysci(7.78,2,2)'), + array('775.5', '2', '7.76 \times 10^{2}', '7,76 \times 10^{2}', '7.76E2', 'displaysci(7.76,2,2)'), + array('775.55', '2', '7.76 \times 10^{2}', '7,76 \times 10^{2}', '7.76E2', 'displaysci(7.76,2,2)'), + array('0.5555', '2', '5.56 \times 10^{-1}', '5,56 \times 10^{-1}', '5.56E-1', 'displaysci(5.56,2,-1)'), + array('0.05555', '2', '5.56 \times 10^{-2}', '5,56 \times 10^{-2}', '5.56E-2', 'displaysci(5.56,2,-2)'), + + array('cos(23*pi/180)', '3', '9.205 \times 10^{-1}', '9,205 \times 10^{-1}', '9.205E-1', + 'displaysci(9.205,3,-1)'), + array('9000', '1', '9.0 \times 10^{3}', '9,0 \times 10^{3}', '9.0E3', 'displaysci(9,1,3)'), + array('8000', '0', '8 \times 10^{3}', '8 \times 10^{3}', '8E3', 'displaysci(8,0,3)'), // Edge case. Want these ones to be 1*10^3, not 10.0*10^2. - array('1000', '2', '1.00 \times 10^{3}', '1.00E3', 'displaysci(1,2,3)'), + array('1000', '2', '1.00 \times 10^{3}', '1,00 \times 10^{3}', '1.00E3', 'displaysci(1,2,3)'), // If we don't supply a number of decimal places, then we return a value form. // This is entered as scientific_notation(x). // This is displayed normally (without a \times) and always returns a *float*. // Notice this has different behaviour with simp:true. - array('7000', '', '7.0\cdot 10^3', '7.0*10^3', '7.0*10^3', - '7000.0', '7000.0', '7000.0'), - array('1000', '', '1.0\cdot 10^3', '1.0*10^3', '1.0*10^3', - '1000.0', '1000.0', '1000.0'), - array('-1000', '', '-1.0\cdot 10^3', '-(1.0*10^3)', '(-1.0)*10^3', - '-1000.0', '-1000.0', '-1000.0'), - array('1e50', '', '1.0\cdot 10^{50}', '1.0*10^50', '1.0*10^50', - '1.0E+50', '1.0E+50', '1.0E+50'), + array('7000', '', '7.0\cdot 10^3', '7,0\cdot 10^3', '7.0*10^3', '7.0*10^3', + '7000.0', '7000.0', '7000.0', '7000,0'), + array('1000', '', '1.0\cdot 10^3', '1,0\cdot 10^3', '1.0*10^3', '1.0*10^3', + '1000.0', '1000.0', '1000.0', '1000,0'), + array('-1000', '', '-1.0\cdot 10^3', '-1,0\cdot 10^3', '-(1.0*10^3)', '(-1.0)*10^3', + '-1000.0', '-1000.0', '-1000.0', '-1000,0'), + array('1e50', '', '1.0\cdot 10^{50}', '1,0\cdot 10^{50}', '1.0*10^50', '1.0*10^50', + '1.0E+50', '1.0E+50', '1.0E+50', null), // @codingStandardsIgnoreStart // In some versions of Maxima this comes out as -\frac{1.0}{10^8} with simp:true. @@ -1529,12 +1585,12 @@ public function test_scientific_notation() { // Fail: 37.0, 37.1, 37.2, 37.3. // @codingStandardsIgnoreEnd - array('-0.00000001', '', '-1.0\cdot 10^ {- 8 }', '-(1.0*10^-8)', '(-1.0)*10^-8', - '-1.0E-8', '-1.0E-8', '-1.0E-8'), - array('-0.000000001', '', '-1.0\cdot 10^ {- 9 }', '-(1.0*10^-9)', '(-1.0)*10^-9', - '-1.0E-9', '-1.0E-9', '-1.0E-9'), - array('-0.000000000001', '', '-1.0\cdot 10^ {- 12 }', '-(1.0*10^-12)', '(-1.0)*10^-12', - '-1.0E-12', '-1.0E-12', '-1.0E-12'), + array('-0.00000001', '', '-1.0\cdot 10^ {- 8 }', '-1,0\cdot 10^ {- 8 }', '-(1.0*10^-8)', '(-1.0)*10^-8', + '-1.0E-8', '-1.0E-8', '-1.0E-8', null), + array('-0.000000001', '', '-1.0\cdot 10^ {- 9 }', '-1,0\cdot 10^ {- 9 }', '-(1.0*10^-9)', '(-1.0)*10^-9', + '-1.0E-9', '-1.0E-9', '-1.0E-9', null), + array('-0.000000000001', '', '-1.0\cdot 10^ {- 12 }', '-1,0\cdot 10^ {- 12 }', '-(1.0*10^-12)', '(-1.0)*10^-12', + '-1.0E-12', '-1.0E-12', '-1.0E-12', null), ); $s1 = array(); @@ -1565,11 +1621,10 @@ public function test_scientific_notation() { if ($s1[$key]->is_correctly_evaluated()) { // Turn 1.0E-5 to lower case 1.0e-5. $this->assertEquals($c[2], $this->prepare_actual_maths_floats($s1[$key]->get_display())); - $this->assertEquals($c[3], $this->prepare_actual_maths_floats($s1[$key]->get_dispvalue())); - $this->assertEquals($c[4], $this->prepare_actual_maths_floats($s1[$key]->get_value())); + $this->assertEquals($c[4], $this->prepare_actual_maths_floats($s1[$key]->get_dispvalue())); + $this->assertEquals($c[5], $this->prepare_actual_maths_floats($s1[$key]->get_value())); } else { // Help output which test fails. - $this->assertEquals(null, $c[0]); } } @@ -1594,21 +1649,59 @@ public function test_scientific_notation() { $this->assertEquals('', $at2->get_errors()); foreach ($tests as $key => $c) { $simpdisp = $c[2]; - if (array_key_exists(5, $c)) { - $simpdisp = $c[5]; - } - $dispval = $c[3]; if (array_key_exists(6, $c)) { - $dispval = $c[6]; + $simpdisp = $c[6]; } - $val = $c[4]; + $dispval = $c[4]; if (array_key_exists(7, $c)) { - $val = $c[7]; + $dispval = $c[7]; + } + $val = $c[5]; + if (array_key_exists(8, $c)) { + $val = $c[8]; } $this->assertEquals($simpdisp, $this->prepare_actual_maths_floats($s2[$key]->get_display())); $this->assertEquals($dispval, $this->prepare_actual_maths_floats($s2[$key]->get_dispvalue())); $this->assertEquals($val, $this->prepare_actual_maths_floats($s2[$key]->get_value())); } + + // Now check comma output as well. + $s2 = array(stack_ast_container::make_from_teacher_source('stackfltsep:","', '', + new stack_cas_security(), array())); + foreach ($tests as $key => $c) { + $s = "p{$key}:scientific_notation({$c[0]},{$c[1]})"; + if ($c[1] == '') { + $s = "p{$key}:scientific_notation({$c[0]})"; + } + $s2[] = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), array()); + } + + $options = new stack_options(); + $options->set_option('simplify', true); + $at2 = new stack_cas_session2($s2, $options, 0); + $at2->instantiate(); + + $this->assertEquals('', $at2->get_errors()); + foreach ($tests as $key => $c) { + $simpdisp = $c[3]; + if (array_key_exists(6, $c)) { + $simpdisp = $c[9]; + } + $dispval = $c[4]; + if (array_key_exists(7, $c)) { + $dispval = $c[7]; + } + $val = $c[5]; + if (array_key_exists(8, $c)) { + $val = $c[8]; + } + // TODO: update prepare_actual_maths_floats for comma separated values. + if ($simpdisp !== null) { + $this->assertEquals($simpdisp, $this->prepare_actual_maths_floats($s2[$key + 1]->get_display())); + } + $this->assertEquals($dispval, $this->prepare_actual_maths_floats($s2[$key + 1]->get_dispvalue())); + $this->assertEquals($val, $this->prepare_actual_maths_floats($s2[$key + 1]->get_value())); + } } public function test_pm_simp_false() { @@ -1634,8 +1727,8 @@ public function test_pm_simp_false() { $at1->instantiate(); $this->assertEquals('a#pm#b', $s1[0]->get_value()); - $this->assertEquals('{a \pm b}', $s1[0]->get_display()); - $this->assertEquals('x = (-b#pm#sqrt(b^2-4*a*c))/(2*a)', $s1[1]->get_value()); + $this->assertEquals('{a \pm b}', $s1[0]->get_display()); + $this->assertEquals('x = (-b#pm#sqrt(b^2-4*a*c))/(2*a)', $s1[1]->get_value()); $this->assertEquals('x=\frac{{-b \pm \sqrt{b^2-4\cdot a\cdot c}}}{2\cdot a}', $s1[1]->get_display()); $this->assertEquals('b#pm#a^2', $s1[2]->get_value()); $this->assertEquals('{b \pm a^2}', $s1[2]->get_display()); diff --git a/tests/castext_test.php b/tests/castext_test.php index 6d3a1d73395..18c598b45f0 100644 --- a/tests/castext_test.php +++ b/tests/castext_test.php @@ -1321,7 +1321,7 @@ public function test_numerical_display_commas() { $st = 'The number {@3.1415@} is written with commas. '; $st .= 'Sets {@{1.2, 4, 5, 3.123}@} and lists {@[1.2, 4, 5, 3.123]@}'; - $a2 = array('stackfltfmt:"comma"'); + $a2 = array('stackfltsep:","'); $s2 = array(); foreach ($a2 as $s) { $s2[] = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), array()); From 556578ad1f907b4e06ddb4366d4bd4ea06f0bba4 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Mon, 10 Jul 2023 09:37:43 +0100 Subject: [PATCH 04/41] Update Syntax_numbers.md Edit docs. --- doc/en/Developer/Syntax_numbers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index 588aa1178fb..4f688b2e0ef 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -38,7 +38,7 @@ It is reasonable to expect students to be consistent in their use of the '`,`' w Therefore students cannot use all of '`.`', '`,`' and '`;`' in a single expression without inconsistency. In the current STACK design student input of `1,23` would be invalid and generate an error: "A comma in your expression appears in a strange way." Many users will wish to retain this behaviour. Therefore although this expression is not ambiguous, in a British context it does not follow common usage and could well indicate a misunderstanding about how to type in sets, lists, coordinates functions etc. -A similar problem occurs in a continental context where `1;23` contains an unencapsulated list separation. This expression is not ambiguous and a similar error message such as "A in your expression appears in a strange way." would be similarly helpful. +A similar problem occurs in a continental context where `1;23` contains an unencapsulated list separation. This expression is not ambiguous and a similar error message such as "A semicolon in your expression appears in a strange way." would be similarly helpful. Examples. From d92cbcb252cd96eb17c48db87ebc858002b4bce5 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Mon, 10 Jul 2023 09:55:07 +0100 Subject: [PATCH 05/41] Update Syntax_numbers.md Add in very helpful comments on how this issue is solved in NUMBAS. --- doc/en/Developer/Syntax_numbers.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index 4f688b2e0ef..5be44f04ad7 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -26,6 +26,8 @@ These standards to not provide advice on how to separate items, e.g. in lists, a 1. A set containing the single number six fifths, \(\frac{6}{5}\). 2. A set containing the two integers one and two. +There is also a discussion of [number styles in the NUMBAS system](https://docs.numbas.org.uk/en/latest/number-notation.html). + ## Design of syntax for decimal separators The only opportunity for ambiguity arises in the use of a comma '`,`' in an expression, which could be a decimal separator or a list separator. @@ -95,3 +97,6 @@ Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima sy (Note to self, strings may contain harmless punctuation characters which should not be changed...) +## Practial implementation in other software + +1. NUMBAS also uses the semicolon to separate list items, as discussed in [NUMBAS issue 889](https://github.com/numbas/Numbas/issues/889) From 0d8630006d8a95c434165bf86221508318b2f239 Mon Sep 17 00:00:00 2001 From: Peter Mayer Date: Mon, 10 Jul 2023 13:36:02 +0200 Subject: [PATCH 06/41] Comments on handling decimal separation --- doc/en/Developer/Syntax_numbers.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index 5be44f04ad7..1f043763348 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -100,3 +100,14 @@ Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima sy ## Practial implementation in other software 1. NUMBAS also uses the semicolon to separate list items, as discussed in [NUMBAS issue 889](https://github.com/numbas/Numbas/issues/889) + + +### Comments from Peter Mayer: +In the school context, it is almost exclusively common in German-speaking countries to use the "," as a decimal separator. In contrast, I have never encountered a "." as a decimal separator. the "." is usually used as a thousands separator: 1002 = 1.002 and can also be used in conjunction with a comma: 1002,54 = 1.002,54. A ";" is usually used in schools only in geometry as an alternative to A(4|5): A(4;5). + +As a suggestion, I would like to point out the behavior of Microsoft Excel at this point here, in the German version, the "," and "." are also used according to my comment from above. In formulas, the individual arguments are separated by a ";". +If you switch to the English version, however, thousands are separated by "," and decimal numbers by "." as well as the parameters of functions by ",". Maybe it is advisable to approach this behavior, because there could be synnergies. + + +***As a silver bullet, however, I would suggest the following:*** +In Moodle there is a method (unformat_float lib/moodlelib.php:8880) that converts local numbers entered by the user into a standard-compliant number, which can then also be stored in the DB. Depending on the viewer, this can then be output again in the respective local representation (format_float; lib/moodlelib.php:8847) of the viewer. The advantage would be that thereby no special cases must be considered but, everything can be kept as before. Only the user input and output has to be converted accordingly, and moodle does that itself. From 963f9d9583e4ad513ba3f846f475745d6e2a26e0 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Mon, 10 Jul 2023 12:45:10 +0100 Subject: [PATCH 07/41] Update Syntax_numbers.md Fix typo. --- doc/en/Developer/Syntax_numbers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index 5be44f04ad7..8a5b925d6ef 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -2,7 +2,7 @@ ___This is a proposal for discussion as of 10 July 2023.___ -This document discusses entry of numbers into STACK. This discussion will also be relevant to other online assessment systems and technology more generally. When we type in a string of symbols into a computer there is a context and assumptions which arise from that context. For example is `e` the base of the natural logarithms or is `e` to be interpreted as part of a floating point number, e.g. `6.6263−34`? There are two related issues. +This document discusses entry of numbers into STACK. This discussion will also be relevant to other online assessment systems and technology more generally. When we type in a string of symbols into a computer there is a context and assumptions which arise from that context. For example is `e` the base of the natural logarithms or is `e` to be interpreted as part of a floating point number, e.g. `6.6263e−34`? There are two related issues. * Which symbol to use as the decimal separator, '`,`' or '`.`'? * Support for number bases (other than decimal). From 84266d847de35c7c5275f600d1a8fe01f0349286 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Mon, 10 Jul 2023 13:54:42 +0100 Subject: [PATCH 08/41] Update Syntax_numbers.md Update the docs. --- doc/en/Developer/Syntax_numbers.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index af631d3717c..c8e3f062f94 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -101,13 +101,11 @@ Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima sy 1. NUMBAS also uses the semicolon to separate list items, as discussed in [NUMBAS issue 889](https://github.com/numbas/Numbas/issues/889) - ### Comments from Peter Mayer: -In the school context, it is almost exclusively common in German-speaking countries to use the "," as a decimal separator. In contrast, I have never encountered a "." as a decimal separator. the "." is usually used as a thousands separator: 1002 = 1.002 and can also be used in conjunction with a comma: 1002,54 = 1.002,54. A ";" is usually used in schools only in geometry as an alternative to A(4|5): A(4;5). -As a suggestion, I would like to point out the behavior of Microsoft Excel at this point here, in the German version, the "," and "." are also used according to my comment from above. In formulas, the individual arguments are separated by a ";". -If you switch to the English version, however, thousands are separated by "," and decimal numbers by "." as well as the parameters of functions by ",". Maybe it is advisable to approach this behavior, because there could be synnergies. +In the school context, it is almost exclusively common in German-speaking countries to use the '`,`' as a decimal separator. In contrast, I have never encountered a '`.`' as a decimal separator. The '`.`' is usually used as a thousands separator: \(1002 = 1.002\) and can also be used in conjunction with a comma: \(1002,54 = 1.002,54\). A '`;`' is usually used in schools only in geometry as an alternative to \(A(4|5)\): \(A(4;5)\). +As a suggestion, I would like to point out the behavior of Microsoft Excel. In the German version, the '`,`' and '`.`' are also used according to my comment above. In formulas, the individual arguments are separated by a '`;`'. If you switch to the English version, however, thousands are separated by '`,`' and decimal numbers by '`.`' as well as the parameters of functions by '`,`'. Maybe it is advisable to approach this behavior, because there could be synnergies. ***As a silver bullet, however, I would suggest the following:*** -In Moodle there is a method (unformat_float lib/moodlelib.php:8880) that converts local numbers entered by the user into a standard-compliant number, which can then also be stored in the DB. Depending on the viewer, this can then be output again in the respective local representation (format_float; lib/moodlelib.php:8847) of the viewer. The advantage would be that thereby no special cases must be considered but, everything can be kept as before. Only the user input and output has to be converted accordingly, and moodle does that itself. +In Moodle there is a method (unformat_float lib/moodlelib.php:8880) that converts local numbers entered by the user into a standard-compliant number, which can then also be stored in the DB. Depending on the viewer, this can then be output again in the respective local representation ([format_float; lib/moodlelib.php](https://github.com/moodle/moodle/blob/master/lib/moodlelib.php#L8830)) of the viewer. The advantage would be that thereby no special cases must be considered but, everything can be kept as before. Only the user input and output has to be converted accordingly, and moodle does that itself. From bd25089f52cd7fac9e60dbe737e29806f52cc009 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Thu, 13 Jul 2023 02:28:07 +0100 Subject: [PATCH 09/41] Add a question level "decimals" option, and use this in sat string generation for teacher's answers. --- doc/en/Authoring/Inputs.md | 2 ++ doc/en/CAS/Numbers.md | 6 +--- doc/en/Developer/Development_track.md | 4 ++- doc/en/Developer/Syntax_numbers.md | 3 +- stack/cas/ast.container.silent.class.php | 17 +++++++++-- stack/input/algebraic/algebraic.class.php | 2 +- stack/input/equiv/equiv.class.php | 2 +- stack/input/inputbase.class.php | 13 +++++++- stack/input/matrix/matrix.class.php | 6 ++-- stack/input/numerical/numerical.class.php | 3 ++ stack/input/textarea/textarea.class.php | 2 +- stack/input/units/units.class.php | 3 ++ stack/input/varmatrix/varmatrix.class.php | 7 +++-- stack/maximaparser/MP_classes.php | 37 +++++++++++++++++++---- stack/options.class.php | 9 ++++++ tests/answertest_general_cas_test.php | 23 ++++++++++++-- tests/input_algebraic_test.php | 20 ++++++++++++ tests/input_matrix_test.php | 24 +++++++++++++++ tests/input_units_test.php | 18 +++++++++++ tests/input_varmatrix_test.php | 22 +++++++++++--- 20 files changed, 190 insertions(+), 33 deletions(-) diff --git a/doc/en/Authoring/Inputs.md b/doc/en/Authoring/Inputs.md index 5db98fc8e50..0749a657c44 100644 --- a/doc/en/Authoring/Inputs.md +++ b/doc/en/Authoring/Inputs.md @@ -76,6 +76,8 @@ We cannot use the `EMPTYANSWER` tag for the teacher's answer with the matrix inp ta:transpose(matrix([null,null,null])); +The shape of the parentheses surrounding the brackets is taken from the question level options, except matrix inputs cannont display curly brackets `{`. (If you can create CSS to do this, please contact the developers!) + #### Text area #### This input allows the user to type in multiple lines, where each line must be a valid algebraic expression. STACK passes the result to [Maxima](../CAS/Maxima.md) as a list. Note, the teacher's answer and any syntax hint must be a list, of valid Maxima exprssions! If you just pass in an expression strange behaviour may result. diff --git a/doc/en/CAS/Numbers.md b/doc/en/CAS/Numbers.md index de8b33de8b5..75b65868212 100644 --- a/doc/en/CAS/Numbers.md +++ b/doc/en/CAS/Numbers.md @@ -123,11 +123,7 @@ Maxima has a separate system for controlling the number of decimal digits used i ## Changing the decimal separator, e.g. using a comma for separating decimals ## -STACK now supports a mechanism for changing the decimal separator and using a comma for separating decimals. Using the following in the question variables will print floating point numbers with a comma instead of the decimal point throughout the question, including students' inputs - - texput_decimal(","); - -For finer control in other parts of the question, just set the variable +STACK now supports a mechanism for changing the decimal separator and using a comma for separating decimals. A question level option can be used to choose `,` or `.` as the decimal separator. For finer control in other parts of the question, just set the variable stackfltsep:","; diff --git a/doc/en/Developer/Development_track.md b/doc/en/Developer/Development_track.md index e35915b006f..969bbe7fac7 100644 --- a/doc/en/Developer/Development_track.md +++ b/doc/en/Developer/Development_track.md @@ -14,11 +14,13 @@ DONE 4. Add in the `[[todo]]` [question block](../Authoring/Question_blocks/Static_blocks.md). 5. Caschat page now saves question variables and general feedback back into the question. Fixes issue #984. 6. Confirm support for Maxima 5.46.0 and 5.47.0. +7. Shape of brackets surrounding matrix/var matrix input types now matches question level option for matrix parentheses. (TODO: possible option to change shape at the input level?) Support the use of a [comma as the decimal separator](Syntax_numbers.md) 1. (Done) Mechanism for Maxima to output LaTeX. -2. Mechanism to output expressions as they should be typed. +2. (Done) Mechanism to output expressions as they should be typed. 3. Input parsing mechanism. +4. (Done) Question wide option for decimal separator added. TODO: how to give access to this? TODO: diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index c8e3f062f94..9ec841e0d49 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -92,7 +92,7 @@ Students do not type in expression termination symbols `;`, freeing up this symb Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima syntax, so that numbers are typed in base 10, and the decimal point (`.`) must be used by teachers as the decimal separator._ This simplifies the problem considerably, as input parsing is only required for students' answers. 1. Mechanism for Maxima to output LaTeX. (Done - but more work needed on testing and question-wide options) -2. Mechanism to output expressions as they should be typed. E.g. "The teacher's answer is \(???\) which can be typed as `???`". +2. Mechanism to output expressions as they should be typed. E.g. "The teacher's answer is \(???\) which can be typed as `???`". (Done - but more work needed on testing and question-wide options) 3. Input parsing mechanism for _students' answers only_. (Note to self, strings may contain harmless punctuation characters which should not be changed...) @@ -108,4 +108,5 @@ In the school context, it is almost exclusively common in German-speaking countr As a suggestion, I would like to point out the behavior of Microsoft Excel. In the German version, the '`,`' and '`.`' are also used according to my comment above. In formulas, the individual arguments are separated by a '`;`'. If you switch to the English version, however, thousands are separated by '`,`' and decimal numbers by '`.`' as well as the parameters of functions by '`,`'. Maybe it is advisable to approach this behavior, because there could be synnergies. ***As a silver bullet, however, I would suggest the following:*** + In Moodle there is a method (unformat_float lib/moodlelib.php:8880) that converts local numbers entered by the user into a standard-compliant number, which can then also be stored in the DB. Depending on the viewer, this can then be output again in the respective local representation ([format_float; lib/moodlelib.php](https://github.com/moodle/moodle/blob/master/lib/moodlelib.php#L8830)) of the viewer. The advantage would be that thereby no special cases must be considered but, everything can be kept as before. Only the user input and output has to be converted accordingly, and moodle does that itself. diff --git a/stack/cas/ast.container.silent.class.php b/stack/cas/ast.container.silent.class.php index ef4d37597ba..4df843b9985 100644 --- a/stack/cas/ast.container.silent.class.php +++ b/stack/cas/ast.container.silent.class.php @@ -313,10 +313,21 @@ public function get_evaluationform(): string { } // This returns the fully filtered AST as it should be inputted were it inputted perfectly. - public function get_inputform(bool $keyless = false, $nounify = null, $nontuples = false): string { + public function get_inputform(bool $keyless = false, $nounify = null, $nontuples = false, + $decimals = '.'): string { if (!($nounify === null || is_int($nounify))) { throw new stack_exception('stack_ast_container: nounify must be null or an integer.'); } + if (!($decimals === '.' || $decimals === ',')) { + throw new stack_exception('stack_ast_container: decimal option must be "." or ",".'); + } + $decimal = '.'; + $listsep = ','; + if ($decimals == ',') { + $decimal = ','; + $listsep = ';'; + } + $params = array('inputform' => true, 'qmchar' => true, 'pmchar' => 0, @@ -324,7 +335,9 @@ public function get_inputform(bool $keyless = false, $nounify = null, $nontuples 'keyless' => $keyless, 'dealias' => false, // This is needed to stop pi->%pi etc. 'nounify' => $nounify, - 'nontuples' => $nontuples + 'nontuples' => $nontuples, + 'decimal' => $decimal, + 'listsep' => $listsep ); return $this->ast_to_string($this->ast, $params); } diff --git a/stack/input/algebraic/algebraic.class.php b/stack/input/algebraic/algebraic.class.php index 3e0e07e2f0b..00cf30a72ab 100644 --- a/stack/input/algebraic/algebraic.class.php +++ b/stack/input/algebraic/algebraic.class.php @@ -129,7 +129,7 @@ public function get_teacher_answer_display($value, $display) { } $cs = stack_ast_container::make_from_teacher_source($value, '', new stack_cas_security()); $cs->set_nounify(0); - $value = $cs->get_inputform(true, 0, true); + $value = $cs->get_inputform(true, 0, true, $this->options->get_option('decimals')); return stack_string('teacheranswershow', array('value' => ''.$value.'', 'display' => $display)); } } diff --git a/stack/input/equiv/equiv.class.php b/stack/input/equiv/equiv.class.php index 44531bf41bb..4363ec75cc1 100644 --- a/stack/input/equiv/equiv.class.php +++ b/stack/input/equiv/equiv.class.php @@ -440,7 +440,7 @@ public function get_teacher_answer_display($value, $display) { if (trim($val) !== '' ) { $cs = stack_ast_container::make_from_teacher_source($val); $cs->get_valid(); - $val = ''.$cs->get_inputform(true, 0, true).''; + $val = ''.$cs->get_inputform(true, 0, true, $this->options->get_option('decimals')).''; } $values[$key] = $val; } diff --git a/stack/input/inputbase.class.php b/stack/input/inputbase.class.php index af7c338f23b..1418b06afdc 100644 --- a/stack/input/inputbase.class.php +++ b/stack/input/inputbase.class.php @@ -150,6 +150,9 @@ public function __construct($name, $teacheranswer, $options = null, $parameters throw new stack_exception('stack_input: $options must be stack_options.'); } $this->options = $options; + if ($this->options === null) { + $this->options = new stack_options(); + } if (!(null === $parameters || is_array($parameters))) { throw new stack_exception('stack_input: __construct: 3rd argumenr, $parameters, ' . @@ -1392,6 +1395,12 @@ public function get_correct_response($value) { $cs->set_nounify(0); $val = ''; + $decimal = '.'; + $listsep = ','; + if ($this->options->get_option('decimals') === ',') { + $decimal = ','; + $listsep = ';'; + } $params = array('checkinggroup' => true, 'qmchar' => false, 'pmchar' => 1, @@ -1399,7 +1408,9 @@ public function get_correct_response($value) { 'keyless' => true, 'dealias' => false, // This is needed to stop pi->%pi etc. 'nounify' => 0, - 'nontuples' => false + 'nontuples' => false, + 'decimal' => $decimal, + 'listsep' => $listsep ); if ($cs->get_valid()) { $value = $cs->ast_to_string(null, $params); diff --git a/stack/input/matrix/matrix.class.php b/stack/input/matrix/matrix.class.php index ea999178802..03b6ef34ef4 100644 --- a/stack/input/matrix/matrix.class.php +++ b/stack/input/matrix/matrix.class.php @@ -256,10 +256,10 @@ public function render(stack_input_state $state, $fieldname, $readonly, $tavalue } // Read matrix bracket style from options. - $matrixbrackets = 'matrixroundbrackets'; + $matrixbrackets = 'matrixsquarebrackets'; $matrixparens = $this->options->get_option('matrixparens'); - if ($matrixparens == '[') { - $matrixbrackets = 'matrixsquarebrackets'; + if ($matrixparens == '(') { + $matrixbrackets = 'matrixroundbrackets'; } else if ($matrixparens == '|') { $matrixbrackets = 'matrixbarbrackets'; } else if ($matrixparens == '') { diff --git a/stack/input/numerical/numerical.class.php b/stack/input/numerical/numerical.class.php index 666242bba85..a24591ea096 100644 --- a/stack/input/numerical/numerical.class.php +++ b/stack/input/numerical/numerical.class.php @@ -144,6 +144,9 @@ public function get_teacher_answer_display($value, $display) { if (trim($value) == 'EMPTYANSWER') { return stack_string('teacheranswerempty'); } + $cs = stack_ast_container::make_from_teacher_source($value, '', new stack_cas_security()); + $cs->set_nounify(0); + $value = $cs->get_inputform(true, 0, true, $this->options->get_option('decimals')); return stack_string('teacheranswershow', array('value' => ''.$value.'', 'display' => $display)); } } diff --git a/stack/input/textarea/textarea.class.php b/stack/input/textarea/textarea.class.php index 95643264014..05197538e49 100644 --- a/stack/input/textarea/textarea.class.php +++ b/stack/input/textarea/textarea.class.php @@ -292,7 +292,7 @@ public function get_teacher_answer_display($value, $display) { if (trim($val) !== '' ) { $cs = stack_ast_container::make_from_teacher_source($val); $cs->get_valid(); - $val = ''.$cs->get_inputform(true, 0, true).''; + $val = ''.$cs->get_inputform(true, 0, true, $this->options->get_option('decimals')).''; } $values[$key] = $val; } diff --git a/stack/input/units/units.class.php b/stack/input/units/units.class.php index 247ea1ae2ea..a3ddb796253 100644 --- a/stack/input/units/units.class.php +++ b/stack/input/units/units.class.php @@ -154,6 +154,9 @@ public function get_teacher_answer_display($value, $display) { if (trim($value) == 'EMPTYANSWER') { return stack_string('teacheranswerempty'); } + $cs = stack_ast_container::make_from_teacher_source($value, '', new stack_cas_security()); + $cs->set_nounify(0); + $value = $cs->get_inputform(true, 0, true, $this->options->get_option('decimals')); return stack_string('teacheranswershow', array('value' => ''.$value.'', 'display' => $display)); } diff --git a/stack/input/varmatrix/varmatrix.class.php b/stack/input/varmatrix/varmatrix.class.php index 2608766062c..d7cb8bb59cd 100644 --- a/stack/input/varmatrix/varmatrix.class.php +++ b/stack/input/varmatrix/varmatrix.class.php @@ -96,11 +96,12 @@ public function render(stack_input_state $state, $fieldname, $readonly, $tavalue } // Read matrix bracket style from options. - $matrixbrackets = 'matrixroundbrackets'; + // The default brackets for matrices are square in options. + $matrixbrackets = 'matrixsquarebrackets'; if ($this->options) { $matrixparens = $this->options->get_option('matrixparens'); - if ($matrixparens == '[') { - $matrixbrackets = 'matrixsquarebrackets'; + if ($matrixparens == '(') { + $matrixbrackets = 'matrixroundbrackets'; } else if ($matrixparens == '|') { $matrixbrackets = 'matrixbarbrackets'; } else if ($matrixparens == '') { diff --git a/stack/maximaparser/MP_classes.php b/stack/maximaparser/MP_classes.php index 6c78b495d9c..ab6f98b0677 100644 --- a/stack/maximaparser/MP_classes.php +++ b/stack/maximaparser/MP_classes.php @@ -22,6 +22,7 @@ * The end of the file contains functions the parser uses... * * The function toString should return something which is completely correct in Maxima. + * Some of the paramters change the Maxima sytnax slightly. * Known parameter values for toString. * * 'pretty' Used for debug pretty-printing of the statement. @@ -37,6 +38,8 @@ * 'dealias' If defined unpacks potential aliases. * 'qmchar' If defined and true prints question marks directly if present as QMCHAR. * 'pmchar' If defined prints +- marks directly if present as #pm#. + * 'decimal' If null then '.' else use the string value. + * 'listsep' If null then ', ' else use the string value. * 'flattree' Used for debugging of the internals. Does not print checking groups by design. */ @@ -716,13 +719,20 @@ public function toString($params = null): string { } if ($this->raw !== null) { - return strtoupper($this->raw); + $value = strtoupper('' . $this->raw); + if ($params !== null && isset($params['decimal'])) { + $value = str_replace('.', $params['decimal'], $value); + } + return $value; } else if ($this->value === null) { // This is a special output case for type-inference caching. return 'stack_unknown_float'; } - - return strtoupper('' . $this->value); + $value = strtoupper('' . $this->value); + if ($params !== null && isset($params['decimal'])) { + $value = str_replace('.', $params['decimal'], $value); + } + return $value; } } @@ -1049,6 +1059,10 @@ public function remap_position_data(int $offset=0) { public function toString($params = null): string { $n = $this->name->toString($params); + $sep = ','; + if ($params !== null && isset($params['listsep'])) { + $sep = $params['listsep']; + } if ($params !== null && isset($params['dealias'])) { $feat = null; @@ -1132,6 +1146,7 @@ public function toString($params = null): string { } if ($params !== null && isset($params['flattree'])) { + // Flattree does not use continental commas here. return '([FunctionCall: ' . $n .'] ' . implode(',', $ar) . ')'; } @@ -1153,11 +1168,11 @@ public function toString($params = null): string { // TODO: fix parsing of let. return $prefix .' '. implode('=', $ar); } - return $prefix . implode(',', $ar); + return $prefix . implode($sep, $ar); } } - return $n . '(' . implode(',', $ar) . ')'; + return $n . '(' . implode($sep, $ar) . ')'; } // Covenience functions that work only after $parentnode has been filled in. public function is_definition(): bool { @@ -1329,6 +1344,11 @@ public function remap_position_data(int $offset=0) { } public function toString($params = null): string { + $sep = ','; + if ($params !== null && isset($params['listsep'])) { + $sep = $params['listsep']; + } + $indent = ''; if ($params !== null && isset($params['pretty'])) { if (is_integer($params['pretty'])) { @@ -1365,7 +1385,7 @@ public function toString($params = null): string { return $indent . '{' . implode(', ', $ar) . '}'; } - return '{' . implode(',', $ar) . '}'; + return '{' . implode($sep, $ar) . '}'; } public function replace($node, $with) { @@ -1417,6 +1437,11 @@ public function remap_position_data(int $offset=0) { } public function toString($params = null): string { + $sep = ','; + if ($params !== null && isset($params['listsep'])) { + $sep = $params['listsep']; + } + $indent = ''; if ($params !== null && isset($params['pretty'])) { if (is_integer($params['pretty'])) { diff --git a/stack/options.class.php b/stack/options.class.php index c3d2387ad44..27f682ed891 100644 --- a/stack/options.class.php +++ b/stack/options.class.php @@ -37,6 +37,15 @@ public function __construct($settings = array()) { 'caskey' => 'OPT_OUTPUT', 'castype' => 'string', ), + // Currently no way for users to set this option. + 'decimals' => array( + 'type' => 'list', + 'value' => '.', + 'strict' => true, + 'values' => array('.', ','), + 'caskey' => 'texput_decimal', + 'castype' => 'fun', + ), 'multiplicationsign' => array( 'type' => 'list', 'value' => 'dot', diff --git a/tests/answertest_general_cas_test.php b/tests/answertest_general_cas_test.php index 290517cfb69..c99207762f3 100644 --- a/tests/answertest_general_cas_test.php +++ b/tests/answertest_general_cas_test.php @@ -21,7 +21,7 @@ use stack_answertest_general_cas; use stack_ast_container; use stack_cas_security; - +use stack_options; defined('MOODLE_INTERNAL') || die(); @@ -30,10 +30,11 @@ // @copyright 2012 The University of Birmingham. // @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. -require_once(__DIR__ . '/../stack/answertest/controller.class.php'); require_once(__DIR__ . '/fixtures/test_base.php'); -require_once(__DIR__ . '/../stack/answertest/at_general_cas.class.php'); require_once(__DIR__ . '/../locallib.php'); +require_once(__DIR__ . '/../stack/options.class.php'); +require_once(__DIR__ . '/../stack/answertest/controller.class.php'); +require_once(__DIR__ . '/../stack/answertest/at_general_cas.class.php'); /** * @group qtype_stack @@ -428,6 +429,22 @@ public function test_stack_maxima_translate_algequiv_list() { $this->assert_content_with_maths_equals($fbt, stack_maxima_translate($at->get_at_feedback())); } + public function test_stack_maxima_translate_algequiv_list_decimals() { + // This test points out which element in the list is incorrect. + $options = new stack_options(); + $options->set_option('decimals', ','); + $at = $this->stack_answertest_general_cas_builder('[x^2,x^2,x^4]', '[x^2,x^3,x^4]', 'AlgEquiv', '', $options); + $this->assertFalse($at->do_test()); + $this->assertEquals(0, $at->get_at_mark()); + + $fb = 'stack_trans(\'ATList_wrongentries\' , !quot!\[\left[ x^2 ; {\color{red}{\underline{x^2}}} ; x^4 \right] \]!quot! );'; + $this->assertEquals(stack_maxima_translate($fb), $at->get_at_feedback()); + + $fbt = 'The entries underlined in red below are those that are incorrect. ' . + '\[\left[ x^2 ; {\color{red}{\underline{x^2}}} ; x^4 \right] \]'; + $this->assert_content_with_maths_equals($fbt, stack_maxima_translate($at->get_at_feedback())); + } + public function test_stack_maxima_translate_algequiv_matrix() { // Matrices have newline characters in them. $at = $this->stack_answertest_general_cas_builder('matrix([1,2],[2,4])', 'matrix([1,2],[3,4])', 'AlgEquiv'); diff --git a/tests/input_algebraic_test.php b/tests/input_algebraic_test.php index 606f5f3a7da..401757abbe9 100644 --- a/tests/input_algebraic_test.php +++ b/tests/input_algebraic_test.php @@ -1693,4 +1693,24 @@ public function test_validate_student_response_conjugate() { $this->assertEquals($displayed, $state->contentsdisplayed); $this->assertEquals('\( \left[ x \right]\) ', $state->lvars); } + + public function test_decimal_output_1() { + $options = new stack_options(); + $options->set_option('decimals', ','); + $el = stack_input_factory::make('algebraic', 'state', '{3.1415,2.71}', $options); + $el->set_parameter('forbidFloats', false); + + // TODO: this should fail when we update the student input parsing! + $state = $el->validate_student_response(array('state' => '{3.1415,2.71}'), $options, '{3.1415,2.71}', + new stack_cas_security()); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('{3.1415,2.71}', $state->contentsmodified); + $this->assertEquals('\[ \left \{3,1415 ; 2,7100 \right \} \]', $state->contentsdisplayed); + $this->assertEquals('', + $state->errors); + $this->assertEquals('A correct answer is ' + . '\( \{3,1415 ; 2,7100 \right \} \), which can be typed in as follows: ' + . '{3,1415;2,71}', + $el->get_teacher_answer_display($state->contentsmodified, '\{3,1415 ; 2,7100 \right \}')); + } } diff --git a/tests/input_matrix_test.php b/tests/input_matrix_test.php index e8dce9c542b..4270b7dc84f 100644 --- a/tests/input_matrix_test.php +++ b/tests/input_matrix_test.php @@ -120,6 +120,29 @@ public function test_render_syntax_hint() { 'ans1', false, null)); } + public function test_render_syntax_hint_round() { + $options = new stack_options(); + $options->set_option('matrixparens', '('); + $el = stack_input_factory::make('matrix', 'ans1', 'M', $options); + $el->set_parameter('syntaxHint', 'matrix([a,b],[?,d])'); + $el->adapt_to_model_answer('matrix([1,0],[0,1])'); + $this->assertEquals('
' . + '' . + '' . + '' . + '' . + '' . + '' . + '
  
  
', + $el->render(new stack_input_state(stack_input::VALID, array(), '', '', '', '', ''), + 'ans1', false, null)); + } + public function test_render_syntax_hint_placeholder() { $options = new stack_options(); $el = stack_input_factory::make('matrix', 'ans1', 'M', $options); @@ -487,4 +510,5 @@ public function test_validate_consolidatesubscripts() { $this->assertEquals('\( \left[ a_{1} , a_{2} , {\it abc}_{123} , {\it abc}_{45} \right]\) ', $state->lvars); } + } diff --git a/tests/input_units_test.php b/tests/input_units_test.php index 64a2e7e325b..30b9a097350 100644 --- a/tests/input_units_test.php +++ b/tests/input_units_test.php @@ -592,6 +592,24 @@ public function test_validate_student_response_display_4() { $this->assertEquals('\[ 1.3410\times 10^4\, \mathrm{Hz}\, \mathrm{m} \]', $state->contentsdisplayed); } + public function test_validate_student_response_display_decimals_1() { + $options = new stack_options(); + $options->set_option('decimals', ','); + $el = stack_input_factory::make('units', 'sans1', '9.81*m/s^2', $options); + $el->set_parameter('insertStars', 1); + // TODO: this should fail when we update the student input parsing! + $state = $el->validate_student_response(array('sans1' => '9.81m/s^2'), $options, '9.81*m/s^2', + new stack_cas_security(true)); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('9.81*m/s^2', $state->contentsmodified); + $this->assertEquals('\[ 9,81\, {\mathrm{m}}/{\mathrm{s}^2} \]', $state->contentsdisplayed); + $this->assertEquals('A correct answer is ' + . '\( 9,81\, {\mathrm{m}}/{\mathrm{s}^2} \), which can be typed in as follows: ' + . '9,81*m/s^2', + $el->get_teacher_answer_display($state->contentsmodified, '9,81\, {\mathrm{m}}/{\mathrm{s}^2}')); + + } + public function test_validate_student_response_display_negpow_3() { $options = new stack_options(); $el = stack_input_factory::make('units', 'sans1', '9.81*m*s^-2'); diff --git a/tests/input_varmatrix_test.php b/tests/input_varmatrix_test.php index 71fe2bd9ebf..fb57510b2f3 100644 --- a/tests/input_varmatrix_test.php +++ b/tests/input_varmatrix_test.php @@ -41,7 +41,7 @@ class input_varmatrix_test extends qtype_stack_testcase { public function test_render_blank() { $el = stack_input_factory::make('varmatrix', 'ans1', 'M'); - $this->assertEquals('
', $el->render(new stack_input_state(stack_input::BLANK, array(), '', '', '', '', ''), @@ -52,7 +52,7 @@ public function test_render_no_errors_if_garbled() { // The teacher does not need to use a matrix here but there will be errors later! $el = stack_input_factory::make('varmatrix', 'ans1', 'M'); - $this->assertEquals('
', $el->render(new stack_input_state(stack_input::BLANK, array(), '', '', '', '', ''), @@ -62,7 +62,7 @@ public function test_render_no_errors_if_garbled() { public function test_render_syntax_hint() { $el = stack_input_factory::make('varmatrix', 'ans1', 'M'); $el->set_parameter('syntaxHint', 'matrix([a,b],[?,d])'); - $this->assertEquals('
', $el->render(new stack_input_state(stack_input::VALID, array(), '', '', '', '', ''), @@ -73,13 +73,25 @@ public function test_render_syntax_hint_placeholder() { $el = stack_input_factory::make('varmatrix', 'ans1', 'M'); $el->set_parameter('syntaxHint', 'matrix([a,b],[?,d])'); $el->set_parameter('syntaxAttribute', '1'); - $this->assertEquals('
', $el->render(new stack_input_state(stack_input::VALID, array(), '', '', '', '', ''), 'ans1', false, null)); } + public function test_render_syntax_hint_round() { + $options = new stack_options(); + $options->set_option('matrixparens', '('); + $el = stack_input_factory::make('varmatrix', 'ans1', 'M', $options); + $el->set_parameter('syntaxHint', 'matrix([a,b],[?,d])'); + $this->assertEquals('
', + $el->render(new stack_input_state(stack_input::VALID, array(), '', '', '', '', ''), + 'ans1', false, null)); + } + public function test_validate_student_response_na() { $options = new stack_options(); $el = stack_input_factory::make('varmatrix', 'ans1', 'M'); @@ -187,7 +199,7 @@ public function test_render_blank_allowempty() { $options = new stack_options(); $el = stack_input_factory::make('varmatrix', 'ans1', 'x^2'); $el->set_parameter('options', 'allowempty'); - $this->assertEquals('
', $el->render(new stack_input_state(stack_input::VALID, array(), '', '', '', '', ''), From 84c0c03bdf412a896b4ffa2ec38fac603c2f804f Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Thu, 13 Jul 2023 02:35:54 +0100 Subject: [PATCH 10/41] Code tidy. --- tests/input_units_test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/input_units_test.php b/tests/input_units_test.php index 30b9a097350..852f2df064f 100644 --- a/tests/input_units_test.php +++ b/tests/input_units_test.php @@ -607,7 +607,7 @@ public function test_validate_student_response_display_decimals_1() { . '\( 9,81\, {\mathrm{m}}/{\mathrm{s}^2} \), which can be typed in as follows: ' . '9,81*m/s^2', $el->get_teacher_answer_display($state->contentsmodified, '9,81\, {\mathrm{m}}/{\mathrm{s}^2}')); - + } public function test_validate_student_response_display_negpow_3() { From f6d260c4e43d5012ee49740ed2853d1b0cf823b1 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Mon, 17 Jul 2023 14:45:43 +0100 Subject: [PATCH 11/41] First pass at adding options to parser for decimal support. Issue #789. --- doc/en/Developer/Syntax_numbers.md | 4 + lang/en/qtype_stack.php | 3 + stack/cas/ast.container.silent.class.php | 6 +- stack/input/equiv/equiv.class.php | 2 +- stack/input/inputbase.class.php | 3 +- stack/input/matrix/matrix.class.php | 3 +- stack/maximaparser/corrective_parser.php | 33 ++++++- tests/fixtures/inputfixtures.class.php | 104 ++++++++++++++++++++++- tests/input_algebraic_test.php | 54 ++++++++++-- tests/input_units_test.php | 20 ++++- tests/studentinput_test.php | 27 ++++++ 11 files changed, 237 insertions(+), 22 deletions(-) diff --git a/doc/en/Developer/Syntax_numbers.md b/doc/en/Developer/Syntax_numbers.md index 9ec841e0d49..0561ccdb3a3 100644 --- a/doc/en/Developer/Syntax_numbers.md +++ b/doc/en/Developer/Syntax_numbers.md @@ -97,6 +97,10 @@ Internally, we retain strict Maxima syntax. _Teachers must use strict Maxima sy (Note to self, strings may contain harmless punctuation characters which should not be changed...) +TODO. + +1. Sort out AJAX validation of both matrix inputs. + ## Practial implementation in other software 1. NUMBAS also uses the semicolon to separate list items, as discussed in [NUMBAS issue 889](https://github.com/numbas/Numbas/issues/889) diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index b5e6a72282c..d8eade8a21a 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -735,6 +735,7 @@ $string['stackCas_chained_inequalities'] = 'You appear to have "chained inequalities" e.g. \(a < b < c\). You need to connect individual inequalities with logical operations such as \(and\) or \(or\).'; $string['stackCas_backward_inequalities'] = 'Non-strict inequalities e.g. \( \leq \) or \( \geq \) must be entered as <= or >=. You have {$a->cmd} in your expression, which is backwards.'; $string['stackCas_unencpsulated_comma'] = 'A comma in your expression appears in a strange way. Commas are used to separate items in lists, sets etc. You need to use a decimal point, not a comma, in floating point numbers.'; +$string['stackCas_unencpsulated_semicolon'] = 'A semicolon (;) in your expression appears in a strange way. Semicolons are used to separate items in lists, sets etc.'; $string['stackCas_trigspace'] = 'To apply a trig function to its arguments you must use brackets, not spaces. For example use {$a->trig} instead.'; $string['stackCas_trigop'] = 'You must apply {$a->trig} to an argument. You seem to have {$a->forbid}, which looks like you have tried to use {$a->trig} as a variable name.'; $string['stackCas_trigexp'] = 'You cannot take a power of a trig function by writing {$a->forbid}. The square of the value of \(\{$a->identifier}(x)\) is typed in as {$a->identifier}(x)^2. The inverse of \(\{$a->identifier}(x)\) is written a{$a->identifier}(x) and not \(\{$a->identifier}^{-1}(x)\) .'; @@ -756,6 +757,8 @@ $string['stackCas_overlyComplexSubstitutionGraphOrRandomisation'] = 'The question code has overly complex substitutions or builds randomisation in an incremental and hard to validate way, the validation has timed out to deal with this simplify the logic, check the documentation for quidance.'; $string['stackCas_redefine_built_in'] = 'Redefining a built in function "{$a->name}" is forbidden.'; $string['stackCas_nested_function_declaration'] = 'Definition of a function inside another function is now forbidden, use renaming of the function if you need to switch function definitions from within another function.'; +$string['stackCas_decimal_usedthreesep'] = 'You have used the full stop ., the comma , and semicolon ; in your expression. Please be consistent with decimal position (. or ,) and list item separators (, or ;). Your answer is ambiguous!'; +$string['stackCas_decimal_usedcomma'] = 'You have used the full stop ., but you must use the comma , as a decimal separator!'; // Used in cassession.class.php. $string['stackCas_CASError'] = 'The CAS returned the following error(s):'; diff --git a/stack/cas/ast.container.silent.class.php b/stack/cas/ast.container.silent.class.php index 4df843b9985..611c815f0a1 100644 --- a/stack/cas/ast.container.silent.class.php +++ b/stack/cas/ast.container.silent.class.php @@ -126,12 +126,14 @@ class stack_ast_container_silent implements cas_evaluatable { public static function make_from_student_source(string $raw, string $context, stack_cas_security $securitymodel, array $filterstoapply = array(), - array $filteroptions = array(), string $grammar = 'Root') { + array $filteroptions = array(), string $grammar = 'Root', string $decimals = '.') { $errors = array(); $answernotes = array(); $parseroptions = array('startRule' => $grammar, - 'letToken' => stack_string('equiv_LET')); + 'letToken' => stack_string('equiv_LET'), + 'decimals' => $decimals + ); // Force the security filter to use 's'. if (isset($filteroptions['998_security'])) { diff --git a/stack/input/equiv/equiv.class.php b/stack/input/equiv/equiv.class.php index 4363ec75cc1..88b1bba641a 100644 --- a/stack/input/equiv/equiv.class.php +++ b/stack/input/equiv/equiv.class.php @@ -237,7 +237,7 @@ protected function validate_contents($contents, $basesecurity, $localoptions) { foreach ($contents as $index => $val) { $answer = stack_ast_container::make_from_student_source($val, '', $secrules, $filterstoapply, - array(), 'Equivline'); + array(), 'Equivline', $this->options->get_option('decimals')); // Is the student permitted to include comments in their answer? if (!$this->extraoptions['comments'] && $answer->is_string()) { diff --git a/stack/input/inputbase.class.php b/stack/input/inputbase.class.php index 1418b06afdc..b4d28daade9 100644 --- a/stack/input/inputbase.class.php +++ b/stack/input/inputbase.class.php @@ -966,7 +966,8 @@ protected function validate_contents($contents, $basesecurity, $localoptions) { // One of those things logic nouns hid. $val = ''; } - $answer = stack_ast_container::make_from_student_source($val, '', $secrules, $filterstoapply); + $answer = stack_ast_container::make_from_student_source($val, '', $secrules, $filterstoapply, + array(), 'Root', $this->options->get_option('decimals')); $caslines[] = $answer; $valid = $valid && $answer->get_valid(); diff --git a/stack/input/matrix/matrix.class.php b/stack/input/matrix/matrix.class.php index 03b6ef34ef4..d2c44fb37d5 100644 --- a/stack/input/matrix/matrix.class.php +++ b/stack/input/matrix/matrix.class.php @@ -189,7 +189,8 @@ protected function validate_contents($contents, $basesecurity, $localoptions) { foreach ($contents as $row) { $modifiedrow = array(); foreach ($row as $val) { - $answer = stack_ast_container::make_from_student_source($val, '', $secrules, $filterstoapply); + $answer = stack_ast_container::make_from_student_source($val, '', $secrules, $filterstoapply, + array(), 'Root', $this->options->get_option('decimals')); if ($answer->get_valid()) { $modifiedrow[] = $answer->get_inputform(); } else { diff --git a/stack/maximaparser/corrective_parser.php b/stack/maximaparser/corrective_parser.php index e7c408f4e38..ffd499548f3 100644 --- a/stack/maximaparser/corrective_parser.php +++ b/stack/maximaparser/corrective_parser.php @@ -77,6 +77,29 @@ public static function parse(string $string, array &$errors, array &$answernote, $letters = json_decode(file_get_contents(__DIR__ . '/unicode/letters-stack.json'), true); $stringles = str_replace(array_keys($letters), array_values($letters), $stringles); + // Check for all three of . and , and ; which must indicate inconsistency. + if (strpos($stringles, '.') !== false and + strpos($stringles, ',') !== false and + strpos($stringles, ';') !== false) { + $errors[] = stack_string('stackCas_decimal_usedthreesep'); + } + $decimals = '.'; + if (array_key_exists('decimals', $parseroptions)) { + $decimals = $parseroptions['decimals']; + } + if ($decimals == ',') { + // Clearly there is a lot more work to do here to get this all to work! + if (strpos($stringles, '.') !== false) { + $answernote[] = 'forbiddenCharDecimal'; + $errors[] = stack_string('stackCas_decimal_usedcomma'); + return null; + } + // Now we change from strict continental to British decimals. + // This is just place holders for now. + $stringles = str_replace(array(','), array('.'), $stringles); + $stringles = str_replace(array(';'), array(','), $stringles); + } + // Check for invalid chars at this point as they may prove to be difficult to // handle latter, also strings are safe already. $superscript = json_decode(file_get_contents(__DIR__ . '/unicode/superscript-stack.json'), true); @@ -208,7 +231,7 @@ public static function parse(string $string, array &$errors, array &$answernote, $parser = new MP_Parser(); $ast = $parser->parse($string, $parseroptions); } catch (SyntaxError $e) { - self::handle_parse_error($e, $string, $errors, $answernote); + self::handle_parse_error($e, $string, $errors, $answernote, $decimals); return null; } @@ -237,7 +260,7 @@ public static function parse(string $string, array &$errors, array &$answernote, return $ast; } - public static function handle_parse_error($exception, $string, &$errors, &$answernote) { + public static function handle_parse_error($exception, $string, &$errors, &$answernote, $decimals) { // @codingStandardsIgnoreStart // We also disallow backticks. static $disallowedfinalchars = '/+*^#~=,_&`;:$-.<>'; @@ -408,7 +431,11 @@ public static function handle_parse_error($exception, $string, &$errors, &$answe mb_substr($string, $exception->grammarOffset))); $answernote[] = 'missing_stars'; } else if ($foundchar === ',' || (ctype_digit($foundchar) && $previouschar === ',')) { - $errors[] = stack_string('stackCas_unencpsulated_comma'); + if ($decimals == '.') { + $errors[] = stack_string('stackCas_unencpsulated_comma'); + } else { + $errors[] = stack_string('stackCas_unencpsulated_semicolon'); + } $answernote[] = 'unencapsulated_comma'; } else if ($foundchar === '\\') { $errors[] = stack_string('illegalcaschars'); diff --git a/tests/fixtures/inputfixtures.class.php b/tests/fixtures/inputfixtures.class.php index 173950b73dc..76a0a6a1df5 100644 --- a/tests/fixtures/inputfixtures.class.php +++ b/tests/fixtures/inputfixtures.class.php @@ -37,6 +37,10 @@ class stack_inputvalidation_test_data { const ANSNOTES = 5; const NOTES = 6; + const BRITISH = 1; + const CONTINENTIAL = 2; + + protected static $rawdata = array( array('123', 'php_true', '123', 'cas_true', '123', '', ""), @@ -567,6 +571,71 @@ function at a point \(f(x)\). Maybe a 'gocha' for the question author...."), '\left(\pi+1\right)\, 2\, \mathrm{m}\mathrm{m}', '', ""), ); + protected static $rawdatadecimals = array( + array(0 => '123', + '.' => array(null, 'php_true', '123', 'cas_true', '123', '', ""), + ',' => array(null, 'php_true', '123', 'cas_true', '123', '', "") + ), + array(0 => '1.23', + '.' => array(null, 'php_true', '1.23', 'cas_true', '1.23', '', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', "") + ), + array(0 => '-1.27', + '.' => array(null, 'php_true', '-1.27', 'cas_true', '-1.27', '', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', "") + ), + array(0 => '2.78e-3', + '.' => array(null, 'php_true', '2.78e-3', 'cas_true', '2.78e-3', '', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', "") + ), + array(0 => '1,23', + '.' => array(null, 'php_false', '', '', '', 'unencapsulated_comma', ""), + ',' => array(null, 'php_true', '1.23', 'cas_true', '1.23', '', "") + ), + array(0 => '-1,29', + '.' => array(null, 'php_false', '', '', '', 'unencapsulated_comma', ""), + ',' => array(null, 'php_true', '-1.29', 'cas_true', '-1.29', '', "") + ), + array(0 => '2,79e-5', + '.' => array(null, 'php_false', '', '', '', 'unencapsulated_comma', ""), + ',' => array(null, 'php_true', '2.79e-5', 'cas_true', '2.79e-5', '', "") + ), + // For students' input the character ; is forbidden, but not in this test. + array(0 => '1;23', + '.' => array(null, 'php_true', '1', 'cas_true', '1', '', ""), + ',' => array(null, 'php_false', '1', '', '', 'unencapsulated_comma', "") + ), + // With strict interpretation both the following are invalid. + array(0 => '1.2+2,3*x', + '.' => array(null, 'php_false', '', '', '', 'unencapsulated_comma', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', "") + ), + array(0 => '{1,23}', + '.' => array(null, 'php_true', '{1,23}', 'cas_true', '\left \{1 , 23 \right \}', '', ""), + ',' => array(null, 'php_true', '{1.23}', 'cas_true', '\left \{1.23 \right \}', '', "") + ), + array(0 => '{1.23}', + '.' => array(null, 'php_true', '{1.23}', 'cas_true', '\left \{1.23 \right \}', '', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', "") + ), + array(0 => '{1;23}', + '.' => array(null, 'php_false', '', '', '', 'forbiddenChar_parserError', ""), + ',' => array(null, 'php_true', '{1,23}', 'cas_true', '\left \{1 , 23 \right \}', '', "") + ), + array(0 => '{1.2,3}', + '.' => array(null, 'php_true', '{1.2,3}', 'cas_true', '\left \{1.2 , 3 \right \}', '', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', "") + ), + array(0 => '{1,2;3}', + '.' => array(null, 'php_false', '', '', '', 'forbiddenChar_parserError', ""), + ',' => array(null, 'php_true', '{1.2,3}', 'cas_true', '\left \{1.2 , 3 \right \}', '', ""), + ), + array(0 => '{1,2;3;4.1}', + '.' => array(null, 'php_false', '', '', '', 'forbiddenChar_parserError', ""), + ',' => array(null, 'php_false', '', '', '', 'forbiddenCharDecimal', ""), + ) + ); + public static function get_raw_test_data() { return self::$rawdata; } @@ -575,6 +644,10 @@ public static function get_raw_test_data_units() { return self::$rawdataunits; } + public static function get_raw_test_data_decimals() { + return self::$rawdatadecimals; + } + public static function test_from_raw($data, $validationmethod) { $test = new stdClass(); @@ -586,6 +659,32 @@ public static function test_from_raw($data, $validationmethod) { $test->notes = $data[self::NOTES]; $test->ansnotes = $data[self::ANSNOTES]; $test->validationmethod = $validationmethod; + $test->decimals = '.'; + + $test->passed = null; + $test->errors = null; + $test->caserrors = null; + $test->casdisplay = null; + $test->casvalue = null; + $test->casnotes = null; + return $test; + } + + public static function test_decimals_from_raw($data, $decimals) { + + $test = new stdClass(); + $test->rawstring = $data[self::RAWSTRING]; + $test->phpvalid = $data[$decimals][self::PHPVALID]; + $test->phpcasstring = $data[$decimals][self::PHPCASSTRING]; + $test->casvalid = $data[$decimals][self::CASVALID]; + $test->display = $data[$decimals][self::DISPLAY]; + $test->notes = $data[$decimals][self::NOTES]; + $test->ansnotes = $data[$decimals][self::ANSNOTES]; + $test->validationmethod = 'typeless'; + $test->decimals = '.'; + if ($decimals === self::CONTINENTIAL) { + $test->decimals = ','; + } $test->passed = null; $test->errors = null; @@ -637,8 +736,9 @@ public static function run_test($test) { // We want to apply this as our "insert stars" but not spaces... $filterstoapply[] = '990_no_fixing_spaces'; - $cs = stack_ast_container::make_from_student_source($test->rawstring, '', new stack_cas_security(), $filterstoapply); - $cs->set_cas_validation_context('ans1', true, '', $test->validationmethod, false, 0); + $cs = stack_ast_container::make_from_student_source($test->rawstring, '', new stack_cas_security(), + $filterstoapply, array(), 'Root', $test->decimals); + $cs->set_cas_validation_context('ans1', true, '', $test->validationmethod, false, 0, '.'); $phpvalid = $cs->get_valid(); $phpcasstring = $cs->get_inputform(); diff --git a/tests/input_algebraic_test.php b/tests/input_algebraic_test.php index 201f8e4413a..aef89eb3c81 100644 --- a/tests/input_algebraic_test.php +++ b/tests/input_algebraic_test.php @@ -41,7 +41,7 @@ * @covers \stack_algebraic_input */ class input_algebraic_test extends qtype_stack_testcase { - +/* public function test_internal_validate_parameter() { $el = stack_input_factory::make('algebraic', 'input', 'x^2'); $this->assertTrue($el->validate_parameter('boxWidth', 30)); @@ -1708,24 +1708,60 @@ public function test_validate_student_response_conjugate() { $this->assertEquals($displayed, $state->contentsdisplayed); $this->assertEquals('\( \left[ x \right]\) ', $state->lvars); } - - public function test_decimal_output_1() { +*/ + public function test_decimal_output_0() { $options = new stack_options(); $options->set_option('decimals', ','); $el = stack_input_factory::make('algebraic', 'state', '{3.1415,2.71}', $options); $el->set_parameter('forbidFloats', false); - // TODO: this should fail when we update the student input parsing! $state = $el->validate_student_response(array('state' => '{3.1415,2.71}'), $options, '{3.1415,2.71}', new stack_cas_security()); - $this->assertEquals(stack_input::VALID, $state->status); - $this->assertEquals('{3.1415,2.71}', $state->contentsmodified); - $this->assertEquals('\[ \left \{3,1415 ; 2,7100 \right \} \]', $state->contentsdisplayed); - $this->assertEquals('', + $this->assertEquals(stack_input::INVALID, $state->status); + // With a strict interpretation we would have to change the , to a . In this case it results in ... + $this->assertEquals('', $state->contentsmodified); + $this->assertEquals('{3.1415,2.71}', $state->contentsdisplayed); + $this->assertEquals('You have used the full stop ., but you must use the comma , as a ' . + 'decimal separator!', $state->errors); $this->assertEquals('A correct answer is ' . '\( \{3,1415 ; 2,7100 \right \} \), which can be typed in as follows: ' . '{3,1415;2,71}', - $el->get_teacher_answer_display($state->contentsmodified, '\{3,1415 ; 2,7100 \right \}')); + $el->get_teacher_answer_display('{3.1415,2.71}', '\{3,1415 ; 2,7100 \right \}')); + } + + public function test_decimal_output_1() { + $options = new stack_options(); + $options->set_option('decimals', ','); + $el = stack_input_factory::make('algebraic', 'state', '{3.1415,2.71}', $options); + $el->set_parameter('forbidFloats', false); + + $state = $el->validate_student_response(array('state' => '{3.1415;2.71}'), $options, '{3.1415,2.71}', + new stack_cas_security()); + $this->assertEquals(stack_input::INVALID, $state->status); + // With a strict interpretation we have to change the , to a . But actually we don't change it... + $this->assertEquals('', $state->contentsmodified); + $this->assertEquals('{3.1415;2.71}', $state->contentsdisplayed); + $this->assertEquals('You have used the full stop ., but you must use the comma ' . + ', as a decimal separator!', $state->errors); + $this->assertEquals('A correct answer is ' + . '\( \{3,1415 ; 2,7100 \right \} \), which can be typed in as follows: ' + . '{3,1415;2,71}', + $el->get_teacher_answer_display('{3.1415,2.71}', '\{3,1415 ; 2,7100 \right \}')); + } + + public function test_decimal_output_2() { + $options = new stack_options(); + $options->set_option('decimals', ','); + $el = stack_input_factory::make('algebraic', 'state', '{3.1415,2.71}', $options); + $el->set_parameter('forbidFloats', false); + + $state = $el->validate_student_response(array('state' => '{3,1415;2,71}'), $options, '{3.1415,2.71}', + new stack_cas_security()); + $this->assertEquals(stack_input::VALID, $state->status); + // With a strict interpretation we have to change the , to a . + $this->assertEquals('{3.1415,2.71}', $state->contentsmodified); + $this->assertEquals('\[ \left \{3,1415 ; 2,7100 \right \} \]', $state->contentsdisplayed); + $this->assertEquals('', $state->errors); } } diff --git a/tests/input_units_test.php b/tests/input_units_test.php index 852f2df064f..09a1bb28223 100644 --- a/tests/input_units_test.php +++ b/tests/input_units_test.php @@ -592,14 +592,29 @@ public function test_validate_student_response_display_4() { $this->assertEquals('\[ 1.3410\times 10^4\, \mathrm{Hz}\, \mathrm{m} \]', $state->contentsdisplayed); } - public function test_validate_student_response_display_decimals_1() { + public function test_validate_student_response_display_decimals_0() { $options = new stack_options(); $options->set_option('decimals', ','); $el = stack_input_factory::make('units', 'sans1', '9.81*m/s^2', $options); $el->set_parameter('insertStars', 1); - // TODO: this should fail when we update the student input parsing! $state = $el->validate_student_response(array('sans1' => '9.81m/s^2'), $options, '9.81*m/s^2', new stack_cas_security(true)); + $this->assertEquals(stack_input::INVALID, $state->status); + $this->assertEquals('', $state->contentsmodified); + $this->assertEquals('9.81m/s^2', $state->contentsdisplayed); + $this->assertEquals('A correct answer is ' + . '\( 9,81\, {\mathrm{m}}/{\mathrm{s}^2} \), which can be typed in as follows: ' + . '9,81*m/s^2', + $el->get_teacher_answer_display('9.81*m/s^2', '9,81\, {\mathrm{m}}/{\mathrm{s}^2}')); + } + + public function test_validate_student_response_display_decimals_1() { + $options = new stack_options(); + $options->set_option('decimals', ','); + $el = stack_input_factory::make('units', 'sans1', '9.81*m/s^2', $options); + $el->set_parameter('insertStars', 1); + $state = $el->validate_student_response(array('sans1' => '9,81m/s^2'), $options, '9.81*m/s^2', + new stack_cas_security(true)); $this->assertEquals(stack_input::VALID, $state->status); $this->assertEquals('9.81*m/s^2', $state->contentsmodified); $this->assertEquals('\[ 9,81\, {\mathrm{m}}/{\mathrm{s}^2} \]', $state->contentsdisplayed); @@ -607,7 +622,6 @@ public function test_validate_student_response_display_decimals_1() { . '\( 9,81\, {\mathrm{m}}/{\mathrm{s}^2} \), which can be typed in as follows: ' . '9,81*m/s^2', $el->get_teacher_answer_display($state->contentsmodified, '9,81\, {\mathrm{m}}/{\mathrm{s}^2}')); - } public function test_validate_student_response_display_negpow_3() { diff --git a/tests/studentinput_test.php b/tests/studentinput_test.php index 037027ca30b..1bec78de939 100644 --- a/tests/studentinput_test.php +++ b/tests/studentinput_test.php @@ -62,4 +62,31 @@ public function test_studentinput_units() { $this->assertEquals($result->ansnotes, $result->casnotes); $this->assertTrue($result->passed); } + + /** + * @dataProvider stack_inputvalidation_test_data::get_raw_test_data_decimals + */ + public function test_studentinput_decimals_british() { + $test = stack_inputvalidation_test_data::test_decimals_from_raw(func_get_args(), 1); + $result = stack_inputvalidation_test_data::run_test($test); + + $this->assert_equals_ignore_spaces_and_e($result->display, $result->casdisplay); + $this->assertEquals($result->ansnotes, $result->casnotes); + if (!$result->passed) { + print_r($result); + } + $this->assertTrue($result->passed); + } + + /** + * @dataProvider stack_inputvalidation_test_data::get_raw_test_data_decimals + */ + public function test_studentinput_decimals_continental() { + $test = stack_inputvalidation_test_data::test_decimals_from_raw(func_get_args(), 2); + $result = stack_inputvalidation_test_data::run_test($test); + + $this->assert_equals_ignore_spaces_and_e($result->display, $result->casdisplay); + $this->assertEquals($result->ansnotes, $result->casnotes); + $this->assertTrue($result->passed); + } } From 1f7f47274daa877ee6b5268566fd9926f157231f Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Sat, 29 Jul 2023 11:10:20 +0100 Subject: [PATCH 12/41] Hard-wire "," for decimals for testing/development purposes. --- stack/options.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack/options.class.php b/stack/options.class.php index 3db99be5dd5..0b38a5bc79a 100644 --- a/stack/options.class.php +++ b/stack/options.class.php @@ -40,7 +40,7 @@ public function __construct($settings = array()) { // Currently no way for users to set this option. 'decimals' => array( 'type' => 'list', - 'value' => '.', + 'value' => ',', 'strict' => true, 'values' => array('.', ','), 'caskey' => 'texput_decimal', From a0c42803c021789bebba64c07eecc430ca0573f2 Mon Sep 17 00:00:00 2001 From: Chris Sangwin Date: Sat, 29 Jul 2023 14:24:53 +0100 Subject: [PATCH 13/41] Change matrix validation JS to JSON to facilitate decimal numbers. --- amd/build/input.min.js | 2 +- amd/build/input.min.js.map | 2 +- amd/build/stackjsvle.min.js | 4 +- amd/build/stackjsvle.min.js.map | 2 +- amd/src/input.js | 2 +- doc/en/Developer/Syntax_numbers.md | 6 +- stack/input/matrix/matrix.class.php | 75 +++++++++++++++++++++++- stack/maximaparser/corrective_parser.php | 6 +- stack/options.class.php | 2 +- tests/input_algebraic_test.php | 4 +- tests/input_matrix_test.php | 23 ++++++++ tests/studentinput_test.php | 3 - 12 files changed, 109 insertions(+), 22 deletions(-) diff --git a/amd/build/input.min.js b/amd/build/input.min.js index 9c8699bf7b6..34aa4116ff7 100644 --- a/amd/build/input.min.js +++ b/amd/build/input.min.js @@ -19,6 +19,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_stack/input",["core/ajax","core/event"],(function(Ajax,CustomEvents){function StackInput(validationDiv,prefix,qaid,name,input){var TYPING_DELAY=1e3,delayTimeoutHandle=null,validationResults={},lastValidatedValue=getInputValue();function cancelTypingDelay(){delayTimeoutHandle&&clearTimeout(delayTimeoutHandle),delayTimeoutHandle=null}function valueChanging(){cancelTypingDelay(),showWaiting(),delayTimeoutHandle=setTimeout(valueChanged,TYPING_DELAY),setTimeout((function(){checkNoChange()}),0)}function checkNoChange(){getInputValue()===lastValidatedValue&&(cancelTypingDelay(),validationDiv.classList.remove("waiting"))}function valueChanged(){cancelTypingDelay(),showValidationResults()||validateInput()}function validateInput(){Ajax.call([{methodname:"qtype_stack_validate_input",args:{qaid:qaid,name:name,input:getInputValue()},done:function(response){validationReceived(response)},fail:function(response){showValidationFailure(response)}}]),showLoading()}function getInputValue(){return input.getValue()}function validationReceived(response){"invalid"!==response.status?(validationResults[response.input]=response,showValidationResults()):showValidationFailure(response)}function extractScripts(html,scriptCommands){for(var result,scriptregexp=/]*>([\s\S]*?)<\/script>/g;null!==(result=scriptregexp.exec(html));)scriptCommands.push(result[1]);return html.replace(scriptregexp,"")}function showValidationResults(){var val=getInputValue();if(!validationResults[val])return showWaiting(),!1;var results=validationResults[val];lastValidatedValue=val;var scriptCommands=[];validationDiv.innerHTML=extractScripts(results.message,scriptCommands);for(var i=0;i")}}function StackRadioInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){var selected=container.querySelector(":checked");return selected?selected.value:""}}function StackCheckboxInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var selected=container.querySelectorAll(":checked"),result=[],i=0;i0?result.join(","):""}}function StackMatrixInput(idPrefix,container){var numcol=0,numrow=0;container.querySelectorAll("input[type=text]").forEach((function(element){if(element.name.slice(0,idPrefix.length+5)===idPrefix+"_sub_"){var bits=element.name.substring(idPrefix.length+5).split("_");numrow=Math.max(numrow,parseInt(bits[0],10)+1),numcol=Math.max(numcol,parseInt(bits[1],10)+1)}})),this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var values=new Array(numrow),i=0;i]*>([\s\S]*?)<\/script>/g;null!==(result=scriptregexp.exec(html));)scriptCommands.push(result[1]);return html.replace(scriptregexp,"")}function showValidationResults(){var val=getInputValue();if(!validationResults[val])return showWaiting(),!1;var results=validationResults[val];lastValidatedValue=val;var scriptCommands=[];validationDiv.innerHTML=extractScripts(results.message,scriptCommands);for(var i=0;i")}}function StackRadioInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){var selected=container.querySelector(":checked");return selected?selected.value:""}}function StackCheckboxInput(container){this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var selected=container.querySelectorAll(":checked"),result=[],i=0;i0?result.join(","):""}}function StackMatrixInput(idPrefix,container){var numcol=0,numrow=0;container.querySelectorAll("input[type=text]").forEach((function(element){if(element.name.slice(0,idPrefix.length+5)===idPrefix+"_sub_"){var bits=element.name.substring(idPrefix.length+5).split("_");numrow=Math.max(numrow,parseInt(bits[0],10)+1),numcol=Math.max(numcol,parseInt(bits[1],10)+1)}})),this.addEventHandlers=function(valueChanging){container.addEventListener("input",valueChanging)},this.getValue=function(){for(var values=new Array(numrow),i=0;i.\n\n/**\n * A javascript module to handle the real-time validation of the input the student types\n * into STACK questions.\n *\n * The overall way this works is as follows:\n *\n * - right at the end of this file are the init methods, which set things up.\n * - The work common to all input types is done by StackInput.\n * - Sending the Ajax request.\n * - Updating the validation display.\n * - The work specific to different input types (getting the content of the inputs) is done by\n * the classes like\n * - StackSimpleInput\n * - StackTextareaInput\n * - StackMatrixInput\n * objects of these types need to implement the two methods addEventHandlers and getValue().\n *\n * @module qtype_stack/input\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core/ajax',\n 'core/event'\n], function(\n Ajax,\n CustomEvents\n) {\n\n \"use strict\";\n\n /**\n * Class constructor representing an input in a Stack question.\n *\n * @constructor\n * @param {HTMLElement} validationDiv The div to display the validation in.\n * @param {String} prefix prefix added to the input name to get HTML ids.\n * @param {String} qaid id of the question_attempt.\n * @param {String} name the name of the input we are validating.\n * @param {Object} input An object representing the input element for this input.\n */\n function StackInput(validationDiv, prefix, qaid, name, input) {\n /** @type {number} delay between the user stopping typing, and the ajax request being sent. */\n var TYPING_DELAY = 1000;\n\n /** @type {?int} if not null, the id of the timer for the typing delay. */\n var delayTimeoutHandle = null;\n\n /** @type {Object} cache of validation results we have already received. */\n var validationResults = {};\n\n /** @type {String} the last value that we sent to be validated. */\n var lastValidatedValue = getInputValue();\n\n /**\n * Cancel any typing pause timer.\n */\n function cancelTypingDelay() {\n if (delayTimeoutHandle) {\n clearTimeout(delayTimeoutHandle);\n }\n delayTimeoutHandle = null;\n }\n\n input.addEventHandlers(valueChanging);\n\n /**\n * Called when the input contents changes. Will validate after TYPING_DELAY if nothing else happens.\n */\n function valueChanging() {\n cancelTypingDelay();\n showWaiting();\n delayTimeoutHandle = setTimeout(valueChanged, TYPING_DELAY);\n setTimeout(function() {\n checkNoChange();\n }, 0);\n }\n\n /**\n * After a small delay, detect the case where the user has got the input back\n * to where they started, so no validation is necessary.\n */\n function checkNoChange() {\n if (getInputValue() === lastValidatedValue) {\n cancelTypingDelay();\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Called to actually validate the input now.\n */\n function valueChanged() {\n cancelTypingDelay();\n if (!showValidationResults()) {\n validateInput();\n }\n }\n\n /**\n * Make an ajax call to validate the input.\n */\n function validateInput() {\n Ajax.call([{\n methodname: 'qtype_stack_validate_input',\n args: {qaid: qaid, name: name, input: getInputValue()},\n done: function(response) {\n validationReceived(response);\n },\n fail: function(response) {\n showValidationFailure(response);\n }\n }]);\n showLoading();\n }\n\n /**\n * Returns the current value of the input.\n *\n * @return {String}.\n */\n function getInputValue() {\n return input.getValue();\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function validationReceived(response) {\n if (response.status === 'invalid') {\n showValidationFailure(response);\n return;\n }\n validationResults[response.input] = response;\n showValidationResults();\n }\n\n /**\n * Some browsers cannot execute JavaScript just by inserting script tags.\n * To avoid that problem, remove all script tags from the given content,\n * and run them later.\n *\n * @param {String} html HTML content\n * @param {Array} scriptCommands An array of script tags for later use.\n * @return {String} HTML with JS removed\n */\n function extractScripts(html, scriptCommands) {\n var scriptregexp = /]*>([\\s\\S]*?)<\\/script>/g;\n var result;\n while ((result = scriptregexp.exec(html)) !== null) {\n scriptCommands.push(result[1]);\n }\n return html.replace(scriptregexp, '');\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @return {boolean} true if we could show the validation. false we we are we don't have it.\n */\n function showValidationResults() {\n /* eslint no-eval: \"off\" */\n var val = getInputValue();\n if (!validationResults[val]) {\n showWaiting();\n return false;\n }\n var results = validationResults[val];\n lastValidatedValue = val;\n var scriptCommands = [];\n validationDiv.innerHTML = extractScripts(results.message, scriptCommands);\n // Run script commands.\n for (var i = 0; i < scriptCommands.length; i++) {\n eval(scriptCommands[i]);\n }\n removeAllClasses();\n if (!results.message) {\n validationDiv.classList.add('empty');\n }\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n return true;\n }\n\n /**\n * Update the validation div after an ajax validation call failed.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function showValidationFailure(response) {\n lastValidatedValue = '';\n // Reponse usually contains backtrace, debuginfo, errorcode, link, message and moreinfourl.\n validationDiv.innerHTML = response.message;\n removeAllClasses();\n validationDiv.classList.add('error');\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n }\n\n /**\n * Display the loader icon.\n */\n function showLoading() {\n removeAllClasses();\n validationDiv.classList.add('loading');\n }\n\n /**\n * Update the validation div to show that the input contents have changed,\n * so the validation results are no longer relevant.\n */\n function showWaiting() {\n removeAllClasses();\n validationDiv.classList.add('waiting');\n }\n\n /**\n * Strip all our class names from the validation div.\n */\n function removeAllClasses() {\n validationDiv.classList.remove('empty');\n validationDiv.classList.remove('error');\n validationDiv.classList.remove('loading');\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Input type for inputs that are a single input or select.\n *\n * @constructor\n * @param {HTMLElement} input the HTML input that is this STACK input.\n */\n function StackSimpleInput(input) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n input.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n return input.value.replace(/^\\s+|\\s+$/g, '');\n };\n }\n\n /**\n * Input type for textarea inputs.\n *\n * @constructor\n * @param {Object} textarea The input element wrapped in jquery.\n */\n function StackTextareaInput(textarea) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n textarea.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var raw = textarea.value.replace(/^\\s+|\\s+$/g, '');\n // Using
here is weird, but it gets sorted out at the PHP end.\n return raw.split(/\\s*[\\r\\n]\\s*/).join('
');\n };\n }\n\n /**\n * Input type for inputs that are a set of radio buttons.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackRadioInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelector(':checked');\n if (selected) {\n return selected.value;\n } else {\n return '';\n }\n };\n }\n\n /**\n * Input type for inputs that are a set of checkboxes.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackCheckboxInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelectorAll(':checked');\n var result = [];\n for (var i = 0; i < selected.length; i++) {\n result[i] = selected[i].value;\n }\n if (result.length > 0) {\n return result.join(',');\n } else {\n return '';\n }\n };\n }\n\n /**\n * Class constructor representing matrix inputs (one input).\n *\n * @constructor\n * @param {String} idPrefix input id, which is the start of the id of all the different text boxes.\n * @param {HTMLElement} container
of this input.\n */\n function StackMatrixInput(idPrefix, container) {\n var numcol = 0;\n var numrow = 0;\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n numrow = Math.max(numrow, parseInt(bits[0], 10) + 1);\n numcol = Math.max(numcol, parseInt(bits[1], 10) + 1);\n });\n\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var values = new Array(numrow);\n for (var i = 0; i < numrow; i++) {\n values[i] = new Array(numcol);\n }\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n values[bits[0]][bits[1]] = element.value.replace(/^\\s+|\\s+$/g, '');\n });\n return 'matrix([' + values.join('],[') + '])';\n };\n }\n\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n function initInputs(questionDivId, prefix, qaid, inputs) {\n var questionDiv = document.getElementById(questionDivId);\n\n // Initialise all inputs.\n var allok = true;\n for (var i = 0; i < inputs.length; i++) {\n allok = initInput(questionDiv, prefix, qaid, inputs[i]) && allok;\n }\n\n // With JS With instant validation, we don't need the Check button, so hide it.\n if (allok && (questionDiv.classList.contains('dfexplicitvaildate') ||\n questionDiv.classList.contains('dfcbmexplicitvaildate'))) {\n questionDiv.querySelector('.im-controls input.submit').hidden = true;\n }\n }\n\n /**\n * Initialise one input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String} name the input to initialise.\n * @return {boolean} true if this input was successfully initialised, else false.\n */\n function initInput(questionDiv, prefix, qaid, name) {\n var validationDiv = document.getElementById(prefix + name + '_val');\n if (!validationDiv) {\n return false;\n }\n\n var inputTypeHandler = getInputTypeHandler(questionDiv, prefix, name);\n if (inputTypeHandler) {\n new StackInput(validationDiv, prefix, qaid, name, inputTypeHandler);\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * Get the input type handler for a named input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} name the input to initialise.\n * @return {?Object} the input hander, if we can handle it, else null.\n */\n function getInputTypeHandler(questionDiv, prefix, name) {\n // See if it is an ordinary input.\n var input = questionDiv.querySelector('[name=\"' + prefix + name + '\"]');\n if (input) {\n if (input.nodeName === 'TEXTAREA') {\n return new StackTextareaInput(input);\n } else if (input.type === 'radio') {\n return new StackRadioInput(input.closest('.answer'));\n } else {\n return new StackSimpleInput(input);\n }\n }\n\n // See if it is a checkbox input.\n input = questionDiv.querySelector('[name=\"' + prefix + name + '_1\"]');\n if (input && input.type === 'checkbox') {\n return new StackCheckboxInput(input.closest('.answer'));\n }\n\n // See if it is a matrix input.\n var matrix = document.getElementById(prefix + name + '_container');\n if (matrix) {\n return new StackMatrixInput(prefix + name, matrix);\n }\n\n return null;\n }\n\n /** Export our entry point. */\n return {\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n initInputs: initInputs\n };\n});\n"],"names":["define","Ajax","CustomEvents","StackInput","validationDiv","prefix","qaid","name","input","TYPING_DELAY","delayTimeoutHandle","validationResults","lastValidatedValue","getInputValue","cancelTypingDelay","clearTimeout","valueChanging","showWaiting","setTimeout","valueChanged","checkNoChange","classList","remove","showValidationResults","validateInput","call","methodname","args","done","response","validationReceived","fail","showValidationFailure","showLoading","getValue","status","extractScripts","html","scriptCommands","result","scriptregexp","exec","push","replace","val","results","innerHTML","message","i","length","eval","removeAllClasses","add","notifyFilterContentUpdated","addEventHandlers","StackSimpleInput","addEventListener","value","StackTextareaInput","textarea","split","join","StackRadioInput","container","selected","querySelector","StackCheckboxInput","querySelectorAll","StackMatrixInput","idPrefix","numcol","numrow","forEach","element","slice","bits","substring","Math","max","parseInt","values","Array","initInputs","questionDivId","inputs","questionDiv","document","getElementById","allok","initInput","contains","hidden","inputTypeHandler","getInputTypeHandler","nodeName","type","closest","matrix"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"input.min.js","sources":["../src/input.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A javascript module to handle the real-time validation of the input the student types\n * into STACK questions.\n *\n * The overall way this works is as follows:\n *\n * - right at the end of this file are the init methods, which set things up.\n * - The work common to all input types is done by StackInput.\n * - Sending the Ajax request.\n * - Updating the validation display.\n * - The work specific to different input types (getting the content of the inputs) is done by\n * the classes like\n * - StackSimpleInput\n * - StackTextareaInput\n * - StackMatrixInput\n * objects of these types need to implement the two methods addEventHandlers and getValue().\n *\n * @module qtype_stack/input\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core/ajax',\n 'core/event'\n], function(\n Ajax,\n CustomEvents\n) {\n\n \"use strict\";\n\n /**\n * Class constructor representing an input in a Stack question.\n *\n * @constructor\n * @param {HTMLElement} validationDiv The div to display the validation in.\n * @param {String} prefix prefix added to the input name to get HTML ids.\n * @param {String} qaid id of the question_attempt.\n * @param {String} name the name of the input we are validating.\n * @param {Object} input An object representing the input element for this input.\n */\n function StackInput(validationDiv, prefix, qaid, name, input) {\n /** @type {number} delay between the user stopping typing, and the ajax request being sent. */\n var TYPING_DELAY = 1000;\n\n /** @type {?int} if not null, the id of the timer for the typing delay. */\n var delayTimeoutHandle = null;\n\n /** @type {Object} cache of validation results we have already received. */\n var validationResults = {};\n\n /** @type {String} the last value that we sent to be validated. */\n var lastValidatedValue = getInputValue();\n\n /**\n * Cancel any typing pause timer.\n */\n function cancelTypingDelay() {\n if (delayTimeoutHandle) {\n clearTimeout(delayTimeoutHandle);\n }\n delayTimeoutHandle = null;\n }\n\n input.addEventHandlers(valueChanging);\n\n /**\n * Called when the input contents changes. Will validate after TYPING_DELAY if nothing else happens.\n */\n function valueChanging() {\n cancelTypingDelay();\n showWaiting();\n delayTimeoutHandle = setTimeout(valueChanged, TYPING_DELAY);\n setTimeout(function() {\n checkNoChange();\n }, 0);\n }\n\n /**\n * After a small delay, detect the case where the user has got the input back\n * to where they started, so no validation is necessary.\n */\n function checkNoChange() {\n if (getInputValue() === lastValidatedValue) {\n cancelTypingDelay();\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Called to actually validate the input now.\n */\n function valueChanged() {\n cancelTypingDelay();\n if (!showValidationResults()) {\n validateInput();\n }\n }\n\n /**\n * Make an ajax call to validate the input.\n */\n function validateInput() {\n Ajax.call([{\n methodname: 'qtype_stack_validate_input',\n args: {qaid: qaid, name: name, input: getInputValue()},\n done: function(response) {\n validationReceived(response);\n },\n fail: function(response) {\n showValidationFailure(response);\n }\n }]);\n showLoading();\n }\n\n /**\n * Returns the current value of the input.\n *\n * @return {String}.\n */\n function getInputValue() {\n return input.getValue();\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function validationReceived(response) {\n if (response.status === 'invalid') {\n showValidationFailure(response);\n return;\n }\n validationResults[response.input] = response;\n showValidationResults();\n }\n\n /**\n * Some browsers cannot execute JavaScript just by inserting script tags.\n * To avoid that problem, remove all script tags from the given content,\n * and run them later.\n *\n * @param {String} html HTML content\n * @param {Array} scriptCommands An array of script tags for later use.\n * @return {String} HTML with JS removed\n */\n function extractScripts(html, scriptCommands) {\n var scriptregexp = /]*>([\\s\\S]*?)<\\/script>/g;\n var result;\n while ((result = scriptregexp.exec(html)) !== null) {\n scriptCommands.push(result[1]);\n }\n return html.replace(scriptregexp, '');\n }\n\n /**\n * Update the validation div to show the results of the validation.\n *\n * @return {boolean} true if we could show the validation. false we we are we don't have it.\n */\n function showValidationResults() {\n /* eslint no-eval: \"off\" */\n var val = getInputValue();\n if (!validationResults[val]) {\n showWaiting();\n return false;\n }\n var results = validationResults[val];\n lastValidatedValue = val;\n var scriptCommands = [];\n validationDiv.innerHTML = extractScripts(results.message, scriptCommands);\n // Run script commands.\n for (var i = 0; i < scriptCommands.length; i++) {\n eval(scriptCommands[i]);\n }\n removeAllClasses();\n if (!results.message) {\n validationDiv.classList.add('empty');\n }\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n return true;\n }\n\n /**\n * Update the validation div after an ajax validation call failed.\n *\n * @param {Object} response The data that came back from the ajax validation call.\n */\n function showValidationFailure(response) {\n lastValidatedValue = '';\n // Reponse usually contains backtrace, debuginfo, errorcode, link, message and moreinfourl.\n validationDiv.innerHTML = response.message;\n removeAllClasses();\n validationDiv.classList.add('error');\n // This fires the Maths filters for content in the validation div.\n CustomEvents.notifyFilterContentUpdated(validationDiv);\n }\n\n /**\n * Display the loader icon.\n */\n function showLoading() {\n removeAllClasses();\n validationDiv.classList.add('loading');\n }\n\n /**\n * Update the validation div to show that the input contents have changed,\n * so the validation results are no longer relevant.\n */\n function showWaiting() {\n removeAllClasses();\n validationDiv.classList.add('waiting');\n }\n\n /**\n * Strip all our class names from the validation div.\n */\n function removeAllClasses() {\n validationDiv.classList.remove('empty');\n validationDiv.classList.remove('error');\n validationDiv.classList.remove('loading');\n validationDiv.classList.remove('waiting');\n }\n }\n\n /**\n * Input type for inputs that are a single input or select.\n *\n * @constructor\n * @param {HTMLElement} input the HTML input that is this STACK input.\n */\n function StackSimpleInput(input) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n input.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n return input.value.replace(/^\\s+|\\s+$/g, '');\n };\n }\n\n /**\n * Input type for textarea inputs.\n *\n * @constructor\n * @param {Object} textarea The input element wrapped in jquery.\n */\n function StackTextareaInput(textarea) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n textarea.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var raw = textarea.value.replace(/^\\s+|\\s+$/g, '');\n // Using
here is weird, but it gets sorted out at the PHP end.\n return raw.split(/\\s*[\\r\\n]\\s*/).join('
');\n };\n }\n\n /**\n * Input type for inputs that are a set of radio buttons.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackRadioInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelector(':checked');\n if (selected) {\n return selected.value;\n } else {\n return '';\n }\n };\n }\n\n /**\n * Input type for inputs that are a set of checkboxes.\n *\n * @constructor\n * @param {HTMLElement} container container
of this input.\n */\n function StackCheckboxInput(container) {\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n // The input event fires on any change in value, even if pasted in or added by speech\n // recognition to dictate text. Change only fires after loosing focus.\n // Should also work on mobile.\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var selected = container.querySelectorAll(':checked');\n var result = [];\n for (var i = 0; i < selected.length; i++) {\n result[i] = selected[i].value;\n }\n if (result.length > 0) {\n return result.join(',');\n } else {\n return '';\n }\n };\n }\n\n /**\n * Class constructor representing matrix inputs (one input).\n *\n * @constructor\n * @param {String} idPrefix input id, which is the start of the id of all the different text boxes.\n * @param {HTMLElement} container
of this input.\n */\n function StackMatrixInput(idPrefix, container) {\n var numcol = 0;\n var numrow = 0;\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n numrow = Math.max(numrow, parseInt(bits[0], 10) + 1);\n numcol = Math.max(numcol, parseInt(bits[1], 10) + 1);\n });\n\n /**\n * Add the event handler to call when the user input changes.\n *\n * @param {Function} valueChanging the callback to call when we detect a value change.\n */\n this.addEventHandlers = function(valueChanging) {\n container.addEventListener('input', valueChanging);\n };\n\n /**\n * Get the current value of this input.\n *\n * @return {String}.\n */\n this.getValue = function() {\n var values = new Array(numrow);\n for (var i = 0; i < numrow; i++) {\n values[i] = new Array(numcol);\n }\n container.querySelectorAll('input[type=text]').forEach(function(element) {\n if (element.name.slice(0, idPrefix.length + 5) !== idPrefix + '_sub_') {\n return;\n }\n var bits = element.name.substring(idPrefix.length + 5).split('_');\n values[bits[0]][bits[1]] = element.value.replace(/^\\s+|\\s+$/g, '');\n });\n return JSON.stringify(values);\n return 'matrix([' + values.join('],[', ';') + '])';\n };\n }\n\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n function initInputs(questionDivId, prefix, qaid, inputs) {\n var questionDiv = document.getElementById(questionDivId);\n\n // Initialise all inputs.\n var allok = true;\n for (var i = 0; i < inputs.length; i++) {\n allok = initInput(questionDiv, prefix, qaid, inputs[i]) && allok;\n }\n\n // With JS With instant validation, we don't need the Check button, so hide it.\n if (allok && (questionDiv.classList.contains('dfexplicitvaildate') ||\n questionDiv.classList.contains('dfcbmexplicitvaildate'))) {\n questionDiv.querySelector('.im-controls input.submit').hidden = true;\n }\n }\n\n /**\n * Initialise one input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String} name the input to initialise.\n * @return {boolean} true if this input was successfully initialised, else false.\n */\n function initInput(questionDiv, prefix, qaid, name) {\n var validationDiv = document.getElementById(prefix + name + '_val');\n if (!validationDiv) {\n return false;\n }\n\n var inputTypeHandler = getInputTypeHandler(questionDiv, prefix, name);\n if (inputTypeHandler) {\n new StackInput(validationDiv, prefix, qaid, name, inputTypeHandler);\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * Get the input type handler for a named input.\n *\n * @param {HTMLElement} questionDiv outer
of this question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} name the input to initialise.\n * @return {?Object} the input hander, if we can handle it, else null.\n */\n function getInputTypeHandler(questionDiv, prefix, name) {\n // See if it is an ordinary input.\n var input = questionDiv.querySelector('[name=\"' + prefix + name + '\"]');\n if (input) {\n if (input.nodeName === 'TEXTAREA') {\n return new StackTextareaInput(input);\n } else if (input.type === 'radio') {\n return new StackRadioInput(input.closest('.answer'));\n } else {\n return new StackSimpleInput(input);\n }\n }\n\n // See if it is a checkbox input.\n input = questionDiv.querySelector('[name=\"' + prefix + name + '_1\"]');\n if (input && input.type === 'checkbox') {\n return new StackCheckboxInput(input.closest('.answer'));\n }\n\n // See if it is a matrix input.\n var matrix = document.getElementById(prefix + name + '_container');\n if (matrix) {\n return new StackMatrixInput(prefix + name, matrix);\n }\n\n return null;\n }\n\n /** Export our entry point. */\n return {\n /**\n * Initialise all the inputs in a STACK question.\n *\n * @param {String} questionDivId id of the outer dic of the question.\n * @param {String} prefix prefix added to the input names for this question.\n * @param {String} qaid Moodle question_attempt id.\n * @param {String[]} inputs names of all the inputs that should have instant validation.\n */\n initInputs: initInputs\n };\n});\n"],"names":["define","Ajax","CustomEvents","StackInput","validationDiv","prefix","qaid","name","input","TYPING_DELAY","delayTimeoutHandle","validationResults","lastValidatedValue","getInputValue","cancelTypingDelay","clearTimeout","valueChanging","showWaiting","setTimeout","valueChanged","checkNoChange","classList","remove","showValidationResults","validateInput","call","methodname","args","done","response","validationReceived","fail","showValidationFailure","showLoading","getValue","status","extractScripts","html","scriptCommands","result","scriptregexp","exec","push","replace","val","results","innerHTML","message","i","length","eval","removeAllClasses","add","notifyFilterContentUpdated","addEventHandlers","StackSimpleInput","addEventListener","value","StackTextareaInput","textarea","split","join","StackRadioInput","container","selected","querySelector","StackCheckboxInput","querySelectorAll","StackMatrixInput","idPrefix","numcol","numrow","forEach","element","slice","bits","substring","Math","max","parseInt","values","Array","JSON","stringify","initInputs","questionDivId","inputs","questionDiv","document","getElementById","allok","initInput","contains","hidden","inputTypeHandler","getInputTypeHandler","nodeName","type","closest","matrix"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/stackjsvle.min.js b/amd/build/stackjsvle.min.js index fe017c90a21..198252ae046 100644 --- a/amd/build/stackjsvle.min.js +++ b/amd/build/stackjsvle.min.js @@ -1,4 +1,3 @@ -function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i{if(!("string"==typeof e.data||e.data instanceof String))return;let msg=null;try{msg=JSON.parse(e.data)}catch(e){return}if(!("version"in msg)||!msg.version.startsWith("STACK-JS"))return;if(!("src"in msg&&"type"in msg&&msg.src in IFRAMES))return;let element=null,input=null,response={version:"STACK-JS:1.0.0"};switch(msg.type){case"register-input-listener":if(input=vle_get_input_element(msg.name,msg.src),null===input)return response.type="error",response.msg='Failed to connect to input: "'+msg.name+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");if(response.type="initial-input",response.name=msg.name,response.tgt=msg.src,"select"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="select"):"checkbox"===input.type?(response.value=input.checked,response["input-type"]="checkbox"):(response.value=input.value,response["input-type"]=input.type),"radio"===input.type){response.value="";for(let inp of document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"))inp.checked&&(response.value=inp.value)}if(input.id in INPUTS){if(msg.src in INPUTS[input.id])return;if("radio"!==input.type)INPUTS[input.id].push(msg.src);else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id].push(msg.src)}}else{if("radio"!==input.type)INPUTS[input.id]=[msg.src];else{let radgroup=document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]");for(let inp of radgroup)INPUTS[inp.id]=[msg.src]}if("radio"!==input.type)input.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));else{document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]").forEach((inp=>{inp.addEventListener("change",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};if(inp.checked){resp.value=inp.value;for(let tgt of INPUTS[inp.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}))}))}}if("track-input"in msg&&msg["track-input"]&&"radio"!==input.type)if(input.id in INPUTS_INPUT_EVENT){if(msg.src in INPUTS_INPUT_EVENT[input.id])return;INPUTS_INPUT_EVENT[input.id].push(msg.src)}else INPUTS_INPUT_EVENT[input.id]=[msg.src],input.addEventListener("input",(()=>{if(DISABLE_CHANGES)return;let resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;for(let tgt of INPUTS_INPUT_EVENT[input.id])resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}));msg.src in INPUTS[input.id]||IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"changed-input":if(input=vle_get_input_element(msg.name,msg.src),null===input){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to modify input: "'+msg.name+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}DISABLE_CHANGES=!0,"checkbox"===input.type?input.checked=msg.value:input.value=msg.value,function(inputelement){const c=new Event("change");inputelement.dispatchEvent(c);const i=new Event("input");inputelement.dispatchEvent(i)}(input),DISABLE_CHANGES=!1,response.type="changed-input",response.name=msg.name,response.value=msg.value;for(let tgt of INPUTS[input.id])tgt!==msg.src&&(response.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response),"*"));break;case"toggle-visibility":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}"show"===msg.set?(element.style.display="block",vle_update_dom(element)):"hide"===msg.set&&(element.style.display="none");break;case"change-content":if(element=vle_get_element(msg.target),null===element){const ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}element.replaceChildren(function(src){let doc=(new DOMParser).parseFromString(src);for(let el of doc.querySelectorAll("script, style"))el.remove();for(let el of doc.querySelectorAll("*"))for(let{name:name,value:value}of el.attributes)is_evil_attribute(name,value)&&el.removeAttribute(name);return doc.body}(msg.content)),vle_update_dom(element);break;case"resize-frame":element=IFRAMES[msg.src].parentElement,element.style.width=msg.width,element.style.height=msg.height,IFRAMES[msg.src].style.width="100%",IFRAMES[msg.src].style.height="100%",vle_update_dom(element);break;case"ping":return response.type="ping",response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"initial-input":case"error":break;default:response.type="error",response.msg='Unknown message-type: "'+msg.type+'"',response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")}})),{create_iframe(iframeid,content,targetdivid,title,scrolling){const frm=document.createElement("iframe");frm.id=iframeid,frm.style.width="100%",frm.style.height="100%",frm.style.border=0,!1===scrolling?(frm.scrolling="no",frm.style.overflow="hidden"):frm.scrolling="yes",frm.title=title,frm.referrerpolicy="no-referrer",frm.sandbox="allow-scripts allow-downloads",frm.csp="script-src: 'unsafe-inline' 'self' '*';",document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm;const src=new Blob([content],{type:"text/html; charset=utf-8"});frm.src=URL.createObjectURL(src)}}})); //# sourceMappingURL=stackjsvle.min.js.map \ No newline at end of file diff --git a/amd/build/stackjsvle.min.js.map b/amd/build/stackjsvle.min.js.map index 9aa14aa6ac9..f39fe520a3f 100644 --- a/amd/build/stackjsvle.min.js.map +++ b/amd/build/stackjsvle.min.js.map @@ -1 +1 @@ -{"version":3,"file":"stackjsvle.min.js","sources":["../src/stackjsvle.js"],"sourcesContent":["/**\n * A javascript module to handle separation of author sourced scripts into\n * IFRAMES. All such scripts will have limited access to the actual document\n * on the VLE side and this script represents the VLE side endpoint for\n * message handling needed to give that access. When porting STACK onto VLEs\n * one needs to map this script to do the following:\n *\n * 1. Ensure that searches for target elements/inputs are limited to questions\n * and do not return any elements outside them.\n *\n * 2. Map any identifiers needed to identify inputs by name.\n *\n * 3. Any change handling related to input value modifications through this\n * logic gets connected to any such handling on the VLE side.\n *\n *\n * This script is intenttionally ordered so that the VLE specific bits should\n * be at the top.\n *\n *\n * This script assumes the following:\n *\n * 1. Each relevant IFRAME has an `id`-attribute that will be told to this\n * script.\n *\n * 2. Each such IFRAME exists within the question itself, so that one can\n * traverse up the DOM tree from that IFRAME to find the border of\n * the question.\n *\n * @module qtype_stack/stackjsvle\n * @copyright 2023 Aalto University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\"qtype_stack/stackjsvle\", [\"core/event\"], function(CustomEvents) {\n \"use strict\";\n // Note the VLE specific include of logic.\n\n /* All the IFRAMES have unique identifiers that they give in their\n * messages. But we only work with those that have been created by\n * our logic and are found from this map.\n */\n let IFRAMES = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs.\n */\n let INPUTS = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs\n * and their input events. By default we only listen to changes.\n * We report input events as changes to the other side.\n */\n let INPUTS_INPUT_EVENT = {};\n\n /* A flag to disable certain things. */\n let DISABLE_CHANGES = false;\n\n\n /**\n * Returns an element with a given id, if an only if that element exists\n * inside a portion of DOM that represents a question.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} id the identifier of the element we want.\n */\n function vle_get_element(id) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let candidate = document.getElementById(id);\n let iter = candidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n return candidate;\n }\n\n return null;\n }\n\n /**\n * Returns an input element with a given name, if an only if that element\n * exists inside a portion of DOM that represents a question.\n *\n * Note that, the input element may have a name that multiple questions\n * use and to pick the preferred element one needs to pick the one\n * within the same question as the IFRAME.\n *\n * Note that the input can also be a select. In the case of radio buttons\n * returning one of the possible buttons is enough.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} name the name of the input we want\n * @param {String} srciframe the identifier of the iframe wanting it\n */\n function vle_get_input_element(name, srciframe) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n let possible = iter.querySelector('input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n }\n // If none found within the question itself, search everywhere.\n let possible = document.querySelector('.formulation input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation select[id$=\"_' + name + '\"]');\n return possible;\n }\n\n /**\n * Triggers any VLE specific scripting related to updates of the given\n * input element.\n *\n * @param {HTMLElement} inputelement the input element that has changed\n */\n function vle_update_input(inputelement) {\n // Triggering a change event may be necessary.\n const c = new Event('change');\n inputelement.dispatchEvent(c);\n // Also there are those that listen to input events.\n const i = new Event('input');\n inputelement.dispatchEvent(i);\n }\n\n /**\n * Triggers any VLE specific scripting related to DOM updates.\n *\n * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.\n */\n function vle_update_dom(modifiedsubtreerootelement) {\n CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);\n }\n\n /**\n * Does HTML-string cleaning, i.e., removes any script payload. Returns\n * a DOM version of the given input string.\n *\n * This is used when receiving replacement content for a div.\n *\n * @param {String} src a raw string to sanitise\n */\n function vle_html_sanitize(src) {\n // This can be implemented with many libraries or by custom code\n // however as this is typically a thing that a VLE might already have\n // tools for we have it at this level so that the VLE can use its own\n // tools that do things that the VLE developpers consider safe.\n\n // As Moodle does not currently seem to have such a sanitizer in\n // the core libraries, here is one implementation that shows what we\n // are looking for.\n\n // TODO: look into replacing this with DOMPurify or some such.\n\n let parser = new DOMParser();\n let doc = parser.parseFromString(src);\n\n // First remove all