GoalSeek: code cleanup.
[gnumeric.git] / src / dialogs / dialog-goal-seek.c
blob705fa9c213d31e89c12cfabc36cd981d20e24811
1 /*
2 * dialog-goal-seek.c:
4 * Authors:
5 * Jukka-Pekka Iivonen <iivonen@iki.fi>
6 * Morten Welinder (terra@gnome.org)
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, see <https://www.gnu.org/licenses/>.
22 #include <gnumeric-config.h>
23 #include <glib/gi18n-lib.h>
24 #include <gnumeric.h>
25 #include <dialogs/dialogs.h>
26 #include <dialogs/help.h>
28 #include <gui-util.h>
29 #include <cell.h>
30 #include <sheet.h>
31 #include <expr.h>
32 #include <commands.h>
33 #include <dependent.h>
34 #include <gnm-format.h>
35 #include <value.h>
36 #include <mstyle.h>
37 #include <ranges.h>
38 #include <number-match.h>
39 #include <parse-util.h>
40 #include <workbook.h>
41 #include <workbook-control.h>
42 #include <wbc-gtk.h>
43 #include <workbook-view.h>
44 #include <tools/goal-seek.h>
45 #include <mathfunc.h>
46 #include <widgets/gnumeric-expr-entry.h>
47 #include <selection.h>
48 #include <application.h>
50 #include <math.h>
51 #include <string.h>
53 static const gnm_float max_range_val = GNM_const(1e24);
55 #define MAX_CELL_NAME_LEN 20
56 #define GOALSEEK_KEY "goal-seek-dialog"
58 typedef struct {
59 GtkBuilder *gui;
60 GtkWidget *dialog;
61 GnmExprEntry *set_cell_entry;
62 GnmExprEntry *change_cell_entry;
63 GtkWidget *to_value_entry;
64 GtkWidget *at_least_entry;
65 GtkWidget *at_most_entry;
66 GtkWidget *close_button;
67 GtkWidget *cancel_button;
68 GtkWidget *apply_button;
69 GtkWidget *target_value_label;
70 GtkWidget *current_value_label;
71 GtkWidget *solution_label;
72 GtkWidget *result_label;
73 GtkWidget *result_grid;
74 Sheet *sheet;
75 Workbook *wb;
76 WBCGtk *wbcg;
77 gnm_float target_value;
78 gnm_float xmin;
79 gnm_float xmax;
80 GnmCell *set_cell;
81 GnmCell *change_cell;
82 GnmCell *old_cell;
83 GnmValue *old_value;
84 GtkWidget *warning_dialog;
85 gboolean cancelled;
86 } GoalSeekState;
89 static GnmGoalSeekStatus
90 gnumeric_goal_seek (GoalSeekState *state)
92 GnmGoalSeekData seekdata;
93 GnmGoalSeekCellData celldata;
95 goal_seek_initialize (&seekdata);
96 seekdata.xmin = state->xmin;
97 seekdata.xmax = state->xmax;
99 celldata.xcell = state->change_cell;
100 celldata.ycell = state->set_cell;
101 celldata.ytarget = state->target_value;
103 return gnm_goal_seek_cell (&seekdata, &celldata);
106 static void
107 cb_dialog_destroy (GoalSeekState *state)
109 if (!state->cancelled
110 && state->old_value != NULL
111 && state->old_cell != NULL) {
112 cmd_goal_seek (GNM_WBC(state->wbcg),
113 state->old_cell, state->old_value, NULL);
114 state->old_value = NULL;
117 value_release (state->old_value);
118 if (state->gui != NULL)
119 g_object_unref (state->gui);
121 wbcg_edit_finish (state->wbcg, WBC_EDIT_REJECT, NULL);
122 g_free (state);
125 static void
126 cb_dialog_cancel_clicked (G_GNUC_UNUSED GtkWidget *button,
127 GoalSeekState *state)
129 state->cancelled = TRUE;
131 if ((state->old_cell != NULL) && (state->old_value != NULL)) {
132 sheet_cell_set_value (state->old_cell, state->old_value);
133 state->old_value = NULL;
136 gnm_app_recalc ();
137 gtk_widget_destroy (state->dialog);
140 static void
141 cb_dialog_close_clicked (G_GNUC_UNUSED GtkWidget *button,
142 GoalSeekState *state)
144 gtk_widget_destroy (state->dialog);
148 * cb_dialog_apply_clicked:
149 * @button:
150 * @state:
152 * Close (destroy) the dialog
154 static void
155 cb_dialog_apply_clicked (G_GNUC_UNUSED GtkWidget *button,
156 GoalSeekState *state)
158 char *status_str;
159 GnmGoalSeekStatus status;
160 GnmValue *target;
161 GnmRangeRef const *r;
162 GOFormat const *format;
164 if (state->warning_dialog != NULL)
165 gtk_widget_destroy (state->warning_dialog);
167 /* set up source */
168 target = gnm_expr_entry_parse_as_value (state->set_cell_entry,
169 state->sheet);
170 if (target == NULL) {
171 go_gtk_notice_nonmodal_dialog (GTK_WINDOW(state->dialog),
172 &(state->warning_dialog),
173 GTK_MESSAGE_ERROR,
174 _("You should introduce a valid cell "
175 "name in 'Set Cell:'!"));
176 gnm_expr_entry_grab_focus (state->set_cell_entry, TRUE);
177 return;
179 r = &target->v_range.cell;
180 state->set_cell = sheet_cell_get (r->a.sheet, r->a.col, r->a.row);
181 value_release (target);
182 if (state->set_cell == NULL || !gnm_cell_has_expr (state->set_cell)) {
183 go_gtk_notice_nonmodal_dialog (GTK_WINDOW(state->dialog),
184 &(state->warning_dialog),
185 GTK_MESSAGE_ERROR,
186 _("The cell named in 'Set Cell:' "
187 "must contain a formula!"));
188 gnm_expr_entry_grab_focus (state->set_cell_entry, TRUE);
189 return;
192 /* set up source */
193 target = gnm_expr_entry_parse_as_value (state->change_cell_entry,
194 state->sheet);
195 if (target == NULL) {
196 go_gtk_notice_nonmodal_dialog (GTK_WINDOW(state->dialog),
197 &(state->warning_dialog),
198 GTK_MESSAGE_ERROR,
199 _("You should introduce a valid cell "
200 "name in 'By Changing Cell:'!"));
201 gnm_expr_entry_grab_focus (state->change_cell_entry, TRUE);
202 return;
205 r = &target->v_range.cell;
206 state->change_cell = sheet_cell_fetch (r->a.sheet, r->a.col, r->a.row);
207 value_release (target);
208 if (gnm_cell_has_expr (state->change_cell)) {
209 go_gtk_notice_nonmodal_dialog (GTK_WINDOW(state->dialog),
210 &(state->warning_dialog),
211 GTK_MESSAGE_ERROR,
212 _("The cell named in 'By changing cell' "
213 "must not contain a formula."));
214 gnm_expr_entry_grab_focus (state->change_cell_entry, TRUE);
215 return;
219 format = gnm_style_get_format (gnm_cell_get_style (state->set_cell));
220 if (entry_to_float_with_format (GTK_ENTRY(state->to_value_entry),
221 &state->target_value, TRUE, format)){
222 go_gtk_notice_nonmodal_dialog (GTK_WINDOW(state->dialog),
223 &(state->warning_dialog),
224 GTK_MESSAGE_ERROR,
225 _("The value given in 'To Value:' "
226 "is not valid."));
227 focus_on_entry (GTK_ENTRY(state->to_value_entry));
228 return;
231 format = gnm_style_get_format (gnm_cell_get_style (state->change_cell));
232 if (entry_to_float_with_format (GTK_ENTRY(state->at_least_entry),
233 &state->xmin, TRUE, format)) {
234 state->xmin = -max_range_val;
235 gtk_entry_set_text (GTK_ENTRY (state->at_least_entry), "");
238 if (entry_to_float_with_format (GTK_ENTRY(state->at_most_entry), &state->xmax,
239 TRUE, format)) {
240 state->xmax = +max_range_val;
241 gtk_entry_set_text (GTK_ENTRY (state->at_most_entry), "");
244 if ((state->old_cell != NULL) && (state->old_value != NULL)) {
245 sheet_cell_set_value (state->old_cell, state->old_value);
246 state->old_value = NULL;
248 gnm_app_recalc ();
249 state->old_cell = state->change_cell;
250 state->old_value = value_dup (state->change_cell->value);
252 status = gnumeric_goal_seek (state);
254 gnm_app_recalc ();
256 switch (status) {
257 case GOAL_SEEK_OK: {
258 const char *actual_str;
259 const char *solution_str;
260 GOFormat *format = go_format_general ();
261 GnmValue *error_value = value_new_float (state->target_value -
262 value_get_as_float (state->set_cell->value));
263 char *target_str = format_value (format, error_value, -1,
264 workbook_date_conv (state->wb));
265 gtk_label_set_text (GTK_LABEL (state->target_value_label), target_str);
266 g_free (target_str);
267 value_release (error_value);
269 status_str =
270 g_strdup_printf (_("Goal seeking with cell %s found a solution."),
271 cell_name (state->set_cell));
272 gtk_label_set_text (GTK_LABEL (state->result_label), status_str);
273 g_free (status_str);
275 /* FIXME? Do a format? */
276 actual_str = state->set_cell->value
277 ? value_peek_string (state->set_cell->value)
278 : "";
279 gtk_label_set_text (GTK_LABEL (state->current_value_label), actual_str);
281 solution_str = state->change_cell->value
282 ? value_peek_string (state->change_cell->value)
283 : "";
284 gtk_label_set_text (GTK_LABEL (state->solution_label), solution_str);
286 break;
289 default:
290 status_str =
291 g_strdup_printf (_("Goal seeking with cell %s did not find a solution."),
292 cell_name (state->set_cell));
293 gtk_label_set_text (GTK_LABEL (state->result_label), status_str);
294 g_free (status_str);
295 gtk_label_set_text (GTK_LABEL (state->current_value_label), "");
296 gtk_label_set_text (GTK_LABEL (state->solution_label), "");
297 gtk_label_set_text (GTK_LABEL (state->target_value_label), "");
298 break;
300 state->cancelled = FALSE;
302 gtk_widget_show (state->result_grid);
303 return;
307 * dialog_realized:
308 * @widget
309 * @state:
314 static void
315 dialog_realized (G_GNUC_UNUSED GtkWidget *dialog,
316 GoalSeekState *state)
318 gtk_widget_hide (state->result_grid);
322 * dialog_preload_selection:
323 * @state:
324 * @entry
328 static void
329 dialog_preload_selection (GoalSeekState *state, GnmExprEntry *entry)
331 GnmRange const *sel;
333 sel = selection_first_range
334 (wb_control_cur_sheet_view
335 (GNM_WBC (state->wbcg)), NULL, NULL);
336 if (sel)
337 gnm_expr_entry_load_from_range (entry,
338 state->sheet, sel);
342 * dialog_init:
343 * @state:
345 * Create the dialog (guru).
348 static gboolean
349 dialog_init (GoalSeekState *state)
351 GtkGrid *grid;
353 state->dialog = go_gtk_builder_get_widget (state->gui, "GoalSeek");
354 if (state->dialog == NULL)
355 return TRUE;
357 gnm_dialog_setup_destroy_handlers (GTK_DIALOG (state->dialog),
358 state->wbcg,
359 GNM_DIALOG_DESTROY_CURRENT_SHEET_REMOVED);
361 state->close_button = go_gtk_builder_get_widget (state->gui, "closebutton");
362 g_signal_connect (G_OBJECT (state->close_button),
363 "clicked",
364 G_CALLBACK (cb_dialog_close_clicked), state);
366 state->cancel_button = go_gtk_builder_get_widget (state->gui, "cancelbutton");
367 g_signal_connect (G_OBJECT (state->cancel_button),
368 "clicked",
369 G_CALLBACK (cb_dialog_cancel_clicked), state);
370 state->apply_button = go_gtk_builder_get_widget (state->gui, "applybutton");
371 g_signal_connect (G_OBJECT (state->apply_button),
372 "clicked",
373 G_CALLBACK (cb_dialog_apply_clicked), state);
375 gnm_init_help_button (
376 go_gtk_builder_get_widget (state->gui, "helpbutton"),
377 GNUMERIC_HELP_LINK_GOAL_SEEK);
379 state->to_value_entry = go_gtk_builder_get_widget (state->gui, "to_value_entry");
380 state->at_least_entry = go_gtk_builder_get_widget (state->gui, "at_least-entry");
381 state->at_most_entry = go_gtk_builder_get_widget (state->gui, "at_most-entry");
382 state->target_value_label = go_gtk_builder_get_widget (state->gui, "target-value");
383 gtk_label_set_justify (GTK_LABEL (state->target_value_label), GTK_JUSTIFY_RIGHT);
384 state->current_value_label = go_gtk_builder_get_widget (state->gui, "current-value");
385 gtk_label_set_justify (GTK_LABEL (state->current_value_label), GTK_JUSTIFY_RIGHT);
386 state->solution_label = go_gtk_builder_get_widget (state->gui, "solution");
387 gtk_label_set_justify (GTK_LABEL (state->solution_label), GTK_JUSTIFY_RIGHT);
389 state->result_label = go_gtk_builder_get_widget (state->gui, "result-label");
390 state->result_grid = go_gtk_builder_get_widget (state->gui, "result-grid");
392 grid = GTK_GRID (go_gtk_builder_get_widget (state->gui, "goal-grid"));
393 state->set_cell_entry = gnm_expr_entry_new (state->wbcg, TRUE);
394 gnm_expr_entry_set_flags (state->set_cell_entry,
395 GNM_EE_SINGLE_RANGE |
396 GNM_EE_FORCE_ABS_REF,
397 GNM_EE_MASK);
398 gtk_grid_attach (grid, GTK_WIDGET (state->set_cell_entry),
399 1, 0, 1, 1);
400 gtk_widget_set_hexpand (GTK_WIDGET (state->set_cell_entry), TRUE);
401 gnm_editable_enters (GTK_WINDOW (state->dialog),
402 GTK_WIDGET (state->set_cell_entry));
403 dialog_preload_selection (state, state->set_cell_entry);
404 gtk_widget_show (GTK_WIDGET (state->set_cell_entry));
406 state->change_cell_entry = gnm_expr_entry_new (state->wbcg, TRUE);
407 gnm_expr_entry_set_flags (state->change_cell_entry,
408 GNM_EE_SINGLE_RANGE |
409 GNM_EE_FORCE_ABS_REF,
410 GNM_EE_MASK);
411 gtk_grid_attach (grid, GTK_WIDGET (state->change_cell_entry),
412 1, 2, 1, 1);
413 gtk_widget_set_hexpand (GTK_WIDGET (state->change_cell_entry), TRUE);
414 gnm_editable_enters (GTK_WINDOW (state->dialog),
415 GTK_WIDGET (state->change_cell_entry));
416 dialog_preload_selection (state, state->change_cell_entry);
417 gtk_widget_show (GTK_WIDGET (state->change_cell_entry));
420 g_signal_connect (G_OBJECT (state->dialog),
421 "realize",
422 G_CALLBACK (dialog_realized), state);
424 state->old_value = NULL;
425 state->old_cell = NULL;
427 wbc_gtk_attach_guru (state->wbcg, state->dialog);
428 g_object_set_data_full (G_OBJECT (state->dialog),
429 "state", state, (GDestroyNotify) cb_dialog_destroy);
431 gnm_expr_entry_grab_focus (state->set_cell_entry, FALSE);
433 return FALSE;
437 * We need a horizontal strip of 5 cells containing:
439 * 0. Formula cell.
440 * 1: X value cell.
441 * 2: Y target value.
442 * 3: Min value.
443 * 4: Max value.
445 static void
446 dialog_goal_seek_test (Sheet *sheet, const GnmRange *range)
448 GoalSeekState state;
449 GnmCell *cell;
450 int r, c;
451 GnmGoalSeekStatus status;
453 g_return_if_fail (range->start.row == range->end.row);
454 g_return_if_fail (range->start.col + 4 == range->end.col);
456 memset (&state, 0, sizeof (state));
457 r = range->start.row;
458 c = range->start.col;
460 state.wb = sheet->workbook;
461 state.sheet = sheet;
463 state.set_cell = sheet_cell_fetch (sheet, c + 0, r);
464 state.change_cell = sheet_cell_fetch (sheet, c + 1, r);
465 state.old_value = value_dup (state.change_cell->value);
467 cell = sheet_cell_fetch (sheet, c + 2, r);
468 state.target_value = value_get_as_float (cell->value);
470 cell = sheet_cell_fetch (sheet, c + 3, r);
471 state.xmin = VALUE_IS_EMPTY (cell->value)
472 ? -max_range_val
473 : value_get_as_float (cell->value);
475 cell = sheet_cell_fetch (sheet, c + 4, r);
476 state.xmax = VALUE_IS_EMPTY (cell->value)
477 ? max_range_val
478 : value_get_as_float (cell->value);
480 status = gnumeric_goal_seek (&state);
481 if (status == GOAL_SEEK_OK) {
482 /* Nothing */
483 } else {
484 sheet_cell_set_value (state.change_cell,
485 value_new_error_VALUE (NULL));
488 value_release (state.old_value);
492 * dialog_goal_seek:
493 * @wbcg:
494 * @sheet:
496 * Create the dialog (guru).
499 void
500 dialog_goal_seek (WBCGtk *wbcg, Sheet *sheet)
502 GoalSeekState *state;
503 GtkBuilder *gui;
505 g_return_if_fail (IS_SHEET (sheet));
507 /* Testing hook. */
508 if (wbcg == NULL) {
509 GnmRangeRef *range =
510 g_object_get_data (G_OBJECT (sheet), "ssconvert-goal-seek");
511 if (range) {
512 Sheet *start_sheet, *end_sheet;
513 GnmEvalPos ep;
514 GnmRange r;
516 gnm_rangeref_normalize (range,
517 eval_pos_init_sheet (&ep, sheet),
518 &start_sheet, &end_sheet,
519 &r);
520 g_return_if_fail (start_sheet == sheet);
522 dialog_goal_seek_test (sheet, &r);
523 return;
527 g_return_if_fail (wbcg != NULL);
529 /* Only pop up one copy per workbook */
530 if (gnm_dialog_raise_if_exists (wbcg, GOALSEEK_KEY))
531 return;
532 gui = gnm_gtk_builder_load ("res:ui/goalseek.ui", NULL, GO_CMD_CONTEXT (wbcg));
533 if (gui == NULL)
534 return;
536 state = g_new (GoalSeekState, 1);
537 state->wbcg = wbcg;
538 state->wb = wb_control_get_workbook (GNM_WBC (wbcg));
539 state->sheet = sheet;
540 state->gui = gui;
541 state->warning_dialog = NULL;
542 state->cancelled = TRUE;
544 if (dialog_init (state)) {
545 go_gtk_notice_dialog (wbcg_toplevel (wbcg), GTK_MESSAGE_ERROR,
546 _("Could not create the Goal-Seek dialog."));
547 g_free (state);
548 return;
551 gnm_keyed_dialog (state->wbcg, GTK_WINDOW (state->dialog),
552 GOALSEEK_KEY);
554 gtk_widget_show (state->dialog);