1 /*
2 +----------------------------------------------------------------------+
3 | PHP Version 7 |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 1997-2018 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
28 static int metaphone(unsigned char *word, size_t word_len, zend_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 */
PHP_FUNCTION(metaphone)32 PHP_FUNCTION(metaphone)
33 {
34 zend_string *str;
35 zend_string *result = NULL;
36 zend_long phones = 0;
37
38 ZEND_PARSE_PARAMETERS_START(1, 2)
39 Z_PARAM_STR(str)
40 Z_PARAM_OPTIONAL
41 Z_PARAM_LONG(phones)
42 ZEND_PARSE_PARAMETERS_END();
43
44 if (metaphone((unsigned char *)ZSTR_VAL(str), ZSTR_LEN(str), phones, &result, 1) == 0) {
45 RETVAL_STR(result);
46 } else {
47 if (result) {
48 zend_string_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... */
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 diphthongs 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((char *) 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... */
Lookahead(char * word,int how_far)127 static 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 = zend_string_extend(*phoned_word, 2 * sizeof(char) + max_buffer_len, 0); \
148 max_buffer_len += 2; \
149 } \
150 ZSTR_VAL(*phoned_word)[p_idx++] = c; \
151 ZSTR_LEN(*phoned_word) = 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 = zend_string_extend(*phoned_word, 1 * sizeof(char) + max_buffer_len, 0); \
157 max_buffer_len += 1; \
158 } \
159 ZSTR_VAL(*phoned_word)[p_idx] = '\0'; \
160 ZSTR_LEN(*phoned_word) = 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 */
metaphone(unsigned char * word,size_t word_len,zend_long max_phonemes,zend_string ** phoned_word,int traditional)170 static int metaphone(unsigned char *word, size_t word_len, zend_long max_phonemes, zend_string **phoned_word, int traditional)
171 {
172 int w_idx = 0; /* point in the phonization we're at. */
173 size_t p_idx = 0; /* end of the phoned phrase */
174 size_t 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 = zend_string_alloc(sizeof(char) * word_len + 1, 0);
193 } else {
194 max_buffer_len = max_phonemes;
195 *phoned_word = zend_string_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 < (size_t)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