Completed
Pull Request — master (#141)
by Chris
13:03
created

abydos.phonetic._dolby   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 33
eloc 107
dl 0
loc 305
ccs 90
cts 90
cp 1
rs 9.76
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
A dolby() 0 62 1

1 Method

Rating   Name   Duplication   Size   Complexity  
F Dolby.encode() 0 193 32
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2018 by Christopher C. Little.
4
# This file is part of Abydos.
5
#
6
# Abydos is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# Abydos is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with Abydos. If not, see <http://www.gnu.org/licenses/>.
18
19 1
"""abydos.phonetic._dolby.
20
21
The phonetic._dolby module implements the Dolby Code algorithm.
22
"""
23
24 1
from __future__ import unicode_literals
25
26 1
from unicodedata import normalize as unicode_normalize
27
28 1
from six import text_type
29
30 1
from ._phonetic import Phonetic
31
32 1
__all__ = ['Dolby', 'dolby']
33
34
35 1
class Dolby(Phonetic):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
36
    """Dolby Code.
37
38
    This follows "A Spelling Equivalent Abbreviation Algorithm For Personal
39
    Names" from :cite:`Dolby:1970` and :cite:`Cunningham:1969`.
40
    """
41
42 1
    def encode(self, word, max_length=-1, keep_vowels=False, vowel_char='*'):
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'encode' method
Loading history...
43
        r"""Return the Dolby Code of a name.
44
45
        Args:
46
            word (str): The word to transform
47
            max_length (int): Maximum length of the returned Dolby code -- this
48
                also activates the fixed-length code mode if it is greater than
49
                0
50
            keep_vowels (bool): If True, retains all vowel markers
51
            vowel_char (str): The vowel marker character (default to \*)
52
53
        Returns:
54
            str: The Dolby Code
55
56
        Examples:
57
            >>> pe = Dolby()
58
            >>> pe.encode('Hansen')
59
            'H*NSN'
60
            >>> pe.encode('Larsen')
61
            'L*RSN'
62
            >>> pe.encode('Aagaard')
63
            '*GR'
64
            >>> pe.encode('Braaten')
65
            'BR*DN'
66
            >>> pe.encode('Sandvik')
67
            'S*NVK'
68
            >>> pe.encode('Hansen', max_length=6)
69
            'H*NS*N'
70
            >>> pe.encode('Larsen', max_length=6)
71
            'L*RS*N'
72
            >>> pe.encode('Aagaard', max_length=6)
73
            '*G*R  '
74
            >>> pe.encode('Braaten', max_length=6)
75
            'BR*D*N'
76
            >>> pe.encode('Sandvik', max_length=6)
77
            'S*NF*K'
78
79
            >>> pe.encode('Smith')
80
            'SM*D'
81
            >>> pe.encode('Waters')
82
            'W*DRS'
83
            >>> pe.encode('James')
84
            'J*MS'
85
            >>> pe.encode('Schmidt')
86
            'SM*D'
87
            >>> pe.encode('Ashcroft')
88
            '*SKRFD'
89
            >>> pe.encode('Smith', max_length=6)
90
            'SM*D  '
91
            >>> pe.encode('Waters', max_length=6)
92
            'W*D*RS'
93
            >>> pe.encode('James', max_length=6)
94
            'J*M*S '
95
            >>> pe.encode('Schmidt', max_length=6)
96
            'SM*D  '
97
            >>> pe.encode('Ashcroft', max_length=6)
98
            '*SKRFD'
99
100
        """
101
        # uppercase, normalize, decompose, and filter non-A-Z out
102 1
        word = unicode_normalize('NFKD', text_type(word.upper()))
103 1
        word = word.replace('ß', 'SS')
104 1
        word = ''.join(c for c in word if c in self._uc_set)
105
106
        # Rule 1 (FL2)
107 1
        if word[:3] in {'MCG', 'MAG', 'MAC'}:
108 1
            word = 'MK' + word[3:]
109 1
        elif word[:2] == 'MC':
110 1
            word = 'MK' + word[2:]
111
112
        # Rule 2 (FL3)
113 1
        pos = len(word) - 2
114 1
        while pos > -1:
115 1
            if word[pos : pos + 2] in {
116
                'DT',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
117
                'LD',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
118
                'ND',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
119
                'NT',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
120
                'RC',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
121
                'RD',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
122
                'RT',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
123
                'SC',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
124
                'SK',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
125
                'ST',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
126
            }:
127 1
                word = word[: pos + 1] + word[pos + 2 :]
128 1
                pos += 1
129 1
            pos -= 1
130
131
        # Rule 3 (FL4)
132
        # Although the rule indicates "after the first letter", the test cases
133
        # make it clear that these apply to the first letter also.
134 1
        word = word.replace('X', 'KS')
135 1
        word = word.replace('CE', 'SE')
136 1
        word = word.replace('CI', 'SI')
137 1
        word = word.replace('CY', 'SI')
138
139
        # not in the rule set, but they seem to have intended it
140 1
        word = word.replace('TCH', 'CH')
141
142 1
        pos = word.find('CH', 1)
143 1
        while pos != -1:
144 1
            if word[pos - 1 : pos] not in self._uc_vy_set:
145 1
                word = word[:pos] + 'S' + word[pos + 1 :]
146 1
            pos = word.find('CH', pos + 1)
147
148 1
        word = word.replace('C', 'K')
149 1
        word = word.replace('Z', 'S')
150
151 1
        word = word.replace('WR', 'R')
152 1
        word = word.replace('DG', 'G')
153 1
        word = word.replace('QU', 'K')
154 1
        word = word.replace('T', 'D')
155 1
        word = word.replace('PH', 'F')
156
157
        # Rule 4 (FL5)
158
        # Although the rule indicates "after the first letter", the test cases
159
        # make it clear that these apply to the first letter also.
160 1
        pos = word.find('K', 0)
161 1
        while pos != -1:
162 1
            if pos > 1 and word[pos - 1 : pos] not in self._uc_vy_set | {
163
                'L',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
164
                'N',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
165
                'R',
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
166
            }:
167 1
                word = word[: pos - 1] + word[pos:]
168 1
                pos -= 1
169 1
            pos = word.find('K', pos + 1)
170
171
        # Rule FL6
172 1
        if max_length > 0 and word[-1:] == 'E':
173 1
            word = word[:-1]
174
175
        # Rule 5 (FL7)
176 1
        word = self._delete_consecutive_repeats(word)
177
178
        # Rule 6 (FL8)
179 1
        if word[:2] == 'PF':
180 1
            word = word[1:]
181 1
        if word[-2:] == 'PF':
182 1
            word = word[:-1]
183 1
        elif word[-2:] == 'GH':
184 1
            if word[-3:-2] in self._uc_vy_set:
185 1
                word = word[:-2] + 'F'
186
            else:
187 1
                word = word[:-2] + 'G'
188 1
        word = word.replace('GH', '')
189
190
        # Rule FL9
191 1
        if max_length > 0:
192 1
            word = word.replace('V', 'F')
193
194
        # Rules 7-9 (FL10-FL12)
195 1
        first = 1 + (1 if max_length > 0 else 0)
196 1
        code = ''
197 1
        for pos, char in enumerate(word):
198 1
            if char in self._uc_vy_set:
199 1
                if first or keep_vowels:
200 1
                    code += vowel_char
201 1
                    first -= 1
202 1
            elif pos > 0 and char in {'W', 'H'}:
203 1
                continue
204
            else:
205 1
                code += char
206
207 1
        if max_length > 0:
0 ignored issues
show
unused-code introduced by
Too many nested blocks (6/5)
Loading history...
208
            # Rule FL13
209 1
            if len(code) > max_length and code[-1:] == 'S':
210 1
                code = code[:-1]
211 1
            if keep_vowels:
212 1
                code = code[:max_length]
213
            else:
214
                # Rule FL14
215 1
                code = code[: max_length + 2]
216
                # Rule FL15
217 1
                while len(code) > max_length:
218 1
                    vowels = len(code) - max_length
219 1
                    excess = vowels - 1
220 1
                    word = code
221 1
                    code = ''
222 1
                    for char in word:
223 1
                        if char == vowel_char:
224 1
                            if vowels:
225 1
                                code += char
226 1
                                vowels -= 1
227
                        else:
228 1
                            code += char
229 1
                    code = code[: max_length + excess]
230
231
            # Rule FL16
232 1
            code += ' ' * (max_length - len(code))
233
234 1
        return code
235
236
237 1
def dolby(word, max_length=-1, keep_vowels=False, vowel_char='*'):
238
    r"""Return the Dolby Code of a name.
239
240
    This follows "A Spelling Equivalent Abbreviation Algorithm For Personal
241
    Names" from :cite:`Dolby:1970` and :cite:`Cunningham:1969`.
242
243
    Args:
244
        word (str): The word to transform
245
        max_length (int): Maximum length of the returned Dolby code -- this
246
            also activates the fixed-length code mode if it is greater than
247
            0
248
        keep_vowels (bool): If True, retains all vowel markers
249
        vowel_char (str): The vowel marker character (default to \*)
250
251
    Returns:
252
        str: The Dolby Code
253
254
    Examples:
255
        >>> dolby('Hansen')
256
        'H*NSN'
257
        >>> dolby('Larsen')
258
        'L*RSN'
259
        >>> dolby('Aagaard')
260
        '*GR'
261
        >>> dolby('Braaten')
262
        'BR*DN'
263
        >>> dolby('Sandvik')
264
        'S*NVK'
265
        >>> dolby('Hansen', max_length=6)
266
        'H*NS*N'
267
        >>> dolby('Larsen', max_length=6)
268
        'L*RS*N'
269
        >>> dolby('Aagaard', max_length=6)
270
        '*G*R  '
271
        >>> dolby('Braaten', max_length=6)
272
        'BR*D*N'
273
        >>> dolby('Sandvik', max_length=6)
274
        'S*NF*K'
275
276
        >>> dolby('Smith')
277
        'SM*D'
278
        >>> dolby('Waters')
279
        'W*DRS'
280
        >>> dolby('James')
281
        'J*MS'
282
        >>> dolby('Schmidt')
283
        'SM*D'
284
        >>> dolby('Ashcroft')
285
        '*SKRFD'
286
        >>> dolby('Smith', max_length=6)
287
        'SM*D  '
288
        >>> dolby('Waters', max_length=6)
289
        'W*D*RS'
290
        >>> dolby('James', max_length=6)
291
        'J*M*S '
292
        >>> dolby('Schmidt', max_length=6)
293
        'SM*D  '
294
        >>> dolby('Ashcroft', max_length=6)
295
        '*SKRFD'
296
297
    """
298 1
    return Dolby().encode(word, max_length, keep_vowels, vowel_char)
299
300
301
if __name__ == '__main__':
302
    import doctest
303
304
    doctest.testmod()
305