File indexing completed on 2024-05-12 16:35:19
0001 /* This file is part of the KDE project 0002 Copyright (C) 2002-2003 Norbert Andres <nandres@web.de> 0003 (C) 2002-2003 Philipp Mueller <philipp.mueller@gmx.de> 0004 (C) 2002 Laurent Montel <montel@kde.org> 0005 (C) 2002 John Dailey <dailey@vt.edu> 0006 (C) 2002 Ariya Hidayat <ariya@kde.org> 0007 (C) 2002 Werner Trobin <trobin@kde.org> 0008 (C) 2002 Harri Porten <porten@kde.org> 0009 0010 This library is free software; you can redistribute it and/or 0011 modify it under the terms of the GNU Library General Public 0012 License as published by the Free Software Foundation; either 0013 version 2 of the License, or (at your option) any later version. 0014 0015 This library is distributed in the hope that it will be useful, 0016 but WITHOUT ANY WARRANTY; without even the implied warranty of 0017 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 0018 Library General Public License for more details. 0019 0020 You should have received a copy of the GNU Library General Public License 0021 along with this library; see the file COPYING.LIB. If not, write to 0022 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 0023 Boston, MA 02110-1301, USA. 0024 */ 0025 0026 #include "GoalSeekDialog.h" 0027 0028 #include <ktextedit.h> 0029 #include <klocale.h> 0030 0031 #include "CalculationSettings.h" 0032 #include "Cell.h" 0033 #include "ui/RegionSelector.h" 0034 #include "Formula.h" 0035 #include "Map.h" 0036 #include "ui/Selection.h" 0037 #include "Sheet.h" 0038 #include "Util.h" 0039 0040 // commands 0041 #include "commands/DataManipulators.h" 0042 0043 #include "ui_GoalSeekWidget.h" 0044 0045 #include <kmessagebox.h> 0046 0047 #include <QCloseEvent> 0048 0049 #include <math.h> 0050 0051 using namespace Calligra::Sheets; 0052 0053 class GoalSeekDialog::Private 0054 { 0055 public: 0056 Selection * selection; 0057 Cell sourceCell; 0058 Cell targetCell; 0059 double result; 0060 int maxIter; 0061 double oldSource; 0062 bool firstRun; 0063 0064 Ui::GoalSeekWidget widget; 0065 }; 0066 0067 0068 GoalSeekDialog::GoalSeekDialog(QWidget* parent, Selection* selection) 0069 : KoDialog(parent) 0070 , d(new Private) 0071 { 0072 d->selection = selection; 0073 d->result = 0.0; 0074 d->maxIter = 1000; // TODO make this configurable 0075 d->oldSource = 0.0; 0076 d->firstRun = true; 0077 0078 setButtons(Ok | Cancel); 0079 enableButtonOk(false); 0080 setModal(false); 0081 0082 setObjectName(QLatin1String("GoalSeekDialog")); 0083 0084 setWindowTitle(i18n("Goal Seek")); 0085 0086 QWidget* mainWidget = new QWidget(this); 0087 d->widget.setupUi(mainWidget); 0088 d->widget.selector1->setDialog(this); 0089 d->widget.selector1->setSelection(d->selection); 0090 d->widget.selector1->setSelectionMode(RegionSelector::SingleCell); 0091 d->widget.selector2->setDialog(this); 0092 d->widget.selector2->setSelection(d->selection); 0093 d->widget.selector2->setSelectionMode(RegionSelector::SingleCell); 0094 d->widget.selector3->setDialog(this); 0095 d->widget.selector3->setSelection(d->selection); 0096 d->widget.selector3->setSelectionMode(RegionSelector::SingleCell); 0097 d->widget.preview->hide(); 0098 setMainWidget(mainWidget); 0099 0100 // connections 0101 connect(d->widget.selector1->textEdit(), SIGNAL(textChanged()), 0102 this, SLOT(textChanged())); 0103 connect(d->widget.selector2->textEdit(), SIGNAL(textChanged()), 0104 this, SLOT(textChanged())); 0105 connect(d->widget.selector3->textEdit(), SIGNAL(textChanged()), 0106 this, SLOT(textChanged())); 0107 0108 // Allow the user to select cells on the spreadsheet. 0109 // d->selection->canvasWidget()->startChoose(); 0110 } 0111 0112 GoalSeekDialog::~GoalSeekDialog() 0113 { 0114 delete d; 0115 } 0116 0117 void GoalSeekDialog::closeEvent(QCloseEvent * e) 0118 { 0119 d->selection->endReferenceSelection(); 0120 e->accept(); 0121 deleteLater(); 0122 } 0123 0124 void GoalSeekDialog::textChanged() 0125 { 0126 d->widget.preview->hide(); 0127 const bool s1 = !d->widget.selector1->textEdit()->toPlainText().isEmpty(); 0128 const bool s2 = !d->widget.selector2->textEdit()->toPlainText().isEmpty(); 0129 const bool s3 = !d->widget.selector3->textEdit()->toPlainText().isEmpty(); 0130 enableButtonOk(s1 && s2 && s3); 0131 } 0132 0133 void GoalSeekDialog::accept() 0134 { 0135 if (!d->widget.preview->isVisible()) { 0136 Sheet * sheet = d->selection->activeSheet(); 0137 0138 const Region source(d->widget.selector3->textEdit()->toPlainText(), sheet->map(), sheet); 0139 if (!source.isValid() || !source.isSingular()) { 0140 KMessageBox::error(this, i18n("Cell reference is invalid.")); 0141 d->widget.selector3->textEdit()->selectAll(); 0142 d->widget.selector3->textEdit()->setFocus(); 0143 0144 d->selection->emitModified(); 0145 return; 0146 } 0147 0148 const Region target(d->widget.selector1->textEdit()->toPlainText(), sheet->map(), sheet); 0149 if (!target.isValid() || !target.isSingular()) { 0150 KMessageBox::error(this, i18n("Cell reference is invalid.")); 0151 d->widget.selector1->textEdit()->selectAll(); 0152 d->widget.selector1->textEdit()->setFocus(); 0153 0154 d->selection->emitModified(); 0155 return; 0156 } 0157 0158 bool ok = false; 0159 double goal = d->selection->activeSheet()->map()->calculationSettings()->locale()->readNumber(d->widget.selector2->textEdit()->toPlainText(), &ok); 0160 if (!ok) { 0161 KMessageBox::error(this, i18n("Target value is invalid.")); 0162 d->widget.selector2->textEdit()->selectAll(); 0163 d->widget.selector2->textEdit()->setFocus(); 0164 0165 d->selection->emitModified(); 0166 return; 0167 } 0168 0169 d->sourceCell = Cell(source.firstSheet(), source.firstRange().topLeft()); 0170 d->targetCell = Cell(target.firstSheet(), target.firstRange().topLeft()); 0171 0172 if (!d->sourceCell.value().isNumber()) { 0173 KMessageBox::error(this, i18n("Source cell must contain a numeric value.")); 0174 d->widget.selector3->textEdit()->selectAll(); 0175 d->widget.selector3->textEdit()->setFocus(); 0176 0177 d->selection->emitModified(); 0178 return; 0179 } 0180 0181 if (!d->targetCell.isFormula()) { 0182 KMessageBox::error(this, i18n("Target cell must contain a formula.")); 0183 d->widget.selector1->textEdit()->selectAll(); 0184 d->widget.selector1->textEdit()->setFocus(); 0185 0186 d->selection->emitModified(); 0187 return; 0188 } 0189 0190 enableButtonOk(false); 0191 enableButtonCancel(false); 0192 0193 d->widget.preview->show(); 0194 0195 startCalc(numToDouble(d->sourceCell.value().asFloat()), goal); 0196 d->selection->emitModified(); 0197 return; 0198 } 0199 0200 // Reset the value for a proper undo value. 0201 const Value value = d->sourceCell.value(); 0202 d->sourceCell.setValue(Value(d->oldSource)); 0203 Sheet *const sheet = d->selection->activeSheet(); 0204 DataManipulator *const command = new DataManipulator(); 0205 command->setSheet(sheet); 0206 command->add(Region(d->sourceCell.cellPosition(), sheet)); 0207 command->setValue(value); 0208 sheet->map()->addCommand(command); 0209 0210 d->selection->endReferenceSelection(); 0211 d->selection->emitModified(); 0212 deleteLater(); 0213 } 0214 0215 void GoalSeekDialog::reject() 0216 { 0217 if (d->widget.preview->isVisible()) { 0218 d->sourceCell.setValue(Value(d->oldSource)); 0219 } 0220 0221 deleteLater(); 0222 } 0223 0224 void GoalSeekDialog::startCalc(double _start, double _goal) 0225 { 0226 d->widget.label4->setText(i18n("Starting...")); 0227 d->widget.label5->setText(i18n("Iteration:")); 0228 0229 // lets be optimistic 0230 bool ok = true; 0231 0232 // TODO: make this configurable 0233 double eps = 0.0000001; 0234 0235 double startA = 0.0, startB; 0236 double resultA, resultB; 0237 0238 // save old value 0239 if (d->firstRun) { 0240 d->firstRun = false; 0241 d->oldSource = numToDouble(d->sourceCell.value().asFloat()); 0242 } 0243 resultA = numToDouble(d->targetCell.value().asFloat()) - _goal; 0244 0245 // initialize start value 0246 startB = _start; 0247 double x = startB + 0.5; 0248 0249 int iterations = d->maxIter; 0250 const Formula formula = d->targetCell.formula(); 0251 0252 // while the result is not close enough to zero 0253 // or while the max number of iterations is not reached... 0254 while (fabs(resultA) > eps && (iterations >= 0)) { 0255 startA = startB; 0256 startB = x; 0257 0258 d->sourceCell.setValue(Value(startA)); 0259 const double targetValueA = numToDouble(formula.eval().asFloat()); 0260 resultA = targetValueA - _goal; 0261 // debugSheets << "Target A:" << targetValueA << "," << d->targetCell.userInput() << "Calc:" << resultA; 0262 0263 d->sourceCell.setValue(Value(startB)); 0264 const double targetValueB = numToDouble(formula.eval().asFloat()); 0265 resultB = targetValueB - _goal; 0266 // debugSheets << "Target B:" << targetValueB << "," << d->targetCell.userInput() << "Calc:" << resultB; 0267 0268 // debugSheets <<"Iteration:" << iterations <<", StartA:" << startA 0269 // << ", ResultA: " << resultA << " (eps: " << eps << "), StartB: " 0270 // << startB << ", ResultB: " << resultB << endl; 0271 0272 0273 // find zero with secant method (rough implementation was provided by Franz-Xaver Meier): 0274 // if the function returns the same for two different 0275 // values we have something like a horizontal line 0276 // => can't get zero. 0277 if (resultB == resultA) { 0278 // debugSheets <<" resultA == resultB"; 0279 if (fabs(resultA) < eps) { 0280 ok = true; 0281 break; 0282 } 0283 0284 ok = false; 0285 break; 0286 } 0287 0288 // Point of intersection of secant with x-axis 0289 x = (startA * resultB - startB * resultA) / (resultB - resultA); 0290 0291 if (fabs(x) > 100000000) { 0292 // debugSheets <<"fabs(x) > 100000000:" << x; 0293 ok = false; 0294 break; 0295 } 0296 0297 // debugSheets <<"X:" << x <<", fabs (resultA):" << fabs(resultA) <<", Real start:" << startA <<", Real result:" << resultA <<", Iteration:" << iterations; 0298 0299 --iterations; 0300 if (iterations % 20 == 0) 0301 d->widget.newValue->setText(QString::number(iterations)); 0302 } 0303 0304 d->widget.label5->setText(i18n("New value:")); 0305 if (ok) { 0306 d->sourceCell.setValue(Value(startA)); 0307 0308 d->widget.label4->setText(i18n("Goal seeking with cell %1 found a solution:", 0309 d->widget.selector3->textEdit()->toPlainText())); 0310 d->widget.newValue->setText(d->selection->activeSheet()->map()->calculationSettings()->locale()->formatNumber(startA)); 0311 d->widget.currentValue->setText(d->selection->activeSheet()->map()->calculationSettings()->locale()->formatNumber(d->oldSource)); 0312 } else { 0313 // restore the old value 0314 d->sourceCell.setValue(Value(d->oldSource)); 0315 d->widget.label4->setText(i18n("Goal seeking with cell %1 has found NO solution.", 0316 d->widget.selector3->textEdit()->toPlainText())); 0317 d->widget.newValue->setText(""); 0318 d->widget.currentValue->setText(d->selection->activeSheet()->map()->calculationSettings()->locale()->formatNumber(d->oldSource)); 0319 } 0320 0321 enableButtonOk(ok); 0322 enableButtonCancel(true); 0323 }