# -*- coding: utf-8 -*-
# Copyright 2014-2018 by Christopher C. Little.
# This file is part of Abydos.
#
# Abydos is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Abydos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Abydos. If not, see <http://www.gnu.org/licenses/>.
"""abydos.phonetic._dm.
The phonetic._dm module implements Daitch-Mokotoff Soundex.
"""
from __future__ import unicode_literals
from unicodedata import normalize as unicode_normalize
from six import text_type
from ._util import _delete_consecutive_repeats
__all__ = ['dm_soundex']
[docs]def dm_soundex(word, max_length=6, zero_pad=True):
"""Return the Daitch-Mokotoff Soundex code for a word.
Based on Daitch-Mokotoff Soundex :cite:`Mokotoff:1997`, this returns values
of a word as a set. A collection is necessary since there can be multiple
values for a single word.
:param word: the word to transform
:param max_length: the length of the code returned (defaults to 6; must be
between 6 and 64)
:param zero_pad: pad the end of the return value with 0s to achieve a
max_length string
:returns: the Daitch-Mokotoff Soundex value
:rtype: str
>>> sorted(dm_soundex('Christopher'))
['494379', '594379']
>>> dm_soundex('Niall')
{'680000'}
>>> dm_soundex('Smith')
{'463000'}
>>> dm_soundex('Schmidt')
{'463000'}
>>> sorted(dm_soundex('The quick brown fox', max_length=20,
... zero_pad=False))
['35457976754', '3557976754']
"""
_dms_table = {
'STCH': (2, 4, 4),
'DRZ': (4, 4, 4),
'ZH': (4, 4, 4),
'ZHDZH': (2, 4, 4),
'DZH': (4, 4, 4),
'DRS': (4, 4, 4),
'DZS': (4, 4, 4),
'SCHTCH': (2, 4, 4),
'SHTSH': (2, 4, 4),
'SZCZ': (2, 4, 4),
'TZS': (4, 4, 4),
'SZCS': (2, 4, 4),
'STSH': (2, 4, 4),
'SHCH': (2, 4, 4),
'D': (3, 3, 3),
'H': (5, 5, '_'),
'TTSCH': (4, 4, 4),
'THS': (4, 4, 4),
'L': (8, 8, 8),
'P': (7, 7, 7),
'CHS': (5, 54, 54),
'T': (3, 3, 3),
'X': (5, 54, 54),
'OJ': (0, 1, '_'),
'OI': (0, 1, '_'),
'SCHTSH': (2, 4, 4),
'OY': (0, 1, '_'),
'Y': (1, '_', '_'),
'TSH': (4, 4, 4),
'ZDZ': (2, 4, 4),
'TSZ': (4, 4, 4),
'SHT': (2, 43, 43),
'SCHTSCH': (2, 4, 4),
'TTSZ': (4, 4, 4),
'TTZ': (4, 4, 4),
'SCH': (4, 4, 4),
'TTS': (4, 4, 4),
'SZD': (2, 43, 43),
'AI': (0, 1, '_'),
'PF': (7, 7, 7),
'TCH': (4, 4, 4),
'PH': (7, 7, 7),
'TTCH': (4, 4, 4),
'SZT': (2, 43, 43),
'ZDZH': (2, 4, 4),
'EI': (0, 1, '_'),
'G': (5, 5, 5),
'EJ': (0, 1, '_'),
'ZD': (2, 43, 43),
'IU': (1, '_', '_'),
'K': (5, 5, 5),
'O': (0, '_', '_'),
'SHTCH': (2, 4, 4),
'S': (4, 4, 4),
'TRZ': (4, 4, 4),
'SHD': (2, 43, 43),
'DSH': (4, 4, 4),
'CSZ': (4, 4, 4),
'EU': (1, 1, '_'),
'TRS': (4, 4, 4),
'ZS': (4, 4, 4),
'STRZ': (2, 4, 4),
'UY': (0, 1, '_'),
'STRS': (2, 4, 4),
'CZS': (4, 4, 4),
'MN': ('6_6', '6_6', '6_6'),
'UI': (0, 1, '_'),
'UJ': (0, 1, '_'),
'UE': (0, '_', '_'),
'EY': (0, 1, '_'),
'W': (7, 7, 7),
'IA': (1, '_', '_'),
'FB': (7, 7, 7),
'STSCH': (2, 4, 4),
'SCHT': (2, 43, 43),
'NM': ('6_6', '6_6', '6_6'),
'SCHD': (2, 43, 43),
'B': (7, 7, 7),
'DSZ': (4, 4, 4),
'F': (7, 7, 7),
'N': (6, 6, 6),
'CZ': (4, 4, 4),
'R': (9, 9, 9),
'U': (0, '_', '_'),
'V': (7, 7, 7),
'CS': (4, 4, 4),
'Z': (4, 4, 4),
'SZ': (4, 4, 4),
'TSCH': (4, 4, 4),
'KH': (5, 5, 5),
'ST': (2, 43, 43),
'KS': (5, 54, 54),
'SH': (4, 4, 4),
'SC': (2, 4, 4),
'SD': (2, 43, 43),
'DZ': (4, 4, 4),
'ZHD': (2, 43, 43),
'DT': (3, 3, 3),
'ZSH': (4, 4, 4),
'DS': (4, 4, 4),
'TZ': (4, 4, 4),
'TS': (4, 4, 4),
'TH': (3, 3, 3),
'TC': (4, 4, 4),
'A': (0, '_', '_'),
'E': (0, '_', '_'),
'I': (0, '_', '_'),
'AJ': (0, 1, '_'),
'M': (6, 6, 6),
'Q': (5, 5, 5),
'AU': (0, 7, '_'),
'IO': (1, '_', '_'),
'AY': (0, 1, '_'),
'IE': (1, '_', '_'),
'ZSCH': (4, 4, 4),
'CH': ((5, 4), (5, 4), (5, 4)),
'CK': ((5, 45), (5, 45), (5, 45)),
'C': ((5, 4), (5, 4), (5, 4)),
'J': ((1, 4), ('_', 4), ('_', 4)),
'RZ': ((94, 4), (94, 4), (94, 4)),
'RS': ((94, 4), (94, 4), (94, 4)),
}
_dms_order = {
'A': ('AI', 'AJ', 'AU', 'AY', 'A'),
'B': ('B',),
'C': ('CHS', 'CSZ', 'CZS', 'CH', 'CK', 'CS', 'CZ', 'C'),
'D': ('DRS', 'DRZ', 'DSH', 'DSZ', 'DZH', 'DZS', 'DS', 'DT', 'DZ', 'D'),
'E': ('EI', 'EJ', 'EU', 'EY', 'E'),
'F': ('FB', 'F'),
'G': ('G',),
'H': ('H',),
'I': ('IA', 'IE', 'IO', 'IU', 'I'),
'J': ('J',),
'K': ('KH', 'KS', 'K'),
'L': ('L',),
'M': ('MN', 'M'),
'N': ('NM', 'N'),
'O': ('OI', 'OJ', 'OY', 'O'),
'P': ('PF', 'PH', 'P'),
'Q': ('Q',),
'R': ('RS', 'RZ', 'R'),
'S': (
'SCHTSCH',
'SCHTCH',
'SCHTSH',
'SHTCH',
'SHTSH',
'STSCH',
'SCHD',
'SCHT',
'SHCH',
'STCH',
'STRS',
'STRZ',
'STSH',
'SZCS',
'SZCZ',
'SCH',
'SHD',
'SHT',
'SZD',
'SZT',
'SC',
'SD',
'SH',
'ST',
'SZ',
'S',
),
'T': (
'TTSCH',
'TSCH',
'TTCH',
'TTSZ',
'TCH',
'THS',
'TRS',
'TRZ',
'TSH',
'TSZ',
'TTS',
'TTZ',
'TZS',
'TC',
'TH',
'TS',
'TZ',
'T',
),
'U': ('UE', 'UI', 'UJ', 'UY', 'U'),
'V': ('V',),
'W': ('W',),
'X': ('X',),
'Y': ('Y',),
'Z': (
'ZHDZH',
'ZDZH',
'ZSCH',
'ZDZ',
'ZHD',
'ZSH',
'ZD',
'ZH',
'ZS',
'Z',
),
}
_vowels = {'A', 'E', 'I', 'J', 'O', 'U', 'Y'}
dms = [''] # initialize empty code list
# Require a max_length of at least 6 and not more than 64
if max_length != -1:
max_length = min(max(6, max_length), 64)
else:
max_length = 64
# uppercase, normalize, decompose, and filter non-A-Z
word = unicode_normalize('NFKD', text_type(word.upper()))
word = word.replace('ß', 'SS')
word = ''.join(
c
for c in word
if c
in {
'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',
}
)
# Nothing to convert, return base case
if not word:
if zero_pad:
return {'0' * max_length}
return {'0'}
pos = 0
while pos < len(word):
# Iterate through _dms_order, which specifies the possible substrings
# for which codes exist in the Daitch-Mokotoff coding
for sstr in _dms_order[word[pos]]: # pragma: no branch
if word[pos:].startswith(sstr):
# Having determined a valid substring start, retrieve the code
dm_val = _dms_table[sstr]
# Having retried the code (triple), determine the correct
# positional variant (first, pre-vocalic, elsewhere)
if pos == 0:
dm_val = dm_val[0]
elif (
pos + len(sstr) < len(word)
and word[pos + len(sstr)] in _vowels
):
dm_val = dm_val[1]
else:
dm_val = dm_val[2]
# Build the code strings
if isinstance(dm_val, tuple):
dms = [_ + text_type(dm_val[0]) for _ in dms] + [
_ + text_type(dm_val[1]) for _ in dms
]
else:
dms = [_ + text_type(dm_val) for _ in dms]
pos += len(sstr)
break
# Filter out double letters and _ placeholders
dms = (
''.join(c for c in _delete_consecutive_repeats(_) if c != '_')
for _ in dms
)
# Trim codes and return set
if zero_pad:
dms = ((_ + ('0' * max_length))[:max_length] for _ in dms)
else:
dms = (_[:max_length] for _ in dms)
return set(dms)
if __name__ == '__main__':
import doctest
doctest.testmod()