]> git.ozlabs.org Git - ccan/blob - ccan/strgrp/strgrp.c
strgrp: Use ratio of hypotenuse for consistent comparisons
[ccan] / ccan / strgrp / strgrp.c
1 /*
2     Group similar strings
3     Copyright (C) 2014  Andrew Jeffery <andrew@aj.id.au>
4
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU Lesser General Public License for more details.
14
15     You should have received a copy of the GNU Lesser General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 #include <assert.h>
19 #include <limits.h>
20 #include <math.h>
21 #include <stdbool.h>
22 #include <stdint.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include "ccan/darray/darray.h"
27 #include "ccan/stringmap/stringmap.h"
28 #include "ccan/tal/tal.h"
29 #include "ccan/tal/str/str.h"
30 #include "strgrp.h"
31 #include "config.h"
32
33 #define CHAR_N_VALUES (1 << CHAR_BIT)
34
35 typedef darray(struct strgrp_grp *) darray_grp;
36 typedef darray(struct strgrp_item *) darray_item;
37
38 typedef stringmap(struct strgrp_grp *) stringmap_grp;
39
40 struct grp_score {
41     struct strgrp_grp *grp;
42     double score;
43 };
44
45 typedef darray(struct grp_score *) darray_score;
46
47 struct strgrp {
48     double threshold;
49     stringmap_grp known;
50     unsigned int n_grps;
51     darray_grp grps;
52     struct grp_score *scores;
53     int16_t pop[CHAR_N_VALUES];
54 };
55
56 struct strgrp_iter {
57     const struct strgrp *ctx;
58     int i;
59 };
60
61 struct strgrp_grp {
62     const char *key;
63     size_t key_len;
64     darray_item items;
65     int32_t n_items;
66     int16_t pop[CHAR_N_VALUES];
67 };
68
69 struct strgrp_grp_iter {
70     const struct strgrp_grp *grp;
71     int i;
72 };
73
74 struct strgrp_item {
75     const char *key;
76     void *value;
77 };
78
79
80 /* String vector cosine similarity[1]
81  *
82  * [1] http://blog.nishtahir.com/2015/09/19/fuzzy-string-matching-using-cosine-similarity/
83  */
84
85 static inline void
86 strpopcnt(const char *const str, int16_t pop[CHAR_N_VALUES]) {
87     const char *c;
88     memset(pop, 0, CHAR_N_VALUES * sizeof(*pop));
89     for(c = str; *c; c++) {
90         assert(*c >= 0);
91         pop[(unsigned char)*c]++;
92     }
93 }
94
95 static inline double
96 strcossim(const int16_t ref[CHAR_N_VALUES], const int16_t key[CHAR_N_VALUES]) {
97     int32_t saibi = 0;
98     int32_t sai2 = 0;
99     int32_t sbi2 = 0;
100     size_t i;
101     for (i = 0; i < CHAR_N_VALUES; i++) {
102         saibi += ref[i] * key[i];
103         sai2 += ref[i] * ref[i];
104         sbi2 += key[i] * key[i];
105     }
106     return saibi / (sqrt(sai2) * sqrt(sbi2));
107 }
108
109 /* Low-cost filter functions */
110
111 static inline bool
112 should_grp_score_cos(const struct strgrp *const ctx,
113         struct strgrp_grp *const grp, const char *const str) {
114     return ctx->threshold <= strcossim(ctx->pop, grp->pop);
115 }
116
117 static inline bool
118 should_grp_score_len(const struct strgrp *const ctx,
119         const struct strgrp_grp *const grp, const char *const str) {
120     const double lstr = (double) strlen(str);
121     const double lkey = (double) grp->key_len;
122     const double lmin = (lstr > lkey) ? lkey : lstr;
123     const double s = sqrt((2 * lmin * lmin) / (1.0 * lstr * lstr + lkey * lkey));
124     return ctx->threshold <= s;
125 }
126
127 /* Scoring - Longest Common Subsequence[2]
128  *
129  * [2] https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
130  */
131 #define ROWS 2
132
133 static inline int cmi(int i, int j) {
134     return ROWS * j + i;
135 }
136
137 static inline int16_t
138 lcs(const char *const a, const char *const b) {
139     const int lb = strlen(b);
140     const int lbp1 = lb + 1;
141     int16_t *const lookup = calloc(ROWS * lbp1, sizeof(int16_t));
142     if (!lookup) {
143         return -1;
144     }
145     int ia, ib;
146     for (ia = (strlen(a) - 1); ia >= 0; ia--) {
147         const char iav = a[ia];
148         const int ial = (ia + 1) & 1; // ia last
149         for (ib = lb - 1; ib >= 0; ib--) {
150             const char ibv = b[ib];
151             const int iac = ia & 1; // ia current
152             const int ibl = ib + 1; // ib last
153             // don't need separate "ib current" as it's just ib
154             if (iav == ibv) {
155                 lookup[cmi(iac, ib)] = 1 + lookup[cmi(ial, ibl)];
156             } else {
157                 const int16_t valb = lookup[cmi(ial, ib)];
158                 const int16_t vabl = lookup[cmi(iac, ibl)];
159                 lookup[cmi(iac, ib)] = (valb > vabl) ? valb : vabl;
160             }
161         }
162     }
163     int16_t result = lookup[0];
164     free(lookup);
165     return result;
166 }
167
168 #undef ROWS
169
170 static inline double
171 nlcs(const char *const a, const char *const b) {
172     const double lcss = lcs(a, b);
173     const double la = (double) strlen(a);
174     const double lb = (double) strlen(b);
175     const double s = sqrt((2 * lcss * lcss) / (la * la + lb * lb));
176     return s;
177 }
178
179 static inline double
180 grp_score(const struct strgrp_grp *const grp, const char *const str) {
181     return nlcs(grp->key, str);
182 }
183
184 /* Structure management */
185
186 static struct strgrp_item *
187 new_item(tal_t *const tctx, const char *const str, void *const data) {
188     struct strgrp_item *i = talz(tctx, struct strgrp_item);
189     if (!i) {
190         return NULL;
191     }
192     i->key = tal_strdup(i, str);
193     i->value = data;
194     return i;
195 }
196
197 static bool
198 add_item(struct strgrp_grp *const ctx, const char *const str,
199         void *const data) {
200     struct strgrp_item *i = new_item(ctx, str, data);
201     if (!i) {
202         return false;
203     }
204     darray_push(ctx->items, i);
205     ctx->n_items++;
206     return true;
207 }
208
209 static void
210 free_grp(struct strgrp_grp *grp) {
211     darray_free(grp->items);
212 }
213
214 static struct strgrp_grp *
215 new_grp(tal_t *const tctx, const char *const str, void *const data) {
216     struct strgrp_grp *b = talz(tctx, struct strgrp_grp);
217     if (!b) {
218         return NULL;
219     }
220     b->key = tal_strdup(b, str);
221     b->key_len = strlen(str);
222     b->n_items = 0;
223     darray_init(b->items);
224     tal_add_destructor(b, free_grp);
225     if (!add_item(b, str, data)) {
226         return tal_free(b);
227     }
228     return b;
229 }
230
231 static struct strgrp_grp *
232 add_grp(struct strgrp *const ctx, const char *const str,
233         void *const data) {
234     struct strgrp_grp *b = new_grp(ctx, str, data);
235     if (!b) {
236         return NULL;
237     }
238     memcpy(b->pop, ctx->pop, sizeof(ctx->pop));
239     darray_push(ctx->grps, b);
240     ctx->n_grps++;
241     if (ctx->scores) {
242         if (!tal_resize(&ctx->scores, ctx->n_grps)) {
243             return NULL;
244         }
245     } else {
246         ctx->scores = tal_arr(ctx, struct grp_score, ctx->n_grps);
247         if (!ctx->scores) {
248             return NULL;
249         }
250     }
251     return b;
252 }
253
254 struct strgrp *
255 strgrp_new(const double threshold) {
256     struct strgrp *ctx = talz(NULL, struct strgrp);
257     ctx->threshold = threshold;
258     stringmap_init(ctx->known, NULL);
259     // n threads compare strings
260     darray_init(ctx->grps);
261     return ctx;
262 }
263
264 static inline void
265 cache(struct strgrp *const ctx, struct strgrp_grp *const grp,
266         const char *const str) {
267     *(stringmap_enter(ctx->known, str)) = grp;
268 }
269
270 static struct strgrp_grp *
271 grp_for(struct strgrp *const ctx, const char *const str) {
272     // Ensure ctx->pop is always populated. Returning null here indicates a new
273     // group should be created, at which point add_grp() copies ctx->pop into
274     // the new group's struct.
275     strpopcnt(str, ctx->pop);
276     if (!ctx->n_grps) {
277         return NULL;
278     }
279     {
280         struct strgrp_grp **const grp = stringmap_lookup(ctx->known, str);
281         if (grp) {
282             return *grp;
283         }
284     }
285     int i;
286 // Keep ccanlint happy in reduced feature mode
287 #if HAVE_OPENMP
288     #pragma omp parallel for schedule(dynamic)
289 #endif
290     for (i = 0; i < ctx->n_grps; i++) {
291         struct strgrp_grp *grp = darray_item(ctx->grps, i);
292         ctx->scores[i].grp = grp;
293         ctx->scores[i].score = 0;
294         if (should_grp_score_len(ctx, grp, str)) {
295             if (should_grp_score_cos(ctx, grp, str)) {
296                 ctx->scores[i].score = grp_score(grp, str);
297             }
298         }
299     }
300     struct grp_score *max = NULL;
301     for (i = 0; i < ctx->n_grps; i++) {
302         if (!max || ctx->scores[i].score > max->score) {
303             max = &(ctx->scores[i]);
304         }
305     }
306     return (max && max->score >= ctx->threshold) ? max->grp : NULL;
307 }
308
309 const struct strgrp_grp *
310 strgrp_grp_for(struct strgrp *const ctx, const char *const str) {
311     return grp_for(ctx, str);
312 }
313
314 const struct strgrp_grp *
315 strgrp_add(struct strgrp *const ctx, const char *const str,
316         void *const data) {
317     bool inserted = false;
318     // grp_for() populates the ctx->pop memory. add_grp() copies this memory
319     // into the strgrp_grp that it creates. It's assumed the ctx->pop memory
320     // has not been modified between the grp_for() and add_grp() calls.
321     struct strgrp_grp *pick = grp_for(ctx, str);
322     if (pick) {
323         inserted = add_item(pick, str, data);
324     } else {
325         pick = add_grp(ctx, str, data);
326         inserted = (NULL != pick);
327     }
328     if (inserted) {
329         assert(NULL != pick);
330         cache(ctx, pick, str);
331     }
332     return pick;
333 }
334
335 struct strgrp_iter *
336 strgrp_iter_new(struct strgrp *const ctx) {
337     struct strgrp_iter *iter = talz(ctx, struct strgrp_iter);
338     if (!iter) {
339         return NULL;
340     }
341     iter->ctx = ctx;
342     iter->i = 0;
343     return iter;
344 }
345
346 const struct strgrp_grp *
347 strgrp_iter_next(struct strgrp_iter *const iter) {
348     return (iter->ctx->n_grps == iter->i) ?
349         NULL : darray_item(iter->ctx->grps, iter->i++);
350 }
351
352 void
353 strgrp_iter_free(struct strgrp_iter *const iter) {
354     tal_free(iter);
355 }
356
357 struct strgrp_grp_iter *
358 strgrp_grp_iter_new(const struct strgrp_grp *const grp) {
359     struct strgrp_grp_iter *iter = talz(grp, struct strgrp_grp_iter);
360     if (!iter) {
361         return NULL;
362     }
363     iter->grp = grp;
364     iter->i = 0;
365     return iter;
366 }
367
368 const struct strgrp_item *
369 strgrp_grp_iter_next(struct strgrp_grp_iter *const iter) {
370     return (iter->grp->n_items == iter->i) ?
371         NULL : darray_item(iter->grp->items, iter->i++);
372 }
373
374 void
375 strgrp_grp_iter_free(struct strgrp_grp_iter *iter) {
376     tal_free(iter);
377 }
378
379 const char *
380 strgrp_grp_key(const struct strgrp_grp *const grp) {
381     return grp->key;
382 }
383
384 const char *
385 strgrp_item_key(const struct strgrp_item *const item) {
386     return item->key;
387 }
388
389 void *
390 strgrp_item_value(const struct strgrp_item *const item) {
391     return item->value;
392 }
393
394 void
395 strgrp_free(struct strgrp *const ctx) {
396     darray_free(ctx->grps);
397     stringmap_free(ctx->known);
398     tal_free(ctx);
399 }
400
401 void
402 strgrp_free_cb(struct strgrp *const ctx, void (*cb)(void *data)) {
403     struct strgrp_grp **grp;
404     struct strgrp_item **item;
405     darray_foreach(grp, ctx->grps) {
406         darray_foreach(item, (*grp)->items) {
407             cb((*item)->value);
408         }
409     }
410     strgrp_free(ctx);
411 }
412
413 static void
414 print_item(const struct strgrp_item *item) {
415     printf("\t%s\n", item->key);
416 }
417
418 static void
419 print_grp(const struct strgrp_grp *const grp) {
420     struct strgrp_item **item;
421     printf("%s:\n", grp->key);
422     darray_foreach(item, grp->items) {
423         print_item(*item);
424     }
425     printf("\n");
426 }
427
428 void
429 strgrp_print(const struct strgrp *const ctx) {
430     struct strgrp_grp **grp;
431     darray_foreach(grp, ctx->grps) {
432         print_grp(*grp);
433     }
434 }