1/*
2   +----------------------------------------------------------------------+
3   | PHP Version 5                                                        |
4   +----------------------------------------------------------------------+
5   | Copyright (c) 1997-2014 The PHP Group                                |
6   +----------------------------------------------------------------------+
7   | This source file is subject to version 3.01 of the PHP license,      |
8   | that is bundled with this package in the file LICENSE, and is        |
9   | available through the world-wide-web at the following url:           |
10   | http://www.php.net/license/3_01.txt                                  |
11   | If you did not receive a copy of the PHP license and are unable to   |
12   | obtain it through the world-wide-web, please send a note to          |
13   | license@php.net so we can mail you a copy immediately.               |
14   +----------------------------------------------------------------------+
15   | Author: Thies C. Arntzen <thies@thieso.net>                          |
16   +----------------------------------------------------------------------+
17*/
18
19/* $Id$ */
20
21/*
22    Based on CPANs "Text-Metaphone-1.96" by Michael G Schwern <schwern@pobox.com>
23*/
24
25#include "php.h"
26#include "php_metaphone.h"
27
28static int metaphone(unsigned char *word, int word_len, long max_phonemes, zend_string **phoned_word, int traditional);
29
30/* {{{ proto string metaphone(string text[, int phones])
31   Break english phrases down into their phonemes */
32PHP_FUNCTION(metaphone)
33{
34    char *str;
35    zend_string *result = NULL;
36    int str_len;
37    long phones = 0;
38
39    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &str, &str_len,
40                              &phones) == FAILURE) {
41        return;
42    }
43
44    if (metaphone((unsigned char *)str, str_len, phones, &result, 1) == 0) {
45        RETVAL_STR(result);
46    } else {
47        if (result) {
48            STR_FREE(result);
49        }
50        RETURN_FALSE;
51    }
52}
53/* }}} */
54
55/*
56   this is now the original code by Michael G Schwern:
57   i've changed it just a slightly bit (use emalloc,
58   get rid of includes etc)
59    - thies - 13.09.1999
60*/
61
62/*-----------------------------  */
63/* this used to be "metaphone.h" */
64/*-----------------------------  */
65
66/* Special encodings */
67#define  SH     'X'
68#define  TH     '0'
69
70/*-----------------------------  */
71/* end of "metaphone.h"          */
72/*-----------------------------  */
73
74/*----------------------------- */
75/* this used to be "metachar.h" */
76/*----------------------------- */
77
78/* Metachar.h ... little bits about characters for metaphone */
79/*-- Character encoding array & accessing macros --*/
80/* Stolen directly out of the book... */
81char _codes[26] =
82{
83    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
84/*  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 */
85};
86
87
88#define ENCODE(c) (isalpha(c) ? _codes[((toupper(c)) - 'A')] : 0)
89
90#define isvowel(c)  (ENCODE(c) & 1)     /* AEIOU */
91
92/* These letters are passed through unchanged */
93#define NOCHANGE(c) (ENCODE(c) & 2)     /* FJMNR */
94
95/* These form dipthongs when preceding H */
96#define AFFECTH(c)  (ENCODE(c) & 4)     /* CGPST */
97
98/* These make C and G soft */
99#define MAKESOFT(c) (ENCODE(c) & 8)     /* EIY */
100
101/* These prevent GH from becoming F */
102#define NOGHTOF(c)  (ENCODE(c) & 16)    /* BDH */
103
104/*----------------------------- */
105/* end of "metachar.h"          */
106/*----------------------------- */
107
108/* I suppose I could have been using a character pointer instead of
109 * accesssing the array directly... */
110
111/* Look at the next letter in the word */
112#define Next_Letter (toupper(word[w_idx+1]))
113/* Look at the current letter in the word */
114#define Curr_Letter (toupper(word[w_idx]))
115/* Go N letters back. */
116#define Look_Back_Letter(n) (w_idx >= n ? toupper(word[w_idx-n]) : '\0')
117/* Previous letter.  I dunno, should this return null on failure? */
118#define Prev_Letter (Look_Back_Letter(1))
119/* Look two letters down.  It makes sure you don't walk off the string. */
120#define After_Next_Letter   (Next_Letter != '\0' ? toupper(word[w_idx+2]) \
121                                                 : '\0')
122#define Look_Ahead_Letter(n) (toupper(Lookahead(word+w_idx, n)))
123
124
125/* Allows us to safely look ahead an arbitrary # of letters */
126/* I probably could have just used strlen... */
127static char Lookahead(char *word, int how_far)
128{
129    char letter_ahead = '\0';   /* null by default */
130    int idx;
131    for (idx = 0; word[idx] != '\0' && idx < how_far; idx++);
132    /* Edge forward in the string... */
133
134    letter_ahead = word[idx];   /* idx will be either == to how_far or
135                                 * at the end of the string
136                                 */
137    return letter_ahead;
138}
139
140
141/* phonize one letter
142 * We don't know the buffers size in advance. On way to solve this is to just
143 * re-allocate the buffer size. We're using an extra of 2 characters (this
144 * could be one though; or more too). */
145#define Phonize(c)  { \
146                        if (p_idx >= max_buffer_len) { \
147                            *phoned_word = STR_REALLOC(*phoned_word, 2 * sizeof(char) + max_buffer_len, 0); \
148                            max_buffer_len += 2; \
149                        } \
150                        (*phoned_word)->val[p_idx++] = c; \
151                        (*phoned_word)->len = p_idx; \
152                    }
153/* Slap a null character on the end of the phoned word */
154#define End_Phoned_Word { \
155                            if (p_idx == max_buffer_len) { \
156                                *phoned_word = STR_REALLOC(*phoned_word, 1 * sizeof(char) + max_buffer_len, 0); \
157                                max_buffer_len += 1; \
158                            } \
159                            (*phoned_word)->val[p_idx] = '\0'; \
160                            (*phoned_word)->len = p_idx; \
161                        }
162/* How long is the phoned word? */
163#define Phone_Len   (p_idx)
164
165/* Note is a letter is a 'break' in the word */
166#define Isbreak(c)  (!isalpha(c))
167
168/* {{{ metaphone
169 */
170static int metaphone(unsigned char *word, int word_len, long max_phonemes, zend_string **phoned_word, int traditional)
171{
172    int w_idx = 0;              /* point in the phonization we're at. */
173    int p_idx = 0;              /* end of the phoned phrase */
174    int max_buffer_len = 0;     /* maximum length of the destination buffer */
175
176/*-- Parameter checks --*/
177    /* Negative phoneme length is meaningless */
178
179    if (max_phonemes < 0)
180        return -1;
181
182    /* Empty/null string is meaningless */
183    /* Overly paranoid */
184    /* assert(word != NULL && word[0] != '\0'); */
185
186    if (word == NULL)
187        return -1;
188
189/*-- Allocate memory for our phoned_phrase --*/
190    if (max_phonemes == 0) {    /* Assume largest possible */
191        max_buffer_len = word_len;
192        *phoned_word = STR_ALLOC(sizeof(char) * word_len + 1, 0);
193    } else {
194        max_buffer_len = max_phonemes;
195        *phoned_word = STR_ALLOC(sizeof(char) * max_phonemes + 1, 0);
196    }
197
198
199/*-- The first phoneme has to be processed specially. --*/
200    /* Find our first letter */
201    for (; !isalpha(Curr_Letter); w_idx++) {
202        /* On the off chance we were given nothing but crap... */
203        if (Curr_Letter == '\0') {
204            End_Phoned_Word
205                return SUCCESS; /* For testing */
206        }
207    }
208
209    switch (Curr_Letter) {
210        /* AE becomes E */
211    case 'A':
212        if (Next_Letter == 'E') {
213            Phonize('E');
214            w_idx += 2;
215        }
216        /* Remember, preserve vowels at the beginning */
217        else {
218            Phonize('A');
219            w_idx++;
220        }
221        break;
222        /* [GKP]N becomes N */
223    case 'G':
224    case 'K':
225    case 'P':
226        if (Next_Letter == 'N') {
227            Phonize('N');
228            w_idx += 2;
229        }
230        break;
231        /* WH becomes W,
232           WR becomes R
233           W if followed by a vowel */
234    case 'W':
235        if (Next_Letter == 'R') {
236            Phonize(Next_Letter);
237            w_idx += 2;
238        } else if (Next_Letter == 'H' || isvowel(Next_Letter)) {
239            Phonize('W');
240            w_idx += 2;
241        }
242        /* else ignore */
243        break;
244        /* X becomes S */
245    case 'X':
246        Phonize('S');
247        w_idx++;
248        break;
249        /* Vowels are kept */
250        /* We did A already
251           case 'A':
252           case 'a':
253         */
254    case 'E':
255    case 'I':
256    case 'O':
257    case 'U':
258        Phonize(Curr_Letter);
259        w_idx++;
260        break;
261    default:
262        /* do nothing */
263        break;
264    }
265
266
267
268    /* On to the metaphoning */
269    for (; Curr_Letter != '\0' &&
270         (max_phonemes == 0 || Phone_Len < max_phonemes);
271         w_idx++) {
272        /* How many letters to skip because an eariler encoding handled
273         * multiple letters */
274        unsigned short int skip_letter = 0;
275
276
277        /* THOUGHT:  It would be nice if, rather than having things like...
278         * well, SCI.  For SCI you encode the S, then have to remember
279         * to skip the C.  So the phonome SCI invades both S and C.  It would
280         * be better, IMHO, to skip the C from the S part of the encoding.
281         * Hell, I'm trying it.
282         */
283
284        /* Ignore non-alphas */
285        if (!isalpha(Curr_Letter))
286            continue;
287
288        /* Drop duplicates, except CC */
289        if (Curr_Letter == Prev_Letter &&
290            Curr_Letter != 'C')
291            continue;
292
293        switch (Curr_Letter) {
294            /* B -> B unless in MB */
295        case 'B':
296            if (Prev_Letter != 'M')
297                Phonize('B');
298            break;
299            /* 'sh' if -CIA- or -CH, but not SCH, except SCHW.
300             * (SCHW is handled in S)
301             *  S if -CI-, -CE- or -CY-
302             *  dropped if -SCI-, SCE-, -SCY- (handed in S)
303             *  else K
304             */
305        case 'C':
306            if (MAKESOFT(Next_Letter)) {    /* C[IEY] */
307                if (After_Next_Letter == 'A' &&
308                    Next_Letter == 'I') {   /* CIA */
309                    Phonize(SH);
310                }
311                /* SC[IEY] */
312                else if (Prev_Letter == 'S') {
313                    /* Dropped */
314                } else {
315                    Phonize('S');
316                }
317            } else if (Next_Letter == 'H') {
318                if ((!traditional) && (After_Next_Letter == 'R' || Prev_Letter == 'S')) {   /* Christ, School */
319                    Phonize('K');
320                } else {
321                    Phonize(SH);
322                }
323                skip_letter++;
324            } else {
325                Phonize('K');
326            }
327            break;
328            /* J if in -DGE-, -DGI- or -DGY-
329             * else T
330             */
331        case 'D':
332            if (Next_Letter == 'G' &&
333                MAKESOFT(After_Next_Letter)) {
334                Phonize('J');
335                skip_letter++;
336            } else
337                Phonize('T');
338            break;
339            /* F if in -GH and not B--GH, D--GH, -H--GH, -H---GH
340             * else dropped if -GNED, -GN,
341             * else dropped if -DGE-, -DGI- or -DGY- (handled in D)
342             * else J if in -GE-, -GI, -GY and not GG
343             * else K
344             */
345        case 'G':
346            if (Next_Letter == 'H') {
347                if (!(NOGHTOF(Look_Back_Letter(3)) ||
348                      Look_Back_Letter(4) == 'H')) {
349                    Phonize('F');
350                    skip_letter++;
351                } else {
352                    /* silent */
353                }
354            } else if (Next_Letter == 'N') {
355                if (Isbreak(After_Next_Letter) ||
356                    (After_Next_Letter == 'E' &&
357                     Look_Ahead_Letter(3) == 'D')) {
358                    /* dropped */
359                } else
360                    Phonize('K');
361            } else if (MAKESOFT(Next_Letter) &&
362                       Prev_Letter != 'G') {
363                Phonize('J');
364            } else {
365                Phonize('K');
366            }
367            break;
368            /* H if before a vowel and not after C,G,P,S,T */
369        case 'H':
370            if (isvowel(Next_Letter) &&
371                !AFFECTH(Prev_Letter))
372                Phonize('H');
373            break;
374            /* dropped if after C
375             * else K
376             */
377        case 'K':
378            if (Prev_Letter != 'C')
379                Phonize('K');
380            break;
381            /* F if before H
382             * else P
383             */
384        case 'P':
385            if (Next_Letter == 'H') {
386                Phonize('F');
387            } else {
388                Phonize('P');
389            }
390            break;
391            /* K
392             */
393        case 'Q':
394            Phonize('K');
395            break;
396            /* 'sh' in -SH-, -SIO- or -SIA- or -SCHW-
397             * else S
398             */
399        case 'S':
400            if (Next_Letter == 'I' &&
401                (After_Next_Letter == 'O' ||
402                 After_Next_Letter == 'A')) {
403                Phonize(SH);
404            } else if (Next_Letter == 'H') {
405                Phonize(SH);
406                skip_letter++;
407            } else if ((!traditional) && (Next_Letter == 'C' && Look_Ahead_Letter(2) == 'H' && Look_Ahead_Letter(3) == 'W')) {
408                Phonize(SH);
409                skip_letter += 2;
410            } else {
411                Phonize('S');
412            }
413            break;
414            /* 'sh' in -TIA- or -TIO-
415             * else 'th' before H
416             * else T
417             */
418        case 'T':
419            if (Next_Letter == 'I' &&
420                (After_Next_Letter == 'O' ||
421                 After_Next_Letter == 'A')) {
422                Phonize(SH);
423            } else if (Next_Letter == 'H') {
424                Phonize(TH);
425                skip_letter++;
426            } else if (!(Next_Letter == 'C' && After_Next_Letter == 'H')) {
427                Phonize('T');
428            }
429            break;
430            /* F */
431        case 'V':
432            Phonize('F');
433            break;
434            /* W before a vowel, else dropped */
435        case 'W':
436            if (isvowel(Next_Letter))
437                Phonize('W');
438            break;
439            /* KS */
440        case 'X':
441            Phonize('K');
442            Phonize('S');
443            break;
444            /* Y if followed by a vowel */
445        case 'Y':
446            if (isvowel(Next_Letter))
447                Phonize('Y');
448            break;
449            /* S */
450        case 'Z':
451            Phonize('S');
452            break;
453            /* No transformation */
454        case 'F':
455        case 'J':
456        case 'L':
457        case 'M':
458        case 'N':
459        case 'R':
460            Phonize(Curr_Letter);
461            break;
462        default:
463            /* nothing */
464            break;
465        }                       /* END SWITCH */
466
467        w_idx += skip_letter;
468    }                           /* END FOR */
469
470    End_Phoned_Word;
471
472    return 0;
473}                               /* END metaphone */
474/* }}} */
475
476/*
477 * Local variables:
478 * tab-width: 4
479 * c-basic-offset: 4
480 * End:
481 * vim600: sw=4 ts=4 fdm=marker
482 * vim<600: sw=4 ts=4
483 */
484