xref: /PHP-8.1/ext/standard/metaphone.c (revision 01b3fc03)
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 #define ENCODE(c) (isalpha(c) ? _codes[((toupper(c)) - 'A')] : 0)
82 
83 #define isvowel(c)  (ENCODE(c) & 1)		/* AEIOU */
84 
85 /* These letters are passed through unchanged */
86 #define NOCHANGE(c) (ENCODE(c) & 2)		/* FJMNR */
87 
88 /* These form diphthongs when preceding H */
89 #define AFFECTH(c)  (ENCODE(c) & 4)		/* CGPST */
90 
91 /* These make C and G soft */
92 #define MAKESOFT(c) (ENCODE(c) & 8)		/* EIY */
93 
94 /* These prevent GH from becoming F */
95 #define NOGHTOF(c)  (ENCODE(c) & 16)	/* BDH */
96 
97 /*----------------------------- */
98 /* end of "metachar.h"          */
99 /*----------------------------- */
100 
101 /* I suppose I could have been using a character pointer instead of
102  * accesssing the array directly... */
103 
104 /* Look at the next letter in the word */
105 #define Next_Letter (toupper(word[w_idx+1]))
106 /* Look at the current letter in the word */
107 #define Curr_Letter (toupper(word[w_idx]))
108 /* Go N letters back. */
109 #define Look_Back_Letter(n)	(w_idx >= n ? toupper(word[w_idx-n]) : '\0')
110 /* Previous letter.  I dunno, should this return null on failure? */
111 #define Prev_Letter (Look_Back_Letter(1))
112 /* Look two letters down.  It makes sure you don't walk off the string. */
113 #define After_Next_Letter	(Next_Letter != '\0' ? toupper(word[w_idx+2]) \
114 											     : '\0')
115 #define Look_Ahead_Letter(n) (toupper(Lookahead((char *) word+w_idx, n)))
116 
117 
118 /* Allows us to safely look ahead an arbitrary # of letters */
119 /* I probably could have just used strlen... */
Lookahead(char * word,int how_far)120 static char Lookahead(char *word, int how_far)
121 {
122 	char letter_ahead = '\0';	/* null by default */
123 	int idx;
124 	for (idx = 0; word[idx] != '\0' && idx < how_far; idx++);
125 	/* Edge forward in the string... */
126 
127 	letter_ahead = word[idx];	/* idx will be either == to how_far or
128 								 * at the end of the string
129 								 */
130 	return letter_ahead;
131 }
132 
133 
134 /* phonize one letter
135  * We don't know the buffers size in advance. On way to solve this is to just
136  * re-allocate the buffer size. We're using an extra of 2 characters (this
137  * could be one though; or more too). */
138 #define Phonize(c)	{ \
139 						if (p_idx >= max_buffer_len) { \
140 							*phoned_word = zend_string_extend(*phoned_word, 2 * sizeof(char) + max_buffer_len, 0); \
141 							max_buffer_len += 2; \
142 						} \
143 						ZSTR_VAL(*phoned_word)[p_idx++] = c; \
144 						ZSTR_LEN(*phoned_word) = p_idx; \
145 					}
146 /* Slap a null character on the end of the phoned word */
147 #define End_Phoned_Word()	{ \
148 							if (p_idx == max_buffer_len) { \
149 								*phoned_word = zend_string_extend(*phoned_word, 1 * sizeof(char) + max_buffer_len, 0); \
150 								max_buffer_len += 1; \
151 							} \
152 							ZSTR_VAL(*phoned_word)[p_idx] = '\0'; \
153 							ZSTR_LEN(*phoned_word) = p_idx; \
154 						}
155 /* How long is the phoned word? */
156 #define Phone_Len	(p_idx)
157 
158 /* Note is a letter is a 'break' in the word */
159 #define Isbreak(c)  (!isalpha(c))
160 
161 /* {{{ metaphone */
metaphone(unsigned char * word,size_t word_len,zend_long max_phonemes,zend_string ** phoned_word,int traditional)162 static void metaphone(unsigned char *word, size_t word_len, zend_long max_phonemes, zend_string **phoned_word, int traditional)
163 {
164 	int w_idx = 0;				/* point in the phonization we're at. */
165 	size_t p_idx = 0;				/* end of the phoned phrase */
166 	size_t max_buffer_len = 0;		/* maximum length of the destination buffer */
167 	ZEND_ASSERT(word != NULL);
168 	ZEND_ASSERT(max_phonemes >= 0);
169 
170 /*-- Allocate memory for our phoned_phrase --*/
171 	if (max_phonemes == 0) {	/* Assume largest possible */
172 		max_buffer_len = word_len;
173 		*phoned_word = zend_string_alloc(sizeof(char) * word_len + 1, 0);
174 	} else {
175 		max_buffer_len = max_phonemes;
176 		*phoned_word = zend_string_alloc(sizeof(char) * max_phonemes + 1, 0);
177 	}
178 
179 
180 /*-- The first phoneme has to be processed specially. --*/
181 	/* Find our first letter */
182 	for (; !isalpha(Curr_Letter); w_idx++) {
183 		/* On the off chance we were given nothing but crap... */
184 		if (Curr_Letter == '\0') {
185 			End_Phoned_Word();
186 			return;
187 		}
188 	}
189 
190 	switch (Curr_Letter) {
191 		/* AE becomes E */
192 	case 'A':
193 		if (Next_Letter == 'E') {
194 			Phonize('E');
195 			w_idx += 2;
196 		}
197 		/* Remember, preserve vowels at the beginning */
198 		else {
199 			Phonize('A');
200 			w_idx++;
201 		}
202 		break;
203 		/* [GKP]N becomes N */
204 	case 'G':
205 	case 'K':
206 	case 'P':
207 		if (Next_Letter == 'N') {
208 			Phonize('N');
209 			w_idx += 2;
210 		}
211 		break;
212 		/* WH becomes W,
213 		   WR becomes R
214 		   W if followed by a vowel */
215 	case 'W':
216 		if (Next_Letter == 'R') {
217 			Phonize(Next_Letter);
218 			w_idx += 2;
219 		} else if (Next_Letter == 'H' || isvowel(Next_Letter)) {
220 			Phonize('W');
221 			w_idx += 2;
222 		}
223 		/* else ignore */
224 		break;
225 		/* X becomes S */
226 	case 'X':
227 		Phonize('S');
228 		w_idx++;
229 		break;
230 		/* Vowels are kept */
231 		/* We did A already
232 		   case 'A':
233 		   case 'a':
234 		 */
235 	case 'E':
236 	case 'I':
237 	case 'O':
238 	case 'U':
239 		Phonize(Curr_Letter);
240 		w_idx++;
241 		break;
242 	default:
243 		/* do nothing */
244 		break;
245 	}
246 
247 
248 
249 	/* On to the metaphoning */
250 	for (; Curr_Letter != '\0' &&
251 		 (max_phonemes == 0 || Phone_Len < (size_t)max_phonemes);
252 		 w_idx++) {
253 		/* How many letters to skip because an eariler encoding handled
254 		 * multiple letters */
255 		unsigned short int skip_letter = 0;
256 
257 
258 		/* THOUGHT:  It would be nice if, rather than having things like...
259 		 * well, SCI.  For SCI you encode the S, then have to remember
260 		 * to skip the C.  So the phonome SCI invades both S and C.  It would
261 		 * be better, IMHO, to skip the C from the S part of the encoding.
262 		 * Hell, I'm trying it.
263 		 */
264 
265 		/* Ignore non-alphas */
266 		if (!isalpha(Curr_Letter))
267 			continue;
268 
269 		/* Drop duplicates, except CC */
270 		if (Curr_Letter == Prev_Letter &&
271 			Curr_Letter != 'C')
272 			continue;
273 
274 		switch (Curr_Letter) {
275 			/* B -> B unless in MB */
276 		case 'B':
277 			if (Prev_Letter != 'M')
278 				Phonize('B');
279 			break;
280 			/* 'sh' if -CIA- or -CH, but not SCH, except SCHW.
281 			 * (SCHW is handled in S)
282 			 *  S if -CI-, -CE- or -CY-
283 			 *  dropped if -SCI-, SCE-, -SCY- (handed in S)
284 			 *  else K
285 			 */
286 		case 'C':
287 			if (MAKESOFT(Next_Letter)) {	/* C[IEY] */
288 				if (After_Next_Letter == 'A' &&
289 					Next_Letter == 'I') {	/* CIA */
290 					Phonize(SH);
291 				}
292 				/* SC[IEY] */
293 				else if (Prev_Letter == 'S') {
294 					/* Dropped */
295 				} else {
296 					Phonize('S');
297 				}
298 			} else if (Next_Letter == 'H') {
299 				if ((!traditional) && (After_Next_Letter == 'R' || Prev_Letter == 'S')) {	/* Christ, School */
300 					Phonize('K');
301 				} else {
302 					Phonize(SH);
303 				}
304 				skip_letter++;
305 			} else {
306 				Phonize('K');
307 			}
308 			break;
309 			/* J if in -DGE-, -DGI- or -DGY-
310 			 * else T
311 			 */
312 		case 'D':
313 			if (Next_Letter == 'G' &&
314 				MAKESOFT(After_Next_Letter)) {
315 				Phonize('J');
316 				skip_letter++;
317 			} else
318 				Phonize('T');
319 			break;
320 			/* F if in -GH and not B--GH, D--GH, -H--GH, -H---GH
321 			 * else dropped if -GNED, -GN,
322 			 * else dropped if -DGE-, -DGI- or -DGY- (handled in D)
323 			 * else J if in -GE-, -GI, -GY and not GG
324 			 * else K
325 			 */
326 		case 'G':
327 			if (Next_Letter == 'H') {
328 				if (!(NOGHTOF(Look_Back_Letter(3)) ||
329 					  Look_Back_Letter(4) == 'H')) {
330 					Phonize('F');
331 					skip_letter++;
332 				} else {
333 					/* silent */
334 				}
335 			} else if (Next_Letter == 'N') {
336 				if (Isbreak(After_Next_Letter) ||
337 					(After_Next_Letter == 'E' &&
338 					 Look_Ahead_Letter(3) == 'D')) {
339 					/* dropped */
340 				} else
341 					Phonize('K');
342 			} else if (MAKESOFT(Next_Letter) &&
343 					   Prev_Letter != 'G') {
344 				Phonize('J');
345 			} else {
346 				Phonize('K');
347 			}
348 			break;
349 			/* H if before a vowel and not after C,G,P,S,T */
350 		case 'H':
351 			if (isvowel(Next_Letter) &&
352 				!AFFECTH(Prev_Letter))
353 				Phonize('H');
354 			break;
355 			/* dropped if after C
356 			 * else K
357 			 */
358 		case 'K':
359 			if (Prev_Letter != 'C')
360 				Phonize('K');
361 			break;
362 			/* F if before H
363 			 * else P
364 			 */
365 		case 'P':
366 			if (Next_Letter == 'H') {
367 				Phonize('F');
368 			} else {
369 				Phonize('P');
370 			}
371 			break;
372 			/* K
373 			 */
374 		case 'Q':
375 			Phonize('K');
376 			break;
377 			/* 'sh' in -SH-, -SIO- or -SIA- or -SCHW-
378 			 * else S
379 			 */
380 		case 'S':
381 			if (Next_Letter == 'I' &&
382 				(After_Next_Letter == 'O' ||
383 				 After_Next_Letter == 'A')) {
384 				Phonize(SH);
385 			} else if (Next_Letter == 'H') {
386 				Phonize(SH);
387 				skip_letter++;
388 			} else if ((!traditional) && (Next_Letter == 'C' && Look_Ahead_Letter(2) == 'H' && Look_Ahead_Letter(3) == 'W')) {
389 				Phonize(SH);
390 				skip_letter += 2;
391 			} else {
392 				Phonize('S');
393 			}
394 			break;
395 			/* 'sh' in -TIA- or -TIO-
396 			 * else 'th' before H
397 			 * else T
398 			 */
399 		case 'T':
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(TH);
406 				skip_letter++;
407 			} else if (!(Next_Letter == 'C' && After_Next_Letter == 'H')) {
408 				Phonize('T');
409 			}
410 			break;
411 			/* F */
412 		case 'V':
413 			Phonize('F');
414 			break;
415 			/* W before a vowel, else dropped */
416 		case 'W':
417 			if (isvowel(Next_Letter))
418 				Phonize('W');
419 			break;
420 			/* KS */
421 		case 'X':
422 			Phonize('K');
423 			Phonize('S');
424 			break;
425 			/* Y if followed by a vowel */
426 		case 'Y':
427 			if (isvowel(Next_Letter))
428 				Phonize('Y');
429 			break;
430 			/* S */
431 		case 'Z':
432 			Phonize('S');
433 			break;
434 			/* No transformation */
435 		case 'F':
436 		case 'J':
437 		case 'L':
438 		case 'M':
439 		case 'N':
440 		case 'R':
441 			Phonize(Curr_Letter);
442 			break;
443 		default:
444 			/* nothing */
445 			break;
446 		}						/* END SWITCH */
447 
448 		w_idx += skip_letter;
449 	}							/* END FOR */
450 
451 	End_Phoned_Word();
452 }								/* END metaphone */
453 /* }}} */
454