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