exp10 replaced with pow when exp10 is not available
[pspp] / src / ui / gui / find-dialog.c
1 /* PSPPIRE - a graphical user interface for PSPP.
2    Copyright (C) 2007, 2009, 2011, 2012, 2015, 2020  Free Software Foundation
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17
18 /* This module implements the "Find" dialog; a dialog box to locate cases
19 which match particular strings */
20
21 #include <config.h>
22
23 #include <ctype.h>
24 #include <gtk/gtk.h>
25 #include <regex.h>
26 #include <stdlib.h>
27 #include <sys/types.h>
28 #include <math.h>
29
30 #include "data/data-in.h"
31 #include "data/datasheet.h"
32 #include "data/format.h"
33 #include "data/value.h"
34 #include "libpspp/cast.h"
35 #include "libpspp/message.h"
36 #include "ui/gui/builder-wrapper.h"
37 #include "ui/gui/dict-display.h"
38 #include "ui/gui/find-dialog.h"
39 #include "ui/gui/helper.h"
40 #include "ui/gui/psppire-data-store.h"
41 #include "ui/gui/psppire-data-window.h"
42 #include "ui/gui/psppire-dialog.h"
43 #include "ui/gui/psppire-selector.h"
44 #include <ssw-sheet.h>
45
46 #include "gl/xalloc.h"
47
48 #include <gettext.h>
49 #define _(msgid) gettext (msgid)
50 #define N_(msgid) msgid
51
52 /* Some systems like MacOS do not have exp10 */
53 #ifndef HAVE_EXP10
54 inline static double exp10(double x)
55 {
56   return pow(10.0, x);
57 }
58 #endif
59
60 struct find_dialog
61 {
62   GtkBuilder *xml;
63   PsppireDict *dict;
64   struct datasheet *data;
65   PsppireDataWindow *de;
66   GtkWidget *variable_entry;
67   GtkWidget *value_entry;
68   GtkWidget *value_labels_checkbox;
69   GtkWidget *match_regexp_checkbox;
70   GtkWidget *match_substring_checkbox;
71 };
72
73 static void
74 find_value (const struct find_dialog *fd, casenumber current_row,
75            casenumber *row, int *column);
76
77
78 /* A callback which occurs whenever the "Refresh" button is clicked,
79    and when the dialog pops up.
80    It restores the dialog to its default state.
81 */
82 static void
83 refresh (GObject *obj, const struct find_dialog *fd)
84 {
85   gtk_toggle_button_set_active
86     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-wrap")),
87      FALSE);
88
89   gtk_toggle_button_set_active
90     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-backwards")),
91      FALSE);
92
93   gtk_entry_set_text (GTK_ENTRY (fd->variable_entry), "");
94   gtk_entry_set_text (GTK_ENTRY (fd->value_entry), "");
95
96   gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (fd->match_regexp_checkbox),
97                                 FALSE);
98
99   gtk_toggle_button_set_active
100     (GTK_TOGGLE_BUTTON (fd->match_substring_checkbox), FALSE);
101
102
103   gtk_toggle_button_set_active
104     (GTK_TOGGLE_BUTTON (fd->match_substring_checkbox), FALSE);
105 }
106
107 /* Callback on the "Find" button */
108 static void
109 do_find (GObject *obj, const struct find_dialog *fd)
110 {
111   casenumber x = -1;
112   gint column = -1;
113   glong row = -1;
114
115   find_value (fd, row, &x, &column);
116
117   if (x != -1)
118     {
119       SswSheet *sheet = SSW_SHEET (fd->de->data_editor->data_sheet);
120       gtk_notebook_set_current_page (GTK_NOTEBOOK (fd->de->data_editor),
121                                      PSPPIRE_DATA_EDITOR_DATA_VIEW);
122
123       ssw_sheet_scroll_to (sheet, column, x);
124       ssw_sheet_set_active_cell (sheet, column, x, NULL);
125     }
126 }
127
128 /* Callback on the selector.
129    It gets invoked whenever a variable is selected */
130 static void
131 on_select (GtkEntry *entry, gpointer data)
132 {
133   struct find_dialog *fd = data;
134   const char *var_name = gtk_entry_get_text (GTK_ENTRY (fd->variable_entry));
135   struct variable *var = dict_lookup_var (fd->dict->dict, var_name);
136   gboolean search_labels ;
137
138   g_return_if_fail (var);
139
140   gtk_widget_set_sensitive (fd->value_labels_checkbox,
141                             var_has_value_labels (var));
142
143   search_labels =
144     gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (fd->value_labels_checkbox));
145
146   gtk_widget_set_sensitive (fd->match_regexp_checkbox,
147                             var_is_alpha (var) || search_labels);
148
149
150   gtk_widget_set_sensitive (fd->match_substring_checkbox,
151                             var_is_alpha (var) || search_labels);
152 }
153
154 /* Callback on the selector.
155    It gets invoked whenever a variable is unselected */
156 static void
157 on_deselect (GtkEntry *entry, gpointer data)
158 {
159   struct find_dialog *fd = data;
160
161   gtk_widget_set_sensitive (fd->value_labels_checkbox, FALSE);
162   gtk_widget_set_sensitive (fd->match_substring_checkbox, FALSE);
163   gtk_widget_set_sensitive (fd->match_regexp_checkbox, FALSE);
164 }
165
166 static void
167 value_labels_toggled (GtkToggleButton *tb, gpointer data)
168 {
169   struct find_dialog *fd = data;
170
171   const char *var_name = gtk_entry_get_text (GTK_ENTRY (fd->variable_entry));
172   const struct variable *var = dict_lookup_var (fd->dict->dict, var_name);
173
174   gboolean active = gtk_toggle_button_get_active  (tb) ;
175
176   gtk_widget_set_sensitive (fd->match_substring_checkbox,
177                             active || (var && var_is_alpha (var)));
178
179   gtk_widget_set_sensitive (fd->match_regexp_checkbox,
180                               active || (var && var_is_alpha (var)));
181 }
182
183 /* Pops up the Find dialog box
184  */
185 void
186 find_dialog (PsppireDataWindow *de)
187 {
188   struct find_dialog fd;
189
190   GtkWidget *dialog ;
191   GtkWidget *source ;
192   GtkWidget *selector;
193   GtkWidget *find_button;
194
195   GtkWidget *buttonbox;
196
197   PsppireDataStore *ds ;
198
199   fd.xml = builder_new ("find.ui");
200   fd.de = de;
201
202   find_button = gtk_button_new_with_label (_("Find"));
203   gtk_widget_show (find_button);
204
205   buttonbox = get_widget_assert (fd.xml, "find-buttonbox");
206
207   psppire_box_pack_start_defaults (GTK_BOX (buttonbox), find_button);
208   gtk_box_reorder_child (GTK_BOX (buttonbox), find_button, 0);
209
210   dialog = get_widget_assert (fd.xml, "find-dialog");
211   source = get_widget_assert (fd.xml, "find-variable-treeview");
212   selector = get_widget_assert (fd.xml, "find-selector");
213
214   g_object_get (de->data_editor,
215                 "dictionary", &fd.dict,
216                 "data-store", &ds,
217                 NULL);
218
219   fd.data = ds->datasheet;
220
221   fd.variable_entry        = get_widget_assert (fd.xml, "find-variable-entry");
222   fd.value_entry           = get_widget_assert (fd.xml, "find-value-entry");
223   fd.value_labels_checkbox =
224     get_widget_assert (fd.xml,
225                        "find-value-labels-checkbutton");
226
227   fd.match_regexp_checkbox =
228     get_widget_assert (fd.xml,
229                        "find-match-regexp-checkbutton");
230
231   fd.match_substring_checkbox =
232     get_widget_assert (fd.xml,
233                        "find-match-substring-checkbutton");
234
235
236
237   gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (de));
238
239
240   g_object_set (source, "model", fd.dict,
241         "selection-mode", GTK_SELECTION_SINGLE,
242         NULL);
243
244
245   psppire_selector_set_filter_func (PSPPIRE_SELECTOR (selector),
246                                     is_currently_in_entry);
247
248   g_signal_connect (dialog, "refresh", G_CALLBACK (refresh),  &fd);
249
250   g_signal_connect (find_button, "clicked", G_CALLBACK (do_find),  &fd);
251
252   g_signal_connect (selector, "selected",
253                     G_CALLBACK (on_select),  &fd);
254
255   g_signal_connect (selector, "de-selected",
256                     G_CALLBACK (on_deselect),  &fd);
257
258   g_signal_connect (fd.value_labels_checkbox, "toggled",
259                     G_CALLBACK (value_labels_toggled),  &fd);
260
261
262   psppire_dialog_run (PSPPIRE_DIALOG (dialog));
263
264   g_object_unref (fd.xml);
265 }
266
267 \f
268 /* Iterators */
269
270 static void
271 forward (casenumber *i, struct datasheet *data UNUSED)
272 {
273   ++*i;
274 }
275
276
277 static void
278 forward_wrap (casenumber *i, struct datasheet *data)
279 {
280   if (++*i >=  datasheet_get_n_rows (data)) *i = 0;
281 }
282
283 static void
284 backward (casenumber *i, struct datasheet *data UNUSED)
285 {
286   --*i;
287 }
288
289
290 static void
291 backward_wrap (casenumber *i, struct datasheet *data)
292 {
293   if (--*i < 0)
294     *i = datasheet_get_n_rows (data) - 1;
295 }
296
297
298 /* Current plus one */
299 static casenumber
300 cp1 (casenumber current, struct datasheet *data)
301 {
302   return current + 1;
303 }
304
305 /* Current plus one, circular */
306 static casenumber
307 cp1c (casenumber current, struct datasheet *data)
308 {
309   casenumber next = current;
310
311   forward_wrap (&next, data);
312
313   return next;
314 }
315
316
317 /* Current minus one */
318 static casenumber
319 cm1 (casenumber current, struct datasheet *data)
320 {
321   if (current == -1)
322     return datasheet_get_n_rows (data);
323
324   return current - 1;
325 }
326
327 /* Current minus one, circular */
328 static casenumber
329 cm1c (casenumber current, struct datasheet *data)
330 {
331   casenumber next = current;
332
333   if (current == -1)
334     return datasheet_get_n_rows (data);
335
336   backward_wrap (&next, data);
337
338   return next;
339 }
340
341
342 static casenumber
343 last (casenumber current, struct datasheet *data)
344 {
345   return datasheet_get_n_rows (data) ;
346 }
347
348 static casenumber
349 minus1 (casenumber current, struct datasheet *data)
350 {
351   if (current == -1)
352     return 0;
353
354   return -1;
355 }
356
357 /* An type to facilitate iterating through casenumbers */
358 struct casenum_iterator
359 {
360   /* returns the first case to access */
361   casenumber (*start) (casenumber, struct datasheet *);
362
363   /* Returns one past the last case to access */
364   casenumber (*end) (casenumber, struct datasheet *);
365
366   /* Sets the first arg to the next case to access */
367   void (*next) (casenumber *, struct datasheet *);
368 };
369
370 enum iteration_type{
371   FORWARD = 0,
372   FORWARD_WRAP,
373   REVERSE,
374   REVERSE_WRAP,
375   n_iterators
376 };
377
378 static const struct casenum_iterator ip[n_iterators] =
379   {
380     {cp1, last, forward},
381     {cp1c, cm1, forward_wrap},
382     {cm1, minus1, backward},
383     {cm1c, cp1, backward_wrap}
384   };
385
386
387 \f
388 /* A factory returning an iterator according to the dialog box's settings */
389 static const struct casenum_iterator *
390 get_iteration_params (const struct find_dialog *fd)
391 {
392   gboolean wrap = gtk_toggle_button_get_active
393     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-wrap")));
394
395   gboolean reverse = gtk_toggle_button_get_active
396     (GTK_TOGGLE_BUTTON (get_widget_assert (fd->xml, "find-backwards")));
397
398   if (wrap)
399     {
400       if (reverse)
401         return &ip[REVERSE_WRAP];
402       else
403         return &ip[FORWARD_WRAP];
404     }
405   else
406     {
407       if (reverse)
408         return &ip[REVERSE];
409       else
410         return &ip[FORWARD];
411     }
412 }
413
414
415 enum string_cmp_flags
416   {
417     STR_CMP_SUBSTR = 0x01, /* Find strings which are substrings of the
418                               values */
419     STR_CMP_REGEXP = 0x02, /* Match against a regular expression */
420
421     STR_CMP_LABELS = 0x04  /* Match against the values' labels instead
422                               of the data */
423   };
424
425
426 /* An abstract base type for comparing union values against a reference */
427 struct comparator
428 {
429   const struct variable *var;
430   enum string_cmp_flags flags;
431
432   bool (*compare) (const struct comparator *,
433                    const union value *);
434
435   void (*destroy) (struct comparator *);
436 };
437
438
439 /* A comparator which operates on the numerical values,
440    rounded to the number of decimal places indicated by
441    the variable's format.  */
442 struct numeric_comparator
443 {
444   struct comparator parent;
445   double rounded_ref;
446 };
447
448 /* A comparator which matches string values or parts thereof */
449 struct string_comparator
450 {
451   struct comparator parent;
452   const char *pattern;
453 };
454
455 /* A comparator to match string values against a POSIX.2 regular expression */
456 struct regexp_comparator
457 {
458   struct comparator parent;
459   regex_t re;
460 };
461
462
463 static bool
464 value_compare (const struct comparator *cmptr,
465                const union value *v)
466 {
467   const struct numeric_comparator *nc = (const struct numeric_comparator *) cmptr;
468   const struct fmt_spec *fs = var_get_print_format (cmptr->var);
469
470   double c = nearbyint (v->f * exp10 (fs->d));
471
472   return c == nc->rounded_ref;
473 }
474
475
476 /* Return true if the label of VAL matches the reference string*/
477 static bool
478 string_label_compare (const struct comparator *cmptr,
479                 const union value *val)
480 {
481   const struct string_comparator *ssc =
482     (const struct string_comparator *) cmptr;
483
484   int width;
485
486   const char *text = var_lookup_value_label (cmptr->var, val);
487   if (text == NULL)
488     return false;
489
490   width = strlen (text);
491
492   assert (cmptr->flags & STR_CMP_LABELS);
493
494   g_return_val_if_fail (width > 0, false);
495
496   if (cmptr->flags & STR_CMP_SUBSTR)
497     return (NULL != g_strstr_len (text, width, ssc->pattern));
498   else
499     return (0 == strncmp (text, ssc->pattern, width));
500 }
501
502 /* Return true if VAL matches the reference string*/
503 static bool
504 string_value_compare (const struct comparator *cmptr,
505                       const union value *val)
506 {
507   bool found;
508   char *text;
509   const struct string_comparator *ssc =
510     (const struct string_comparator *) cmptr;
511
512   int width = var_get_width (cmptr->var);
513   g_return_val_if_fail (width > 0, false);
514   assert (! (cmptr->flags & STR_CMP_LABELS));
515
516   text = value_to_text (*val, cmptr->var);
517
518   if (cmptr->flags & STR_CMP_SUBSTR)
519     found =  (NULL != g_strstr_len (text, width, ssc->pattern));
520   else
521     found = (0 == strncmp (text, ssc->pattern, width));
522
523   free (text);
524   return found;
525 }
526
527
528
529 /* Return true if VAL matched the regexp */
530 static bool
531 regexp_value_compare (const struct comparator *cmptr,
532                 const union value *val)
533 {
534   char *text;
535   bool retval;
536   const struct regexp_comparator *rec =
537     (const struct regexp_comparator *) cmptr;
538
539   int width = var_get_width (cmptr->var);
540
541   assert  (! (cmptr->flags & STR_CMP_LABELS));
542
543   g_return_val_if_fail (width > 0, false);
544
545   text = value_to_text (*val, cmptr->var);
546   /* We must remove trailing whitespace, otherwise $ will not match where
547      one would expect */
548   g_strchomp (text);
549
550   retval = (0 == regexec (&rec->re, text, 0, 0, 0));
551
552   g_free (text);
553
554   return retval;
555 }
556
557 /* Return true if the label of VAL matched the regexp */
558 static bool
559 regexp_label_compare (const struct comparator *cmptr,
560                       const union value *val)
561 {
562   const char *text;
563   const struct regexp_comparator *rec =
564     (const struct regexp_comparator *) cmptr;
565
566   int width ;
567
568   assert (cmptr->flags & STR_CMP_LABELS);
569
570   text = var_lookup_value_label (cmptr->var, val);
571   width = strlen (text);
572
573   g_return_val_if_fail (width > 0, false);
574
575   return (0 == regexec (&rec->re, text, 0, 0, 0));
576 }
577
578
579
580 static void
581 regexp_destroy (struct comparator *cmptr)
582 {
583   struct regexp_comparator *rec
584     = UP_CAST (cmptr, struct regexp_comparator, parent);
585
586   regfree (&rec->re);
587 }
588
589 static struct comparator *
590 numeric_comparator_create (const struct variable *var, const char *target)
591 {
592   struct numeric_comparator *nc = xzalloc (sizeof (*nc));
593   struct comparator *cmptr = &nc->parent;
594
595   cmptr->flags = 0;
596   cmptr->var = var;
597   cmptr->compare  = value_compare;
598   const struct fmt_spec *fs = var_get_write_format (var);
599
600   union value val;
601   text_to_value (target, var, &val);
602   nc->rounded_ref = nearbyint (val.f * exp10 (fs->d));
603   value_destroy (&val, var_get_width (var));
604
605   return cmptr;
606 }
607
608 static struct comparator *
609 string_comparator_create (const struct variable *var, const char *target,
610                           enum string_cmp_flags flags)
611 {
612   struct string_comparator *ssc = xzalloc (sizeof (*ssc));
613   struct comparator *cmptr = &ssc->parent;
614
615   cmptr->flags = flags;
616   cmptr->var = var;
617
618   if (flags & STR_CMP_LABELS)
619     cmptr->compare = string_label_compare;
620   else
621     cmptr->compare = string_value_compare;
622
623   ssc->pattern = target;
624
625   return cmptr;
626 }
627
628
629 static struct comparator *
630 regexp_comparator_create (const struct variable *var, const char *target,
631                           enum string_cmp_flags flags)
632 {
633   int code;
634   struct regexp_comparator *rec = xzalloc (sizeof (*rec));
635   struct comparator *cmptr = &rec->parent;
636
637   cmptr->flags = flags;
638   cmptr->var = var;
639   cmptr->compare  = (flags & STR_CMP_LABELS)
640     ? regexp_label_compare : regexp_value_compare ;
641
642   cmptr->destroy  = regexp_destroy;
643
644   code = regcomp (&rec->re, target, 0);
645   if (code != 0)
646     {
647       char *errbuf = NULL;
648       size_t errbuf_size = regerror (code, &rec->re, errbuf,  0);
649
650       errbuf = xmalloc (errbuf_size);
651
652       regerror (code, &rec->re, errbuf, errbuf_size);
653
654       msg (ME, _("Bad regular expression: %s"), errbuf);
655
656       free (cmptr);
657       free (errbuf);
658       return NULL;
659     }
660
661   return cmptr;
662 }
663
664
665 /* Compare V against CMPTR's reference */
666 static bool
667 comparator_compare (const struct comparator *cmptr,
668                     const union value *v)
669 {
670   return cmptr->compare (cmptr, v);
671 }
672
673 /* Destroy CMPTR */
674 static void
675 comparator_destroy (struct comparator *cmptr)
676 {
677   if (! cmptr)
678     return ;
679
680   if (cmptr->destroy)
681     cmptr->destroy (cmptr);
682
683   free (cmptr);
684 }
685
686
687 static struct comparator *
688 comparator_factory (const struct variable *var, const char *str,
689                     enum string_cmp_flags flags)
690 {
691   if (flags & STR_CMP_REGEXP)
692     return regexp_comparator_create (var, str, flags);
693
694   if (flags & (STR_CMP_SUBSTR | STR_CMP_LABELS))
695     return string_comparator_create (var, str, flags);
696
697   return numeric_comparator_create (var, str);
698 }
699
700
701 /* Find the row and column specified by the dialog FD, starting at CURRENT_ROW.
702    After the function returns, *ROW contains the row and *COLUMN the column.
703    If no such case is found, then *ROW will be set to -1
704  */
705 static void
706 find_value (const struct find_dialog *fd, casenumber current_row,
707            casenumber *row, int *column)
708 {
709   int width;
710   const struct variable *var;
711   const char *var_name = gtk_entry_get_text (GTK_ENTRY (fd->variable_entry));
712   const char *target_string = gtk_entry_get_text (GTK_ENTRY (fd->value_entry));
713
714   enum string_cmp_flags flags = 0;
715
716   var = dict_lookup_var (fd->dict->dict, var_name);
717   if (! var)
718     return ;
719
720   width = var_get_width (var);
721
722   *column = var_get_dict_index (var);
723   *row = -1;
724
725   if (gtk_toggle_button_get_active
726        (GTK_TOGGLE_BUTTON (fd->match_substring_checkbox)))
727     flags |= STR_CMP_SUBSTR;
728
729   if (gtk_toggle_button_get_active
730        (GTK_TOGGLE_BUTTON (fd->match_regexp_checkbox)))
731     flags |= STR_CMP_REGEXP;
732
733   if (gtk_toggle_button_get_active
734        (GTK_TOGGLE_BUTTON (fd->value_labels_checkbox)))
735     flags |= STR_CMP_LABELS;
736
737   {
738     union value val;
739     casenumber i;
740     const struct casenum_iterator *ip = get_iteration_params (fd);
741     struct comparator *cmptr =
742       comparator_factory (var, target_string, flags);
743
744     value_init (&val, width);
745     if (! cmptr)
746       goto finish;
747
748     for (i = ip->start (current_row, fd->data);
749          i != ip->end (current_row, fd->data);
750          ip->next (&i, fd->data))
751       {
752         datasheet_get_value (fd->data, i, var_get_case_index (var), &val);
753
754         if (comparator_compare (cmptr, &val))
755           {
756             *row = i;
757             break;
758           }
759       }
760
761   finish:
762     comparator_destroy (cmptr);
763     value_destroy (&val, width);
764   }
765 }