diff --git a/EidosScribe/EidosHelpClasses.rtf b/EidosScribe/EidosHelpClasses.rtf index 407a7430..f92ddca1 100644 --- a/EidosScribe/EidosHelpClasses.rtf +++ b/EidosScribe/EidosHelpClasses.rtf @@ -130,7 +130,7 @@ \f3\fs18 Dictionary \f4\fs20 , like \f3\fs18 DataFrame -\f4\fs20 ) to make a copy, or with a singleton string in JSON format. See the +\f4\fs20 ) to make a copy, or with a string in JSON format. See the \f3\fs18 Dictionary \f4\fs20 class for further documentation. However, note that \f3\fs18 DataFrame @@ -567,11 +567,19 @@ Another alternative is to call \f4\fs20 to copy key-value pairs over; it is just a shorthand for convenience.\ A final alternative is to call \f3\fs18 Dictionary() -\f4\fs20 with a singleton +\f4\fs20 with a \f3\fs18 string -\f4\fs20 as its only argument; this creates a new +\f4\fs20 vector as its only argument; this creates a new \f3\fs18 Dictionary -\f4\fs20 from the string, assuming that it is a data archive in JSON format. Note that a JSON string can be generated from the +\f4\fs20 from the string, assuming that it is a data archive in JSON format. If the +\f3\fs18 string +\f4\fs20 value is not a singleton, its elements will be joined together by newlines to make a singleton +\f3\fs18 string +\f4\fs20 value; this allows the result from +\f3\fs18 readFile() +\f4\fs20 to be passed directly to +\f3\fs18 Dictionary() +\f4\fs20 even for a multiline (prettyprinted) JSON file. Note that a JSON string can be generated from the \f3\fs18 serialize() \f4\fs20 method of \f3\fs18 Dictionary diff --git a/QtSLiM/help/EidosHelpClasses.html b/QtSLiM/help/EidosHelpClasses.html index b82808c0..a63e953f 100644 --- a/QtSLiM/help/EidosHelpClasses.html +++ b/QtSLiM/help/EidosHelpClasses.html @@ -33,7 +33,7 @@

Returns a singleton string value that represents the receiving object.  By default, this is simply the name of the class of the receiving object; however, many subclasses of Object provide a different string representation.  The value returned by stringRepresentation() is the same string that would be printed by print() for the object, so stringRepresentation() allows the same representation to be used in other contexts such as paste() and cat().

5.2  Class DataFrame

(object<DataFrame>$)DataFrame(...)

-

The DataFrame constructor can be called in the same ways as the constructor for Dictionary (its superclass): with no parameters to make an empty DataFrame, with key-value pairs, with a singleton Dictionary (or a subclass of Dictionary, like DataFrame) to make a copy, or with a singleton string in JSON format.  See the Dictionary class for further documentation.  However, note that DataFrame can only use string keys; integer keys are not allowed.

+

The DataFrame constructor can be called in the same ways as the constructor for Dictionary (its superclass): with no parameters to make an empty DataFrame, with key-value pairs, with a singleton Dictionary (or a subclass of Dictionary, like DataFrame) to make a copy, or with a string in JSON format.  See the Dictionary class for further documentation.  However, note that DataFrame can only use string keys; integer keys are not allowed.

5.2.1  DataFrame properties

colNames => (string)

A vector containing all of the string column names in the DataFrame, in order.  This property is currently an alias for the Dictionary property allKeys.

@@ -72,7 +72,7 @@

Creates a new Dictionary object.  Called without arguments, as Dictionary(), this creates a new empty Dictionary.

Alternatively, key-value pairs can be passed to set up the initial state of the new Dictionary.  These are set, sequentially, on the new Dictionary, just as setValue() would do.  For example, calling Dictionary("a", 0:3, "b", c("foo", "bar")) is equivalent to calling Dictionary() and then calling setValue("a", 0:3) and then setValue("b", c("foo", "bar")) on it; it is just a shorthand for convenience.  Keys may be of type string or integer, but must all be of the same type; Dictionary supports using either string or integer keys, but they cannot be mixed in a single Dictionary object.

Another alternative is to call Dictionary() with a singleton Dictionary as its only argument; this creates a new Dictionary that is a copy of the Dictionary passed, containing the same keys and values.  This is equivalent to creating a new empty Dictionary and then calling addKeysAndValuesFrom() to copy key-value pairs over; it is just a shorthand for convenience.

-

A final alternative is to call Dictionary() with a singleton string as its only argument; this creates a new Dictionary from the string, assuming that it is a data archive in JSON format.  Note that a JSON string can be generated from the serialize() method of Dictionary; together with this way of creating a Dictionary, this provides the ability to persist arbitrary information to a string (perhaps a file on disk) and back again.  The recreated Dictionary should be identical to the original, except that zero length vectors such as integer(0), float(0), logical(0), and string(0) will all be serialized as "[]" and recreated as integer(0) since JSON does not provide a way to specify the type of a zero-length array.

+

A final alternative is to call Dictionary() with a string vector as its only argument; this creates a new Dictionary from the string, assuming that it is a data archive in JSON format.  If the string value is not a singleton, its elements will be joined together by newlines to make a singleton string value; this allows the result from readFile() to be passed directly to Dictionary() even for a multiline (prettyprinted) JSON file.  Note that a JSON string can be generated from the serialize() method of Dictionary; together with this way of creating a Dictionary, this provides the ability to persist arbitrary information to a string (perhaps a file on disk) and back again.  The recreated Dictionary should be identical to the original, except that zero length vectors such as integer(0), float(0), logical(0), and string(0) will all be serialized as "[]" and recreated as integer(0) since JSON does not provide a way to specify the type of a zero-length array.

5.3.1  Dictionary properties

allKeys => (is)

A vector containing all of the string or integer keys that have been assigned values using setValue(), in sorted (ascending alphabetic or numeric) order.

diff --git a/VERSIONS b/VERSIONS index 68dce164..cdf438e5 100644 --- a/VERSIONS +++ b/VERSIONS @@ -35,6 +35,7 @@ development head (in the master branch): events and callbacks may now be scheduled for a non-consecutive set of ticks, not just a range; for example, seq() or c() may be used to construct the times at which a script block runs as a part of this work, rescheduleScriptBlock() no longer makes copies of the script block when non-consecutive ticks are requested; a single script block can now be scheduled non-consecutively update recipe 17.4 to use this new facility instead of rescheduleScriptBlock(), and add recipe 4.1.10 demonstrating defining constants for parameters and using them for tick scheduling; also update manual sections 20.2 and 26.1 for this + make the Dictionary constructor allow a non-singleton string vector, so the (JSON) result of readFile() can be passed directly to it version 4.1 (Eidos version 3.1): diff --git a/eidos/eidos_class_Dictionary.cpp b/eidos/eidos_class_Dictionary.cpp index 05fd33d8..01de0f19 100644 --- a/eidos/eidos_class_Dictionary.cpp +++ b/eidos/eidos_class_Dictionary.cpp @@ -1716,16 +1716,26 @@ void EidosDictionaryRetained::ConstructFromEidos(const std::vectorCount() != 1) - EIDOS_TERMINATION << "ERROR (" << p_caller_name << "): " << p_constructor_name << "(x) requires that x be a singleton (Dictionary, Dictionary subclass, or JSON string)." << EidosTerminate(nullptr); + int source_count = source_value->Count(); if (source_value->Type() == EidosValueType::kValueString) { - // Construct from a JSON string - std::string json_string = source_value->StringAtIndex_NOCAST(0, nullptr); + // Construct from a JSON string; beginning in SLiM 4.2 this is allowed to be a string vector, + // as returned by readFile(). We just paste the lines back together with newlines. Note that + // if the user tries to put a newline inside a string value, that is actually not legal JSON + // (see https://stackoverflow.com/a/16690186/2752221), and nlohmann will error below. + std::string json_string; + + for (int source_index = 0; source_index < source_count; ++source_index) + { + if (source_index > 0) + json_string.append("\n"); + + json_string.append(source_value->StringAtIndex_NOCAST(source_index, nullptr)); + } + nlohmann::json json_rep; try { @@ -1738,11 +1748,14 @@ void EidosDictionaryRetained::ConstructFromEidos(const std::vectorType() != EidosValueType::kValueObject) ? nullptr : dynamic_cast(source_value->ObjectElementAtIndex_NOCAST(0, nullptr)); if (!source) - EIDOS_TERMINATION << "ERROR (" << p_caller_name << "): " << p_constructor_name << "(x) requires that x be a singleton Dictionary (or a singleton subclass of Dictionary)." << EidosTerminate(nullptr); + EIDOS_TERMINATION << "ERROR (" << p_caller_name << "): " << p_constructor_name << "(x) requires that x be a singleton Dictionary (or a singleton subclass of Dictionary), or a string vector." << EidosTerminate(nullptr); AddKeysAndValuesFrom(source); } diff --git a/eidos/eidos_test_functions_other.cpp b/eidos/eidos_test_functions_other.cpp index fef6c955..e1825ae1 100644 --- a/eidos/eidos_test_functions_other.cpp +++ b/eidos/eidos_test_functions_other.cpp @@ -1359,6 +1359,12 @@ void _RunClassTests(const std::string &temp_path) EidosAssertScriptSuccess_L("a = Dictionary(); a.setValue('logical_empty', logical(0)); a.setValue('logical_T', T); a.setValue('logical_F', F); a.setValue('logical_vector', c(T, F, T, F)); a.setValue('int_empty', integer(0)); a.setValue('int_singleton', 1); a.setValue('int_vector', 1:3); a.setValue('float_empty', float(0)); a.setValue('float_singleton', 1.0); a.setValue('float_vector', 1.0:3); a.setValue('string_empty', string(0)); a.setValue('string_singleton', 'foo'); a.setValue('string_vector', c('foo', 'bar', 'baz')); sa_json = a.serialize('json'); b = Dictionary(sa_json); sb_json = b.serialize('json'); identical(sa_json,sb_json);", true); EidosAssertScriptSuccess_L("x = Dictionary('a', 5:7, 'b', 'foo'); x.setValue('c', Dictionary('d', 18)); y = x.serialize('json'); z = Dictionary(y); z = z.serialize('json'); identical(y, z);", true); + EidosAssertScriptSuccess_L("x = Dictionary('a', 1:3, 'b', 2:4); z = Dictionary('{\"a\":[1,2,3],\"b\":[2,3,4]}'); x.identicalContents(z);", true); + EidosAssertScriptSuccess_L("x = Dictionary('a', 1:3, 'b', c('foo', 'bar')); z = Dictionary('{\"a\":[1,2,3],\"b\":[\"foo\",\"bar\"]}'); x.identicalContents(z);", true); + EidosAssertScriptSuccess_L("x = Dictionary('a', 1:3, 'b', c('foo', 'bar')); z = Dictionary('{\\n \"a\":[1,2,3],\\n \"b\":[\"foo\",\"bar\"]\\n}'); x.identicalContents(z);", true); // multiline string singleton + EidosAssertScriptSuccess_L("x = Dictionary('a', 1:3, 'b', c('foo', 'bar')); z = Dictionary(c('{', ' \"a\":[1,2,3],', ' \"b\":[\"foo\",\"bar\"]', '}')); x.identicalContents(z);", true); // string vector, as from readFile() + EidosAssertScriptRaise("Dictionary(c('{', ' \"a\":[1,2,3],', ' \"b\":[\"fo', 'o\",\"bar\"]', '}'));", 0, "valid JSON string"); // line break inside a JSON string value -- illegal + // DataFrame(...) // identicalContents() EidosAssertScriptSuccess_L("x = DataFrame(); x.setValue('a', 0:2); x.setValue('b', c('foo', 'bar', 'baz')); x.setValue('c', c(T, F, T)); x.setValue('d', c(1.1, 2.2, 3.3)); y = DataFrame('a', 0:2, 'b', c('foo', 'bar', 'baz'), 'c', c(T, F, T), 'd', c(1.1, 2.2, 3.3)); x.identicalContents(y);", true);