xref: /PHP-7.4/ext/standard/metaphone.c (revision 92ac598a)
1 /*
2    +----------------------------------------------------------------------+
3    | PHP Version 7                                                        |
4    +----------------------------------------------------------------------+
5    | Copyright (c) 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