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