1 : /*
2 : +----------------------------------------------------------------------+
3 : | PHP Version 5 |
4 : +----------------------------------------------------------------------+
5 : | Copyright (c) 1997-2007 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: metaphone.c,v 1.28.2.1.2.4 2007/01/01 09:36:08 sebastian Exp $ */
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 :
28 : static int metaphone(unsigned char *word, int word_len, long max_phonemes, char **phoned_word, int traditional);
29 :
30 : /* {{{ proto string metaphone(string text[, int phones])
31 : Break english phrases down into their phonemes */
32 : PHP_FUNCTION(metaphone)
33 0 : {
34 : char *str;
35 0 : char *result = 0;
36 : int str_len;
37 0 : long phones = 0;
38 :
39 0 : if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &str, &str_len,
40 : &phones) == FAILURE) {
41 0 : return;
42 : }
43 :
44 0 : if (metaphone((unsigned char *)str, str_len, phones, &result, 1) == 0) {
45 0 : RETVAL_STRING(result, 0);
46 : } else {
47 0 : if (result) {
48 0 : efree(result);
49 : }
50 0 : 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... */
81 : char _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... */
127 : static char Lookahead(char *word, int how_far)
128 0 : {
129 0 : char letter_ahead = '\0'; /* null by default */
130 : int idx;
131 0 : for (idx = 0; word[idx] != '\0' && idx < how_far; idx++);
132 : /* Edge forward in the string... */
133 :
134 0 : letter_ahead = word[idx]; /* idx will be either == to how_far or
135 : * at the end of the string
136 : */
137 0 : 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 = erealloc(*phoned_word, max_buffer_len + 2); \
148 : max_buffer_len += 2; \
149 : } \
150 : (*phoned_word)[p_idx++] = c; \
151 : }
152 : /* Slap a null character on the end of the phoned word */
153 : #define End_Phoned_Word {(*phoned_word)[p_idx] = '\0';}
154 : /* How long is the phoned word? */
155 : #define Phone_Len (p_idx)
156 :
157 : /* Note is a letter is a 'break' in the word */
158 : #define Isbreak(c) (!isalpha(c))
159 :
160 : /* {{{ metaphone
161 : */
162 : static int metaphone(unsigned char *word, int word_len, long max_phonemes, char **phoned_word, int traditional)
163 0 : {
164 0 : int w_idx = 0; /* point in the phonization we're at. */
165 0 : int p_idx = 0; /* end of the phoned phrase */
166 0 : int max_buffer_len = 0; /* maximum length of the destination buffer */
167 :
168 : /*-- Parameter checks --*/
169 : /* Negative phoneme length is meaningless */
170 :
171 0 : if (max_phonemes < 0)
172 0 : return -1;
173 :
174 : /* Empty/null string is meaningless */
175 : /* Overly paranoid */
176 : /* assert(word != NULL && word[0] != '\0'); */
177 :
178 0 : if (word == NULL)
179 0 : return -1;
180 :
181 : /*-- Allocate memory for our phoned_phrase --*/
182 0 : if (max_phonemes == 0) { /* Assume largest possible */
183 0 : max_buffer_len = word_len;
184 0 : *phoned_word = safe_emalloc(sizeof(char), word_len, 1);
185 : } else {
186 0 : max_buffer_len = max_phonemes;
187 0 : *phoned_word = safe_emalloc(sizeof(char), max_phonemes, 1);
188 : }
189 :
190 :
191 : /*-- The first phoneme has to be processed specially. --*/
192 : /* Find our first letter */
193 0 : for (; !isalpha(Curr_Letter); w_idx++) {
194 : /* On the off chance we were given nothing but crap... */
195 0 : if (Curr_Letter == '\0') {
196 0 : End_Phoned_Word
197 0 : return SUCCESS; /* For testing */
198 : }
199 : }
200 :
201 0 : switch (Curr_Letter) {
202 : /* AE becomes E */
203 : case 'A':
204 0 : if (Next_Letter == 'E') {
205 0 : Phonize('E');
206 0 : w_idx += 2;
207 : }
208 : /* Remember, preserve vowels at the beginning */
209 : else {
210 0 : Phonize('A');
211 0 : w_idx++;
212 : }
213 0 : break;
214 : /* [GKP]N becomes N */
215 : case 'G':
216 : case 'K':
217 : case 'P':
218 0 : if (Next_Letter == 'N') {
219 0 : Phonize('N');
220 0 : w_idx += 2;
221 : }
222 0 : break;
223 : /* WH becomes H,
224 : WR becomes R
225 : W if followed by a vowel */
226 : case 'W':
227 0 : if (Next_Letter == 'H' ||
228 : Next_Letter == 'R') {
229 0 : Phonize(Next_Letter);
230 0 : w_idx += 2;
231 0 : } else if (isvowel(Next_Letter)) {
232 0 : Phonize('W');
233 0 : w_idx += 2;
234 : }
235 : /* else ignore */
236 0 : break;
237 : /* X becomes S */
238 : case 'X':
239 0 : Phonize('S');
240 0 : w_idx++;
241 0 : break;
242 : /* Vowels are kept */
243 : /* We did A already
244 : case 'A':
245 : case 'a':
246 : */
247 : case 'E':
248 : case 'I':
249 : case 'O':
250 : case 'U':
251 0 : Phonize(Curr_Letter);
252 0 : w_idx++;
253 : break;
254 : default:
255 : /* do nothing */
256 : break;
257 : }
258 :
259 :
260 :
261 : /* On to the metaphoning */
262 0 : for (; Curr_Letter != '\0' &&
263 : (max_phonemes == 0 || Phone_Len < max_phonemes);
264 0 : w_idx++) {
265 : /* How many letters to skip because an eariler encoding handled
266 : * multiple letters */
267 0 : unsigned short int skip_letter = 0;
268 :
269 :
270 : /* THOUGHT: It would be nice if, rather than having things like...
271 : * well, SCI. For SCI you encode the S, then have to remember
272 : * to skip the C. So the phonome SCI invades both S and C. It would
273 : * be better, IMHO, to skip the C from the S part of the encoding.
274 : * Hell, I'm trying it.
275 : */
276 :
277 : /* Ignore non-alphas */
278 0 : if (!isalpha(Curr_Letter))
279 0 : continue;
280 :
281 : /* Drop duplicates, except CC */
282 0 : if (Curr_Letter == Prev_Letter &&
283 : Curr_Letter != 'C')
284 0 : continue;
285 :
286 0 : switch (Curr_Letter) {
287 : /* B -> B unless in MB */
288 : case 'B':
289 0 : if (Prev_Letter != 'M')
290 0 : Phonize('B');
291 0 : break;
292 : /* 'sh' if -CIA- or -CH, but not SCH, except SCHW.
293 : * (SCHW is handled in S)
294 : * S if -CI-, -CE- or -CY-
295 : * dropped if -SCI-, SCE-, -SCY- (handed in S)
296 : * else K
297 : */
298 : case 'C':
299 0 : if (MAKESOFT(Next_Letter)) { /* C[IEY] */
300 0 : if (After_Next_Letter == 'A' &&
301 : Next_Letter == 'I') { /* CIA */
302 0 : Phonize(SH);
303 : }
304 : /* SC[IEY] */
305 0 : else if (Prev_Letter == 'S') {
306 : /* Dropped */
307 : } else {
308 0 : Phonize('S');
309 : }
310 0 : } else if (Next_Letter == 'H') {
311 0 : if ((!traditional) && (After_Next_Letter == 'R' || Prev_Letter == 'S')) { /* Christ, School */
312 0 : Phonize('K');
313 : } else {
314 0 : Phonize(SH);
315 : }
316 0 : skip_letter++;
317 : } else {
318 0 : Phonize('K');
319 : }
320 0 : break;
321 : /* J if in -DGE-, -DGI- or -DGY-
322 : * else T
323 : */
324 : case 'D':
325 0 : if (Next_Letter == 'G' &&
326 : MAKESOFT(After_Next_Letter)) {
327 0 : Phonize('J');
328 0 : skip_letter++;
329 : } else
330 0 : Phonize('T');
331 0 : break;
332 : /* F if in -GH and not B--GH, D--GH, -H--GH, -H---GH
333 : * else dropped if -GNED, -GN,
334 : * else dropped if -DGE-, -DGI- or -DGY- (handled in D)
335 : * else J if in -GE-, -GI, -GY and not GG
336 : * else K
337 : */
338 : case 'G':
339 0 : if (Next_Letter == 'H') {
340 0 : if (!(NOGHTOF(Look_Back_Letter(3)) ||
341 : Look_Back_Letter(4) == 'H')) {
342 0 : Phonize('F');
343 0 : skip_letter++;
344 : } else {
345 : /* silent */
346 : }
347 0 : } else if (Next_Letter == 'N') {
348 0 : if (Isbreak(After_Next_Letter) ||
349 : (After_Next_Letter == 'E' &&
350 : Look_Ahead_Letter(3) == 'D')) {
351 : /* dropped */
352 : } else
353 0 : Phonize('K');
354 0 : } else if (MAKESOFT(Next_Letter) &&
355 : Prev_Letter != 'G') {
356 0 : Phonize('J');
357 : } else {
358 0 : Phonize('K');
359 : }
360 0 : break;
361 : /* H if before a vowel and not after C,G,P,S,T */
362 : case 'H':
363 0 : if (isvowel(Next_Letter) &&
364 : !AFFECTH(Prev_Letter))
365 0 : Phonize('H');
366 0 : break;
367 : /* dropped if after C
368 : * else K
369 : */
370 : case 'K':
371 0 : if (Prev_Letter != 'C')
372 0 : Phonize('K');
373 0 : break;
374 : /* F if before H
375 : * else P
376 : */
377 : case 'P':
378 0 : if (Next_Letter == 'H') {
379 0 : Phonize('F');
380 : } else {
381 0 : Phonize('P');
382 : }
383 0 : break;
384 : /* K
385 : */
386 : case 'Q':
387 0 : Phonize('K');
388 0 : break;
389 : /* 'sh' in -SH-, -SIO- or -SIA- or -SCHW-
390 : * else S
391 : */
392 : case 'S':
393 0 : if (Next_Letter == 'I' &&
394 : (After_Next_Letter == 'O' ||
395 : After_Next_Letter == 'A')) {
396 0 : Phonize(SH);
397 0 : } else if (Next_Letter == 'H') {
398 0 : Phonize(SH);
399 0 : skip_letter++;
400 0 : } else if ((!traditional) && (Next_Letter == 'C' && Look_Ahead_Letter(2) == 'H' && Look_Ahead_Letter(3) == 'W')) {
401 0 : Phonize(SH);
402 0 : skip_letter += 2;
403 : } else {
404 0 : Phonize('S');
405 : }
406 0 : break;
407 : /* 'sh' in -TIA- or -TIO-
408 : * else 'th' before H
409 : * else T
410 : */
411 : case 'T':
412 0 : if (Next_Letter == 'I' &&
413 : (After_Next_Letter == 'O' ||
414 : After_Next_Letter == 'A')) {
415 0 : Phonize(SH);
416 0 : } else if (Next_Letter == 'H') {
417 0 : Phonize(TH);
418 0 : skip_letter++;
419 : } else {
420 0 : Phonize('T');
421 : }
422 0 : break;
423 : /* F */
424 : case 'V':
425 0 : Phonize('F');
426 0 : break;
427 : /* W before a vowel, else dropped */
428 : case 'W':
429 0 : if (isvowel(Next_Letter))
430 0 : Phonize('W');
431 0 : break;
432 : /* KS */
433 : case 'X':
434 0 : Phonize('K');
435 0 : Phonize('S');
436 0 : break;
437 : /* Y if followed by a vowel */
438 : case 'Y':
439 0 : if (isvowel(Next_Letter))
440 0 : Phonize('Y');
441 0 : break;
442 : /* S */
443 : case 'Z':
444 0 : Phonize('S');
445 0 : break;
446 : /* No transformation */
447 : case 'F':
448 : case 'J':
449 : case 'L':
450 : case 'M':
451 : case 'N':
452 : case 'R':
453 0 : Phonize(Curr_Letter);
454 : break;
455 : default:
456 : /* nothing */
457 : break;
458 : } /* END SWITCH */
459 :
460 0 : w_idx += skip_letter;
461 : } /* END FOR */
462 :
463 0 : End_Phoned_Word;
464 :
465 0 : return 0;
466 : } /* END metaphone */
467 : /* }}} */
468 :
469 : /*
470 : * Local variables:
471 : * tab-width: 4
472 : * c-basic-offset: 4
473 : * End:
474 : * vim600: sw=4 ts=4 fdm=marker
475 : * vim<600: sw=4 ts=4
476 : */
|