/src/whoosh/lang/dmetaphone.py
Python | 415 lines | 374 code | 7 blank | 34 comment | 162 complexity | 9e2c9318db1debcff6bd85adea29e2a7 MD5 | raw file
Possible License(s): Apache-2.0
- # coding= utf-8
- # This script implements the Double Metaphone algorythm (c) 1998, 1999 by
- # Lawrence Philips. It was translated to Python from the C source written by
- # Kevin Atkinson (http://aspell.net/metaphone/) By Andrew Collins - January 12,
- # 2007 who claims no rights to this work.
- # http://atomboy.isa-geek.com:8080/plone/Members/acoil/programing/double-metaphone
- import re
- from whoosh.compat import u
- vowels = frozenset("AEIOUY")
- slavo_germ_exp = re.compile("W|K|CZ|WITZ")
- silent_starts = re.compile("GN|KN|PN|WR|PS")
- def double_metaphone(text):
- text = text.upper()
- slavo_germanic = bool(slavo_germ_exp.search(text))
- length = len(text)
- text = "--" + text + " "
- first = pos = 2
- last = first + length - 1
- primary = secondary = ""
- if silent_starts.match(text, pos):
- pos += 1
- while pos < length + 2:
- ch = text[pos]
- if ch in vowels:
- # all init vowels now map to 'A'
- if pos != first:
- next = (None, 1)
- else:
- next = ("A", 1)
- elif ch == "B":
- #"-mb", e.g", "dumb", already skipped over... see 'M' below
- if text[pos + 1] == "B":
- next = ("P", 2)
- else:
- next = ("P", 1)
- elif ch == "C":
- # various germanic
- if (pos > (first + 1) and text[pos - 2] not in vowels and text[pos - 1:pos + 2] == 'ACH' and \
- (text[pos + 2] not in ['I', 'E'] or text[pos - 2:pos + 4] in ['BACHER', 'MACHER'])):
- next = ('K', 2)
- # special case 'CAESAR'
- elif pos == first and text[first:first + 6] == 'CAESAR':
- next = ('S', 2)
- elif text[pos:pos + 4] == 'CHIA': # italian 'chianti'
- next = ('K', 2)
- elif text[pos:pos + 2] == 'CH':
- # find 'michael'
- if pos > first and text[pos:pos + 4] == 'CHAE':
- next = ('K', 'X', 2)
- elif pos == first and (text[pos + 1:pos + 6] in ['HARAC', 'HARIS'] or \
- text[pos + 1:pos + 4] in ["HOR", "HYM", "HIA", "HEM"]) and text[first:first + 5] != 'CHORE':
- next = ('K', 2)
- # germanic, greek, or otherwise 'ch' for 'kh' sound
- elif text[first:first + 4] in ['VAN ', 'VON '] or text[first:first + 3] == 'SCH' \
- or text[pos - 2:pos + 4] in ["ORCHES", "ARCHIT", "ORCHID"] \
- or text[pos + 2] in ['T', 'S'] \
- or ((text[pos - 1] in ["A", "O", "U", "E"] or pos == first) \
- and text[pos + 2] in ["L", "R", "N", "M", "B", "H", "F", "V", "W", " "]):
- next = ('K', 1)
- else:
- if pos > first:
- if text[first:first + 2] == 'MC':
- next = ('K', 2)
- else:
- next = ('X', 'K', 2)
- else:
- next = ('X', 2)
- # e.g, 'czerny'
- elif text[pos:pos + 2] == 'CZ' and text[pos - 2:pos + 2] != 'WICZ':
- next = ('S', 'X', 2)
- # e.g., 'focaccia'
- elif text[pos + 1:pos + 4] == 'CIA':
- next = ('X', 3)
- # double 'C', but not if e.g. 'McClellan'
- elif text[pos:pos + 2] == 'CC' and not (pos == (first + 1) and text[first] == 'M'):
- # 'bellocchio' but not 'bacchus'
- if text[pos + 2] in ["I", "E", "H"] and text[pos + 2:pos + 4] != 'HU':
- # 'accident', 'accede' 'succeed'
- if (pos == (first + 1) and text[first] == 'A') or \
- text[pos - 1:pos + 4] in ['UCCEE', 'UCCES']:
- next = ('KS', 3)
- # 'bacci', 'bertucci', other italian
- else:
- next = ('X', 3)
- else:
- next = ('K', 2)
- elif text[pos:pos + 2] in ["CK", "CG", "CQ"]:
- next = ('K', 'K', 2)
- elif text[pos:pos + 2] in ["CI", "CE", "CY"]:
- # italian vs. english
- if text[pos:pos + 3] in ["CIO", "CIE", "CIA"]:
- next = ('S', 'X', 2)
- else:
- next = ('S', 2)
- else:
- # name sent in 'mac caffrey', 'mac gregor
- if text[pos + 1:pos + 3] in [" C", " Q", " G"]:
- next = ('K', 3)
- else:
- if text[pos + 1] in ["C", "K", "Q"] and text[pos + 1:pos + 3] not in ["CE", "CI"]:
- next = ('K', 2)
- else: # default for 'C'
- next = ('K', 1)
- elif ch == u('\xc7'):
- next = ('S', 1)
- elif ch == 'D':
- if text[pos:pos + 2] == 'DG':
- if text[pos + 2] in ['I', 'E', 'Y']: # e.g. 'edge'
- next = ('J', 3)
- else:
- next = ('TK', 2)
- elif text[pos:pos + 2] in ['DT', 'DD']:
- next = ('T', 2)
- else:
- next = ('T', 1)
- elif ch == 'F':
- if text[pos + 1] == 'F':
- next = ('F', 2)
- else:
- next = ('F', 1)
- elif ch == 'G':
- if text[pos + 1] == 'H':
- if pos > first and text[pos - 1] not in vowels:
- next = ('K', 2)
- elif pos < (first + 3):
- if pos == first: # 'ghislane', ghiradelli
- if text[pos + 2] == 'I':
- next = ('J', 2)
- else:
- next = ('K', 2)
- # Parker's rule (with some further refinements) - e.g., 'hugh'
- elif (pos > (first + 1) and text[pos - 2] in ['B', 'H', 'D']) \
- or (pos > (first + 2) and text[pos - 3] in ['B', 'H', 'D']) \
- or (pos > (first + 3) and text[pos - 4] in ['B', 'H']):
- next = (None, 2)
- else:
- # e.g., 'laugh', 'McLaughlin', 'cough', 'gough', 'rough', 'tough'
- if pos > (first + 2) and text[pos - 1] == 'U' \
- and text[pos - 3] in ["C", "G", "L", "R", "T"]:
- next = ('F', 2)
- else:
- if pos > first and text[pos - 1] != 'I':
- next = ('K', 2)
- elif text[pos + 1] == 'N':
- if pos == (first + 1) and text[first] in vowels and not slavo_germanic:
- next = ('KN', 'N', 2)
- else:
- # not e.g. 'cagney'
- if text[pos + 2:pos + 4] != 'EY' and text[pos + 1] != 'Y' and not slavo_germanic:
- next = ('N', 'KN', 2)
- else:
- next = ('KN', 2)
- # 'tagliaro'
- elif text[pos + 1:pos + 3] == 'LI' and not slavo_germanic:
- next = ('KL', 'L', 2)
- # -ges-,-gep-,-gel-, -gie- at beginning
- elif pos == first and (text[pos + 1] == 'Y' \
- or text[pos + 1:pos + 3] in ["ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER"]):
- next = ('K', 'J', 2)
- # -ger-, -gy-
- elif (text[pos + 1:pos + 2] == 'ER' or text[pos + 1] == 'Y') \
- and text[first:first + 6] not in ["DANGER", "RANGER", "MANGER"] \
- and text[pos - 1] not in ['E', 'I'] and text[pos - 1:pos + 2] not in ['RGY', 'OGY']:
- next = ('K', 'J', 2)
- # italian e.g, 'biaggi'
- elif text[pos + 1] in ['E', 'I', 'Y'] or text[pos - 1:pos + 3] in ["AGGI", "OGGI"]:
- # obvious germanic
- if text[first:first + 4] in ['VON ', 'VAN '] or text[first:first + 3] == 'SCH' \
- or text[pos + 1:pos + 3] == 'ET':
- next = ('K', 2)
- else:
- # always soft if french ending
- if text[pos + 1:pos + 5] == 'IER ':
- next = ('J', 2)
- else:
- next = ('J', 'K', 2)
- elif text[pos + 1] == 'G':
- next = ('K', 2)
- else:
- next = ('K', 1)
- elif ch == 'H':
- # only keep if first & before vowel or btw. 2 vowels
- if (pos == first or text[pos - 1] in vowels) and text[pos + 1] in vowels:
- next = ('H', 2)
- else: # (also takes care of 'HH')
- next = (None, 1)
- elif ch == 'J':
- # obvious spanish, 'jose', 'san jacinto'
- if text[pos:pos + 4] == 'JOSE' or text[first:first + 4] == 'SAN ':
- if (pos == first and text[pos + 4] == ' ') or text[first:first + 4] == 'SAN ':
- next = ('H',)
- else:
- next = ('J', 'H')
- elif pos == first and text[pos:pos + 4] != 'JOSE':
- next = ('J', 'A') # Yankelovich/Jankelowicz
- else:
- # spanish pron. of e.g. 'bajador'
- if text[pos - 1] in vowels and not slavo_germanic \
- and text[pos + 1] in ['A', 'O']:
- next = ('J', 'H')
- else:
- if pos == last:
- next = ('J', ' ')
- else:
- if text[pos + 1] not in ["L", "T", "K", "S", "N", "M", "B", "Z"] \
- and text[pos - 1] not in ["S", "K", "L"]:
- next = ('J',)
- else:
- next = (None,)
- if text[pos + 1] == 'J':
- next = next + (2,)
- else:
- next = next + (1,)
- elif ch == 'K':
- if text[pos + 1] == 'K':
- next = ('K', 2)
- else:
- next = ('K', 1)
- elif ch == 'L':
- if text[pos + 1] == 'L':
- # spanish e.g. 'cabrillo', 'gallegos'
- if (pos == (last - 2) and text[pos - 1:pos + 3] in ["ILLO", "ILLA", "ALLE"]) \
- or ((text[last - 1:last + 1] in ["AS", "OS"] or text[last] in ["A", "O"]) \
- and text[pos - 1:pos + 3] == 'ALLE'):
- next = ('L', '', 2)
- else:
- next = ('L', 2)
- else:
- next = ('L', 1)
- elif ch == 'M':
- if text[pos + 1:pos + 4] == 'UMB' \
- and (pos + 1 == last or text[pos + 2:pos + 4] == 'ER') \
- or text[pos + 1] == 'M':
- next = ('M', 2)
- else:
- next = ('M', 1)
- elif ch == 'N':
- if text[pos + 1] == 'N':
- next = ('N', 2)
- else:
- next = ('N', 1)
- elif ch == u('\xd1'):
- next = ('N', 1)
- elif ch == 'P':
- if text[pos + 1] == 'H':
- next = ('F', 2)
- elif text[pos + 1] in ['P', 'B']: # also account for "campbell", "raspberry"
- next = ('P', 2)
- else:
- next = ('P', 1)
- elif ch == 'Q':
- if text[pos + 1] == 'Q':
- next = ('K', 2)
- else:
- next = ('K', 1)
- elif ch == 'R':
- # french e.g. 'rogier', but exclude 'hochmeier'
- if pos == last and not slavo_germanic \
- and text[pos - 2:pos] == 'IE' and text[pos - 4:pos - 2] not in ['ME', 'MA']:
- next = ('', 'R')
- else:
- next = ('R',)
- if text[pos + 1] == 'R':
- next = next + (2,)
- else:
- next = next + (1,)
- elif ch == 'S':
- # special cases 'island', 'isle', 'carlisle', 'carlysle'
- if text[pos - 1:pos + 2] in ['ISL', 'YSL']:
- next = (None, 1)
- # special case 'sugar-'
- elif pos == first and text[first:first + 5] == 'SUGAR':
- next = ('X', 'S', 1)
- elif text[pos:pos + 2] == 'SH':
- # germanic
- if text[pos + 1:pos + 5] in ["HEIM", "HOEK", "HOLM", "HOLZ"]:
- next = ('S', 2)
- else:
- next = ('X', 2)
- # italian & armenian
- elif text[pos:pos + 3] in ["SIO", "SIA"] or text[pos:pos + 4] == 'SIAN':
- if not slavo_germanic:
- next = ('S', 'X', 3)
- else:
- next = ('S', 3)
- # german & anglicisations, e.g. 'smith' match 'schmidt', 'snider' match 'schneider'
- # also, -sz- in slavic language altho in hungarian it is pronounced 's'
- elif (pos == first and text[pos + 1] in ["M", "N", "L", "W"]) or text[pos + 1] == 'Z':
- next = ('S', 'X')
- if text[pos + 1] == 'Z':
- next = next + (2,)
- else:
- next = next + (1,)
- elif text[pos:pos + 2] == 'SC':
- # Schlesinger's rule
- if text[pos + 2] == 'H':
- # dutch origin, e.g. 'school', 'schooner'
- if text[pos + 3:pos + 5] in ["OO", "ER", "EN", "UY", "ED", "EM"]:
- # 'schermerhorn', 'schenker'
- if text[pos + 3:pos + 5] in ['ER', 'EN']:
- next = ('X', 'SK', 3)
- else:
- next = ('SK', 3)
- else:
- if pos == first and text[first + 3] not in vowels and text[first + 3] != 'W':
- next = ('X', 'S', 3)
- else:
- next = ('X', 3)
- elif text[pos + 2] in ['I', 'E', 'Y']:
- next = ('S', 3)
- else:
- next = ('SK', 3)
- # french e.g. 'resnais', 'artois'
- elif pos == last and text[pos - 2:pos] in ['AI', 'OI']:
- next = ('', 'S', 1)
- else:
- next = ('S',)
- if text[pos + 1] in ['S', 'Z']:
- next = next + (2,)
- else:
- next = next + (1,)
- elif ch == 'T':
- if text[pos:pos + 4] == 'TION':
- next = ('X', 3)
- elif text[pos:pos + 3] in ['TIA', 'TCH']:
- next = ('X', 3)
- elif text[pos:pos + 2] == 'TH' or text[pos:pos + 3] == 'TTH':
- # special case 'thomas', 'thames' or germanic
- if text[pos + 2:pos + 4] in ['OM', 'AM'] or text[first:first + 4] in ['VON ', 'VAN '] \
- or text[first:first + 3] == 'SCH':
- next = ('T', 2)
- else:
- next = ('0', 'T', 2)
- elif text[pos + 1] in ['T', 'D']:
- next = ('T', 2)
- else:
- next = ('T', 1)
- elif ch == 'V':
- if text[pos + 1] == 'V':
- next = ('F', 2)
- else:
- next = ('F', 1)
- elif ch == 'W':
- # can also be in middle of word
- if text[pos:pos + 2] == 'WR':
- next = ('R', 2)
- elif pos == first and (text[pos + 1] in vowels or text[pos:pos + 2] == 'WH'):
- # Wasserman should match Vasserman
- if text[pos + 1] in vowels:
- next = ('A', 'F', 1)
- else:
- next = ('A', 1)
- # Arnow should match Arnoff
- elif (pos == last and text[pos - 1] in vowels) \
- or text[pos - 1:pos + 5] in ["EWSKI", "EWSKY", "OWSKI", "OWSKY"] \
- or text[first:first + 3] == 'SCH':
- next = ('', 'F', 1)
- # polish e.g. 'filipowicz'
- elif text[pos:pos + 4] in ["WICZ", "WITZ"]:
- next = ('TS', 'FX', 4)
- else: # default is to skip it
- next = (None, 1)
- elif ch == 'X':
- # french e.g. breaux
- next = (None,)
- if not(pos == last and (text[pos - 3:pos] in ["IAU", "EAU"] \
- or text[pos - 2:pos] in ['AU', 'OU'])):
- next = ('KS',)
- if text[pos + 1] in ['C', 'X']:
- next = next + (2,)
- else:
- next = next + (1,)
- elif ch == 'Z':
- # chinese pinyin e.g. 'zhao'
- if text[pos + 1] == 'H':
- next = ('J',)
- elif text[pos + 1:pos + 3] in ["ZO", "ZI", "ZA"] \
- or (slavo_germanic and pos > first and text[pos - 1] != 'T'):
- next = ('S', 'TS')
- else:
- next = ('S',)
- if text[pos + 1] == 'Z':
- next = next + (2,)
- else:
- next = next + (1,)
- else:
- next = (None, 1)
- if len(next) == 2:
- if next[0]:
- primary += next[0]
- secondary += next[0]
- pos += next[1]
- elif len(next) == 3:
- if next[0]:
- primary += next[0]
- if next[1]:
- secondary += next[1]
- pos += next[2]
- if primary == secondary:
- return (primary, None)
- else:
- return (primary, secondary)