xref: /PHP-7.2/ext/standard/metaphone.c (revision 7a7ec01a)
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