diff --git a/offapi/UnoApi_offapi.mk b/offapi/UnoApi_offapi.mk index 959f805d3894..6ff00937bd81 100644 --- a/offapi/UnoApi_offapi.mk +++ b/offapi/UnoApi_offapi.mk @@ -1245,6 +1245,7 @@ $(eval $(call gb_UnoApi_add_idlfiles_noheader,offapi,com/sun/star/sheet,\ SheetRangesQuery \ SheetSortDescriptor \ SheetSortDescriptor2 \ + SolverSettings \ Spreadsheet \ SpreadsheetDocument \ SpreadsheetDocumentSettings \ @@ -3457,6 +3458,7 @@ $(eval $(call gb_UnoApi_add_idlfiles,offapi,com/sun/star/sheet,\ LocalizedName \ MemberResult \ MemberResultFlags \ + ModelConstraint \ MoveDirection \ NamedRangeFlag \ NameToken \ @@ -3469,6 +3471,8 @@ $(eval $(call gb_UnoApi_add_idlfiles,offapi,com/sun/star/sheet,\ SingleReference \ SolverConstraint \ SolverConstraintOperator \ + SolverStatus \ + SolverObjectiveType \ SpreadsheetViewObjectsMode \ StatusBarFunction \ SubTotalColumn \ @@ -3590,6 +3594,7 @@ $(eval $(call gb_UnoApi_add_idlfiles,offapi,com/sun/star/sheet,\ XSheetPastable \ XSolver \ XSolverDescription \ + XSolverSettings \ XSpreadsheet \ XSpreadsheetDocument \ XSpreadsheetView \ diff --git a/offapi/com/sun/star/sheet/ModelConstraint.idl b/offapi/com/sun/star/sheet/ModelConstraint.idl new file mode 100644 index 000000000000..75b3b54e0a33 --- /dev/null +++ b/offapi/com/sun/star/sheet/ModelConstraint.idl @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + + +module com { module sun { module star { module sheet { + + +/** This is used to specify a constraint for a solver model. + * In this struct, both sides of the model constraint can be expressed as cell ranges. + * Note that com::sun::star::sheet::SolverConstraint only supports single cell addresses + * + * @since LibreOffice 25.2 + */ +struct ModelConstraint +{ + /** The range of cells in the left side of the constraint + * Must be either: + * 1) com::sun::star::table::CellRangeAddress + * 2) A valid string representing a cell range + */ + any Left; + + /** Constraint operator + */ + SolverConstraintOperator Operator; + + /** The range of cells or numeric value in the right side of the constraint + * Must be either: + * 1) com::sun::star::table::CellRangeAddress + * 2) A valid string representing a cell range + * 3) A numeric value + */ + any Right; +}; + + +}; }; }; }; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/offapi/com/sun/star/sheet/SolverObjectiveType.idl b/offapi/com/sun/star/sheet/SolverObjectiveType.idl new file mode 100644 index 000000000000..2c6a308974a4 --- /dev/null +++ b/offapi/com/sun/star/sheet/SolverObjectiveType.idl @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + + +module com { module sun { module star { module sheet { + + +/** Specifies the objective type of the solver model + * + * @since LibreOffice 25.2 + */ +constants SolverObjectiveType +{ + /// Maximizes the objective function. + const byte MAXIMIZE = 0; + + /// Minimizes the objective function. + const byte MINIMIZE = 1; + + /// Finds a solution where the objective function has a given value + const byte VALUE = 2; +}; + + +}; }; }; }; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/offapi/com/sun/star/sheet/SolverSettings.idl b/offapi/com/sun/star/sheet/SolverSettings.idl new file mode 100644 index 000000000000..473281cfd562 --- /dev/null +++ b/offapi/com/sun/star/sheet/SolverSettings.idl @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +module com { module sun { module star { module sheet { + + +/** A solver model defined in a sheet and accessible via the solver dialog + * + * @since LibreOffice 25.2 + */ +service SolverSettings: XSolverSettings; + + +}; }; }; }; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/offapi/com/sun/star/sheet/SolverStatus.idl b/offapi/com/sun/star/sheet/SolverStatus.idl new file mode 100644 index 000000000000..ec3fb2588a4e --- /dev/null +++ b/offapi/com/sun/star/sheet/SolverStatus.idl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + + +module com { module sun { module star { module sheet { + + +/** Describes the status reported by the solver + * Used in com::sun::star::sheet::XSolverSettings + * + * @since LibreOffice 25.2 + */ +constants SolverStatus +{ + /// No status to report. Happens when the solver has not been called. + const byte NONE = 0; + + /// The solver finished running and a solution was successfully found + const byte SOLUTION_FOUND = 1; + + /// A parse error occurred after running the solver + const byte PARSE_ERROR = 2; + + /// An error occurred while loading or running the solver + const byte ENGINE_ERROR = 3; + + /// The solver finished running but a solution was not found + const byte SOLUTION_NOT_FOUND = 4; +}; + + +}; }; }; }; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/offapi/com/sun/star/sheet/XSolverSettings.idl b/offapi/com/sun/star/sheet/XSolverSettings.idl new file mode 100644 index 000000000000..2291a27768a4 --- /dev/null +++ b/offapi/com/sun/star/sheet/XSolverSettings.idl @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + + +module com { module sun { module star { module sheet { + + +/** Provides access to the solver settings stored in a specific Calc sheet + * + * @since LibreOffice 25.2 + */ +interface XSolverSettings: com::sun::star::uno::XInterface +{ + /** Determines the model objective function type + Possible values defined in css::sheet::SolverObjectiveType + */ + [attribute] byte ObjectiveType; + + /** Cell pointing to the objective function of the model + */ + [attribute] any ObjectiveCell; + + /** Value or cell address used in the "Value of" field. + To set this attribute, the value must be either: + 1) a numeric value; + 2) a string representing the address of a single cell or; + 3) a CellAddress struct instance + */ + [attribute] any GoalValue; + + /** Set the solver engine using its implementation name + */ + [attribute] string Engine; + + /** Returns a sequence of strings containing all available solver implementation names + */ + [attribute, readonly] sequence AvailableEngines; + + /** Variable cells in the model, represented as a sequence of cell ranges + */ + [attribute] sequence VariableCells; + + /** Constraints of the solver model + */ + [attribute] sequence Constraints; + + /** Returns the number of constraints in the solver model + */ + [attribute, readonly] long ConstraintCount; + + /** Solver engine options expressed as a sequence of property values. + Each solver engine has its own set of supported properties. + Only the options supported by the current solver engine are returned. + */ + [attribute] sequence EngineOptions; + + /** Last reported solver status. This can be used f.i. to check if an + error occurred or if a solution was found. + Possible values defined in css::sheet::SolverStatus + */ + [attribute, readonly] byte Status; + + /** Stores the last error message reported after calling the "solve()" method + */ + [attribute, readonly] string ErrorMessage; + + /** Set this attrivute to True to suppress dialogs shown the method "solve()" + */ + [attribute] boolean SuppressDialog; + + /** Resets the solver model to its defaults settings + */ + void reset(); + + /** Run the solver model + Check the values of the Status and ErrorMessage attributes to know + what happened after calling this method + */ + void solve(); + + /** Save the solver model to the file. + The next time the file is loaded, the model will be in the sheet where it was created. + */ + void saveToFile(); +}; + + +}; }; }; }; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sc/CppunitTest_sc_solverobj.mk b/sc/CppunitTest_sc_solverobj.mk new file mode 100644 index 000000000000..403e0774f8ed --- /dev/null +++ b/sc/CppunitTest_sc_solverobj.mk @@ -0,0 +1,14 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +#************************************************************************* +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +#************************************************************************* + +$(eval $(call sc_unoapi_common,solverobj)) + +# vim: set noet sw=4 ts=4: diff --git a/sc/Library_sc.mk b/sc/Library_sc.mk index 09dc856b3eae..71103f726be8 100644 --- a/sc/Library_sc.mk +++ b/sc/Library_sc.mk @@ -616,6 +616,7 @@ $(eval $(call gb_Library_add_exception_objects,sc,\ sc/source/ui/unoobj/ChartTools \ sc/source/ui/unoobj/servuno \ sc/source/ui/unoobj/shapeuno \ + sc/source/ui/unoobj/solveruno \ sc/source/ui/unoobj/srchuno \ sc/source/ui/unoobj/styleuno \ sc/source/ui/unoobj/targuno \ diff --git a/sc/Module_sc.mk b/sc/Module_sc.mk index ee4f23eeab0a..88172016d52b 100644 --- a/sc/Module_sc.mk +++ b/sc/Module_sc.mk @@ -224,6 +224,7 @@ $(eval $(call gb_Module_add_subsequentcheck_targets,sc,\ CppunitTest_sc_shapeobj \ CppunitTest_sc_sheetlinkobj \ CppunitTest_sc_sheetlinksobj \ + CppunitTest_sc_solverobj \ CppunitTest_sc_sortdescriptorbaseobj \ CppunitTest_sc_spreadsheetsettings \ CppunitTest_sc_spreadsheetsettingsobj \ diff --git a/sc/inc/SolverSettings.hxx b/sc/inc/SolverSettings.hxx index dd5afc5211fa..b9f59aa1ba3f 100644 --- a/sc/inc/SolverSettings.hxx +++ b/sc/inc/SolverSettings.hxx @@ -295,7 +295,8 @@ public: SC_DLLPUBLIC ObjectiveType GetObjectiveType() { return m_eObjType; } SC_DLLPUBLIC void SetObjectiveType(ObjectiveType eType); SC_DLLPUBLIC void GetEngineOptions(css::uno::Sequence& aOptions); - SC_DLLPUBLIC void SetEngineOptions(css::uno::Sequence& aOptions); + SC_DLLPUBLIC void + SetEngineOptions(const css::uno::Sequence& aOptions); SC_DLLPUBLIC std::vector GetConstraints() { return m_aConstraints; } SC_DLLPUBLIC void SetConstraints(std::vector aConstraints); diff --git a/sc/inc/globstr.hrc b/sc/inc/globstr.hrc index 49a186325f46..76f1859c51c4 100644 --- a/sc/inc/globstr.hrc +++ b/sc/inc/globstr.hrc @@ -522,7 +522,11 @@ #define STR_NOFORMULA NC_("STR_NOFORMULA", "Formula cell must contain a formula.") #define STR_SOLVER_ENGINE_ERROR NC_("STR_SOLVER_ENGINE_ERROR", "An internal error occurred while running the solver engine.") #define STR_INVALIDINPUT NC_("STR_INVALIDINPUT", "Invalid input.") -#define STR_INVALIDCONDITION NC_("STR_INVALIDCONDITION", "Invalid condition.") +#define STR_INVALIDCONDITION NC_("STR_INVALIDCONDITION", "Invalid constraint.") +#define STR_SOLVER_LOAD_FAIL NC_("STR_SOLVER_LOAD_FAIL", "Unable to load solver engine.") +#define STR_SOLVER_OBJCELL_FAIL NC_("STR_SOLVER_OBJCELL_FAIL", "Invalid objective cell.") +#define STR_SOLVER_VARCELL_FAIL NC_("STR_SOLVER_VARCELL_FAIL", "Invalid variable cells.") +#define STR_SOLVER_TARGETVALUE_FAIL NC_("STR_SOLVER_TARGETVALUE_FAIL", "Invalid target value.") #define STR_QUERYREMOVE NC_("STR_QUERYREMOVE", "Should the entry\n#\nbe deleted?") #define STR_COPYLIST NC_("STR_COPYLIST", "Copy List") #define STR_COPYFROM NC_("STR_COPYFROM", "List from") diff --git a/sc/inc/solveruno.hxx b/sc/inc/solveruno.hxx new file mode 100644 index 000000000000..cf13777168f6 --- /dev/null +++ b/sc/inc/solveruno.hxx @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace com::sun::star::container +{ +class XNamed; +} + +namespace sc +{ +class SolverSettings; +} + +class ScDocument; +class ScDocShell; +class ScTable; +class ScRange; +class ScRangeList; + +class ScSolverSettings final + : public ::cppu::WeakImplHelper +{ +private: + ScDocShell* m_pDocShell; + ScDocument& m_rDoc; + css::uno::Reference m_xSheet; + // Status uses constants in css::uno::sheet::SolverStatus + sal_Int8 m_nStatus; + bool m_bSuppressDialog; + OUString m_sErrorMessage; + ScTable* m_pTable; + std::shared_ptr m_pSettings; + + // Parses a reference string (named ranges are also parsed) + // If bAllowRange is "false" then only single cell ranges are acceptable, + // which is the case of the objective cell + bool ParseRef(ScRange& rRange, const OUString& rInput, bool bAllowRange); + + // Parses a reference string composed of various ranges + bool ParseWithNames(ScRangeList& rRanges, std::u16string_view rInput); + + static void ShowErrorMessage(const OUString& rMessage); + +public: + ScSolverSettings(ScDocShell* pDocSh, css::uno::Reference xSheet); + ~ScSolverSettings(); + + // XSolverSettings attributes + virtual sal_Int8 SAL_CALL getObjectiveType() override; + virtual void SAL_CALL setObjectiveType(sal_Int8 aObjType) override; + virtual css::uno::Any SAL_CALL getObjectiveCell() override; + virtual void SAL_CALL setObjectiveCell(const css::uno::Any& aValue) override; + virtual css::uno::Any SAL_CALL getGoalValue() override; + virtual void SAL_CALL setGoalValue(const css::uno::Any& aValue) override; + virtual OUString SAL_CALL getEngine() override; + virtual void SAL_CALL setEngine(const OUString& sEngine) override; + virtual css::uno::Sequence SAL_CALL getAvailableEngines() override; + virtual css::uno::Sequence SAL_CALL getVariableCells() override; + virtual void SAL_CALL + setVariableCells(const css::uno::Sequence& aRanges) override; + virtual css::uno::Sequence SAL_CALL getConstraints() override; + virtual void SAL_CALL + setConstraints(const css::uno::Sequence& aConstraints) override; + virtual sal_Int32 SAL_CALL getConstraintCount() override; + virtual css::uno::Sequence SAL_CALL getEngineOptions() override; + virtual void SAL_CALL + setEngineOptions(const css::uno::Sequence& rProps) override; + virtual sal_Int8 SAL_CALL getStatus() override; + virtual OUString SAL_CALL getErrorMessage() override; + virtual sal_Bool SAL_CALL getSuppressDialog() override; + virtual void SAL_CALL setSuppressDialog(sal_Bool bSuppress) override; + + // XSolverSettings methods + virtual void SAL_CALL reset() override; + virtual void SAL_CALL solve() override; + virtual void SAL_CALL saveToFile() override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService(const OUString& ServiceName) override; + virtual css::uno::Sequence SAL_CALL getSupportedServiceNames() override; +}; diff --git a/sc/inc/unonames.hxx b/sc/inc/unonames.hxx index 13bb2600f471..916fd839d4b7 100644 --- a/sc/inc/unonames.hxx +++ b/sc/inc/unonames.hxx @@ -48,6 +48,7 @@ inline constexpr OUString SC_UNO_SHEETLINKS = u"SheetLinks"_ustr; inline constexpr OUString SC_UNO_FORBIDDEN = u"ForbiddenCharacters"_ustr; inline constexpr OUString SC_UNO_HASDRAWPAGES = u"HasDrawPages"_ustr; inline constexpr OUString SC_UNO_THEME = u"Theme"_ustr; +inline constexpr OUString SC_UNO_SOLVERSETTINGS = u"SolverSettings"_ustr; // CharacterProperties inline constexpr OUString SC_UNONAME_CCOLOR = u"CharColor"_ustr; diff --git a/sc/inc/unowids.hxx b/sc/inc/unowids.hxx index cd1d6baf6ab8..b5de8f1f292c 100644 --- a/sc/inc/unowids.hxx +++ b/sc/inc/unowids.hxx @@ -74,7 +74,8 @@ #define SC_WID_UNO_FORMATID ( SC_WID_UNO_START + 45 ) #define SC_WID_UNO_FORMRT2 ( SC_WID_UNO_START + 46 ) #define SC_WID_UNO_CELLCONTENTTYPE ( SC_WID_UNO_START + 47 ) -#define SC_WID_UNO_END ( SC_WID_UNO_START + 47 ) +#define SC_WID_UNO_SOLVERSETTINGS ( SC_WID_UNO_START + 48 ) +#define SC_WID_UNO_END ( SC_WID_UNO_START + 48 ) inline bool IsScUnoWid( sal_uInt16 nWid ) { diff --git a/sc/qa/extras/scsolverobj.cxx b/sc/qa/extras/scsolverobj.cxx new file mode 100644 index 000000000000..1d66d81edfba --- /dev/null +++ b/sc/qa/extras/scsolverobj.cxx @@ -0,0 +1,218 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace css; + +namespace sc_apitest +{ +class ScSolverSettingsObj : public UnoApiTest +{ +public: + ScSolverSettingsObj(); + + virtual void setUp() override; + void testXSolverSettings(); + void testCellAddress(const table::CellAddress& rExpected, const uno::Any& rActual); + void testCellRangeAddress(const uno::Any& rExpected, const uno::Any& rActual); + + CPPUNIT_TEST_SUITE(ScSolverSettingsObj); + CPPUNIT_TEST(testXSolverSettings); + CPPUNIT_TEST_SUITE_END(); +}; + +ScSolverSettingsObj::ScSolverSettingsObj() + : UnoApiTest(u"/sc/qa/extras/testdocuments"_ustr) +{ +} + +void ScSolverSettingsObj::testCellAddress(const table::CellAddress& rExpected, + const uno::Any& rActual) +{ + table::CellAddress aActualAddress; + rActual >>= aActualAddress; + CPPUNIT_ASSERT_EQUAL(rExpected.Column, aActualAddress.Column); + CPPUNIT_ASSERT_EQUAL(rExpected.Row, aActualAddress.Row); + CPPUNIT_ASSERT_EQUAL(rExpected.Sheet, aActualAddress.Sheet); +} + +void ScSolverSettingsObj::testCellRangeAddress(const uno::Any& rExpected, const uno::Any& rActual) +{ + table::CellRangeAddress aActualAddress; + table::CellRangeAddress aExpectedAddress; + rActual >>= aActualAddress; + rExpected >>= aExpectedAddress; + CPPUNIT_ASSERT_EQUAL(aExpectedAddress.Sheet, aActualAddress.Sheet); + CPPUNIT_ASSERT_EQUAL(aExpectedAddress.StartRow, aActualAddress.StartRow); + CPPUNIT_ASSERT_EQUAL(aExpectedAddress.StartColumn, aActualAddress.StartColumn); + CPPUNIT_ASSERT_EQUAL(aExpectedAddress.EndRow, aActualAddress.EndRow); + CPPUNIT_ASSERT_EQUAL(aExpectedAddress.EndColumn, aActualAddress.EndColumn); +} + +// Creates a model using the XSolverSettings API checks if it is accessible via the API +void ScSolverSettingsObj::testXSolverSettings() +{ + uno::Reference xDoc(mxComponent, uno::UNO_QUERY_THROW); + uno::Reference xIndex(xDoc->getSheets(), uno::UNO_QUERY_THROW); + uno::Reference xSheet(xIndex->getByIndex(0), uno::UNO_QUERY_THROW); + uno::Reference xPropSet(xSheet, uno::UNO_QUERY_THROW); + uno::Reference xSolverModel( + xPropSet->getPropertyValue("SolverSettings"), uno::UNO_QUERY_THROW); + + // Objective cell is in G7 + table::CellAddress aObjCell(0, 6, 6); + xSolverModel->setObjectiveCell(uno::Any(aObjCell)); + xSolverModel->setObjectiveType(sheet::SolverObjectiveType::MAXIMIZE); + + // Variable cells (E3:E17) + uno::Sequence aVarCells(1); + auto pVarCells = aVarCells.getArray(); + pVarCells[0] <<= table::CellRangeAddress(0, 4, 2, 4, 16); + xSolverModel->setVariableCells(aVarCells); + + // Constraints + uno::Sequence aConstraints(2); + auto pConstraints = aConstraints.getArray(); + pConstraints[0].Left <<= OUString("$E$3:$E$17"); + pConstraints[0].Operator = sheet::SolverConstraintOperator_BINARY; + pConstraints[1].Left <<= OUString("$G$5"); + pConstraints[1].Operator = sheet::SolverConstraintOperator_LESS_EQUAL; + pConstraints[1].Right <<= OUString("$G$3"); + xSolverModel->setConstraints(aConstraints); + + // Set solver engine options + xSolverModel->setEngine(u"com.sun.star.comp.Calc.LpsolveSolver"_ustr); + uno::Sequence aEngineOptions{ + comphelper::makePropertyValue(u"Timeout"_ustr, uno::Any(static_cast(10))), + comphelper::makePropertyValue(u"NonNegative"_ustr, true), + }; + xSolverModel->setEngineOptions(aEngineOptions); + + // Run the model and check the results + xSolverModel->saveToFile(); + xSolverModel->setSuppressDialog(true); + xSolverModel->solve(); + + // The correct solution value is 6981 (using LpsolveSolver) + CPPUNIT_ASSERT_EQUAL(static_cast(6981), xSheet->getCellByPosition(6, 6)->getValue()); + + // Check objective function and variable cells + testCellAddress(aObjCell, xSolverModel->getObjectiveCell()); + CPPUNIT_ASSERT_EQUAL(sheet::SolverObjectiveType::MAXIMIZE, xSolverModel->getObjectiveType()); + uno::Sequence aSeq = xSolverModel->getVariableCells(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), aSeq.getLength()); + testCellRangeAddress(aVarCells[0], aSeq[0]); + + // Check if constraints were set + uno::Sequence aSeqConstr = xSolverModel->getConstraints(); + CPPUNIT_ASSERT_EQUAL(static_cast(2), aSeqConstr.getLength()); + CPPUNIT_ASSERT_EQUAL(sheet::SolverConstraintOperator::SolverConstraintOperator_BINARY, + aSeqConstr[0].Operator); + table::CellRangeAddress aLeft1(0, 4, 2, 4, 16); + table::CellRangeAddress aRight1(0, 0, 0, 0, 0); + testCellRangeAddress(uno::Any(aLeft1), aSeqConstr[0].Left); + testCellRangeAddress(uno::Any(aRight1), aSeqConstr[0].Right); + CPPUNIT_ASSERT_EQUAL(sheet::SolverConstraintOperator::SolverConstraintOperator_LESS_EQUAL, + aSeqConstr[1].Operator); + table::CellRangeAddress aLeft2(0, 6, 4, 6, 4); + table::CellRangeAddress aRight2(0, 6, 2, 6, 2); + testCellRangeAddress(uno::Any(aLeft2), aSeqConstr[1].Left); + testCellRangeAddress(uno::Any(aRight2), aSeqConstr[1].Right); + + // Check solver engine options + CPPUNIT_ASSERT_EQUAL(u"com.sun.star.comp.Calc.LpsolveSolver"_ustr, xSolverModel->getEngine()); + + // Check solver engine options + uno::Sequence aEngProps = xSolverModel->getEngineOptions(); + CPPUNIT_ASSERT_EQUAL(OUString("EpsilonLevel"), aEngProps[0].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(static_cast(0)), aEngProps[0].Value); + CPPUNIT_ASSERT_EQUAL(u"Integer"_ustr, aEngProps[1].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(false), aEngProps[1].Value); + CPPUNIT_ASSERT_EQUAL(u"LimitBBDepth"_ustr, aEngProps[2].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(true), aEngProps[2].Value); + CPPUNIT_ASSERT_EQUAL(u"NonNegative"_ustr, aEngProps[3].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(true), aEngProps[3].Value); + CPPUNIT_ASSERT_EQUAL(u"Timeout"_ustr, aEngProps[4].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(static_cast(10)), aEngProps[4].Value); + + // Save file and reload to check if solver settings are still there + saveAndReload(u"calc8"_ustr); + uno::Reference xDoc2(mxComponent, uno::UNO_QUERY_THROW); + uno::Reference xIndex2(xDoc2->getSheets(), uno::UNO_QUERY_THROW); + uno::Reference xSheet2(xIndex2->getByIndex(0), uno::UNO_QUERY_THROW); + uno::Reference xPropSet2(xSheet2, uno::UNO_QUERY_THROW); + uno::Reference xSolverModel2( + xPropSet2->getPropertyValue("SolverSettings"), uno::UNO_QUERY_THROW); + + // // Check objective function + testCellAddress(aObjCell, xSolverModel2->getObjectiveCell()); + CPPUNIT_ASSERT_EQUAL(sheet::SolverObjectiveType::MAXIMIZE, xSolverModel2->getObjectiveType()); + + // Check variable cells + uno::Sequence aVarCells2 = xSolverModel2->getVariableCells(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), aVarCells2.getLength()); + testCellRangeAddress(aVarCells[0], aVarCells2[0]); + + // Check constraints + uno::Sequence aSeqConstr2 = xSolverModel2->getConstraints(); + CPPUNIT_ASSERT_EQUAL(static_cast(2), aSeqConstr2.getLength()); + CPPUNIT_ASSERT_EQUAL(sheet::SolverConstraintOperator::SolverConstraintOperator_BINARY, + aSeqConstr2[0].Operator); + testCellRangeAddress(uno::Any(aLeft1), aSeqConstr2[0].Left); + testCellRangeAddress(uno::Any(aRight1), aSeqConstr2[0].Right); + CPPUNIT_ASSERT_EQUAL(sheet::SolverConstraintOperator::SolverConstraintOperator_LESS_EQUAL, + aSeqConstr2[1].Operator); + testCellRangeAddress(uno::Any(aLeft2), aSeqConstr2[1].Left); + testCellRangeAddress(uno::Any(aRight2), aSeqConstr2[1].Right); + + // Check solver engine options + uno::Sequence aEngProps2 = xSolverModel2->getEngineOptions(); + CPPUNIT_ASSERT_EQUAL(OUString("EpsilonLevel"), aEngProps2[0].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(static_cast(0)), aEngProps2[0].Value); + CPPUNIT_ASSERT_EQUAL(u"Integer"_ustr, aEngProps2[1].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(false), aEngProps2[1].Value); + CPPUNIT_ASSERT_EQUAL(u"LimitBBDepth"_ustr, aEngProps2[2].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(true), aEngProps2[2].Value); + CPPUNIT_ASSERT_EQUAL(u"NonNegative"_ustr, aEngProps2[3].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(true), aEngProps2[3].Value); + CPPUNIT_ASSERT_EQUAL(u"Timeout"_ustr, aEngProps2[4].Name); + CPPUNIT_ASSERT_EQUAL(uno::Any(static_cast(10)), aEngProps2[4].Value); +} + +void ScSolverSettingsObj::setUp() +{ + UnoApiTest::setUp(); + loadFromFile(u"knapsack.ods"); +} + +CPPUNIT_TEST_SUITE_REGISTRATION(ScSolverSettingsObj); + +} // namespace sc_apitest + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sc/qa/extras/testdocuments/knapsack.ods b/sc/qa/extras/testdocuments/knapsack.ods new file mode 100644 index 000000000000..b45b91fd3760 Binary files /dev/null and b/sc/qa/extras/testdocuments/knapsack.ods differ diff --git a/sc/source/core/data/SolverSettings.cxx b/sc/source/core/data/SolverSettings.cxx index 9917cdcc59e7..bf325c204b43 100644 --- a/sc/source/core/data/SolverSettings.cxx +++ b/sc/source/core/data/SolverSettings.cxx @@ -685,7 +685,7 @@ void SolverSettings::GetEngineOptions(css::uno::Sequence& aOptions) +void SolverSettings::SetEngineOptions(const css::uno::Sequence& aOptions) { sal_Int32 nOptionsSize = aOptions.getLength(); diff --git a/sc/source/ui/unoobj/cellsuno.cxx b/sc/source/ui/unoobj/cellsuno.cxx index 4d4bf1a03c5a..248c1763badf 100644 --- a/sc/source/ui/unoobj/cellsuno.cxx +++ b/sc/source/ui/unoobj/cellsuno.cxx @@ -66,6 +66,7 @@ #include #include #include +#include #include #include @@ -138,6 +139,7 @@ #include #include #include +#include #include @@ -784,6 +786,7 @@ static const SfxItemPropertySet* lcl_GetSheetPropertySet() { SC_UNONAME_TABCOLOR, SC_WID_UNO_TABCOLOR, cppu::UnoType::get(), 0, 0 }, { SC_UNO_CODENAME, SC_WID_UNO_CODENAME, cppu::UnoType::get(), 0, 0}, { SC_UNO_NAMEDRANGES, SC_WID_UNO_NAMES, cppu::UnoType::get(), 0, 0 }, + { SC_UNO_SOLVERSETTINGS, SC_WID_UNO_SOLVERSETTINGS, cppu::UnoType::get(), 0, 0 }, }; static SfxItemPropertySet aSheetPropertySet( aSheetPropertyMap_Impl ); return &aSheetPropertySet; @@ -8203,6 +8206,10 @@ void ScTableSheetObj::GetOnePropertyValue( const SfxItemPropertyMapEntry* pEntry { rAny <<= uno::Reference(new ScCondFormatsObj(pDocSh, nTab)); } + else if (pEntry->nWID == SC_WID_UNO_SOLVERSETTINGS) + { + rAny <<= uno::Reference(new ScSolverSettings(pDocSh, this)); + } else ScCellRangeObj::GetOnePropertyValue(pEntry, rAny); } diff --git a/sc/source/ui/unoobj/solveruno.cxx b/sc/source/ui/unoobj/solveruno.cxx new file mode 100644 index 000000000000..69a8396480fb --- /dev/null +++ b/sc/source/ui/unoobj/solveruno.cxx @@ -0,0 +1,988 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace css; + +constexpr OUString SC_SOLVERSETTINGS_SERVICE = u"com.sun.star.sheet.SolverSettings"_ustr; + +namespace +{ +// Returns the sc::ConstraintOperator equivalent to the Uno operator +sc::ConstraintOperator getScOperatorFromUno(sheet::SolverConstraintOperator aOperator) +{ + sc::ConstraintOperator aRet(sc::ConstraintOperator::CO_LESS_EQUAL); + + switch (aOperator) + { + case sheet::SolverConstraintOperator_EQUAL: + aRet = sc::ConstraintOperator::CO_EQUAL; + break; + case sheet::SolverConstraintOperator_GREATER_EQUAL: + aRet = sc::ConstraintOperator::CO_GREATER_EQUAL; + break; + case sheet::SolverConstraintOperator_BINARY: + aRet = sc::ConstraintOperator::CO_BINARY; + break; + case sheet::SolverConstraintOperator_INTEGER: + aRet = sc::ConstraintOperator::CO_INTEGER; + break; + default: + { + // This should never be reached + } + } + return aRet; +} + +// Returns the sheet::SolverConstraintOperator equivalent to sc::ConstraintOperator +sheet::SolverConstraintOperator getUnoOperatorFromSc(sc::ConstraintOperator nOperator) +{ + sheet::SolverConstraintOperator aRet(sheet::SolverConstraintOperator_LESS_EQUAL); + + switch (nOperator) + { + case sc::ConstraintOperator::CO_EQUAL: + aRet = sheet::SolverConstraintOperator_EQUAL; + break; + case sc::ConstraintOperator::CO_GREATER_EQUAL: + aRet = sheet::SolverConstraintOperator_GREATER_EQUAL; + break; + case sc::ConstraintOperator::CO_BINARY: + aRet = sheet::SolverConstraintOperator_BINARY; + break; + case sc::ConstraintOperator::CO_INTEGER: + aRet = sheet::SolverConstraintOperator_INTEGER; + break; + default: + { + // This should never be reached + } + } + return aRet; +} + +// Returns the CellRangeAddress struct from a ScRange +table::CellRangeAddress getRangeAddress(ScRange aRange) +{ + table::CellRangeAddress aRet; + aRet.Sheet = aRange.aStart.Tab(); + aRet.StartColumn = aRange.aStart.Col(); + aRet.StartRow = aRange.aStart.Row(); + aRet.EndColumn = aRange.aEnd.Col(); + aRet.EndRow = aRange.aEnd.Row(); + return aRet; +} + +// Tests if a string is a valid number +bool isValidNumber(const OUString& sValue, double& fValue) +{ + if (sValue.isEmpty()) + return false; + + rtl_math_ConversionStatus eConvStatus; + sal_Int32 nEnd; + fValue = rtl::math::stringToDouble(sValue, ScGlobal::getLocaleData().getNumDecimalSep()[0], + ScGlobal::getLocaleData().getNumThousandSep()[0], + &eConvStatus, &nEnd); + // A conversion is only valid if nEnd is equal to the string length (all chars processed) + return nEnd == sValue.getLength(); +} +} + +ScSolverSettings::ScSolverSettings(ScDocShell* pDocSh, uno::Reference xSheet) + : m_pDocShell(pDocSh) + , m_rDoc(m_pDocShell->GetDocument()) + , m_xSheet(std::move(xSheet)) + , m_nStatus(sheet::SolverStatus::NONE) + , m_bSuppressDialog(false) +{ + // Initialize member variables with information about the current sheet + OUString aName = m_xSheet->getName(); + SCTAB nTab; + if (m_rDoc.GetTable(aName, nTab)) + { + m_pTable = m_rDoc.FetchTable(nTab); + m_pSettings = m_pTable->GetSolverSettings(); + } +} + +ScSolverSettings::~ScSolverSettings() {} + +bool ScSolverSettings::ParseRef(ScRange& rRange, const OUString& rInput, bool bAllowRange) +{ + ScAddress::Details aDetails(m_rDoc.GetAddressConvention(), 0, 0); + ScRefFlags nFlags = rRange.ParseAny(rInput, m_rDoc, aDetails); + SCTAB nCurTab(m_pTable->GetTab()); + if (nFlags & ScRefFlags::VALID) + { + if ((nFlags & ScRefFlags::TAB_3D) == ScRefFlags::ZERO) + rRange.aStart.SetTab(nCurTab); + if ((nFlags & ScRefFlags::TAB2_3D) == ScRefFlags::ZERO) + rRange.aEnd.SetTab(rRange.aStart.Tab()); + return (bAllowRange || rRange.aStart == rRange.aEnd); + } + else if (ScRangeUtil::MakeRangeFromName(rInput, m_rDoc, nCurTab, rRange, RUTL_NAMES, aDetails)) + return (bAllowRange || rRange.aStart == rRange.aEnd); + + return false; +} + +bool ScSolverSettings::ParseWithNames(ScRangeList& rRanges, std::u16string_view rInput) +{ + if (rInput.empty()) + return true; + + ScAddress::Details aDetails(m_rDoc.GetAddressConvention(), 0, 0); + SCTAB nCurTab(m_pTable->GetTab()); + sal_Unicode cDelimiter = ScCompiler::GetNativeSymbolChar(OpCode::ocSep); + bool bError = false; + sal_Int32 nIdx(0); + do + { + ScRange aRange; + OUString aRangeStr(o3tl::getToken(rInput, 0, cDelimiter, nIdx)); + ScRefFlags nFlags = aRange.ParseAny(aRangeStr, m_rDoc, aDetails); + if (nFlags & ScRefFlags::VALID) + { + if ((nFlags & ScRefFlags::TAB_3D) == ScRefFlags::ZERO) + aRange.aStart.SetTab(nCurTab); + if ((nFlags & ScRefFlags::TAB2_3D) == ScRefFlags::ZERO) + aRange.aEnd.SetTab(aRange.aStart.Tab()); + rRanges.push_back(aRange); + } + else if (ScRangeUtil::MakeRangeFromName(aRangeStr, m_rDoc, nCurTab, aRange, RUTL_NAMES, + aDetails)) + rRanges.push_back(aRange); + else + bError = true; + } while (nIdx > 0); + + return !bError; +} + +void ScSolverSettings::ShowErrorMessage(const OUString& rMessage) +{ + std::unique_ptr xBox(Application::CreateMessageDialog( + Application::GetDefDialogParent(), VclMessageType::Warning, VclButtonsType::Ok, rMessage)); + xBox->run(); +} + +// XSolverSettings +sal_Int8 SAL_CALL ScSolverSettings::getObjectiveType() +{ + sal_Int8 aRet(sheet::SolverObjectiveType::MAXIMIZE); + switch (m_pSettings->GetObjectiveType()) + { + case sc::ObjectiveType::OT_MINIMIZE: + aRet = sheet::SolverObjectiveType::MINIMIZE; + break; + case sc::ObjectiveType::OT_VALUE: + aRet = sheet::SolverObjectiveType::VALUE; + break; + default: + { + // This should never be reached + } + } + return aRet; +} + +void SAL_CALL ScSolverSettings::setObjectiveType(sal_Int8 aObjType) +{ + sc::ObjectiveType eType(sc::ObjectiveType::OT_MAXIMIZE); + switch (aObjType) + { + case sheet::SolverObjectiveType::MINIMIZE: + eType = sc::ObjectiveType::OT_MINIMIZE; + break; + case sheet::SolverObjectiveType::VALUE: + eType = sc::ObjectiveType::OT_VALUE; + break; + default: + { + // This should never be reached + } + } + m_pSettings->SetObjectiveType(eType); +} + +uno::Any SAL_CALL ScSolverSettings::getObjectiveCell() +{ + // The objective cell must be a valid cell address + OUString sValue(m_pSettings->GetParameter(sc::SolverParameter::SP_OBJ_CELL)); + + // Test if it is a valid cell reference; if so, return its CellAddress + ScRange aRange; + const formula::FormulaGrammar::AddressConvention eConv = m_rDoc.GetAddressConvention(); + bool bOk = (aRange.ParseAny(sValue, m_rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + if (bOk) + { + SCTAB nTab1, nTab2; + SCROW nRow1, nRow2; + SCCOL nCol1, nCol2; + aRange.GetVars(nCol1, nRow1, nTab1, nCol2, nRow2, nTab2); + table::CellAddress aAddress(nTab1, nCol1, nRow1); + return uno::Any(aAddress); + } + + // If converting to a CellAddress fails, returns the raw string + return uno::Any(sValue); +} + +// The value being set must be either a string referencing a single cell or +// a CellAddress instance +void SAL_CALL ScSolverSettings::setObjectiveCell(const uno::Any& aValue) +{ + // Check if a string value is being used + OUString sValue; + bool bIsString(aValue >>= sValue); + if (bIsString) + { + // The string must correspond to a valid range; if not, an empty string is set + ScRange aRange; + OUString sRet; + ScDocument& rDoc = m_pDocShell->GetDocument(); + const formula::FormulaGrammar::AddressConvention eConv = rDoc.GetAddressConvention(); + bool bOk = (aRange.ParseAny(sValue, rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + if (bOk) + { + SCTAB nTab1, nTab2; + SCROW nRow1, nRow2; + SCCOL nCol1, nCol2; + aRange.GetVars(nCol1, nRow1, nTab1, nCol2, nRow2, nTab2); + // The range must consist of a single cell + if (nTab1 == nTab2 && nCol1 == nCol2 && nRow1 == nRow2) + sRet = sValue; + } + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_CELL, sRet); + return; + } + + // Check if a CellAddress is being used + table::CellAddress aUnoAddress; + bool bIsAddress(aValue >>= aUnoAddress); + if (bIsAddress) + { + OUString sRet; + ScAddress aAdress(aUnoAddress.Column, aUnoAddress.Row, aUnoAddress.Sheet); + sRet = aAdress.Format(ScRefFlags::RANGE_ABS, &m_rDoc); + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_CELL, sRet); + return; + } + + // If all fails, set an empty string + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_CELL, ""); +} + +uno::Any SAL_CALL ScSolverSettings::getGoalValue() +{ + OUString sValue(m_pSettings->GetParameter(sc::SolverParameter::SP_OBJ_VAL)); + + // Test if it is a valid cell reference; if so, return its CellAddress + ScRange aRange; + const formula::FormulaGrammar::AddressConvention eConv = m_rDoc.GetAddressConvention(); + bool bOk = (aRange.ParseAny(sValue, m_rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + if (bOk) + { + SCTAB nTab1, nTab2; + SCROW nRow1, nRow2; + SCCOL nCol1, nCol2; + aRange.GetVars(nCol1, nRow1, nTab1, nCol2, nRow2, nTab2); + table::CellAddress aAddress(nTab1, nCol1, nRow1); + return uno::Any(aAddress); + } + + double fValue; + bool bValid = isValidNumber(sValue, fValue); + if (bValid) + return uno::Any(fValue); + + // If the conversion was not successfull, return "empty" + return uno::Any(); +} + +void SAL_CALL ScSolverSettings::setGoalValue(const uno::Any& aValue) +{ + // Check if a numeric value is being used + double fValue; + bool bIsDouble(aValue >>= fValue); + if (bIsDouble) + { + // The value must be set as a localized number + OUString sLocalizedValue = rtl::math::doubleToUString( + fValue, rtl_math_StringFormat_Automatic, rtl_math_DecimalPlaces_Max, + ScGlobal::getLocaleData().getNumDecimalSep()[0], true); + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_VAL, sLocalizedValue); + return; + } + + // Check if a string value is being used + OUString sValue; + bool bIsString(aValue >>= sValue); + if (bIsString) + { + // The string must correspond to a valid range; if not, an empty string is set + ScRange aRange; + OUString sRet; + ScDocument& rDoc = m_pDocShell->GetDocument(); + const formula::FormulaGrammar::AddressConvention eConv = rDoc.GetAddressConvention(); + bool bOk = (aRange.ParseAny(sValue, rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + if (bOk) + { + SCTAB nTab1, nTab2; + SCROW nRow1, nRow2; + SCCOL nCol1, nCol2; + aRange.GetVars(nCol1, nRow1, nTab1, nCol2, nRow2, nTab2); + // The range must consist of a single cell + if (nTab1 == nTab2 && nCol1 == nCol2 && nRow1 == nRow2) + sRet = sValue; + } + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_VAL, sRet); + return; + } + + // Check if a CellAddress is being used + table::CellAddress aUnoAddress; + bool bIsAddress(aValue >>= aUnoAddress); + if (bIsAddress) + { + OUString sRet; + ScAddress aAdress(aUnoAddress.Column, aUnoAddress.Row, aUnoAddress.Sheet); + sRet = aAdress.Format(ScRefFlags::RANGE_ABS, &m_rDoc); + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_VAL, sRet); + return; + } + + // If all fails, set an empty string + m_pSettings->SetParameter(sc::SolverParameter::SP_OBJ_VAL, ""); +} + +OUString SAL_CALL ScSolverSettings::getEngine() +{ + return m_pSettings->GetParameter(sc::SP_LO_ENGINE); +} + +void SAL_CALL ScSolverSettings::setEngine(const OUString& sEngine) +{ + // Only change the engine if the new engine exists; otherwise leave it unchanged + uno::Sequence arrEngineNames; + uno::Sequence arrDescriptions; + ScSolverUtil::GetImplementations(arrEngineNames, arrDescriptions); + if (comphelper::findValue(arrEngineNames, sEngine) == -1) + return; + + m_pSettings->SetParameter(sc::SP_LO_ENGINE, sEngine); +} + +uno::Sequence SAL_CALL ScSolverSettings::getAvailableEngines() +{ + uno::Sequence arrEngineNames; + uno::Sequence arrDescriptions; + ScSolverUtil::GetImplementations(arrEngineNames, arrDescriptions); + return arrEngineNames; +} + +uno::Sequence SAL_CALL ScSolverSettings::getVariableCells() +{ + // Variable cells parameter is stored as a single string composed of valid ranges + // separated using the formula separator character + OUString sVarCells(m_pSettings->GetParameter(sc::SP_VAR_CELLS)); + // Delimiter character to separate ranges + sal_Unicode cDelimiter = ScCompiler::GetNativeSymbolChar(OpCode::ocSep); + const formula::FormulaGrammar::AddressConvention eConv = m_rDoc.GetAddressConvention(); + uno::Sequence aRangeSeq; + sal_Int32 nIdx(0); + sal_Int32 nArrPos(0); + + do + { + OUString aRangeStr(o3tl::getToken(sVarCells, 0, cDelimiter, nIdx)); + // Check if range is valid + ScRange aRange; + bool bOk + = (aRange.ParseAny(aRangeStr, m_rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + if (bOk) + { + table::CellRangeAddress aRangeAddress(getRangeAddress(aRange)); + aRangeSeq.realloc(nArrPos + 1); + auto pArrRanges = aRangeSeq.getArray(); + pArrRanges[nArrPos] <<= aRangeAddress; + nArrPos++; + } + } while (nIdx > 0); + + return aRangeSeq; +} + +void SAL_CALL ScSolverSettings::setVariableCells(const uno::Sequence& aRanges) +{ + OUString sVarCells; + bool bFirst(true); + const formula::FormulaGrammar::AddressConvention eConv = m_rDoc.GetAddressConvention(); + OUStringChar cDelimiter(ScCompiler::GetNativeSymbolChar(OpCode::ocSep)); + + for (const auto& rRange : aRanges) + { + OUString sRange; + bool bIsString(rRange >>= sRange); + bool bOk(false); + if (bIsString) + { + ScRange aRange; + bOk = (aRange.ParseAny(sRange, m_rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + } + + table::CellRangeAddress aRangeAddress; + bool bIsRangeAddress(rRange >>= aRangeAddress); + if (bIsRangeAddress) + { + bOk = true; + ScRange aRange(aRangeAddress.StartColumn, aRangeAddress.StartRow, aRangeAddress.Sheet, + aRangeAddress.EndColumn, aRangeAddress.EndRow, aRangeAddress.Sheet); + sRange = aRange.Format(m_rDoc, ScRefFlags::RANGE_ABS); + } + + if (bOk) + { + if (bFirst) + { + sVarCells = sRange; + bFirst = false; + } + else + { + sVarCells += cDelimiter + sRange; + } + } + } + + m_pSettings->SetParameter(sc::SP_VAR_CELLS, sVarCells); +} + +uno::Sequence SAL_CALL ScSolverSettings::getConstraints() +{ + uno::Sequence aRet; + std::vector vConstraints = m_pSettings->GetConstraints(); + const formula::FormulaGrammar::AddressConvention eConv = m_rDoc.GetAddressConvention(); + sal_Int32 nCount(0); + + for (const auto& rConst : vConstraints) + { + sheet::ModelConstraint aConstraint; + + // Left side: must be valid string representing a cell range + ScRange aLeftRange; + bool bIsLeftRange + = (aLeftRange.ParseAny(rConst.aLeftStr, m_rDoc, eConv) & ScRefFlags::VALID) + == ScRefFlags::VALID; + if (bIsLeftRange) + aConstraint.Left <<= getRangeAddress(aLeftRange); + + // Operator + aConstraint.Operator = getUnoOperatorFromSc(rConst.nOperator); + + // Right side: must be either + // - valid string representing a cell range or + // - a numeric value + ScRange aRightRange; + bool bIsRightRange + = (aRightRange.ParseAny(rConst.aRightStr, m_rDoc, eConv) & ScRefFlags::VALID) + == ScRefFlags::VALID; + if (bIsRightRange) + { + aConstraint.Right <<= getRangeAddress(aRightRange); + } + else + { + double fValue; + bool bValid = isValidNumber(rConst.aRightStr, fValue); + if (bValid) + aConstraint.Right <<= fValue; + else + aConstraint.Right = uno::Any(); + } + + // Adds the constraint to the sequence + aRet.realloc(nCount + 1); + auto pArrConstraints = aRet.getArray(); + pArrConstraints[nCount] = aConstraint; + nCount++; + } + + return aRet; +} + +void SAL_CALL +ScSolverSettings::setConstraints(const uno::Sequence& aConstraints) +{ + const formula::FormulaGrammar::AddressConvention eConv = m_rDoc.GetAddressConvention(); + std::vector vRetConstraints; + + for (const auto& rConst : aConstraints) + { + sc::ModelConstraint aNewConst; + + // Left side + OUString sLeft; + bool bOkLeft(false); + bool bIsString(rConst.Left >>= sLeft); + if (bIsString) + { + ScRange aRange; + bOkLeft + = (aRange.ParseAny(sLeft, m_rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + } + + table::CellRangeAddress aLeftRangeAddress; + bool bIsRangeAddress(rConst.Left >>= aLeftRangeAddress); + if (bIsRangeAddress) + { + bOkLeft = true; + ScRange aRange(aLeftRangeAddress.StartColumn, aLeftRangeAddress.StartRow, + aLeftRangeAddress.Sheet, aLeftRangeAddress.EndColumn, + aLeftRangeAddress.EndRow, aLeftRangeAddress.Sheet); + sLeft = aRange.Format(m_rDoc, ScRefFlags::RANGE_ABS); + } + + if (bOkLeft) + aNewConst.aLeftStr = sLeft; + + // Constraint operator + aNewConst.nOperator = getScOperatorFromUno(rConst.Operator); + + // Right side (may have numeric values) + OUString sRight; + bool bOkRight(false); + + double fValue; + bool bIsDouble(rConst.Right >>= fValue); + if (bIsDouble) + { + bOkRight = true; + // The value must be set as a localized number + sRight = rtl::math::doubleToUString( + fValue, rtl_math_StringFormat_Automatic, rtl_math_DecimalPlaces_Max, + ScGlobal::getLocaleData().getNumDecimalSep()[0], true); + } + + bIsString = (rConst.Right >>= sRight); + if (bIsString) + { + ScRange aRange; + bOkRight + = (aRange.ParseAny(sRight, m_rDoc, eConv) & ScRefFlags::VALID) == ScRefFlags::VALID; + } + + table::CellRangeAddress aRightRangeAddress; + bIsRangeAddress = (rConst.Right >>= aRightRangeAddress); + if (bIsRangeAddress) + { + bOkRight = true; + ScRange aRange(aRightRangeAddress.StartColumn, aRightRangeAddress.StartRow, + aRightRangeAddress.Sheet, aRightRangeAddress.EndColumn, + aRightRangeAddress.EndRow, aRightRangeAddress.Sheet); + sRight = aRange.Format(m_rDoc, ScRefFlags::RANGE_ABS); + } + + if (bOkRight) + aNewConst.aRightStr = sRight; + + vRetConstraints.push_back(aNewConst); + } + + m_pSettings->SetConstraints(vRetConstraints); +} + +sal_Int32 SAL_CALL ScSolverSettings::getConstraintCount() +{ + if (!m_pTable) + return -1; + + return static_cast(m_pSettings->GetConstraints().size()); +} + +uno::Sequence SAL_CALL ScSolverSettings::getEngineOptions() +{ + uno::Sequence aRet = ScSolverUtil::GetDefaults(getEngine()); + m_pSettings->GetEngineOptions(aRet); + return aRet; +} + +void SAL_CALL ScSolverSettings::setEngineOptions(const uno::Sequence& rProps) +{ + m_pSettings->SetEngineOptions(rProps); +} + +sal_Int8 SAL_CALL ScSolverSettings::getStatus() { return m_nStatus; } + +OUString SAL_CALL ScSolverSettings::getErrorMessage() { return m_sErrorMessage; } + +sal_Bool SAL_CALL ScSolverSettings::getSuppressDialog() { return m_bSuppressDialog; } + +void SAL_CALL ScSolverSettings::setSuppressDialog(sal_Bool bSuppress) +{ + m_bSuppressDialog = bSuppress; +} + +void SAL_CALL ScSolverSettings::reset() { m_pSettings->ResetToDefaults(); } + +void SAL_CALL ScSolverSettings::solve() +{ + // Show the progress dialog + auto xProgress = std::make_shared(Application::GetDefDialogParent()); + if (!m_bSuppressDialog) + { + // Get the value of the timeout property of the solver engine + uno::Sequence aProps(getEngineOptions()); + sal_Int32 nTimeout(0); + sal_Int32 nPropCount(aProps.getLength()); + bool bHasTimeout(false); + for (sal_Int32 nProp = 0; nProp < nPropCount && !bHasTimeout; ++nProp) + { + const beans::PropertyValue& rValue = aProps[nProp]; + if (rValue.Name == SC_UNONAME_TIMEOUT) + bHasTimeout = (rValue.Value >>= nTimeout); + } + + if (bHasTimeout) + xProgress->SetTimeLimit(nTimeout); + else + xProgress->HideTimeLimit(); + + weld::DialogController::runAsync(xProgress, [](sal_Int32 /*nResult*/) {}); + // try to make sure the progress dialog is painted before continuing + Application::Reschedule(true); + } + + // Check the validity of the objective cell + ScRange aObjRange; + if (!ParseRef(aObjRange, m_pSettings->GetParameter(sc::SP_OBJ_CELL), false)) + { + m_nStatus = sheet::SolverStatus::PARSE_ERROR; + m_sErrorMessage = ScResId(STR_SOLVER_OBJCELL_FAIL); + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(m_sErrorMessage); + return; + } + table::CellAddress aObjCell(aObjRange.aStart.Tab(), aObjRange.aStart.Col(), + aObjRange.aStart.Row()); + + // Check the validity of the variable cells + ScRangeList aVarRanges; + if (!ParseWithNames(aVarRanges, m_pSettings->GetParameter(sc::SP_VAR_CELLS))) + { + m_nStatus = sheet::SolverStatus::PARSE_ERROR; + m_sErrorMessage = ScResId(STR_SOLVER_VARCELL_FAIL); + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(m_sErrorMessage); + return; + } + + // Resolve ranges into single cells + uno::Sequence aVariableCells; + sal_Int32 nVarPos(0); + for (size_t nRangePos = 0, nRange = aVarRanges.size(); nRangePos < nRange; ++nRangePos) + { + ScRange aRange(aVarRanges[nRangePos]); + aRange.PutInOrder(); + SCTAB nTab = aRange.aStart.Tab(); + + sal_Int32 nAdd = (aRange.aEnd.Col() - aRange.aStart.Col() + 1) + * (aRange.aEnd.Row() - aRange.aStart.Row() + 1); + aVariableCells.realloc(nVarPos + nAdd); + auto pVariables = aVariableCells.getArray(); + + for (SCROW nRow = aRange.aStart.Row(); nRow <= aRange.aEnd.Row(); ++nRow) + for (SCCOL nCol = aRange.aStart.Col(); nCol <= aRange.aEnd.Col(); ++nCol) + pVariables[nVarPos++] = table::CellAddress(nTab, nCol, nRow); + } + + // Prepare model constraints + uno::Sequence aConstraints; + sal_Int32 nConstrPos = 0; + for (const auto& rConstr : m_pSettings->GetConstraints()) + { + if (!rConstr.aLeftStr.isEmpty()) + { + sheet::SolverConstraint aConstraint; + aConstraint.Operator = getUnoOperatorFromSc(rConstr.nOperator); + + // The left side of the constraint must be a valid range or a single cell + ScRange aLeftRange; + if (!ParseRef(aLeftRange, rConstr.aLeftStr, true)) + { + m_nStatus = sheet::SolverStatus::PARSE_ERROR; + m_sErrorMessage = ScResId(STR_INVALIDCONDITION); + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(m_sErrorMessage); + return; + } + + // The right side can be either a cell range, a single cell or a numeric value + bool bIsRange(false); + ScRange aRightRange; + if (ParseRef(aRightRange, rConstr.aRightStr, true)) + { + if (aRightRange.aStart == aRightRange.aEnd) + aConstraint.Right + <<= table::CellAddress(aRightRange.aStart.Tab(), aRightRange.aStart.Col(), + aRightRange.aStart.Row()); + + else if (aRightRange.aEnd.Col() - aRightRange.aStart.Col() + == aLeftRange.aEnd.Col() - aLeftRange.aStart.Col() + && aRightRange.aEnd.Row() - aRightRange.aStart.Row() + == aLeftRange.aEnd.Row() - aLeftRange.aStart.Row()) + // If the right side of the constraint is a range, it must have the + // same shape as the left side + bIsRange = true; + else + { + m_nStatus = sheet::SolverStatus::PARSE_ERROR; + m_sErrorMessage = ScResId(STR_INVALIDCONDITION); + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(m_sErrorMessage); + + return; + } + } + else + { + // Test if the right side is a numeric value + sal_uInt32 nFormat; + double fValue(0); + if (m_rDoc.GetFormatTable()->IsNumberFormat(rConstr.aRightStr, nFormat, fValue)) + aConstraint.Right <<= fValue; + else if (aConstraint.Operator != sheet::SolverConstraintOperator_INTEGER + && aConstraint.Operator != sheet::SolverConstraintOperator_BINARY) + { + m_nStatus = sheet::SolverStatus::PARSE_ERROR; + m_sErrorMessage = ScResId(STR_INVALIDCONDITION); + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(ScResId(STR_INVALIDCONDITION)); + return; + } + } + + // Resolve constraint into single cells + sal_Int32 nAdd = (aLeftRange.aEnd.Col() - aLeftRange.aStart.Col() + 1) + * (aLeftRange.aEnd.Row() - aLeftRange.aStart.Row() + 1); + aConstraints.realloc(nConstrPos + nAdd); + auto pConstraints = aConstraints.getArray(); + + for (SCROW nRow = aLeftRange.aStart.Row(); nRow <= aLeftRange.aEnd.Row(); ++nRow) + for (SCCOL nCol = aLeftRange.aStart.Col(); nCol <= aLeftRange.aEnd.Col(); ++nCol) + { + aConstraint.Left = table::CellAddress(aLeftRange.aStart.Tab(), nCol, nRow); + if (bIsRange) + aConstraint.Right <<= table::CellAddress( + aRightRange.aStart.Tab(), + aRightRange.aStart.Col() + (nCol - aLeftRange.aStart.Col()), + aRightRange.aStart.Row() + (nRow - aLeftRange.aStart.Row())); + pConstraints[nConstrPos++] = aConstraint; + } + } + } + + // Type of the objective function + // If the objective is of type VALUE then a minimization model is used + sc::ObjectiveType aObjType(m_pSettings->GetObjectiveType()); + bool bMaximize = aObjType == sc::ObjectiveType::OT_MAXIMIZE; + + if (aObjType == sc::ObjectiveType::OT_VALUE) + { + // An additional constraint is added to the model forcing + // the objective cell to be equal to a given value + sheet::SolverConstraint aConstraint; + aConstraint.Left = aObjCell; + aConstraint.Operator = sheet::SolverConstraintOperator_EQUAL; + + OUString aValStr = m_pSettings->GetParameter(sc::SP_OBJ_VAL); + ScRange aRightRange; + + if (ParseRef(aRightRange, aValStr, false)) + aConstraint.Right <<= table::CellAddress( + aRightRange.aStart.Tab(), aRightRange.aStart.Col(), aRightRange.aStart.Row()); + else + { + // Test if the right side is a numeric value + sal_uInt32 nFormat; + double fValue(0); + if (m_rDoc.GetFormatTable()->IsNumberFormat(aValStr, nFormat, fValue)) + aConstraint.Right <<= fValue; + else + { + m_nStatus = sheet::SolverStatus::PARSE_ERROR; + m_sErrorMessage = ScResId(STR_SOLVER_TARGETVALUE_FAIL); + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(m_sErrorMessage); + return; + } + } + + aConstraints.realloc(nConstrPos + 1); + aConstraints.getArray()[nConstrPos++] = std::move(aConstraint); + } + + // Create a copy of document values in case the user chooses to restore them + sal_Int32 nVarCount = aVariableCells.getLength(); + uno::Sequence aOldValues(nVarCount); + std::transform(std::cbegin(aVariableCells), std::cend(aVariableCells), aOldValues.getArray(), + [this](const table::CellAddress& rVariable) -> double { + ScAddress aCellPos; + ScUnoConversion::FillScAddress(aCellPos, rVariable); + return m_rDoc.GetValue(aCellPos); + }); + + // Create and initialize solver + uno::Reference xSolver = ScSolverUtil::GetSolver(getEngine()); + OSL_ENSURE(xSolver.is(), "Unable to get solver component"); + if (!xSolver.is()) + { + if (!m_bSuppressDialog) + ScSolverSettings::ShowErrorMessage(ScResId(STR_INVALIDINPUT)); + m_nStatus = sheet::SolverStatus::ENGINE_ERROR; + m_sErrorMessage = ScResId(STR_SOLVER_LOAD_FAIL); + return; + } + + rtl::Reference xDocument(m_pDocShell->GetModel()); + xSolver->setDocument(xDocument); + xSolver->setObjective(aObjCell); + xSolver->setVariables(aVariableCells); + xSolver->setConstraints(aConstraints); + xSolver->setMaximize(bMaximize); + + // Set engine options + uno::Reference xOptProp(xSolver, uno::UNO_QUERY); + if (xOptProp.is()) + { + for (const beans::PropertyValue& rValue : getEngineOptions()) + { + try + { + xOptProp->setPropertyValue(rValue.Name, rValue.Value); + } + catch (uno::Exception&) + { + OSL_FAIL("Unable to set solver option property"); + } + } + } + + xSolver->solve(); + bool bSuccess = xSolver->getSuccess(); + + // Close progress dialog + if (!m_bSuppressDialog && xProgress) + xProgress->response(RET_CLOSE); + + if (bSuccess) + { + m_nStatus = sheet::SolverStatus::SOLUTION_FOUND; + // Write solution to the document + uno::Sequence aSolution = xSolver->getSolution(); + if (aSolution.getLength() == nVarCount) + { + m_pDocShell->LockPaint(); + ScDocFunc& rFunc = m_pDocShell->GetDocFunc(); + for (nVarPos = 0; nVarPos < nVarCount; ++nVarPos) + { + ScAddress aCellPos; + ScUnoConversion::FillScAddress(aCellPos, aVariableCells[nVarPos]); + rFunc.SetValueCell(aCellPos, aSolution[nVarPos], false); + } + m_pDocShell->UnlockPaint(); + } + else + { + OSL_FAIL("Wrong number of variables in the solver solution"); + } + + // Show success dialog + if (!m_bSuppressDialog) + { + // Get formatted result from document to show in the Success dialog + OUString aResultStr = m_rDoc.GetString(static_cast(aObjCell.Column), + static_cast(aObjCell.Row), + static_cast(aObjCell.Sheet)); + + ScSolverSuccessDialog xSuccessDialog(Application::GetDefDialogParent(), aResultStr); + bool bRestore(true); + if (xSuccessDialog.run() == RET_OK) + // Keep results in the document + bRestore = false; + + if (bRestore) + { + // Restore values to the document + m_pDocShell->LockPaint(); + ScDocFunc& rFunc = m_pDocShell->GetDocFunc(); + for (nVarPos = 0; nVarPos < nVarCount; ++nVarPos) + { + ScAddress aCellPos; + ScUnoConversion::FillScAddress(aCellPos, aVariableCells[nVarPos]); + rFunc.SetValueCell(aCellPos, aOldValues[nVarPos], false); + } + m_pDocShell->UnlockPaint(); + } + } + } + else + { + // The solver failed to find a solution + m_nStatus = sheet::SolverStatus::SOLUTION_NOT_FOUND; + uno::Reference xDesc(xSolver, uno::UNO_QUERY); + // Get error message reported by the solver + if (xDesc.is()) + m_sErrorMessage = xDesc->getStatusDescription(); + if (!m_bSuppressDialog) + { + ScSolverNoSolutionDialog aDialog(Application::GetDefDialogParent(), m_sErrorMessage); + aDialog.run(); + } + } +} + +void SAL_CALL ScSolverSettings::saveToFile() { m_pSettings->SaveSolverSettings(); } + +// XServiceInfo +OUString SAL_CALL ScSolverSettings::getImplementationName() { return u"ScSolverSettings"_ustr; } + +sal_Bool SAL_CALL ScSolverSettings::supportsService(const OUString& rServiceName) +{ + return cppu::supportsService(this, rServiceName); +} + +uno::Sequence SAL_CALL ScSolverSettings::getSupportedServiceNames() +{ + return { SC_SOLVERSETTINGS_SERVICE }; +}