diff options
Diffstat (limited to 'contrib/fuzzystrmatch/fuzzystrmatch.c')
-rw-r--r-- | contrib/fuzzystrmatch/fuzzystrmatch.c | 794 |
1 files changed, 794 insertions, 0 deletions
diff --git a/contrib/fuzzystrmatch/fuzzystrmatch.c b/contrib/fuzzystrmatch/fuzzystrmatch.c new file mode 100644 index 0000000..5686497 --- /dev/null +++ b/contrib/fuzzystrmatch/fuzzystrmatch.c @@ -0,0 +1,794 @@ +/* + * fuzzystrmatch.c + * + * Functions for "fuzzy" comparison of strings + * + * Joe Conway <mail@joeconway.com> + * + * contrib/fuzzystrmatch/fuzzystrmatch.c + * Copyright (c) 2001-2023, PostgreSQL Global Development Group + * ALL RIGHTS RESERVED; + * + * metaphone() + * ----------- + * Modified for PostgreSQL by Joe Conway. + * Based on CPAN's "Text-Metaphone-1.96" by Michael G Schwern <schwern@pobox.com> + * Code slightly modified for use as PostgreSQL function (palloc, elog, etc). + * Metaphone was originally created by Lawrence Philips and presented in article + * in "Computer Language" December 1990 issue. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose, without fee, and without a written agreement + * is hereby granted, provided that the above copyright notice and this + * paragraph and the following two paragraphs appear in all copies. + * + * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING + * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS + * DOCUMENTATION, EVEN IF THE AUTHOR OR DISTRIBUTORS HAVE BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE AUTHOR AND DISTRIBUTORS HAS NO OBLIGATIONS TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + */ + +#include "postgres.h" + +#include <ctype.h> + +#include "mb/pg_wchar.h" +#include "utils/builtins.h" +#include "utils/varlena.h" +#include "varatt.h" + +PG_MODULE_MAGIC; + +/* + * Soundex + */ +static void _soundex(const char *instr, char *outstr); + +#define SOUNDEX_LEN 4 + +/* ABCDEFGHIJKLMNOPQRSTUVWXYZ */ +static const char *soundex_table = "01230120022455012623010202"; + +static char +soundex_code(char letter) +{ + letter = toupper((unsigned char) letter); + /* Defend against non-ASCII letters */ + if (letter >= 'A' && letter <= 'Z') + return soundex_table[letter - 'A']; + return letter; +} + +/* + * Metaphone + */ +#define MAX_METAPHONE_STRLEN 255 + +/* + * Original code by Michael G Schwern starts here. + * Code slightly modified for use as PostgreSQL function. + */ + + +/************************************************************************** + metaphone -- Breaks english phrases down into their phonemes. + + Input + word -- An english word to be phonized + max_phonemes -- How many phonemes to calculate. If 0, then it + will phonize the entire phrase. + phoned_word -- The final phonized word. (We'll allocate the + memory.) + Output + error -- A simple error flag, returns true or false + + NOTES: ALL non-alpha characters are ignored, this includes whitespace, + although non-alpha characters will break up phonemes. +****************************************************************************/ + + +/* I add modifications to the traditional metaphone algorithm that you + might find in books. Define this if you want metaphone to behave + traditionally */ +#undef USE_TRADITIONAL_METAPHONE + +/* Special encodings */ +#define SH 'X' +#define TH '0' + +static char Lookahead(char *word, int how_far); +static void _metaphone(char *word, int max_phonemes, char **phoned_word); + +/* Metachar.h ... little bits about characters for metaphone */ + + +/*-- Character encoding array & accessing macros --*/ +/* Stolen directly out of the book... */ +static const char _codes[26] = { + 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 +/* 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 */ +}; + +static int +getcode(char c) +{ + if (isalpha((unsigned char) c)) + { + c = toupper((unsigned char) c); + /* Defend against non-ASCII letters */ + if (c >= 'A' && c <= 'Z') + return _codes[c - 'A']; + } + return 0; +} + +#define isvowel(c) (getcode(c) & 1) /* AEIOU */ + +/* These letters are passed through unchanged */ +#define NOCHANGE(c) (getcode(c) & 2) /* FJMNR */ + +/* These form diphthongs when preceding H */ +#define AFFECTH(c) (getcode(c) & 4) /* CGPST */ + +/* These make C and G soft */ +#define MAKESOFT(c) (getcode(c) & 8) /* EIY */ + +/* These prevent GH from becoming F */ +#define NOGHTOF(c) (getcode(c) & 16) /* BDH */ + +PG_FUNCTION_INFO_V1(levenshtein_with_costs); +Datum +levenshtein_with_costs(PG_FUNCTION_ARGS) +{ + text *src = PG_GETARG_TEXT_PP(0); + text *dst = PG_GETARG_TEXT_PP(1); + int ins_c = PG_GETARG_INT32(2); + int del_c = PG_GETARG_INT32(3); + int sub_c = PG_GETARG_INT32(4); + const char *s_data; + const char *t_data; + int s_bytes, + t_bytes; + + /* Extract a pointer to the actual character data */ + s_data = VARDATA_ANY(src); + t_data = VARDATA_ANY(dst); + /* Determine length of each string in bytes */ + s_bytes = VARSIZE_ANY_EXHDR(src); + t_bytes = VARSIZE_ANY_EXHDR(dst); + + PG_RETURN_INT32(varstr_levenshtein(s_data, s_bytes, t_data, t_bytes, + ins_c, del_c, sub_c, false)); +} + + +PG_FUNCTION_INFO_V1(levenshtein); +Datum +levenshtein(PG_FUNCTION_ARGS) +{ + text *src = PG_GETARG_TEXT_PP(0); + text *dst = PG_GETARG_TEXT_PP(1); + const char *s_data; + const char *t_data; + int s_bytes, + t_bytes; + + /* Extract a pointer to the actual character data */ + s_data = VARDATA_ANY(src); + t_data = VARDATA_ANY(dst); + /* Determine length of each string in bytes */ + s_bytes = VARSIZE_ANY_EXHDR(src); + t_bytes = VARSIZE_ANY_EXHDR(dst); + + PG_RETURN_INT32(varstr_levenshtein(s_data, s_bytes, t_data, t_bytes, + 1, 1, 1, false)); +} + + +PG_FUNCTION_INFO_V1(levenshtein_less_equal_with_costs); +Datum +levenshtein_less_equal_with_costs(PG_FUNCTION_ARGS) +{ + text *src = PG_GETARG_TEXT_PP(0); + text *dst = PG_GETARG_TEXT_PP(1); + int ins_c = PG_GETARG_INT32(2); + int del_c = PG_GETARG_INT32(3); + int sub_c = PG_GETARG_INT32(4); + int max_d = PG_GETARG_INT32(5); + const char *s_data; + const char *t_data; + int s_bytes, + t_bytes; + + /* Extract a pointer to the actual character data */ + s_data = VARDATA_ANY(src); + t_data = VARDATA_ANY(dst); + /* Determine length of each string in bytes */ + s_bytes = VARSIZE_ANY_EXHDR(src); + t_bytes = VARSIZE_ANY_EXHDR(dst); + + PG_RETURN_INT32(varstr_levenshtein_less_equal(s_data, s_bytes, + t_data, t_bytes, + ins_c, del_c, sub_c, + max_d, false)); +} + + +PG_FUNCTION_INFO_V1(levenshtein_less_equal); +Datum +levenshtein_less_equal(PG_FUNCTION_ARGS) +{ + text *src = PG_GETARG_TEXT_PP(0); + text *dst = PG_GETARG_TEXT_PP(1); + int max_d = PG_GETARG_INT32(2); + const char *s_data; + const char *t_data; + int s_bytes, + t_bytes; + + /* Extract a pointer to the actual character data */ + s_data = VARDATA_ANY(src); + t_data = VARDATA_ANY(dst); + /* Determine length of each string in bytes */ + s_bytes = VARSIZE_ANY_EXHDR(src); + t_bytes = VARSIZE_ANY_EXHDR(dst); + + PG_RETURN_INT32(varstr_levenshtein_less_equal(s_data, s_bytes, + t_data, t_bytes, + 1, 1, 1, + max_d, false)); +} + + +/* + * Calculates the metaphone of an input string. + * Returns number of characters requested + * (suggested value is 4) + */ +PG_FUNCTION_INFO_V1(metaphone); +Datum +metaphone(PG_FUNCTION_ARGS) +{ + char *str_i = TextDatumGetCString(PG_GETARG_DATUM(0)); + size_t str_i_len = strlen(str_i); + int reqlen; + char *metaph; + + /* return an empty string if we receive one */ + if (!(str_i_len > 0)) + PG_RETURN_TEXT_P(cstring_to_text("")); + + if (str_i_len > MAX_METAPHONE_STRLEN) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("argument exceeds the maximum length of %d bytes", + MAX_METAPHONE_STRLEN))); + + reqlen = PG_GETARG_INT32(1); + if (reqlen > MAX_METAPHONE_STRLEN) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("output exceeds the maximum length of %d bytes", + MAX_METAPHONE_STRLEN))); + + if (!(reqlen > 0)) + ereport(ERROR, + (errcode(ERRCODE_ZERO_LENGTH_CHARACTER_STRING), + errmsg("output cannot be empty string"))); + + _metaphone(str_i, reqlen, &metaph); + PG_RETURN_TEXT_P(cstring_to_text(metaph)); +} + + +/* + * Original code by Michael G Schwern starts here. + * Code slightly modified for use as PostgreSQL + * function (palloc, etc). + */ + +/* I suppose I could have been using a character pointer instead of + * accessing the array directly... */ + +/* Look at the next letter in the word */ +#define Next_Letter (toupper((unsigned char) word[w_idx+1])) +/* Look at the current letter in the word */ +#define Curr_Letter (toupper((unsigned char) word[w_idx])) +/* Go N letters back. */ +#define Look_Back_Letter(n) \ + (w_idx >= (n) ? toupper((unsigned char) word[w_idx-(n)]) : '\0') +/* Previous letter. I dunno, should this return null on failure? */ +#define Prev_Letter (Look_Back_Letter(1)) +/* Look two letters down. It makes sure you don't walk off the string. */ +#define After_Next_Letter \ + (Next_Letter != '\0' ? toupper((unsigned char) word[w_idx+2]) : '\0') +#define Look_Ahead_Letter(n) toupper((unsigned char) Lookahead(word+w_idx, n)) + + +/* Allows us to safely look ahead an arbitrary # of letters */ +/* I probably could have just used strlen... */ +static char +Lookahead(char *word, int how_far) +{ + char letter_ahead = '\0'; /* null by default */ + int idx; + + for (idx = 0; word[idx] != '\0' && idx < how_far; idx++); + /* Edge forward in the string... */ + + letter_ahead = word[idx]; /* idx will be either == to how_far or at the + * end of the string */ + return letter_ahead; +} + + +/* phonize one letter */ +#define Phonize(c) do {(*phoned_word)[p_idx++] = c;} while (0) +/* Slap a null character on the end of the phoned word */ +#define End_Phoned_Word do {(*phoned_word)[p_idx] = '\0';} while (0) +/* How long is the phoned word? */ +#define Phone_Len (p_idx) + +/* Note is a letter is a 'break' in the word */ +#define Isbreak(c) (!isalpha((unsigned char) (c))) + + +static void +_metaphone(char *word, /* IN */ + int max_phonemes, + char **phoned_word) /* OUT */ +{ + int w_idx = 0; /* point in the phonization we're at. */ + int p_idx = 0; /* end of the phoned phrase */ + + /*-- Parameter checks --*/ + + /* + * Shouldn't be necessary, but left these here anyway jec Aug 3, 2001 + */ + + /* Negative phoneme length is meaningless */ + if (!(max_phonemes > 0)) + /* internal error */ + elog(ERROR, "metaphone: Requested output length must be > 0"); + + /* Empty/null string is meaningless */ + if ((word == NULL) || !(strlen(word) > 0)) + /* internal error */ + elog(ERROR, "metaphone: Input string length must be > 0"); + + /*-- Allocate memory for our phoned_phrase --*/ + if (max_phonemes == 0) + { /* Assume largest possible */ + *phoned_word = palloc(sizeof(char) * strlen(word) + 1); + } + else + { + *phoned_word = palloc(sizeof(char) * max_phonemes + 1); + } + + /*-- The first phoneme has to be processed specially. --*/ + /* Find our first letter */ + for (; !isalpha((unsigned char) (Curr_Letter)); w_idx++) + { + /* On the off chance we were given nothing but crap... */ + if (Curr_Letter == '\0') + { + End_Phoned_Word; + return; + } + } + + switch (Curr_Letter) + { + /* AE becomes E */ + case 'A': + if (Next_Letter == 'E') + { + Phonize('E'); + w_idx += 2; + } + /* Remember, preserve vowels at the beginning */ + else + { + Phonize('A'); + w_idx++; + } + break; + /* [GKP]N becomes N */ + case 'G': + case 'K': + case 'P': + if (Next_Letter == 'N') + { + Phonize('N'); + w_idx += 2; + } + break; + + /* + * WH becomes H, WR becomes R W if followed by a vowel + */ + case 'W': + if (Next_Letter == 'H' || + Next_Letter == 'R') + { + Phonize(Next_Letter); + w_idx += 2; + } + else if (isvowel(Next_Letter)) + { + Phonize('W'); + w_idx += 2; + } + /* else ignore */ + break; + /* X becomes S */ + case 'X': + Phonize('S'); + w_idx++; + break; + /* Vowels are kept */ + + /* + * We did A already case 'A': case 'a': + */ + case 'E': + case 'I': + case 'O': + case 'U': + Phonize(Curr_Letter); + w_idx++; + break; + default: + /* do nothing */ + break; + } + + + + /* On to the metaphoning */ + for (; Curr_Letter != '\0' && + (max_phonemes == 0 || Phone_Len < max_phonemes); + w_idx++) + { + /* + * How many letters to skip because an earlier encoding handled + * multiple letters + */ + unsigned short int skip_letter = 0; + + + /* + * THOUGHT: It would be nice if, rather than having things like... + * well, SCI. For SCI you encode the S, then have to remember to skip + * the C. So the phonome SCI invades both S and C. It would be + * better, IMHO, to skip the C from the S part of the encoding. Hell, + * I'm trying it. + */ + + /* Ignore non-alphas */ + if (!isalpha((unsigned char) (Curr_Letter))) + continue; + + /* Drop duplicates, except CC */ + if (Curr_Letter == Prev_Letter && + Curr_Letter != 'C') + continue; + + switch (Curr_Letter) + { + /* B -> B unless in MB */ + case 'B': + if (Prev_Letter != 'M') + Phonize('B'); + break; + + /* + * 'sh' if -CIA- or -CH, but not SCH, except SCHW. (SCHW is + * handled in S) S if -CI-, -CE- or -CY- dropped if -SCI-, + * SCE-, -SCY- (handed in S) else K + */ + case 'C': + if (MAKESOFT(Next_Letter)) + { /* C[IEY] */ + if (After_Next_Letter == 'A' && + Next_Letter == 'I') + { /* CIA */ + Phonize(SH); + } + /* SC[IEY] */ + else if (Prev_Letter == 'S') + { + /* Dropped */ + } + else + Phonize('S'); + } + else if (Next_Letter == 'H') + { +#ifndef USE_TRADITIONAL_METAPHONE + if (After_Next_Letter == 'R' || + Prev_Letter == 'S') + { /* Christ, School */ + Phonize('K'); + } + else + Phonize(SH); +#else + Phonize(SH); +#endif + skip_letter++; + } + else + Phonize('K'); + break; + + /* + * J if in -DGE-, -DGI- or -DGY- else T + */ + case 'D': + if (Next_Letter == 'G' && + MAKESOFT(After_Next_Letter)) + { + Phonize('J'); + skip_letter++; + } + else + Phonize('T'); + break; + + /* + * F if in -GH and not B--GH, D--GH, -H--GH, -H---GH else + * dropped if -GNED, -GN, else dropped if -DGE-, -DGI- or + * -DGY- (handled in D) else J if in -GE-, -GI, -GY and not GG + * else K + */ + case 'G': + if (Next_Letter == 'H') + { + if (!(NOGHTOF(Look_Back_Letter(3)) || + Look_Back_Letter(4) == 'H')) + { + Phonize('F'); + skip_letter++; + } + else + { + /* silent */ + } + } + else if (Next_Letter == 'N') + { + if (Isbreak(After_Next_Letter) || + (After_Next_Letter == 'E' && + Look_Ahead_Letter(3) == 'D')) + { + /* dropped */ + } + else + Phonize('K'); + } + else if (MAKESOFT(Next_Letter) && + Prev_Letter != 'G') + Phonize('J'); + else + Phonize('K'); + break; + /* H if before a vowel and not after C,G,P,S,T */ + case 'H': + if (isvowel(Next_Letter) && + !AFFECTH(Prev_Letter)) + Phonize('H'); + break; + + /* + * dropped if after C else K + */ + case 'K': + if (Prev_Letter != 'C') + Phonize('K'); + break; + + /* + * F if before H else P + */ + case 'P': + if (Next_Letter == 'H') + Phonize('F'); + else + Phonize('P'); + break; + + /* + * K + */ + case 'Q': + Phonize('K'); + break; + + /* + * 'sh' in -SH-, -SIO- or -SIA- or -SCHW- else S + */ + case 'S': + if (Next_Letter == 'I' && + (After_Next_Letter == 'O' || + After_Next_Letter == 'A')) + Phonize(SH); + else if (Next_Letter == 'H') + { + Phonize(SH); + skip_letter++; + } +#ifndef USE_TRADITIONAL_METAPHONE + else if (Next_Letter == 'C' && + Look_Ahead_Letter(2) == 'H' && + Look_Ahead_Letter(3) == 'W') + { + Phonize(SH); + skip_letter += 2; + } +#endif + else + Phonize('S'); + break; + + /* + * 'sh' in -TIA- or -TIO- else 'th' before H else T + */ + case 'T': + if (Next_Letter == 'I' && + (After_Next_Letter == 'O' || + After_Next_Letter == 'A')) + Phonize(SH); + else if (Next_Letter == 'H') + { + Phonize(TH); + skip_letter++; + } + else + Phonize('T'); + break; + /* F */ + case 'V': + Phonize('F'); + break; + /* W before a vowel, else dropped */ + case 'W': + if (isvowel(Next_Letter)) + Phonize('W'); + break; + /* KS */ + case 'X': + Phonize('K'); + if (max_phonemes == 0 || Phone_Len < max_phonemes) + Phonize('S'); + break; + /* Y if followed by a vowel */ + case 'Y': + if (isvowel(Next_Letter)) + Phonize('Y'); + break; + /* S */ + case 'Z': + Phonize('S'); + break; + /* No transformation */ + case 'F': + case 'J': + case 'L': + case 'M': + case 'N': + case 'R': + Phonize(Curr_Letter); + break; + default: + /* nothing */ + break; + } /* END SWITCH */ + + w_idx += skip_letter; + } /* END FOR */ + + End_Phoned_Word; +} /* END metaphone */ + + +/* + * SQL function: soundex(text) returns text + */ +PG_FUNCTION_INFO_V1(soundex); + +Datum +soundex(PG_FUNCTION_ARGS) +{ + char outstr[SOUNDEX_LEN + 1]; + char *arg; + + arg = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + _soundex(arg, outstr); + + PG_RETURN_TEXT_P(cstring_to_text(outstr)); +} + +static void +_soundex(const char *instr, char *outstr) +{ + int count; + + Assert(instr); + Assert(outstr); + + /* Skip leading non-alphabetic characters */ + while (*instr && !isalpha((unsigned char) *instr)) + ++instr; + + /* If no string left, return all-zeroes buffer */ + if (!*instr) + { + memset(outstr, '\0', SOUNDEX_LEN + 1); + return; + } + + /* Take the first letter as is */ + *outstr++ = (char) toupper((unsigned char) *instr++); + + count = 1; + while (*instr && count < SOUNDEX_LEN) + { + if (isalpha((unsigned char) *instr) && + soundex_code(*instr) != soundex_code(*(instr - 1))) + { + *outstr = soundex_code(*instr); + if (*outstr != '0') + { + ++outstr; + ++count; + } + } + ++instr; + } + + /* Fill with 0's */ + while (count < SOUNDEX_LEN) + { + *outstr = '0'; + ++outstr; + ++count; + } + + /* And null-terminate */ + *outstr = '\0'; +} + +PG_FUNCTION_INFO_V1(difference); + +Datum +difference(PG_FUNCTION_ARGS) +{ + char sndx1[SOUNDEX_LEN + 1], + sndx2[SOUNDEX_LEN + 1]; + int i, + result; + + _soundex(text_to_cstring(PG_GETARG_TEXT_PP(0)), sndx1); + _soundex(text_to_cstring(PG_GETARG_TEXT_PP(1)), sndx2); + + result = 0; + for (i = 0; i < SOUNDEX_LEN; i++) + { + if (sndx1[i] == sndx2[i]) + result++; + } + + PG_RETURN_INT32(result); +} |