From e6abf6df98d97b82745c7390da4e6337e9e9df1f Mon Sep 17 00:00:00 2001 From: bruno boutin Date: Mon, 11 Nov 2024 17:49:28 +0100 Subject: [PATCH] Add types in formula (#5728) * Add types to formula * Last fixes * update submodules * Trim variable * Ensure types are correct in formula * remove debuglog --- Modules/jaspCochrane | 2 +- QMLComponents/analysisform.cpp | 7 + .../boundcontrols/boundcontrolbase.cpp | 51 +-- .../boundcontrols/boundcontrolbase.h | 2 +- .../boundcontrols/boundcontrollayers.cpp | 2 +- .../boundcontrols/boundcontrolterms.cpp | 302 +++++------------- .../boundcontrols/boundcontrolterms.h | 5 +- .../components/JASP/Controls/Form.qml | 2 +- .../JASP/Controls/VariablesList.qml | 2 +- QMLComponents/controls/componentslistbase.cpp | 6 +- QMLComponents/controls/componentslistbase.h | 2 +- QMLComponents/controls/factorsformbase.cpp | 43 +-- QMLComponents/controls/jaspcontrol.cpp | 3 +- QMLComponents/controls/jasplistcontrol.cpp | 27 +- QMLComponents/controls/jasplistcontrol.h | 10 +- QMLComponents/controls/sourceitem.cpp | 17 +- QMLComponents/controls/textareabase.cpp | 13 - QMLComponents/controls/textareabase.h | 18 +- QMLComponents/controls/variableslistbase.cpp | 10 +- QMLComponents/models/columntypesmodel.cpp | 5 + QMLComponents/models/columntypesmodel.h | 1 + QMLComponents/models/listmodel.cpp | 28 +- QMLComponents/models/listmodel.h | 9 +- .../models/listmodellayersassigned.cpp | 4 +- QMLComponents/models/term.cpp | 79 ++++- QMLComponents/models/term.h | 16 +- QMLComponents/models/terms.cpp | 32 ++ QMLComponents/models/terms.h | 4 + QMLComponents/rsyntax/formulaparser.cpp | 70 +++- QMLComponents/rsyntax/formulaparser.h | 2 +- QMLComponents/rsyntax/formulasource.cpp | 127 +++++--- QMLComponents/rsyntax/formulasource.h | 6 +- QMLComponents/rsyntax/rsyntax.cpp | 8 +- 33 files changed, 459 insertions(+), 456 deletions(-) diff --git a/Modules/jaspCochrane b/Modules/jaspCochrane index ce8561dfe7..ad00008bae 160000 --- a/Modules/jaspCochrane +++ b/Modules/jaspCochrane @@ -1 +1 @@ -Subproject commit ce8561dfe740059ecee5cf4544d9cec2a88cb072 +Subproject commit ad00008bae846ecb5eb24439a318b608a6ec17f4 diff --git a/QMLComponents/analysisform.cpp b/QMLComponents/analysisform.cpp index c132b86786..09d2813aa0 100644 --- a/QMLComponents/analysisform.cpp +++ b/QMLComponents/analysisform.cpp @@ -129,7 +129,11 @@ void AnalysisForm::runScriptRequestDone(const QString& result, const QString& co clearFormErrors(); if (_rSyntax->parseRSyntaxOptions(options)) { + blockValueChangeSignal(true); + _analysis->clearOptions(); + bindTo(Json::nullValue); bindTo(options); + blockValueChangeSignal(false, false); _analysis->boundValueChangedHandler(); } } @@ -310,6 +314,9 @@ QString AnalysisForm::msgsListToString(const QStringList & list) const if(list.length() == 0) return ""; + if (list.size() == 1) + return list[0]; + QString text; for (const QString & msg : list) if(msg.size()) diff --git a/QMLComponents/boundcontrols/boundcontrolbase.cpp b/QMLComponents/boundcontrols/boundcontrolbase.cpp index 96a5ee465f..4060a48155 100644 --- a/QMLComponents/boundcontrols/boundcontrolbase.cpp +++ b/QMLComponents/boundcontrols/boundcontrolbase.cpp @@ -121,21 +121,12 @@ void BoundControlBase::_readTableValue(const Json::Value &value, const std::stri { for (const Json::Value& row : value) { - std::vector term; - const Json::Value& keyValue = row[key]; - if (keyValue.isArray()) - { - for (const Json::Value& component : keyValue) - term.push_back(component.asString()); - } - else if (keyValue.isString()) - term.push_back(keyValue.asString()); - else - Log::log() << "Key (" << key << ") bind value is not an array or a string in " << getName() << ": " << value.toStyledString() << std::endl; + Json::Value keyValue = row[key]; + Term term = Term::readTerm(keyValue); if (term.size() > 0) { - terms.add(Term(term)); + terms.add(term); QMap controlMap; for (auto itr = row.begin(); itr != row.end(); ++itr) @@ -145,7 +136,7 @@ void BoundControlBase::_readTableValue(const Json::Value &value, const std::stri controlMap[tq(name)] = *itr; } - allControlValues[Term(term).asQString()] = controlMap; + allControlValues[term.asQString()] = controlMap; } } } @@ -158,39 +149,9 @@ Json::Value BoundControlBase::_getTableValueOption(const Terms& terms, const Lis { QMap componentValues = componentValuesMap[term.asQString()]; - Json::Value rowValues(Json::objectValue), - keyValue; // depending of keyHasVariables & hasInteraction, keyValue can be an object, an array or a string - - if (keyHasVariables) - { - // If the key of the row contains variables, set 'value' and 'types' - Json::Value jsonValue, jsonTypes; - - if (hasInteraction) - { - for (const std::string& comp : term.scomponents()) - jsonValue.append(comp); - for (columnType type : term.types()) - jsonTypes.append(columnTypeToString(type)); - } - else - { - jsonValue = term.asString(); - jsonTypes = columnTypeToString(term.type()); - } - keyValue["value"] = jsonValue; - keyValue["types"] = jsonTypes; - } - else - { - if (hasInteraction) - for (const std::string& comp : term.scomponents()) - keyValue.append(comp); - else - keyValue = term.asString(); - } + Json::Value rowValues(Json::objectValue); - rowValues[key] = keyValue; + rowValues[key] = term.toJson(hasInteraction, keyHasVariables); QMapIterator it2(componentValues); while (it2.hasNext()) diff --git a/QMLComponents/boundcontrols/boundcontrolbase.h b/QMLComponents/boundcontrols/boundcontrolbase.h index 21ff748c41..4db22a19f8 100644 --- a/QMLComponents/boundcontrols/boundcontrolbase.h +++ b/QMLComponents/boundcontrols/boundcontrolbase.h @@ -48,7 +48,7 @@ class BoundControlBase : public BoundControl std::string getName() const; void handleComputedColumn(const Json::Value& value); - Json::Value _getTableValueOption(const Terms& terms, const ListModel::RowControlsValues& componentValuesMap, const std::string& key, bool hasInteraction, bool keyHasVariables); + static Json::Value _getTableValueOption(const Terms& terms, const ListModel::RowControlsValues& componentValuesMap, const std::string& key, bool hasInteraction, bool keyHasVariables); void _setTableValue(const Terms& terms, const ListModel::RowControlsValues& componentValuesMap, const std::string& key, bool hasInteraction, bool keyHasVariables = false); void _readTableValue(const Json::Value& value, const std::string& key, bool hasMultipleTerms, Terms& terms, ListModel::RowControlsValues& allControlValues); diff --git a/QMLComponents/boundcontrols/boundcontrollayers.cpp b/QMLComponents/boundcontrols/boundcontrollayers.cpp index ba006b1409..6321564266 100644 --- a/QMLComponents/boundcontrols/boundcontrollayers.cpp +++ b/QMLComponents/boundcontrols/boundcontrollayers.cpp @@ -73,7 +73,7 @@ bool BoundControlLayers::isJsonValid(const Json::Value &optionValue) const { const Json::Value& nameOption = value["name"]; const Json::Value& variablesOption = value["variables"]; - valid = nameOption.type() == Json::stringValue && variablesOption.type() == Json::arrayValue; + valid = nameOption.type() == Json::stringValue && (variablesOption.type() == Json::arrayValue || variablesOption.type() == Json::stringValue); if (!valid) break; diff --git a/QMLComponents/boundcontrols/boundcontrolterms.cpp b/QMLComponents/boundcontrols/boundcontrolterms.cpp index 07f8c25ca7..bc6b442e20 100644 --- a/QMLComponents/boundcontrols/boundcontrolterms.cpp +++ b/QMLComponents/boundcontrols/boundcontrolterms.cpp @@ -17,13 +17,8 @@ // #include "boundcontrolterms.h" -#include "controls/variableslistbase.h" #include "log.h" -#include "analysisform.h" -#include "models/listmodeltermsavailable.h" -#include "models/listmodeltermsassigned.h" -#include "models/listmodelinteractionassigned.h" -#include "controls/rowcontrols.h" +#include "controls/jasplistcontrol.h" BoundControlTerms::BoundControlTerms(ListModelAssignedInterface* listModel, bool isSingleRow) : BoundControlBase(listModel->listView()) { @@ -132,10 +127,6 @@ void BoundControlTerms::bindTo(const Json::Value &value) Log::log() << "Control " << _control->name() << " is bound with a value that is neither an array, an object bor a string :" << valuePart.toStyledString() << std::endl; } - ListModelAssignedInterface* assignedModel = qobject_cast(_listView->model()); - if (assignedModel && !assignedModel->checkAllowedTerms(terms)) - valuePart = addTermsToOption(Json::Value::null, terms); - int i = 0; for (Term& term : terms) { @@ -177,144 +168,52 @@ void BoundControlTerms::bindTo(const Json::Value &value) Json::Value BoundControlTerms::createJson() const { - Json::Value valuePart, - typesPart = Json::arrayValue; + return _makeOption(_termsModel->terms(), _termsModel->getTermsWithComponentValues()); +} - const Terms& terms = _termsModel->terms(); +bool BoundControlTerms::isJsonValid(const Json::Value &optionValue) const +{ + const Json::Value & valuePart = _isValueWithTypes(optionValue) ? optionValue["value"] : optionValue; + const Json::Value & typesPart = _isValueWithTypes(optionValue) ? optionValue["types"] : Json::arrayValue; - if (_listView->containsInteractions() || _listView->hasRowComponent()) - { - valuePart = Json::arrayValue; - for (const Term& term : terms) - { - Json::Value row(Json::objectValue); - if (_listView->containsInteractions()) - { - Json::Value keyValue(Json::arrayValue); - for (const std::string& comp : term.scomponents()) - keyValue.append(comp); - row[_optionKey] = keyValue; - } - else - { - Json::Value keyValue(term.asString()); - row[_optionKey] = keyValue; - } + return (valuePart.isNull() || valuePart.isArray() || valuePart.isString()) && + (typesPart.isArray() || typesPart.isString()); +} - if (_listView->hasRowComponent()) - { - auto allRowControls = _termsModel->getAllRowControls(); - if (allRowControls.contains(term.asQString())) - { - RowControls* rowControls = allRowControls[term.asQString()]; - const QMap& controlsMap = rowControls->getJASPControlsMap(); - for (const QString& controlName : controlsMap.keys()) - { - JASPControl* control = controlsMap[controlName]; - BoundControl* boundControl = control->boundControl(); - if (boundControl) - row[fq(controlName)] = boundControl->createJson(); - } - } - } - valuePart.append(row); - typesPart.append(columnTypeToString(term.type())); - } - } - else if (_isSingleRow) - { - valuePart = (terms.size() > 0) ? terms.at(0).asString() : ""; - typesPart = columnTypeToString((terms.size() > 0) ? terms.at(0).type() : columnType::unknown); - } +Json::Value BoundControlTerms::makeOption(const Terms& terms, const ListModel::RowControlsValues& controlValues, const std::string& optionKey, bool containsInteractions, bool hasRowComponent, bool isSingleRow) +{ + Json::Value result(Json::objectValue); + + Json::Value optionValue; + + if (hasRowComponent || containsInteractions) + optionValue = _getTableValueOption(terms, controlValues, optionKey, containsInteractions, false); + else if (isSingleRow) + optionValue = terms.size() > 0 ? terms[0].asString() : ""; else { - valuePart = Json::arrayValue; + optionValue = Json::arrayValue; for (const Term& term : terms) - { - valuePart.append(term.asString()); - typesPart.append(columnTypeToString(term.type())); - } + optionValue.append(term.asString()); } - Json::Value result = Json::objectValue; - result["value"] = valuePart; - result["types"] = typesPart; + result["value"] = optionValue; + result["types"] = terms.types(); + + if (hasRowComponent || containsInteractions) + result["optionKey"] = optionKey; return result; } -bool BoundControlTerms::isJsonValid(const Json::Value &optionValue) const +Json::Value BoundControlTerms::_makeOption(const Terms& terms, const ListModel::RowControlsValues& controlValues) const { - bool valid = true; - const Json::Value & valuePart = _isValueWithTypes(optionValue) ? optionValue["value"] : optionValue; - const Json::Value & typesPart = _isValueWithTypes(optionValue) ? optionValue["types"] : Json::arrayValue; - - if (_listView->hasRowComponent() || _listView->containsInteractions()) - { - valid = valuePart.type() == Json::arrayValue; - if (valid) - { - for (uint i = 0; i < valuePart.size(); i++) - { - const Json::Value& value = valuePart[i]; - - if (!_listView->hasRowComponent() && (value.type() == Json::stringValue || value.type() == Json::arrayValue)) - { - // If there is no row component, allow stringValue (only one value) or arrayValue (for several values) - valid = true; - } - else if (value.type() == Json::objectValue) - { - const Json::Value& components = value[_optionKey]; - valid = components.type() == Json::arrayValue || components.type() == Json::stringValue; - } - if (!valid) - { - Log::log() << "Wrong type: " << value.toStyledString() << std::endl; - break; - } - } - } - } - else if (_isSingleRow) - valid = valuePart.type() == Json::stringValue; - else - { - valid = valuePart.type() == Json::arrayValue; - if (valid) - { - for (uint i = 0; i < valuePart.size(); i++) - { - const Json::Value& value = valuePart[i]; - valid = value.type() == Json::stringValue; - if (!valid) - break; - } - } - } - - return valid && (typesPart.isArray() || typesPart.isString()); + return makeOption(terms, controlValues, _optionKey, _listView->containsInteractions(), _listView->hasRowComponent(), _isSingleRow); } void BoundControlTerms::resetBoundValue() { - const Terms& terms = _termsModel->terms(); - - if (_listView->hasRowComponent() || _listView->containsInteractions()) - _setTableValue(terms, _termsModel->getTermsWithComponentValues(), _optionKey, _listView->containsInteractions()); - else if (_isSingleRow) - { - std::string str = terms.size() > 0 ? terms[0].asString() : ""; - Json::Value boundValue(str); - setBoundValue(boundValue); - } - else - { - Json::Value boundValue(Json::arrayValue); - for (const Term& term : terms) - boundValue.append(term.asString()); - setBoundValue(boundValue); - } + setBoundValue(_makeOption(_termsModel->terms(), _termsModel->getTermsWithComponentValues())); } void BoundControlTerms::setBoundValue(const Json::Value &value, bool emitChanges) @@ -327,11 +226,17 @@ void BoundControlTerms::setBoundValue(const Json::Value &value, bool emitChanges newValue = value; else { - newValue["value"] = value; - Json::Value types = _listView->valueTypes(); - if (_isSingleRow && types.isArray() && types.size() > 0) - types = types[0]; - newValue["types"] = types; + Json::Value types = _termsModel->getVariableTypes(); + if (_isSingleRow) + { + newValue["types"] = types.isArray() ? (types.size() > 0 ? types[0] : "") : types; + newValue["value"] = value.isArray() ? (value.size() > 0 ? value[0] : "") : value; + } + else + { + newValue["value"] = value; + newValue["types"] = types; + } } if (_listView->hasRowComponent() || _listView->containsInteractions()) newValue["optionKey"] = _optionKey; @@ -343,61 +248,13 @@ void BoundControlTerms::setBoundValue(const Json::Value &value, bool emitChanges Json::Value BoundControlTerms::addTermsToOption(const Json::Value &option, const Terms &terms, const ListModel::RowControlsValues &extraTermsMap) const { Json::Value result = option; - Terms termsAlreadyInOptions = _getValuesFromOptions(option); - - if (_listView->hasRowComponent() || _listView->containsInteractions()) - { - Terms termsToAdd; - for (const Term& term : terms) - { - if (!termsAlreadyInOptions.contains(term)) - continue; // Don't add term that is already in option. - - termsToAdd.add(term); - } - - for (const Term& term : terms) - { - if (termsAlreadyInOptions.contains(term)) - continue; // Don't add term that is already in option. - - Json::Value rowValues(Json::objectValue); - if (_listView->containsInteractions()) - { - Json::Value keyValue(Json::arrayValue); - for (const std::string& comp : term.scomponents()) - keyValue.append(comp); - rowValues[_optionKey] = keyValue; - } - else - { - Json::Value keyValue(term.asString()); - rowValues[_optionKey] = keyValue; - } + Terms newTerms = _getTermsFromOptions(option); + newTerms.add(terms); - QString termStr = term.asQString(); - if (extraTermsMap.contains(termStr)) - { - const QMap& controlsMap = extraTermsMap[termStr]; - QMapIterator it(controlsMap); - while (it.hasNext()) - { - it.next(); - rowValues[fq(it.key())] = it.value(); - } - } - result.append(rowValues); - } - } - else if (_isSingleRow) - result = terms.size() > 0 ? terms[0].asString() : ""; - - else - for (const Term& term : terms) - if (!termsAlreadyInOptions.contains(term)) - result.append(term.asString()); + ListModel::RowControlsValues newRowControlsValues = _termsModel->getTermsWithComponentValues(); + newRowControlsValues.insert(extraTermsMap); - return result; + return _makeOption(newTerms, newRowControlsValues); } bool BoundControlTerms::areTermsInOption(const Json::Value &option, Terms &terms) const @@ -405,7 +262,7 @@ bool BoundControlTerms::areTermsInOption(const Json::Value &option, Terms &terms if (terms.size() == 0) return false; bool result = true; - Terms termsInOptions = _getValuesFromOptions(option); + Terms termsInOptions = _getTermsFromOptions(option); Terms termsToSearch = terms; for (const Term& term : termsToSearch) @@ -415,49 +272,54 @@ bool BoundControlTerms::areTermsInOption(const Json::Value &option, Terms &terms return result; } -Terms BoundControlTerms::_getValuesFromOptions(const Json::Value& _option) const +Terms BoundControlTerms::_getTermsFromOptions(const Json::Value& option) const { Terms result; - Json::Value option = _isValueWithTypes(option) ? _option["value"] : _option; + Json::Value valueOption = _isValueWithTypes(option) ? option["value"] : option; + Json::Value typesOption = _isValueWithTypes(option) ? option["types"] : Json::nullValue; - if (_listView->hasRowComponent() || _listView->containsInteractions()) + if (valueOption.isObject() && valueOption.isMember(_optionKey)) + valueOption = valueOption[_optionKey]; + + auto parseType = [](const Json::Value& jsonType, int i = 0) -> columnType { - if (!option.isArray()) - return result; // Just to be sure + std::string strType = (jsonType.isArray() && jsonType.size() > i ? jsonType[i].asString() : (jsonType.isString() && i == 0 ? jsonType.asString() : "")); + return columnTypeFromString(strType, columnType::unknown); + }; - for (const Json::Value& row : option) - if (_listView->containsInteractions()) + if (valueOption.isArray()) + { + int i = 0; + for (Json::Value jsonValue : valueOption) + { + if (jsonValue.isObject() && jsonValue.isMember(_optionKey)) + jsonValue = jsonValue[_optionKey]; + + const Json::Value& jsonType = typesOption.size() > i ? typesOption[i] : Json::nullValue; + if (jsonValue.isArray()) { - if (row.isArray()) + std::vector components; + columnTypeVec types; + int j = 0; + for (const Json::Value& component : jsonValue) { - std::vector term; - for (const Json::Value& val : row) - if (val.isString()) - term.push_back(val.asString()); - - result.add(Term(term)); + components.push_back(component.asString()); + types.push_back(parseType(typesOption, j)); + j++; } + + result.add(Term(components, types)); } - else if (row.isString()) - result.add(row.asString()); + else if (jsonValue.isString()) + result.add(Term(jsonValue.asString(), parseType(typesOption))); - } - else if (_isSingleRow) - { - if (!option.isString()) - return result; // Just to be sure - result.add(option.asString()); - } - else - { - if (!option.isArray()) - return result; + i++; + } - for (const Json::Value& row : option) - if (row.isString()) - result.add(row.asString()); } + else if (valueOption.isString()) + result.add(valueOption.asString()); return result; } diff --git a/QMLComponents/boundcontrols/boundcontrolterms.h b/QMLComponents/boundcontrols/boundcontrolterms.h index cc075969a9..677e750d71 100644 --- a/QMLComponents/boundcontrols/boundcontrolterms.h +++ b/QMLComponents/boundcontrols/boundcontrolterms.h @@ -37,10 +37,13 @@ class BoundControlTerms : public BoundControlBase Json::Value addTermsToOption(const Json::Value &option, const Terms &terms, const ListModel::RowControlsValues &extraTermsMap = {}) const; bool areTermsInOption(const Json::Value& option, Terms& terms) const; + static Json::Value makeOption(const Terms& terms, const ListModel::RowControlsValues& controlValues, const std::string& optionKey, bool containsInteractions, bool hasRowComponent, bool isSingleRow); + private: - Terms _getValuesFromOptions(const Json::Value& option) const; + Terms _getTermsFromOptions(const Json::Value& option) const; Json::Value _adjustBindingValue(const Json::Value &value) const; Json::Value _adjustBindingType(const Json::Value &value) const; + Json::Value _makeOption(const Terms& terms, const ListModel::RowControlsValues& controlValues) const; ListModelAssignedInterface* _termsModel = nullptr; JASPListControl* _listView = nullptr; diff --git a/QMLComponents/components/JASP/Controls/Form.qml b/QMLComponents/components/JASP/Controls/Form.qml index b8b0da2d54..83cf28a43d 100644 --- a/QMLComponents/components/JASP/Controls/Form.qml +++ b/QMLComponents/components/JASP/Controls/Form.qml @@ -243,7 +243,7 @@ AnalysisForm textType: JASPControl.TextTypeRcode isBound: false onApplyRequest: form.sendRSyntax(text) - autoCheckSyntax: false + checkSyntax: false onInitializedChanged: if (preferencesModel.showRSyntax) control.forceActiveFocus() // If the textarea has already some large text, then it does not display it if it does not get temporarly the focus... } diff --git a/QMLComponents/components/JASP/Controls/VariablesList.qml b/QMLComponents/components/JASP/Controls/VariablesList.qml index d22cdc9ecc..10dab4eab5 100644 --- a/QMLComponents/components/JASP/Controls/VariablesList.qml +++ b/QMLComponents/components/JASP/Controls/VariablesList.qml @@ -401,7 +401,7 @@ VariablesListBase property bool containsDragItem: variablesList.itemContainingDrag === itemRectangle property bool isVirtual: (typeof model.type !== "undefined") && model.type.includes("virtual") property bool isVariable: (typeof model.type !== "undefined") && model.type.includes("variable") - property string preview: !isVariable ? "" : model.preview + property string preview: !isVariable || (typeof model.preview === "undefined") ? "" : model.preview property bool isLayer: (typeof model.type !== "undefined") && model.type.includes("layer") property bool draggable: variablesList.draggable && model.selectable property string columnType: isVariable && (typeof model.columnType !== "undefined") ? model.columnType : "" diff --git a/QMLComponents/controls/componentslistbase.cpp b/QMLComponents/controls/componentslistbase.cpp index b0fc606300..9adf8bd77f 100644 --- a/QMLComponents/controls/componentslistbase.cpp +++ b/QMLComponents/controls/componentslistbase.cpp @@ -302,12 +302,8 @@ QList ComponentsListBase::controlNameXOffsetMap() const return result; } -Json::Value ComponentsListBase::getJsonFromComponentValues(const ListModel::RowControlsValues &termsWithComponentValues) +Json::Value ComponentsListBase::getJsonFromComponentValues(const Terms& terms, const ListModel::RowControlsValues &termsWithComponentValues) { - Terms terms; - for (const QString& term : termsWithComponentValues.keys()) - terms.add(Term::readTerm(term)); - return _getTableValueOption(terms, termsWithComponentValues, fq(_optionKey), containsInteractions(), containsVariables()); } diff --git a/QMLComponents/controls/componentslistbase.h b/QMLComponents/controls/componentslistbase.h index c614c5d5da..0ceb20d16d 100644 --- a/QMLComponents/controls/componentslistbase.h +++ b/QMLComponents/controls/componentslistbase.h @@ -56,7 +56,7 @@ class ComponentsListBase : public JASPListControl, public BoundControlBase QList controlNameXOffsetMap() const; QList headerLabels() const { return _headerLabels; } - Json::Value getJsonFromComponentValues(const ListModel::RowControlsValues& termsWithComponentValues); + Json::Value getJsonFromComponentValues(const Terms& terms, const ListModel::RowControlsValues& termsWithComponentValues); signals: void addItem(); diff --git a/QMLComponents/controls/factorsformbase.cpp b/QMLComponents/controls/factorsformbase.cpp index 4e0ce41110..bd047bc0b7 100644 --- a/QMLComponents/controls/factorsformbase.cpp +++ b/QMLComponents/controls/factorsformbase.cpp @@ -29,6 +29,8 @@ FactorsFormBase::FactorsFormBase(QQuickItem *parent) _controlType = ControlType::FactorsForm; _useControlMouseArea = false; _containsVariables = true; + _mayUseFormula = false; + _useTermsInRSyntax = false; } void FactorsFormBase::setUpModel() @@ -47,49 +49,38 @@ void FactorsFormBase::setUpModel() void FactorsFormBase::bindTo(const Json::Value& value) { ListModelFactorsForm::FactorVec factors; - Json::Value updatedValue = value; // If the value has no types, then we need to update it. - for (Json::Value& factor : updatedValue) + for (const Json::Value& factor : value) { - Json::Value types = factor.isMember("types") ? factor["types"] : Json::arrayValue; - int i = 0; - Terms initTerms; - for (const Json::Value& termsJson : factor[fq(_optionKey)]) + for (const Json::Value& termJson : factor[fq(_optionKey)]) { std::vector components; - if (allowInteraction()) + if (termJson.isArray()) { // For interaction, each term is an array of strings - for (const Json::Value& elt : termsJson) + for (const Json::Value& elt : termJson) if (elt.isString()) components.push_back(elt.asString()); } - else + else if (termJson.isString()) // If not, each term is just a string - components.push_back(termsJson.asString()); + components.push_back(termJson.asString()); - Term term(components); - columnType type = columnType::unknown; - if (types.size() <= i) + if (components.size() > 0) { - if (components.size() == 1) - type = model()->getVariableRealType(tq(components[0])); - types.append(columnTypeToString(type)); + columnTypeVec types; + for (const std::string& component : components) + types.push_back(model()->getVariableRealType(tq(component))); + Term term(components, types); + initTerms.add(term); } - else - type = columnTypeFromString(types[i].asString()); - term.setType(type); - initTerms.add(term); - - i++; } - factor["types"] = types; factors.push_back(ListModelFactorsForm::Factor(tq(factor["name"].asString()), tq(factor["title"].asString()), initTerms)); } - BoundControlBase::bindTo(updatedValue); + BoundControlBase::bindTo(value); _factorsModel->initFactors(factors); } @@ -104,7 +95,6 @@ Json::Value FactorsFormBase::createJson() const row["name"] = fq(baseName() + QString::number(i + startIndex())); row["title"] = fq(baseTitle() + " " + QString::number(i + startIndex())); row[fq(_optionKey)] = Json::Value(Json::arrayValue); - row["types"] = Json::Value(Json::arrayValue); result.append(row); } @@ -145,7 +135,6 @@ void FactorsFormBase::termsChangedHandler() factorJson["name"] = fq(factor.name); factorJson["title"] = fq(factor.title); Json::Value termsJson(Json::arrayValue); - Json::Value typesJson(Json::arrayValue); for (const Term &term : factor.listView ? factor.listView->model()->terms() : factor.initTerms) { @@ -158,10 +147,8 @@ void FactorsFormBase::termsChangedHandler() else termJson = term.asString(); termsJson.append(termJson); - typesJson.append(columnTypeToString(term.type())); } factorJson[fq(_optionKey)] = termsJson; - factorJson["types"] = typesJson; boundValue.append(factorJson); } diff --git a/QMLComponents/controls/jaspcontrol.cpp b/QMLComponents/controls/jaspcontrol.cpp index 121d674586..920a55d88a 100644 --- a/QMLComponents/controls/jaspcontrol.cpp +++ b/QMLComponents/controls/jaspcontrol.cpp @@ -845,7 +845,8 @@ void JASPControl::_setInitialized(const Json::Value &value) BoundControl* bControl = boundControl(); if (bControl) { - bControl->setDefaultBoundValue(bControl->createJson()); + if (!_initialized) // If it was already initialized, don't reset its default value: this can be the case when R syntax is re-run + bControl->setDefaultBoundValue(bControl->createJson()); bControl->bindTo(value == Json::nullValue ? bControl->createJson() : value); } diff --git a/QMLComponents/controls/jasplistcontrol.cpp b/QMLComponents/controls/jasplistcontrol.cpp index 916c65070d..bb8dec1326 100644 --- a/QMLComponents/controls/jasplistcontrol.cpp +++ b/QMLComponents/controls/jasplistcontrol.cpp @@ -145,7 +145,7 @@ void JASPListControl::termsChangedHandler() { if (checkLevelsConstraints()) { - setColumnsTypes(model()->termsTypes()); + setColumnsTypes(model()->getUsedTypes()); setColumnsNames(model()->terms().asQList()); } } @@ -296,26 +296,6 @@ std::vector JASPListControl::usedVariables() const else return {}; } -Json::Value JASPListControl::valueTypes() const -{ - Json::Value types(Json::arrayValue); - - for (const Term& term : model()->terms()) - { - if (term.components().size() == 1) - types.append(columnTypeToString(term.type())); - else - { - Json::Value compTypes(Json::arrayValue); - for (columnType type : term.types()) - compTypes.append(columnTypeToString(type)); - types.append(compTypes); - } - } - - return types; -} - void JASPListControl::sourceChangedHandler() { if (!model()) return; @@ -346,6 +326,11 @@ columnType JASPListControl::defaultType() const return _allowedTypesModel->defaultType(); } +bool JASPListControl::hasMandatoryType() const +{ + return _allowedTypesModel->hasMandatoryType(); +} + bool JASPListControl::_checkLevelsConstraintsForVariable(const QString& variable) { diff --git a/QMLComponents/controls/jasplistcontrol.h b/QMLComponents/controls/jasplistcontrol.h index 89a526a44a..2bc05057d5 100644 --- a/QMLComponents/controls/jasplistcontrol.h +++ b/QMLComponents/controls/jasplistcontrol.h @@ -110,7 +110,7 @@ class JASPListControl : public JASPControl bool allowAnalysisOwnComputedColumns() const { return _allowAnalysisOwnComputedColumns; } bool isTypeAllowed(columnType type) const; columnType defaultType() const; - Json::Value valueTypes() const; + bool hasMandatoryType() const; const QStringList & columnsTypes() const { return _columnsTypes; } const QStringList & columnsNames() const { return _columnsNames; } QAbstractListModel * allowedTypesModel(); @@ -122,6 +122,10 @@ class JASPListControl : public JASPControl int maxNumericLevels() const { return _maxNumericLevels; } const QStringList & allowedColumns() const { return _allowedColumns; } QStringList allowedColumnsIcons() const; + bool mayUseFormula() const { return _mayUseFormula; } + bool useTermsInRSyntax() const { return _useTermsInRSyntax; } + void setMayUseFormula(bool use) { _mayUseFormula = use; } + void setUseTermsInRSyntax(bool use) { _useTermsInRSyntax = use; } signals: void modelChanged(); @@ -202,7 +206,9 @@ protected slots: _useSourceLevels = false, _addAvailableVariablesToAssigned = false, _allowAnalysisOwnComputedColumns = true, - _allowTypeChange = false; + _allowTypeChange = false, + _mayUseFormula = true, + _useTermsInRSyntax = true; int _maxRows = -1, _minNumericLevels = -1, diff --git a/QMLComponents/controls/sourceitem.cpp b/QMLComponents/controls/sourceitem.cpp index b76b70d5a8..1fbb246fe0 100644 --- a/QMLComponents/controls/sourceitem.cpp +++ b/QMLComponents/controls/sourceitem.cpp @@ -126,9 +126,7 @@ void SourceItem::connectModels() connect(variableInfo, &VariableInfo::namesChanged, controlModel, &ListModel::sourceNamesChanged ); connect(variableInfo, &VariableInfo::columnTypeChanged, controlModel, [this, controlModel] (QString colName) { - columnType type = (columnType)requestInfo(VariableInfo::VariableType, colName).toInt(); - Term term(colName); - term.setType(type); + Term term(colName, (columnType)requestInfo(VariableInfo::VariableType, colName).toInt()); controlModel->sourceColumnTypeChanged(term); } ); connect(variableInfo, &VariableInfo::labelsChanged, controlModel, &ListModel::sourceLabelsChanged ); @@ -513,8 +511,7 @@ Terms SourceItem::_readAllTerms() QStringList variableNames = requestInfo(VariableInfo::VariableNames).toStringList(); for (const QString& name : variableNames) { - Term term(name); - term.setType(columnType(requestInfo(VariableInfo::VariableType, name).toInt())); + Term term(name, columnType(requestInfo(VariableInfo::VariableType, name).toInt())); terms.add(term); } if (!_sourceFilter.empty()) @@ -531,10 +528,14 @@ Terms SourceItem::_readAllTerms() for (int i = 0; i < nbRows; i++) { QStringList row; + columnTypeVec types; for (int j = 0; j < nbCols; j++) - row.append(_sourceNativeModel->data(_sourceNativeModel->index(i, j), _nativeModelRole).toString()); - Term term(row); - term.setType(columnType(requestInfo(VariableInfo::VariableType, term.asQString()).toInt())); + { + QString name = _sourceNativeModel->data(_sourceNativeModel->index(i, j), _nativeModelRole).toString(); + row.append(name); + types.push_back(columnType(requestInfo(VariableInfo::VariableType, name).toInt())); + } + Term term(row, types); terms.add(term, false); } if (!_sourceFilter.empty()) diff --git a/QMLComponents/controls/textareabase.cpp b/QMLComponents/controls/textareabase.cpp index 1f34e732d0..5ef3dd6f93 100644 --- a/QMLComponents/controls/textareabase.cpp +++ b/QMLComponents/controls/textareabase.cpp @@ -126,16 +126,3 @@ void TextAreaBase::_setInitialized(const Json::Value &value) if (keepText) setText(currentText); } - -bool TextAreaBase::autoCheckSyntax() const -{ - return _autoCheckSyntax; -} - -void TextAreaBase::setAutoCheckSyntax(bool newAutoCheckSyntax) -{ - if (_autoCheckSyntax == newAutoCheckSyntax) - return; - _autoCheckSyntax = newAutoCheckSyntax; - emit autoCheckSyntaxChanged(); -} diff --git a/QMLComponents/controls/textareabase.h b/QMLComponents/controls/textareabase.h index cf3534fdbf..0c08ebdc61 100644 --- a/QMLComponents/controls/textareabase.h +++ b/QMLComponents/controls/textareabase.h @@ -39,6 +39,7 @@ class TextAreaBase : public JASPListControl, public BoundControl Q_PROPERTY( TextType textType READ textType WRITE setTextType NOTIFY textTypeChanged ) Q_PROPERTY( bool hasScriptError READ hasScriptError WRITE setHasScriptError NOTIFY hasScriptErrorChanged ) Q_PROPERTY( bool autoCheckSyntax READ autoCheckSyntax WRITE setAutoCheckSyntax NOTIFY autoCheckSyntaxChanged ) + Q_PROPERTY( bool checkSyntax READ checkSyntax WRITE setCheckSyntax NOTIFY checkSyntaxChanged ) public: TextAreaBase(QQuickItem* parent = nullptr); @@ -67,23 +68,25 @@ class TextAreaBase : public JASPListControl, public BoundControl QString text(); void setText(const QString& text); - bool autoCheckSyntax() const; - void setAutoCheckSyntax(bool newAutoCheckSyntax); + bool autoCheckSyntax() const { return _autoCheckSyntax; } + bool checkSyntax() const { return _checkSyntax; } public slots: GENERIC_SET_FUNCTION(TextType, _textType, textTypeChanged, TextType ) GENERIC_SET_FUNCTION(HasScriptError, _hasScriptError, hasScriptErrorChanged, bool ) + GENERIC_SET_FUNCTION(AutoCheckSyntax, _autoCheckSyntax, autoCheckSyntaxChanged, bool ) + GENERIC_SET_FUNCTION(CheckSyntax, _checkSyntax, checkSyntaxChanged, bool ) - void checkSyntaxHandler() { _boundControl->checkSyntax(); } - void checkSyntaxMaybeHandler() { if(_autoCheckSyntax) checkSyntaxHandler(); } + void checkSyntaxHandler() { if(_checkSyntax) _boundControl->checkSyntax(); } + void checkSyntaxMaybeHandler() { if(_autoCheckSyntax) checkSyntaxHandler(); } signals: void textTypeChanged(); void hasScriptErrorChanged(); void applyRequest(); void editingFinished(); - - void autoCheckSyntaxChanged(); + void autoCheckSyntaxChanged(); + void checkSyntaxChanged(); protected slots: void termsChangedHandler() override; @@ -94,7 +97,8 @@ protected slots: BoundControlTextArea* _boundControl = nullptr; TextType _textType = TextType::TextTypeDefault; bool _hasScriptError = false, - _autoCheckSyntax = true; + _autoCheckSyntax = true, + _checkSyntax = true; QList _separators; ListModelTermsAvailable* _model = nullptr; diff --git a/QMLComponents/controls/variableslistbase.cpp b/QMLComponents/controls/variableslistbase.cpp index 8453c60d6c..ea6bb3c55c 100644 --- a/QMLComponents/controls/variableslistbase.cpp +++ b/QMLComponents/controls/variableslistbase.cpp @@ -41,7 +41,7 @@ VariablesListBase::VariablesListBase(QQuickItem* parent) : JASPListControl(parent) { _controlType = ControlType::VariablesListView; - _useControlMouseArea = false; + _useControlMouseArea = false; } void VariablesListBase::setUp() @@ -77,10 +77,13 @@ void VariablesListBase::setUp() _draggableModel->setItemType(property("itemType").toString()); JASPControl::DropMode dropMode = JASPControl::DropMode(property("dropMode").toInt()); _draggableModel->setDropMode(dropMode); + + _mayUseFormula = (_columns == 1); //We use macros here because the signals come from QML - QQuickItem::connect(this, SIGNAL(itemDoubleClicked(int)), this, SLOT(itemDoubleClickedHandler(int))); - QQuickItem::connect(this, SIGNAL(itemsDropped(QVariant, QVariant, int)), this, SLOT(itemsDroppedHandler(QVariant, QVariant, int))); + QQuickItem::connect(this, SIGNAL(itemDoubleClicked(int)), this, SLOT(itemDoubleClickedHandler(int)) ); + QQuickItem::connect(this, SIGNAL(itemsDropped(QVariant, QVariant, int)), this, SLOT(itemsDroppedHandler(QVariant, QVariant, int)) ); + connect(this, &VariablesListBase::columnsChanged, this, [&]() { _mayUseFormula = (_columns == 1); } ); } void VariablesListBase::_setInitialized(const Json::Value &value) @@ -123,6 +126,7 @@ void VariablesListBase::setUpModel() auto * layersModel = new ListModelLayersAssigned(this); _boundControl = new BoundControlLayers(layersModel); _draggableModel = layersModel; + _useTermsInRSyntax = false; break; } diff --git a/QMLComponents/models/columntypesmodel.cpp b/QMLComponents/models/columntypesmodel.cpp index a52b113c7c..5a13f1a5d6 100644 --- a/QMLComponents/models/columntypesmodel.cpp +++ b/QMLComponents/models/columntypesmodel.cpp @@ -102,6 +102,11 @@ bool ColumnTypesModel::hasAllTypes() const return true; } +bool ColumnTypesModel::hasMandatoryType() const +{ + return _types.size() == 1 && _types[0] != columnType::unknown; +} + QStringList ColumnTypesModel::iconList() const { QStringList result; diff --git a/QMLComponents/models/columntypesmodel.h b/QMLComponents/models/columntypesmodel.h index 30fc3121c2..0ff40a9d78 100644 --- a/QMLComponents/models/columntypesmodel.h +++ b/QMLComponents/models/columntypesmodel.h @@ -50,6 +50,7 @@ class ColumnTypesModel : public QAbstractListModel void setTypes(columnTypeVec types); bool hasType(columnType type) const; bool hasAllTypes() const; + bool hasMandatoryType() const; columnType defaultType() const { return _defaultType; } QStringList iconList() const; diff --git a/QMLComponents/models/listmodel.cpp b/QMLComponents/models/listmodel.cpp index 3c9e313cc1..153dc0ed85 100644 --- a/QMLComponents/models/listmodel.cpp +++ b/QMLComponents/models/listmodel.cpp @@ -23,6 +23,7 @@ #include "controls/rowcontrols.h" #include "controls/sourceitem.h" #include "log.h" +#include "jsonutilities.h" ListModel::ListModel(JASPListControl* listView) : QAbstractTableModel(listView) @@ -116,7 +117,7 @@ Term ListModel::_checkTermType(const Term &term) const return checkedTerm; } -Terms ListModel::_checkTermsTypes(const std::vector& terms) const +Terms ListModel::checkTermsTypes(const std::vector& terms) const { Terms checkedTerms; for (const Term& term : terms) @@ -126,7 +127,7 @@ Terms ListModel::_checkTermsTypes(const std::vector& terms) const } -Terms ListModel::_checkTermsTypes(const Terms& terms) const +Terms ListModel::checkTermsTypes(const Terms& terms) const { Terms checkedTerms = terms; // Keep terms properties for (Term& term : checkedTerms) @@ -274,7 +275,7 @@ bool ListModel::addRowControl(const QString &key, JASPControl *control) return _rowControlsMap.contains(key) ? _rowControlsMap[key]->addJASPControl(control) : false; } -QStringList ListModel::termsTypes() +QStringList ListModel::getUsedTypes() const { QSet types; @@ -297,7 +298,7 @@ void ListModel::setVariableType(int ind, columnType type) if (term.type() == type) return; - Term newTerm(term); + Term newTerm = term; newTerm.setType(type); sourceColumnTypeChanged(newTerm); } @@ -741,14 +742,14 @@ void ListModel::_setTerms(const Terms &terms, const Terms& parentTerms) void ListModel::_setTerms(const std::vector &terms) { - _checkTermsTypes(terms); + checkTermsTypes(terms); _terms.set(terms); setUpRowControls(); } void ListModel::_setTerms(const Terms &terms) { - _terms.set(_checkTermsTypes(terms)); + _terms.set(checkTermsTypes(terms)); setUpRowControls(); } @@ -778,7 +779,7 @@ void ListModel::_removeLastTerm() void ListModel::_addTerms(const Terms &terms) { - _terms.add(_checkTermsTypes(terms)); + _terms.add(checkTermsTypes(terms)); setUpRowControls(); } @@ -793,3 +794,16 @@ void ListModel::_replaceTerm(int index, const Term &term) _terms.replace(index, _checkTermType(term)); setUpRowControls(); } + +Json::Value ListModel::getVariableTypes(bool onlyChanged) const +{ + return getVariableTypes(_terms, onlyChanged); +} + +Json::Value ListModel::getVariableTypes(const Terms& terms, bool onlyChanged) const +{ + if (onlyChanged && _listView->hasMandatoryType()) // Don't need to ask for the changed types: this avoids to add the type in formula when it is not necessary + return JsonUtilities::vecToJsonArray(stringvec(terms.size(), columnTypeToString(columnType::unknown))); + + return terms.types(onlyChanged, this); +} diff --git a/QMLComponents/models/listmodel.h b/QMLComponents/models/listmodel.h index 4b8e0227cf..b5ba56ca99 100644 --- a/QMLComponents/models/listmodel.h +++ b/QMLComponents/models/listmodel.h @@ -87,12 +87,17 @@ class ListModel : public QAbstractTableModel, public VariableInfoConsumer RowControls* getRowControls(const QString& key) const { return _rowControlsMap.value(key); } virtual JASPControl * getRowControl(const QString& key, const QString& name) const; virtual bool addRowControl(const QString& key, JASPControl* control); - QStringList termsTypes(); QStringList allLevels(const Terms& terms) const; void setVariableType(int index, columnType type); columnType getVariableType( const QString& name) const; + Json::Value getVariableTypes(bool onlyChanged = false) const; + Json::Value getVariableTypes(const Terms& terms, bool onlyChanged = false) const; columnType getVariableRealType(const QString& name) const; QString getVariablePreview( const QString& name) const; + QStringList getUsedTypes() const; + + Terms checkTermsTypes(const Terms& terms) const; + Terms checkTermsTypes(const std::vector& terms) const; Q_INVOKABLE int searchTermWith(QString searchString); Q_INVOKABLE void selectItem(int _index, bool _select); @@ -135,8 +140,6 @@ public slots: void _addTerm(const Term& term, bool isUnique = true); void _replaceTerm(int index, const Term& term); void _connectAllSourcesControls(); - Terms _checkTermsTypes(const Terms& terms) const; - Terms _checkTermsTypes(const std::vector& terms) const; Term _checkTermType(const Term& terms) const; void _setAllowedType(Term& term) const; diff --git a/QMLComponents/models/listmodellayersassigned.cpp b/QMLComponents/models/listmodellayersassigned.cpp index 506e451bca..1f93d870d0 100644 --- a/QMLComponents/models/listmodellayersassigned.cpp +++ b/QMLComponents/models/listmodellayersassigned.cpp @@ -32,6 +32,7 @@ void ListModelLayersAssigned::initLayers(const std::vector& variables : allVariables) { QList layer; @@ -93,8 +94,7 @@ void ListModelLayersAssigned::_setTerms() newTerms.add(tr("Layer %1").arg(layer)); for (const QString& variable : variables) { - Term term(variable); - term.setType(columnType::nominal); + Term term(variable, columnType::nominal); newTerms.add(term); } layer++; diff --git a/QMLComponents/models/term.cpp b/QMLComponents/models/term.cpp index 0ca6b37f83..2cb3d2e43b 100644 --- a/QMLComponents/models/term.cpp +++ b/QMLComponents/models/term.cpp @@ -28,21 +28,23 @@ const char * Term::separator = #endif -Term::Term(const std::vector components) { initFrom(tq(components)); } -Term::Term(const std::string component) { initFrom(tq(component)); } -Term::Term(const QStringList components) { initFrom(components); } -Term::Term(const QString component) { initFrom(component); } +Term::Term(const std::vector components, const columnTypeVec& types) { initFrom(tq(components), types); } +Term::Term(const std::string component, columnType type) { initFrom(tq(component), type); } +Term::Term(const QStringList components, const columnTypeVec& types) { initFrom(components, types); } +Term::Term(const QString component, columnType type) { initFrom(component, type); } -void Term::initFrom(const QStringList components) +void Term::initFrom(const QStringList components, const columnTypeVec& types) { _asQString = components.join(separator); _components = components; + _types = types; } -void Term::initFrom(const QString component) +void Term::initFrom(const QString component, columnType type) { _components.append(component); _asQString = component; + _types = {type}; } const QStringList &Term::components() const @@ -141,7 +143,7 @@ bool Term::replaceVariableName(const std::string & oldName, const std::string & changed = true; } - initFrom(_components); + initFrom(_components, _types); return changed; } @@ -155,3 +157,66 @@ Term Term::readTerm(QString str) { return Term(str.split(separator)); } + +Term Term::readTerm(const Json::Value &json, columnType defaultType) +{ + Json::Value jsonValue = json; + std::vector components; + columnTypeVec types; + + if (json.isObject() && json.isMember("value") && json.isMember("types")) + { + jsonValue = json["value"]; + Json::Value jsonType = json["types"]; + + if (jsonType.isArray()) + { + for (const Json::Value& type : jsonType) + types.push_back(columnTypeFromString(type.asString(), columnType::unknown)); + } + else if (jsonType.isString()) + types.push_back(columnTypeFromString(jsonType.asString(), columnType::unknown)); + } + + if (jsonValue.isArray()) + { + for (const Json::Value& component : jsonValue) + components.push_back(component.asString()); + } + else if (jsonValue.isString()) + components.push_back(jsonValue.asString()); + + while (types.size() < components.size()) + types.push_back(defaultType); + + return Term(components, types); +} + +Json::Value Term::toJson(bool useArray, bool useValueAndType) const +{ + useArray = useArray || _components.size(); + Json::Value result, value, types; + + if (useArray) + { + for (const QString& component : _components) + value.append(fq(component)); + for (columnType type : _types) + types.append(columnTypeToString(type)); + } + else + { + value = asString(); + types = columnTypeToString(type()); + } + + if (useValueAndType) + { + result["value"] = value; + result["types"] = types; + } + else + result = value; + + return result; +} diff --git a/QMLComponents/models/term.h b/QMLComponents/models/term.h index 6248c91555..2e3209d6f7 100644 --- a/QMLComponents/models/term.h +++ b/QMLComponents/models/term.h @@ -25,6 +25,7 @@ #include #include #include "columntype.h" +#include /// /// A term is a basic element of a VariablesList @@ -33,10 +34,10 @@ class Term { public: - Term(const std::vector components); - Term(const std::string component); - Term(const QStringList components); - Term(const QString component); + Term(const std::vector components, const columnTypeVec& types = { columnType::unknown } ); + Term(const std::string component, columnType type = columnType::unknown ); + Term(const QStringList components, const columnTypeVec& types = { columnType::unknown } ); + Term(const QString component, columnType type = columnType::unknown ); const QStringList & components() const; const QString & asQString() const; @@ -76,10 +77,13 @@ class Term static const char* separator; static Term readTerm(std::string str); static Term readTerm(QString str); + static Term readTerm(const Json::Value& json, columnType defaultType = columnType::unknown); + + Json::Value toJson(bool useArray = true, bool useValueAndType = true) const; private: - void initFrom(const QStringList components); - void initFrom(const QString component); + void initFrom(const QStringList components, const columnTypeVec& type); + void initFrom(const QString component, columnType type); QStringList _components; QString _asQString; diff --git a/QMLComponents/models/terms.cpp b/QMLComponents/models/terms.cpp index dc46d549a9..e7b369ff71 100644 --- a/QMLComponents/models/terms.cpp +++ b/QMLComponents/models/terms.cpp @@ -24,6 +24,7 @@ #include #include #include "utilities/qutils.h" +#include "variableinfo.h" using namespace std; @@ -530,7 +531,38 @@ void Terms::setUndraggableTerms(const Terms& undraggableTerms) _terms = newTerms; } +Json::Value Terms::types(bool onlyChanged, const VariableInfoConsumer* info) const +{ + Json::Value types(Json::arrayValue); + + auto changedType = [&, onlyChanged, info] (const QString& term, columnType type) -> Json::Value + { + if (onlyChanged && info && (columnType)info->requestInfo(VariableInfo::VariableType, term).toInt() == type) + return Json::nullValue; + else + return columnTypeToString(type); + }; + for (const Term& term : _terms) + { + if (term.components().size() == 1) + types.append(changedType(term.asQString(), term.type())); + else + { + Json::Value componentTypes(Json::arrayValue); + columnTypeVec termTypes = term.types(); + int i = 0; + for (const QString component : term.components()) + { + componentTypes.append(changedType(component, termTypes.size() > i ? termTypes[i] : columnType::unknown)); + i++; + } + types.append(componentTypes); + } + } + + return types; +} void Terms::set(const QByteArray & array, bool isUnique) { diff --git a/QMLComponents/models/terms.h b/QMLComponents/models/terms.h index 5b324b6bad..00f5c56e4f 100644 --- a/QMLComponents/models/terms.h +++ b/QMLComponents/models/terms.h @@ -30,6 +30,8 @@ #include "term.h" #include "controls/jaspcontrol.h" +class VariableInfoConsumer; + /// /// Terms is a list of Term. They are used in VariablesList /// Some extra functionalities are added to deal with terms with interactions, in order for example to remove all terms that contain some component @@ -124,6 +126,8 @@ class Terms void setDraggable(bool draggable); void setUndraggableTerms(const Terms& undraggableTerms); + Json::Value types(bool onlyChanged = false, const VariableInfoConsumer* info = nullptr) const; + private: int rankOf(const QString &component) const; diff --git a/QMLComponents/rsyntax/formulaparser.cpp b/QMLComponents/rsyntax/formulaparser.cpp index 8a56bbbdb6..394ba28ca0 100644 --- a/QMLComponents/rsyntax/formulaparser.cpp +++ b/QMLComponents/rsyntax/formulaparser.cpp @@ -1,11 +1,12 @@ #include "formulaparser.h" +#include "variableinfo.h" #include #include "log.h" const char FormulaParser::interactionSeparator = ':'; const char FormulaParser::allInterationsSeparator = '*'; -Terms FormulaParser::parseTerm(QString term) +Terms FormulaParser::parseTerm(QString termStr) { auto trim = [] (const QString& input) -> QString { @@ -31,19 +32,53 @@ Terms FormulaParser::parseTerm(QString term) return output; }; - Terms result; - if (term.contains(FormulaParser::interactionSeparator)) + auto readTermWithType = [] (const QString& input) -> std::pair { - term.replace(FormulaParser::interactionSeparator, Term::separator); - result.add(Term::readTerm(term)); - } - else if (term.contains(FormulaParser::allInterationsSeparator)) + QString term = input; + columnType type = columnType::unknown; + int index = input.lastIndexOf('.'); + if (index > 0) + { + QString typeStr = input.mid(index+1); + if (columnTypeValidName(fq(typeStr))) + { + type = columnTypeFromQString(typeStr); + term = input.first(index); + } + } + if (type == columnType::unknown) + type = columnType(VariableInfo::info()->provider()->provideInfo(VariableInfo::VariableType, input).toInt()); + + return std::make_pair(term, type); + }; + + auto readTerm = [=] (const QString& input) -> Term { - Terms baseTerms = trimList(term, FormulaParser::allInterationsSeparator); + QStringList components = input.split(FormulaParser::interactionSeparator), + parsedComponents; + columnTypeVec types; + for (const QString& component : components) + { + std::pair pair = readTermWithType(component); + parsedComponents.push_back(pair.first); + types.push_back(pair.second); + } + + return Term(parsedComponents, types); + }; + + Terms result; + + if (termStr.contains(FormulaParser::allInterationsSeparator)) + { + Terms baseTerms; + QStringList baseTermsStr = trimList(termStr, FormulaParser::allInterationsSeparator); + for (const QString& baseTermStr : baseTermsStr) + baseTerms.add(readTerm(baseTermStr)); result = baseTerms.crossCombinations(); } else - result.add(term); + result.add(readTerm(trim(termStr))); return result; } @@ -131,23 +166,26 @@ bool FormulaParser::parse(const Json::Value& formula, bool isLhs, ParsedTerms& p return true; } -QString FormulaParser::transformToFormulaTerm(const Term &term, char join, bool addQuotes) +QString FormulaParser::transformToFormulaTerm(const Term &term, const Json::Value& changedType, char join, bool addQuotes) { - static QRegularExpression rx("^[a-zA-Z0-9_]+$"); + static QRegularExpression rx("^[a-zA-Z0-9_\\.]+$"); QString result; const QStringList& components = term.components(); + int i = 0; - bool first = true; - for (const QString& component : components) + for (QString component : components) { - if (!first) - result += QString(' ') + join + ' '; + std::string compType = (changedType.isArray() && changedType.size() > i ? changedType[i] : changedType).asString(); + if (columnTypeFromString(compType, columnType::unknown) != columnType::unknown) + component.append("." + tq(compType)); - first = false; + if (i > 0) + result += QString(' ') + join + ' '; if (addQuotes) result += '"' + component + '"'; else if (component.contains(rx)) result += component; else result += '`' + component + '`'; + i++; } return result; } diff --git a/QMLComponents/rsyntax/formulaparser.h b/QMLComponents/rsyntax/formulaparser.h index 90a3b262db..5f58058310 100644 --- a/QMLComponents/rsyntax/formulaparser.h +++ b/QMLComponents/rsyntax/formulaparser.h @@ -36,7 +36,7 @@ class FormulaParser // static ParsedTerm parseTerm(const QString& term, const ParsedTerms& conditionalParsedTerms, bool isCorrelated); // static ParsedTerm parseTerm(const ParsedTerm& term, const ParsedTerms& conditionalParsedTerms, bool isCorrelated); - static QString transformToFormulaTerm(const Term& term, char join = FormulaParser::allInterationsSeparator, bool withCrossCombinations = false); + static QString transformToFormulaTerm(const Term& term, const Json::Value& changedType = Json::nullValue, char join = FormulaParser::allInterationsSeparator, bool withCrossCombinations = false); private: static ParsedTerms squeezeConditionalTerms(const ParsedTerms& terms); diff --git a/QMLComponents/rsyntax/formulasource.cpp b/QMLComponents/rsyntax/formulasource.cpp index f29c7815d7..fe4c7e8d8d 100644 --- a/QMLComponents/rsyntax/formulasource.cpp +++ b/QMLComponents/rsyntax/formulasource.cpp @@ -202,10 +202,11 @@ QString FormulaSource::toString() const if (!_model) return tr("No source found in Formula"); const Terms& terms = _model->terms(); + Json::Value changedTypes = _model->getVariableTypes(true); if (hasRandomEffects()) return _generateRandomEffectsTerms(terms); - if (_model->listView()->containsInteractions()) return generateInteractionTerms(terms); - else return _generateSimpleTerms(terms); + if (_model->listView()->containsInteractions()) return generateInteractionTerms(terms, changedTypes); + else return _generateSimpleTerms(terms, changedTypes); } @@ -245,7 +246,7 @@ QString FormulaSource::_generateRandomEffectsTerms(const Terms& terms) const result += (hasIntercept ? "1" : "0"); if (filteredTerms.size() > 0) - result += " + " + generateInteractionTerms(filteredTerms); + result += " + " + generateInteractionTerms(filteredTerms, componentListModel->getVariableTypes(filteredTerms, true)); result += (hasCorrelation ? " | " : " || "); result += key; @@ -256,7 +257,7 @@ QString FormulaSource::_generateRandomEffectsTerms(const Terms& terms) const return result; } -QString FormulaSource::generateInteractionTerms(const Terms& tterms) +QString FormulaSource::generateInteractionTerms(const Terms& tterms, const Json::Value& changedTypes) { // If the terms has interactions, try to use the '*' symbol when all combinations of the subterms are also present in the terms. QString result; @@ -270,8 +271,10 @@ QString FormulaSource::generateInteractionTerms(const Terms& tterms) if (!first) result += " + "; first = false; const Term& term = terms.at(terms.size() - 1); + int orgInd = tterms.indexOf(term); + const Json::Value& changedType = changedTypes.size() > orgInd ? changedTypes[orgInd] : Json::nullValue; terms.pop_back(); - if (term.components().size() == 1) result += FormulaParser::transformToFormulaTerm(term); + if (term.components().size() == 1) result += FormulaParser::transformToFormulaTerm(term, changedType); else { bool allComponentsAreAlsoInTerms = true; @@ -295,10 +298,10 @@ QString FormulaSource::generateInteractionTerms(const Terms& tterms) if (found != terms.end()) terms.erase(found); } - result += FormulaParser::transformToFormulaTerm(term, FormulaParser::allInterationsSeparator); + result += FormulaParser::transformToFormulaTerm(term, changedType, FormulaParser::allInterationsSeparator); } else - result += FormulaParser::transformToFormulaTerm(term, FormulaParser::interactionSeparator); + result += FormulaParser::transformToFormulaTerm(term, changedType, FormulaParser::interactionSeparator); } } @@ -306,35 +309,47 @@ QString FormulaSource::generateInteractionTerms(const Terms& tterms) } -QString FormulaSource::_generateSimpleTerms(const Terms &terms) const +QString FormulaSource::_generateSimpleTerms(const Terms &terms, const Json::Value& types) const { QString result; - bool first = true; + int i = 0; for (const Term& term : terms) { - if (!first) result += " + "; - first = false; - result += FormulaParser::transformToFormulaTerm(term, FormulaParser::interactionSeparator); + Json::Value changedType = types.size() > i ? types[i] : Json::nullValue; + if (i > 0) result += " + "; + result += FormulaParser::transformToFormulaTerm(term, changedType, FormulaParser::interactionSeparator); + i++; } return result; } -Terms FormulaSource::_onlyTrueTerms(const QString& controlName, const Terms &terms) const +std::pair FormulaSource::_onlyTrueTerms(const QString& controlName) const { - Terms result; - if (!_model) return result; + Terms onlyTrueTerms; + Json::Value onlyTrueTypes(Json::arrayValue); + + if (!_model) return std::make_pair(onlyTrueTerms, onlyTrueTypes); + + Terms terms = _model->terms(); + Json::Value changedTypes = _model->getVariableTypes(true); + int i = 0; for (const Term& term : terms) { + const Json::Value& changedType = changedTypes.size() > i ? changedTypes[i] : Json::nullValue; JASPControl* control = _model->getRowControl(term.asQString(), controlName); BoundControl* boundControl = control ? control->boundControl() : nullptr; - if (boundControl && boundControl->boundValue().asBool()) - result.add(term); + if (boundControl && boundControl->boundValue().asBool()) + { + onlyTrueTerms.add(term); + onlyTrueTypes.append(changedType); + } + i++; } - return result; + return std::make_pair(onlyTrueTerms, onlyTrueTypes); } ListModel::RowControlsValues FormulaSource::_getTermsFromExtraOptions(const Json::Value& options) const @@ -485,9 +500,20 @@ FormulaParser::ParsedTerms FormulaSource::_fillOptionsWithFixedTerms(ListModel* continue; } bool found = false; - Terms terms; + Terms terms, termsToSearch; terms.add(parsedTerm); - Terms termsToSearch = isInteractionModel ? Terms(parsedTerm.components()) : terms; + if (isInteractionModel) + { + int i = 0; + for (const QString component : parsedTerm.components()) + { + columnType type = parsedTerm.types().size() > i ? parsedTerm.types()[i] : columnType::unknown; + termsToSearch.add(Term(component, type)); + i++; + } + } + else + termsToSearch = terms; if (termsToSearch.size() == 0) continue; @@ -515,6 +541,8 @@ FormulaParser::ParsedTerms FormulaSource::_fillOptionsWithFixedTerms(ListModel* if (found) { + terms = sourceModel->checkTermsTypes(terms); + termsToSearch = sourceModel->checkTermsTypes(termsToSearch); _addTermsToOptions(sourceModel, options, termsToSearch); break; } @@ -570,10 +598,12 @@ FormulaParser::ParsedTerms FormulaSource::_fillOptionsWithRandomTerms(const Form std::map sourceMainTermsMap; ListModel::RowControlsValues randomTermsMap; + Terms mainTerms; for (auto i = parsedTerms.randomTerms.begin(); i != parsedTerms.randomTerms.end(); ++i) { - QString mainTerm = i.key(); + Term mainTerm = Term(i.key(), columnType::nominal); + mainTerms.add(mainTerm); const FormulaParser::RandomTerm& randomTerm = i.value(); for (ListModelAssignedInterface* model : sourceModels) @@ -592,35 +622,32 @@ FormulaParser::ParsedTerms FormulaSource::_fillOptionsWithRandomTerms(const Form } QMap componentValues; - Json::Value variables(Json::arrayValue); + Terms variables; + variables.add(interceptTerm); - Json::Value interceptVariable(Json::objectValue); - Json::Value interceptValue(Json::arrayValue); - interceptValue.append(fq(interceptTerm)); - interceptVariable[fq(_randomEffects.variablesKey)] = interceptValue; - interceptVariable[fq(_randomEffects.checkControl)] = randomTerm.intercept; - variables.append(interceptVariable); + ListModel::RowControlsValues controlValues; + QMap interceptCheck; + interceptCheck[_randomEffects.checkControl] = randomTerm.intercept; + controlValues[interceptTerm] = interceptCheck; for (const Term& fixedTerm : fixedEffectTerms) { - Json::Value variable(Json::objectValue); - variable[fq(_randomEffects.checkControl)] = randomTerm.terms.contains(fixedTerm); - Json::Value randomVariables(Json::arrayValue); - for (const std::string& fixedTermComponent : fixedTerm.scomponents()) - randomVariables.append(fixedTermComponent); - - variable[fq(_randomEffects.variablesKey)] = randomVariables; - variables.append(variable); + variables.add(fixedTerm); + QMap checkValue; + checkValue[_randomEffects.checkControl] = randomTerm.terms.contains(fixedTerm); + controlValues[fixedTerm.asQString()] = checkValue; } componentValues[_randomEffects.correlationControl] = randomTerm.correlated; - componentValues[_randomEffects.variablesControl] = variables; - randomTermsMap[mainTerm] = componentValues; + componentValues[_randomEffects.variablesControl] = BoundControlTerms::makeOption(variables, controlValues, fq(_randomEffects.variablesKey), true, true, false); + randomTermsMap[mainTerm.asQString()] = componentValues; } for (auto sourceMainTermsIt : sourceMainTermsMap) _addTermsToOptions(sourceMainTermsIt.first, options, sourceMainTermsIt.second); - options[fq(_sourceName)] = _randomEffects.componentsList->getJsonFromComponentValues(randomTermsMap); + options[fq(_sourceName)] = _randomEffects.componentsList->getJsonFromComponentValues(mainTerms, randomTermsMap); + + //Log::log() << "TATA: options " << options.toStyledString() << std::endl; return remainingParsedTerms; } @@ -648,23 +675,27 @@ QMap FormulaSource::additionalOptionStrings() const const ExtraOption& extraOption = _extraOptions.contains(controlName) ? _extraOptions[controlName] : ExtraOption(controlName); bool valueIsBool = boundControl->boundValue().type() == Json::booleanValue; - Terms terms = valueIsBool ? _onlyTrueTerms(controlName, _model->terms()) : _model->terms(); - if (terms.size() == 0) continue; + std::pair termsWithChangedTypes = valueIsBool ? _onlyTrueTerms(controlName) : std::make_pair(_model->terms(), _model->getVariableTypes(true)); + if (termsWithChangedTypes.first.size() == 0) continue; + QString optionValue; + const Terms& terms = termsWithChangedTypes.first; + const Json::Value& changedTypes = termsWithChangedTypes.second; if (valueIsBool && extraOption.useFormula) - optionValue = "~ " + generateInteractionTerms(terms); + optionValue = "~ " + generateInteractionTerms(terms, changedTypes); else { optionValue = "list("; - bool first = true; - for (const Term& term : _model->terms()) + int i = 0; + for (const Term& term : terms) { - if (!first) optionValue += ", "; - first = false; + const Json::Value& changedType = changedTypes.size() > i ? changedTypes[i] : Json::nullValue; + if (i > 0) optionValue += ", "; - if (valueIsBool) optionValue += FormulaParser::transformToFormulaTerm(term, FormulaParser::allInterationsSeparator, true); - else optionValue += FormulaParser::transformToFormulaTerm(term, FormulaParser::allInterationsSeparator, true) + " = " + RSyntax::transformJsonToR(boundControl->boundValue()); + if (valueIsBool) optionValue += FormulaParser::transformToFormulaTerm(term, changedType, FormulaParser::allInterationsSeparator, true); + else optionValue += FormulaParser::transformToFormulaTerm(term, changedType, FormulaParser::allInterationsSeparator, true) + " = " + RSyntax::transformJsonToR(boundControl->boundValue()); + i++; } optionValue += ")"; } diff --git a/QMLComponents/rsyntax/formulasource.h b/QMLComponents/rsyntax/formulasource.h index e95b047f77..605f91cee9 100644 --- a/QMLComponents/rsyntax/formulasource.h +++ b/QMLComponents/rsyntax/formulasource.h @@ -66,7 +66,7 @@ class FormulaSource : public QObject static const QString interceptTerm; static QVector makeFormulaSources(FormulaBase* formula, const QVariant& var); - static QString generateInteractionTerms(const Terms& terms); + static QString generateInteractionTerms(const Terms& terms, const Json::Value& changedTypes); const QString& sourceName() const { return _sourceName; } QStringList modelSources() const; @@ -82,8 +82,8 @@ class FormulaSource : public QObject void _addError(const QString& error) const; std::string _controlToOptionName(const QString& name) const; QString _generateRandomEffectsTerms(const Terms& terms) const; - QString _generateSimpleTerms(const Terms& terms) const; - Terms _onlyTrueTerms(const QString& controlName, const Terms& terms) const; + QString _generateSimpleTerms(const Terms& terms, const Json::Value& types) const; + std::pair _onlyTrueTerms(const QString& controlName) const; bool _areTermsInOptions(ListModelAssignedInterface* model, const Json::Value& options, Terms& terms) const; void _addTermsToOptions(ListModelAssignedInterface* model, Json::Value& options, const Terms& terms) const; FormulaParser::ParsedTerms _fillOptionsWithFixedTerms(ListModel* model, const FormulaParser::ParsedTerms &parsedTerms, Json::Value &options, QMap* termsMap = nullptr) const; diff --git a/QMLComponents/rsyntax/rsyntax.cpp b/QMLComponents/rsyntax/rsyntax.cpp index 1bb572799a..c338fd7487 100644 --- a/QMLComponents/rsyntax/rsyntax.cpp +++ b/QMLComponents/rsyntax/rsyntax.cpp @@ -123,12 +123,13 @@ QString RSyntax::generateSyntax(bool showAllOptions, bool useHtml) const // Check whether there are really different. if (!showAllOptions && defaultValue.isNumeric() && foundValue.isNumeric()) isDifferent = !qFuzzyCompare(defaultValue.asDouble(), foundValue.asDouble()); + if (isDifferent) { result += "," + newLine + indent + getRSyntaxFromControlName(control) + " = "; JASPListControl* listControl = qobject_cast(control); - if (listControl && !listControl->hasRowComponent() && listControl->containsInteractions()) + if (listControl && !listControl->hasRowComponent() && listControl->containsVariables() && listControl->useTermsInRSyntax()) result += _transformInteractionTerms(listControl->model()); else result += transformJsonToR(foundValue); @@ -145,7 +146,7 @@ QString RSyntax::generateWrapper() const { QString result = "\ #\n\ -# Copyright (C) 2013-2022 University of Amsterdam\n\ +# Copyright (C) 2013-2024 University of Amsterdam\n\ #\n\ # This program 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\ @@ -435,7 +436,7 @@ QString RSyntax::_transformInteractionTerms(ListModel* model) const if (terms.size() == 0) return "NULL"; - if (_areTermsVariables(model, terms)) return "~ " + FormulaSource::generateInteractionTerms(terms); + if (_areTermsVariables(model, terms) && model->listView()->mayUseFormula()) return "~ " + FormulaSource::generateInteractionTerms(terms, model->getVariableTypes(true)); QString result = "list("; bool first = true; @@ -462,6 +463,7 @@ QString RSyntax::_transformInteractionTerms(ListModel* model) const result += ")"; + return result; }