Age Owner Branch data TLA Line data Source code
1 : : /*
2 : : * fuzzystrmatch.c
3 : : *
4 : : * Functions for "fuzzy" comparison of strings
5 : : *
6 : : * Joe Conway <mail@joeconway.com>
7 : : *
8 : : * contrib/fuzzystrmatch/fuzzystrmatch.c
9 : : * Copyright (c) 2001-2026, PostgreSQL Global Development Group
10 : : * ALL RIGHTS RESERVED;
11 : : *
12 : : * metaphone()
13 : : * -----------
14 : : * Modified for PostgreSQL by Joe Conway.
15 : : * Based on CPAN's "Text-Metaphone-1.96" by Michael G Schwern <schwern@pobox.com>
16 : : * Code slightly modified for use as PostgreSQL function (palloc, elog, etc).
17 : : * Metaphone was originally created by Lawrence Philips and presented in article
18 : : * in "Computer Language" December 1990 issue.
19 : : *
20 : : * Permission to use, copy, modify, and distribute this software and its
21 : : * documentation for any purpose, without fee, and without a written agreement
22 : : * is hereby granted, provided that the above copyright notice and this
23 : : * paragraph and the following two paragraphs appear in all copies.
24 : : *
25 : : * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR
26 : : * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
27 : : * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
28 : : * DOCUMENTATION, EVEN IF THE AUTHOR OR DISTRIBUTORS HAVE BEEN ADVISED OF THE
29 : : * POSSIBILITY OF SUCH DAMAGE.
30 : : *
31 : : * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
32 : : * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
33 : : * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
34 : : * ON AN "AS IS" BASIS, AND THE AUTHOR AND DISTRIBUTORS HAS NO OBLIGATIONS TO
35 : : * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
36 : : *
37 : : */
38 : :
39 : : #include "postgres.h"
40 : :
41 : : #include <ctype.h>
42 : :
43 : : #include "utils/builtins.h"
44 : : #include "utils/varlena.h"
45 : : #include "varatt.h"
46 : :
430 tgl@sss.pgh.pa.us 47 :CBC 2 : PG_MODULE_MAGIC_EXT(
48 : : .name = "fuzzystrmatch",
49 : : .version = PG_VERSION
50 : : );
51 : :
52 : : /*
53 : : * Soundex
54 : : */
55 : : static void _soundex(const char *instr, char *outstr);
56 : :
57 : : #define SOUNDEX_LEN 4
58 : :
59 : : /* ABCDEFGHIJKLMNOPQRSTUVWXYZ */
60 : : static const char *const soundex_table = "01230120022455012623010202";
61 : :
62 : : static char
6262 63 : 127 : soundex_code(char letter)
64 : : {
138 jdavis@postgresql.or 65 :GNC 127 : letter = pg_ascii_toupper((unsigned char) letter);
66 : : /* Defend against non-ASCII letters */
6262 tgl@sss.pgh.pa.us 67 [ + + + - ]:CBC 127 : if (letter >= 'A' && letter <= 'Z')
68 : 126 : return soundex_table[letter - 'A'];
69 : 1 : return letter;
70 : : }
71 : :
72 : : /*
73 : : * Metaphone
74 : : */
75 : : #define MAX_METAPHONE_STRLEN 255
76 : :
77 : : /*
78 : : * Original code by Michael G Schwern starts here.
79 : : * Code slightly modified for use as PostgreSQL function.
80 : : */
81 : :
82 : :
83 : : /**************************************************************************
84 : : metaphone -- Breaks english phrases down into their phonemes.
85 : :
86 : : Input
87 : : word -- An english word to be phonized
88 : : max_phonemes -- How many phonemes to calculate. If 0, then it
89 : : will phonize the entire phrase.
90 : : phoned_word -- The final phonized word. (We'll allocate the
91 : : memory.)
92 : : Output
93 : : error -- A simple error flag, returns true or false
94 : :
95 : : NOTES: ALL non-alpha characters are ignored, this includes whitespace,
96 : : although non-alpha characters will break up phonemes.
97 : : ****************************************************************************/
98 : :
99 : :
100 : : /*
101 : : * I add modifications to the traditional metaphone algorithm that you
102 : : * might find in books. Define this if you want metaphone to behave
103 : : * traditionally
104 : : */
105 : : #undef USE_TRADITIONAL_METAPHONE
106 : :
107 : : /* Special encodings */
108 : : #define SH 'X'
109 : : #define TH '0'
110 : :
111 : : static char Lookahead(char *word, int how_far);
112 : : static void _metaphone(char *word, int max_phonemes, char **phoned_word);
113 : :
114 : : /* Metachar.h ... little bits about characters for metaphone */
115 : :
116 : :
117 : : /*-- Character encoding array & accessing macros --*/
118 : : /* Stolen directly out of the book... */
119 : : static const char _codes[26] = {
120 : : 1, 16, 4, 16, 9, 2, 4, 16, 9, 2, 0, 2, 2, 2, 1, 4, 0, 2, 4, 4, 1, 0, 0, 0, 8, 0
121 : : /* a b c d e f g h i j k l m n o p q r s t u v w x y z */
122 : : };
123 : :
124 : : static int
125 : 1 : getcode(char c)
126 : : {
138 jdavis@postgresql.or 127 :GNC 1 : c = pg_ascii_toupper((unsigned char) c);
128 : : /* Defend against non-ASCII letters */
129 [ + - + - ]: 1 : if (c >= 'A' && c <= 'Z')
130 : 1 : return _codes[c - 'A'];
131 : :
6262 tgl@sss.pgh.pa.us 132 :UBC 0 : return 0;
133 : : }
134 : :
135 : : static bool
138 jdavis@postgresql.or 136 :GNC 66 : ascii_isalpha(char c)
137 : : {
138 [ + + + + : 113 : return (c >= 'A' && c <= 'Z') ||
+ + ]
139 [ + - ]: 47 : (c >= 'a' && c <= 'z');
140 : : }
141 : :
142 : : #define isvowel(c) (getcode(c) & 1) /* AEIOU */
143 : :
144 : : /* These letters are passed through unchanged */
145 : : #define NOCHANGE(c) (getcode(c) & 2) /* FJMNR */
146 : :
147 : : /* These form diphthongs when preceding H */
148 : : #define AFFECTH(c) (getcode(c) & 4) /* CGPST */
149 : :
150 : : /* These make C and G soft */
151 : : #define MAKESOFT(c) (getcode(c) & 8) /* EIY */
152 : :
153 : : /* These prevent GH from becoming F */
154 : : #define NOGHTOF(c) (getcode(c) & 16) /* BDH */
155 : :
6631 tgl@sss.pgh.pa.us 156 :CBC 2 : PG_FUNCTION_INFO_V1(levenshtein_with_costs);
157 : : Datum
158 : 1 : levenshtein_with_costs(PG_FUNCTION_ARGS)
159 : : {
5784 rhaas@postgresql.org 160 : 1 : text *src = PG_GETARG_TEXT_PP(0);
161 : 1 : text *dst = PG_GETARG_TEXT_PP(1);
6197 bruce@momjian.us 162 : 1 : int ins_c = PG_GETARG_INT32(2);
163 : 1 : int del_c = PG_GETARG_INT32(3);
164 : 1 : int sub_c = PG_GETARG_INT32(4);
165 : : const char *s_data;
166 : : const char *t_data;
167 : : int s_bytes,
168 : : t_bytes;
169 : :
170 : : /* Extract a pointer to the actual character data */
4216 rhaas@postgresql.org 171 [ - + ]: 1 : s_data = VARDATA_ANY(src);
172 [ - + ]: 1 : t_data = VARDATA_ANY(dst);
173 : : /* Determine length of each string in bytes */
174 [ - + - - : 1 : s_bytes = VARSIZE_ANY_EXHDR(src);
- - - - -
+ ]
175 [ - + - - : 1 : t_bytes = VARSIZE_ANY_EXHDR(dst);
- - - - -
+ ]
176 : :
3781 tgl@sss.pgh.pa.us 177 : 1 : PG_RETURN_INT32(varstr_levenshtein(s_data, s_bytes, t_data, t_bytes,
178 : : ins_c, del_c, sub_c, false));
179 : : }
180 : :
181 : :
6631 182 : 2 : PG_FUNCTION_INFO_V1(levenshtein);
183 : : Datum
184 : 1 : levenshtein(PG_FUNCTION_ARGS)
185 : : {
5784 rhaas@postgresql.org 186 : 1 : text *src = PG_GETARG_TEXT_PP(0);
187 : 1 : text *dst = PG_GETARG_TEXT_PP(1);
188 : : const char *s_data;
189 : : const char *t_data;
190 : : int s_bytes,
191 : : t_bytes;
192 : :
193 : : /* Extract a pointer to the actual character data */
4216 194 [ - + ]: 1 : s_data = VARDATA_ANY(src);
195 [ - + ]: 1 : t_data = VARDATA_ANY(dst);
196 : : /* Determine length of each string in bytes */
197 [ - + - - : 1 : s_bytes = VARSIZE_ANY_EXHDR(src);
- - - - -
+ ]
198 [ - + - - : 1 : t_bytes = VARSIZE_ANY_EXHDR(dst);
- - - - -
+ ]
199 : :
3781 tgl@sss.pgh.pa.us 200 : 1 : PG_RETURN_INT32(varstr_levenshtein(s_data, s_bytes, t_data, t_bytes,
201 : : 1, 1, 1, false));
202 : : }
203 : :
204 : :
5702 rhaas@postgresql.org 205 : 1 : PG_FUNCTION_INFO_V1(levenshtein_less_equal_with_costs);
206 : : Datum
5702 rhaas@postgresql.org 207 :UBC 0 : levenshtein_less_equal_with_costs(PG_FUNCTION_ARGS)
208 : : {
209 : 0 : text *src = PG_GETARG_TEXT_PP(0);
210 : 0 : text *dst = PG_GETARG_TEXT_PP(1);
211 : 0 : int ins_c = PG_GETARG_INT32(2);
212 : 0 : int del_c = PG_GETARG_INT32(3);
213 : 0 : int sub_c = PG_GETARG_INT32(4);
214 : 0 : int max_d = PG_GETARG_INT32(5);
215 : : const char *s_data;
216 : : const char *t_data;
217 : : int s_bytes,
218 : : t_bytes;
219 : :
220 : : /* Extract a pointer to the actual character data */
4216 221 [ # # ]: 0 : s_data = VARDATA_ANY(src);
222 [ # # ]: 0 : t_data = VARDATA_ANY(dst);
223 : : /* Determine length of each string in bytes */
224 [ # # # # : 0 : s_bytes = VARSIZE_ANY_EXHDR(src);
# # # # #
# ]
225 [ # # # # : 0 : t_bytes = VARSIZE_ANY_EXHDR(dst);
# # # # #
# ]
226 : :
3781 tgl@sss.pgh.pa.us 227 : 0 : PG_RETURN_INT32(varstr_levenshtein_less_equal(s_data, s_bytes,
228 : : t_data, t_bytes,
229 : : ins_c, del_c, sub_c,
230 : : max_d, false));
231 : : }
232 : :
233 : :
5702 rhaas@postgresql.org 234 :CBC 2 : PG_FUNCTION_INFO_V1(levenshtein_less_equal);
235 : : Datum
236 : 2 : levenshtein_less_equal(PG_FUNCTION_ARGS)
237 : : {
238 : 2 : text *src = PG_GETARG_TEXT_PP(0);
239 : 2 : text *dst = PG_GETARG_TEXT_PP(1);
240 : 2 : int max_d = PG_GETARG_INT32(2);
241 : : const char *s_data;
242 : : const char *t_data;
243 : : int s_bytes,
244 : : t_bytes;
245 : :
246 : : /* Extract a pointer to the actual character data */
4216 247 [ - + ]: 2 : s_data = VARDATA_ANY(src);
248 [ - + ]: 2 : t_data = VARDATA_ANY(dst);
249 : : /* Determine length of each string in bytes */
250 [ - + - - : 2 : s_bytes = VARSIZE_ANY_EXHDR(src);
- - - - -
+ ]
251 [ - + - - : 2 : t_bytes = VARSIZE_ANY_EXHDR(dst);
- - - - -
+ ]
252 : :
3781 tgl@sss.pgh.pa.us 253 : 2 : PG_RETURN_INT32(varstr_levenshtein_less_equal(s_data, s_bytes,
254 : : t_data, t_bytes,
255 : : 1, 1, 1,
256 : : max_d, false));
257 : : }
258 : :
259 : :
260 : : /*
261 : : * Calculates the metaphone of an input string.
262 : : * Returns number of characters requested
263 : : * (suggested value is 4)
264 : : */
9062 bruce@momjian.us 265 : 2 : PG_FUNCTION_INFO_V1(metaphone);
266 : : Datum
267 : 1 : metaphone(PG_FUNCTION_ARGS)
268 : : {
6640 tgl@sss.pgh.pa.us 269 : 1 : char *str_i = TextDatumGetCString(PG_GETARG_DATUM(0));
270 : 1 : size_t str_i_len = strlen(str_i);
271 : : int reqlen;
272 : : char *metaph;
273 : :
274 : : /* return an empty string if we receive one */
8003 mail@joeconway.com 275 [ - + ]: 1 : if (!(str_i_len > 0))
6640 tgl@sss.pgh.pa.us 276 :UBC 0 : PG_RETURN_TEXT_P(cstring_to_text(""));
277 : :
9062 bruce@momjian.us 278 [ - + ]:CBC 1 : if (str_i_len > MAX_METAPHONE_STRLEN)
8346 tgl@sss.pgh.pa.us 279 [ # # ]:UBC 0 : ereport(ERROR,
280 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
281 : : errmsg("argument exceeds the maximum length of %d bytes",
282 : : MAX_METAPHONE_STRLEN)));
283 : :
9062 bruce@momjian.us 284 :CBC 1 : reqlen = PG_GETARG_INT32(1);
285 [ - + ]: 1 : if (reqlen > MAX_METAPHONE_STRLEN)
8346 tgl@sss.pgh.pa.us 286 [ # # ]:UBC 0 : ereport(ERROR,
287 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
288 : : errmsg("output exceeds the maximum length of %d bytes",
289 : : MAX_METAPHONE_STRLEN)));
290 : :
9062 bruce@momjian.us 291 [ - + ]:CBC 1 : if (!(reqlen > 0))
8346 tgl@sss.pgh.pa.us 292 [ # # ]:UBC 0 : ereport(ERROR,
293 : : (errcode(ERRCODE_ZERO_LENGTH_CHARACTER_STRING),
294 : : errmsg("output cannot be empty string")));
295 : :
3208 peter_e@gmx.net 296 :CBC 1 : _metaphone(str_i, reqlen, &metaph);
297 : 1 : PG_RETURN_TEXT_P(cstring_to_text(metaph));
298 : : }
299 : :
300 : :
301 : : /*
302 : : * Original code by Michael G Schwern starts here.
303 : : * Code slightly modified for use as PostgreSQL
304 : : * function (palloc, etc).
305 : : */
306 : :
307 : : /*
308 : : * I suppose I could have been using a character pointer instead of
309 : : * accessing the array directly...
310 : : */
311 : :
312 : : /* Look at the next letter in the word */
313 : : #define Next_Letter (pg_ascii_toupper((unsigned char) word[w_idx+1]))
314 : : /* Look at the current letter in the word */
315 : : #define Curr_Letter (pg_ascii_toupper((unsigned char) word[w_idx]))
316 : : /* Go N letters back. */
317 : : #define Look_Back_Letter(n) \
318 : : (w_idx >= (n) ? pg_ascii_toupper((unsigned char) word[w_idx-(n)]) : '\0')
319 : : /* Previous letter. I dunno, should this return null on failure? */
320 : : #define Prev_Letter (Look_Back_Letter(1))
321 : : /* Look two letters down. It makes sure you don't walk off the string. */
322 : : #define After_Next_Letter \
323 : : (Next_Letter != '\0' ? pg_ascii_toupper((unsigned char) word[w_idx+2]) : '\0')
324 : : #define Look_Ahead_Letter(n) pg_ascii_toupper((unsigned char) Lookahead(word+w_idx, n))
325 : :
326 : :
327 : : /* Allows us to safely look ahead an arbitrary # of letters */
328 : : /* I probably could have just used strlen... */
329 : : static char
8983 bruce@momjian.us 330 :UBC 0 : Lookahead(char *word, int how_far)
331 : : {
332 : 0 : char letter_ahead = '\0'; /* null by default */
333 : : int idx;
334 : :
335 [ # # # # ]: 0 : for (idx = 0; word[idx] != '\0' && idx < how_far; idx++);
336 : : /* Edge forward in the string... */
337 : :
7532 338 : 0 : letter_ahead = word[idx]; /* idx will be either == to how_far or at the
339 : : * end of the string */
9062 340 : 0 : return letter_ahead;
341 : : }
342 : :
343 : :
344 : : /* phonize one letter */
345 : : #define Phonize(c) do {(*phoned_word)[p_idx++] = c;} while (0)
346 : : /* Slap a null character on the end of the phoned word */
347 : : #define End_Phoned_Word do {(*phoned_word)[p_idx] = '\0';} while (0)
348 : : /* How long is the phoned word? */
349 : : #define Phone_Len (p_idx)
350 : :
351 : : /* Note is a letter is a 'break' in the word */
352 : : #define Isbreak(c) (!ascii_isalpha((unsigned char) (c)))
353 : :
354 : :
355 : : static void
6197 bruce@momjian.us 356 :CBC 1 : _metaphone(char *word, /* IN */
357 : : int max_phonemes,
358 : : char **phoned_word) /* OUT */
359 : : {
8983 360 : 1 : int w_idx = 0; /* point in the phonization we're at. */
361 : 1 : int p_idx = 0; /* end of the phoned phrase */
362 : :
363 : : /*-- Parameter checks --*/
364 : :
365 : : /*
366 : : * Shouldn't be necessary, but left these here anyway jec Aug 3, 2001
367 : : */
368 : :
369 : : /* Negative phoneme length is meaningless */
9062 370 [ - + ]: 1 : if (!(max_phonemes > 0))
371 : : /* internal error */
9062 bruce@momjian.us 372 [ # # ]:UBC 0 : elog(ERROR, "metaphone: Requested output length must be > 0");
373 : :
374 : : /* Empty/null string is meaningless */
9062 bruce@momjian.us 375 [ + - - + ]:CBC 1 : if ((word == NULL) || !(strlen(word) > 0))
376 : : /* internal error */
9062 bruce@momjian.us 377 [ # # ]:UBC 0 : elog(ERROR, "metaphone: Input string length must be > 0");
378 : :
379 : : /*-- Allocate memory for our phoned_phrase --*/
8983 bruce@momjian.us 380 [ - + ]:CBC 1 : if (max_phonemes == 0)
381 : : { /* Assume largest possible */
3265 tgl@sss.pgh.pa.us 382 :UBC 0 : *phoned_word = palloc(sizeof(char) * strlen(word) + 1);
383 : : }
384 : : else
385 : : {
9062 bruce@momjian.us 386 :CBC 1 : *phoned_word = palloc(sizeof(char) * max_phonemes + 1);
387 : : }
388 : :
389 : : /*-- The first phoneme has to be processed specially. --*/
390 : : /* Find our first letter */
138 jdavis@postgresql.or 391 [ - + ]:GNC 1 : for (; !ascii_isalpha((unsigned char) (Curr_Letter)); w_idx++)
392 : : {
393 : : /* On the off chance we were given nothing but crap... */
8983 bruce@momjian.us 394 [ # # ]:UBC 0 : if (Curr_Letter == '\0')
395 : : {
8979 396 : 0 : End_Phoned_Word;
3208 peter_e@gmx.net 397 : 0 : return;
398 : : }
399 : : }
400 : :
8983 bruce@momjian.us 401 [ - + - - :CBC 1 : switch (Curr_Letter)
- - ]
402 : : {
403 : : /* AE becomes E */
9062 bruce@momjian.us 404 :UBC 0 : case 'A':
8983 405 [ # # ]: 0 : if (Next_Letter == 'E')
406 : : {
9062 407 : 0 : Phonize('E');
8983 408 : 0 : w_idx += 2;
409 : : }
410 : : /* Remember, preserve vowels at the beginning */
411 : : else
412 : : {
9062 413 : 0 : Phonize('A');
414 : 0 : w_idx++;
415 : : }
416 : 0 : break;
417 : : /* [GKP]N becomes N */
9062 bruce@momjian.us 418 :CBC 1 : case 'G':
419 : : case 'K':
420 : : case 'P':
8983 421 [ - + ]: 1 : if (Next_Letter == 'N')
422 : : {
9062 bruce@momjian.us 423 :UBC 0 : Phonize('N');
8983 424 : 0 : w_idx += 2;
425 : : }
9062 bruce@momjian.us 426 :CBC 1 : break;
427 : :
428 : : /*
429 : : * WH becomes H, WR becomes R W if followed by a vowel
430 : : */
9062 bruce@momjian.us 431 :UBC 0 : case 'W':
8983 432 [ # # ]: 0 : if (Next_Letter == 'H' ||
[ # # # # ]
433 [ # # ]: 0 : Next_Letter == 'R')
434 : : {
435 : 0 : Phonize(Next_Letter);
436 : 0 : w_idx += 2;
437 : : }
438 [ # # ]: 0 : else if (isvowel(Next_Letter))
439 : : {
440 : 0 : Phonize('W');
441 : 0 : w_idx += 2;
442 : : }
443 : : /* else ignore */
9062 444 : 0 : break;
445 : : /* X becomes S */
446 : 0 : case 'X':
447 : 0 : Phonize('S');
448 : 0 : w_idx++;
449 : 0 : break;
450 : : /* Vowels are kept */
451 : :
452 : : /*
453 : : * We did A already case 'A': case 'a':
454 : : */
455 : 0 : case 'E':
456 : : case 'I':
457 : : case 'O':
458 : : case 'U':
459 : 0 : Phonize(Curr_Letter);
460 : 0 : w_idx++;
461 : 0 : break;
462 : 0 : default:
463 : : /* do nothing */
464 : 0 : break;
465 : : }
466 : :
467 : :
468 : :
469 : : /* On to the metaphoning */
8983 bruce@momjian.us 470 [ + + - + ]:CBC 6 : for (; Curr_Letter != '\0' &&
471 [ + - ]: 5 : (max_phonemes == 0 || Phone_Len < max_phonemes);
472 : 5 : w_idx++)
473 : : {
474 : : /*
475 : : * How many letters to skip because an earlier encoding handled
476 : : * multiple letters
477 : : */
478 : 5 : unsigned short int skip_letter = 0;
479 : :
480 : :
481 : : /*
482 : : * THOUGHT: It would be nice if, rather than having things like...
483 : : * well, SCI. For SCI you encode the S, then have to remember to skip
484 : : * the C. So the phonome SCI invades both S and C. It would be
485 : : * better, IMHO, to skip the C from the S part of the encoding. Hell,
486 : : * I'm trying it.
487 : : */
488 : :
489 : : /* Ignore non-alphas */
138 jdavis@postgresql.or 490 [ - + ]:GNC 5 : if (!ascii_isalpha((unsigned char) (Curr_Letter)))
9062 bruce@momjian.us 491 :UBC 0 : continue;
492 : :
493 : : /* Drop duplicates, except CC */
8983 bruce@momjian.us 494 [ + + - + ]:CBC 5 : if (Curr_Letter == Prev_Letter &&
[ + + - +
- - ]
8983 bruce@momjian.us 495 [ # # ]:UBC 0 : Curr_Letter != 'C')
9062 496 : 0 : continue;
497 : :
8983 bruce@momjian.us 498 :CBC 5 : switch (Curr_Letter)
[ + - - +
- - - - -
- - - - -
- + + ]
499 : : {
500 : : /* B -> B unless in MB */
9062 501 : 1 : case 'B':
8983 502 [ + - - + ]: 1 : if (Prev_Letter != 'M')
9062 bruce@momjian.us 503 :UBC 0 : Phonize('B');
9062 bruce@momjian.us 504 :CBC 1 : break;
505 : :
506 : : /*
507 : : * 'sh' if -CIA- or -CH, but not SCH, except SCHW. (SCHW is
508 : : * handled in S) S if -CI-, -CE- or -CY- dropped if -SCI-,
509 : : * SCE-, -SCY- (handed in S) else K
510 : : */
9062 bruce@momjian.us 511 :UBC 0 : case 'C':
8983 512 [ # # ]: 0 : if (MAKESOFT(Next_Letter))
513 : : { /* C[IEY] */
514 [ # # # # ]: 0 : if (After_Next_Letter == 'A' &&
[ # # # #
# # ]
515 [ # # ]: 0 : Next_Letter == 'I')
516 : : { /* CIA */
9062 517 : 0 : Phonize(SH);
518 : : }
519 : : /* SC[IEY] */
8983 520 [ # # # # ]: 0 : else if (Prev_Letter == 'S')
521 : : {
522 : : /* Dropped */
523 : : }
524 : : else
525 : 0 : Phonize('S');
526 : : }
527 [ # # ]: 0 : else if (Next_Letter == 'H')
528 : : {
529 : : #ifndef USE_TRADITIONAL_METAPHONE
530 [ # # # # : 0 : if (After_Next_Letter == 'R' ||
# # ]
531 [ # # ]: 0 : Prev_Letter == 'S')
532 : : { /* Christ, School */
9062 533 : 0 : Phonize('K');
534 : : }
535 : : else
536 : 0 : Phonize(SH);
537 : : #else
538 : : Phonize(SH);
539 : : #endif
540 : 0 : skip_letter++;
541 : : }
542 : : else
543 : 0 : Phonize('K');
544 : 0 : break;
545 : :
546 : : /*
547 : : * J if in -DGE-, -DGI- or -DGY- else T
548 : : */
549 : 0 : case 'D':
8983 550 [ # # ]: 0 : if (Next_Letter == 'G' &&
551 [ # # # # ]: 0 : MAKESOFT(After_Next_Letter))
552 : : {
9062 553 : 0 : Phonize('J');
554 : 0 : skip_letter++;
555 : : }
556 : : else
557 : 0 : Phonize('T');
558 : 0 : break;
559 : :
560 : : /*
561 : : * F if in -GH and not B--GH, D--GH, -H--GH, -H---GH else
562 : : * dropped if -GNED, -GN, else dropped if -DGE-, -DGI- or
563 : : * -DGY- (handled in D) else J if in -GE-, -GI, -GY and not GG
564 : : * else K
565 : : */
9062 bruce@momjian.us 566 :CBC 1 : case 'G':
8983 567 [ - + ]: 1 : if (Next_Letter == 'H')
568 : : {
8983 bruce@momjian.us 569 [ # # # # :UBC 0 : if (!(NOGHTOF(Look_Back_Letter(3)) ||
# # ]
570 [ # # ]: 0 : Look_Back_Letter(4) == 'H'))
571 : : {
9062 572 : 0 : Phonize('F');
573 : 0 : skip_letter++;
574 : : }
575 : : else
576 : : {
577 : : /* silent */
578 : : }
579 : : }
8983 bruce@momjian.us 580 [ - + ]:CBC 1 : else if (Next_Letter == 'N')
581 : : {
8983 bruce@momjian.us 582 [ # # # # ]:UBC 0 : if (Isbreak(After_Next_Letter) ||
[ # # # #
# # ]
583 [ # # # # ]: 0 : (After_Next_Letter == 'E' &&
584 [ # # ]: 0 : Look_Ahead_Letter(3) == 'D'))
585 : : {
586 : : /* dropped */
587 : : }
588 : : else
9062 589 : 0 : Phonize('K');
590 : : }
8983 bruce@momjian.us 591 [ - + - - ]:CBC 1 : else if (MAKESOFT(Next_Letter) &&
8983 bruce@momjian.us 592 [ # # ]:UBC 0 : Prev_Letter != 'G')
9062 593 : 0 : Phonize('J');
594 : : else
9062 bruce@momjian.us 595 :CBC 1 : Phonize('K');
596 : 1 : break;
597 : : /* H if before a vowel and not after C,G,P,S,T */
9062 bruce@momjian.us 598 :UBC 0 : case 'H':
8983 599 [ # # ]: 0 : if (isvowel(Next_Letter) &&
600 [ # # # # ]: 0 : !AFFECTH(Prev_Letter))
9062 601 : 0 : Phonize('H');
602 : 0 : break;
603 : :
604 : : /*
605 : : * dropped if after C else K
606 : : */
607 : 0 : case 'K':
8983 608 [ # # # # ]: 0 : if (Prev_Letter != 'C')
9062 609 : 0 : Phonize('K');
610 : 0 : break;
611 : :
612 : : /*
613 : : * F if before H else P
614 : : */
615 : 0 : case 'P':
8983 616 [ # # ]: 0 : if (Next_Letter == 'H')
9062 617 : 0 : Phonize('F');
618 : : else
619 : 0 : Phonize('P');
620 : 0 : break;
621 : :
622 : : /*
623 : : * K
624 : : */
625 : 0 : case 'Q':
626 : 0 : Phonize('K');
627 : 0 : break;
628 : :
629 : : /*
630 : : * 'sh' in -SH-, -SIO- or -SIA- or -SCHW- else S
631 : : */
632 : 0 : case 'S':
8983 633 [ # # ]: 0 : if (Next_Letter == 'I' &&
[ # # # # ]
634 [ # # # # ]: 0 : (After_Next_Letter == 'O' ||
635 [ # # ]: 0 : After_Next_Letter == 'A'))
[ # # # # ]
9062 636 : 0 : Phonize(SH);
8983 637 [ # # ]: 0 : else if (Next_Letter == 'H')
638 : : {
9062 639 : 0 : Phonize(SH);
640 : 0 : skip_letter++;
641 : : }
642 : : #ifndef USE_TRADITIONAL_METAPHONE
8983 643 [ # # ]: 0 : else if (Next_Letter == 'C' &&
[ # # # # ]
644 [ # # ]: 0 : Look_Ahead_Letter(2) == 'H' &&
645 [ # # ]: 0 : Look_Ahead_Letter(3) == 'W')
646 : : {
9062 647 : 0 : Phonize(SH);
648 : 0 : skip_letter += 2;
649 : : }
650 : : #endif
651 : : else
652 : 0 : Phonize('S');
653 : 0 : break;
654 : :
655 : : /*
656 : : * 'sh' in -TIA- or -TIO- else 'th' before H else T
657 : : */
658 : 0 : case 'T':
8983 659 [ # # ]: 0 : if (Next_Letter == 'I' &&
[ # # # # ]
660 [ # # # # ]: 0 : (After_Next_Letter == 'O' ||
661 [ # # ]: 0 : After_Next_Letter == 'A'))
[ # # # # ]
9062 662 : 0 : Phonize(SH);
8983 663 [ # # ]: 0 : else if (Next_Letter == 'H')
664 : : {
9062 665 : 0 : Phonize(TH);
666 : 0 : skip_letter++;
667 : : }
668 : : else
669 : 0 : Phonize('T');
670 : 0 : break;
671 : : /* F */
672 : 0 : case 'V':
673 : 0 : Phonize('F');
674 : 0 : break;
675 : : /* W before a vowel, else dropped */
676 : 0 : case 'W':
8983 677 [ # # ]: 0 : if (isvowel(Next_Letter))
9062 678 : 0 : Phonize('W');
679 : 0 : break;
680 : : /* KS */
681 : 0 : case 'X':
682 : 0 : Phonize('K');
8376 683 [ # # # # ]: 0 : if (max_phonemes == 0 || Phone_Len < max_phonemes)
684 : 0 : Phonize('S');
9062 685 : 0 : break;
686 : : /* Y if followed by a vowel */
687 : 0 : case 'Y':
8983 688 [ # # ]: 0 : if (isvowel(Next_Letter))
9062 689 : 0 : Phonize('Y');
690 : 0 : break;
691 : : /* S */
692 : 0 : case 'Z':
693 : 0 : Phonize('S');
694 : 0 : break;
695 : : /* No transformation */
9062 bruce@momjian.us 696 :CBC 1 : case 'F':
697 : : case 'J':
698 : : case 'L':
699 : : case 'M':
700 : : case 'N':
701 : : case 'R':
702 : 1 : Phonize(Curr_Letter);
703 : 1 : break;
704 : 2 : default:
705 : : /* nothing */
706 : 2 : break;
707 : : } /* END SWITCH */
708 : :
709 : 5 : w_idx += skip_letter;
710 : : } /* END FOR */
711 : :
712 : 1 : End_Phoned_Word;
713 : : } /* END metaphone */
714 : :
715 : :
716 : : /*
717 : : * SQL function: soundex(text) returns text
718 : : */
719 : 3 : PG_FUNCTION_INFO_V1(soundex);
720 : :
721 : : Datum
722 : 8 : soundex(PG_FUNCTION_ARGS)
723 : : {
724 : : char outstr[SOUNDEX_LEN + 1];
725 : : char *arg;
726 : :
3366 noah@leadboat.com 727 : 8 : arg = text_to_cstring(PG_GETARG_TEXT_PP(0));
728 : :
9062 bruce@momjian.us 729 : 8 : _soundex(arg, outstr);
730 : :
6640 tgl@sss.pgh.pa.us 731 : 8 : PG_RETURN_TEXT_P(cstring_to_text(outstr));
732 : : }
733 : :
734 : : static void
9062 bruce@momjian.us 735 : 16 : _soundex(const char *instr, char *outstr)
736 : : {
737 : : int count;
738 : :
1310 peter@eisentraut.org 739 [ - + ]: 16 : Assert(instr);
740 [ - + ]: 16 : Assert(outstr);
741 : :
742 : : /* Skip leading non-alphabetic characters */
138 jdavis@postgresql.or 743 [ + + - + ]:GNC 16 : while (*instr && !ascii_isalpha((unsigned char) *instr))
9062 bruce@momjian.us 744 :UBC 0 : ++instr;
745 : :
746 : : /* If no string left, return all-zeroes buffer */
1110 tgl@sss.pgh.pa.us 747 [ + + ]:CBC 16 : if (!*instr)
748 : : {
749 : 3 : memset(outstr, '\0', SOUNDEX_LEN + 1);
9062 bruce@momjian.us 750 : 3 : return;
751 : : }
752 : :
753 : : /* Take the first letter as is */
138 jdavis@postgresql.or 754 :GNC 13 : *outstr++ = (char) pg_ascii_toupper((unsigned char) *instr++);
755 : :
9062 bruce@momjian.us 756 :CBC 13 : count = 1;
757 [ + + + + ]: 60 : while (*instr && count < SOUNDEX_LEN)
758 : : {
138 jdavis@postgresql.or 759 [ + + + + ]:GNC 93 : if (ascii_isalpha((unsigned char) *instr) &&
9062 bruce@momjian.us 760 :CBC 46 : soundex_code(*instr) != soundex_code(*(instr - 1)))
761 : : {
1110 tgl@sss.pgh.pa.us 762 : 35 : *outstr = soundex_code(*instr);
9062 bruce@momjian.us 763 [ + + ]: 35 : if (*outstr != '0')
764 : : {
765 : 23 : ++outstr;
766 : 23 : ++count;
767 : : }
768 : : }
769 : 47 : ++instr;
770 : : }
771 : :
772 : : /* Fill with 0's */
773 [ + + ]: 29 : while (count < SOUNDEX_LEN)
774 : : {
775 : 16 : *outstr = '0';
776 : 16 : ++outstr;
777 : 16 : ++count;
778 : : }
779 : :
780 : : /* And null-terminate */
1110 tgl@sss.pgh.pa.us 781 : 13 : *outstr = '\0';
782 : : }
783 : :
7794 neilc@samurai.com 784 : 2 : PG_FUNCTION_INFO_V1(difference);
785 : :
786 : : Datum
787 : 4 : difference(PG_FUNCTION_ARGS)
788 : : {
789 : : char sndx1[SOUNDEX_LEN + 1],
790 : : sndx2[SOUNDEX_LEN + 1];
791 : : int i,
792 : : result;
793 : :
3366 noah@leadboat.com 794 : 4 : _soundex(text_to_cstring(PG_GETARG_TEXT_PP(0)), sndx1);
795 : 4 : _soundex(text_to_cstring(PG_GETARG_TEXT_PP(1)), sndx2);
796 : :
7794 neilc@samurai.com 797 : 4 : result = 0;
7532 bruce@momjian.us 798 [ + + ]: 20 : for (i = 0; i < SOUNDEX_LEN; i++)
799 : : {
7794 neilc@samurai.com 800 [ + + ]: 16 : if (sndx1[i] == sndx2[i])
801 : 10 : result++;
802 : : }
803 : :
804 : 4 : PG_RETURN_INT32(result);
805 : : }
|