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