]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simastr.py
Fixed str inheritance (calling __new__/super)
[mpd-sima.git] / sima / lib / simastr.py
1 # -*- coding: utf-8 -*-
2
3 #
4 # Copyright (c) 2009, 2010, 2013 Jack Kaliko <kaliko@azylum.org>
5 #
6 #  This program is free software; you can redistribute it and/or modify
7 #  it under the terms of the GNU General Public License as
8 #  published by the Free Software Foundation; either version 3 of the
9 #  License, or (at your option) any later version.
10 #
11 #  This program is distributed in the hope that it will be useful, but
12 #  WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 #  General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public
17 #  License along with this program.
18 #  If not, see <http://www.gnu.org/licenses/>.
19 #
20
21 """
22 SimaStr
23
24 Special unicode() subclass to perform fuzzy match on specific strings with
25 known noise.
26
27  * SimaStr() object removes specific patterns from the string
28  * Diacritic are removed
29  * Equality test is done on lower-cased string
30  * Equality test is not an exact comparison, the levenshtein edition distance
31    between stripped and filtered strings is used
32
33 >>> from simastr import SimaStr
34 >>> art0 = SimaStr('The Desert Sessions & PJ Harvey')
35 >>> art1 = SimaStr('Desert Sessions And PJ Harvey')
36 >>> art0 == art1
37 >>> True
38 >>> art0 == 'Desert Sessions And PJ Harvey'
39 >>> True
40 >>> # diacritic filter + levenshtein  example
41 >>> art0 = sima.lib.simastr.SimaStr('Hubert Félix Thiéphaine')
42 >>> art1 = sima.lib.simastr.SimaStr('Hubert-Felix Thiephaine')
43 >>> art0 == art1
44 >>> True
45 >>>
46
47 Current stripped word patterns (usually English followed by French and
48 Spanish alternatives)
49     leading (case-insensitive):
50             "the","le","la","les","el","los"
51     middle:
52             "[Aa]nd","&","[Nn]'?","[Ee]t"
53     trailing:
54             combination of "[- !?\.]+" "\(? ?[Ll]ive ?\)?"
55
56
57 Possibility to access to stripped string:
58
59 >>> art0 = SimaStr('The Desert Sessions & PJ Harvey')
60 >>> print (art0, art0.stripped)
61 >>> ('The Desert Sessions & PJ Harvey', 'Desert Sessions PJ Harvey')
62
63 TODO:
64     * Have a look to difflib.SequenceMatcher to find possible improvements
65     * Find a way to allow users patterns.
66 """
67
68 __author__ = 'Jack Kaliko'
69 __version__ = '0.4'
70
71 # IMPORTS
72 import unicodedata
73 from re import (compile, U, I)
74
75 from ..utils.leven import levenshtein_ratio
76
77
78 class SimaStr(str):
79     """
80     Specific string object for artist names and song titles.
81     Here follows some class variables for regex to run on strings.
82     """
83     regexp_dict = dict()
84
85     # Leading patterns: The Le Les
86     # case-insensitive matching for this RE
87     regexp_dict.update({'lead': '(the|l[ae][s]?|los|el)'})
88
89     # Middle patterns: And & Et N
90     regexp_dict.update({'mid': '(And|&|and|[Nn]\'?|et)'})
91
92     # Trailing patterns: ! ? live
93     # TODO: add "concert" key word
94     #       add "Live at <somewhere>"
95     regexp_dict.update({'trail': '([- !?\.]|\(? ?[Ll]ive ?\)?)'})
96
97     reg_lead = compile('^(?P<lead>%(lead)s )(?P<root0>.*)$' % regexp_dict, I | U)
98     reg_midl = compile('^(?P<root0>.*)(?P<mid> %(mid)s )(?P<root1>.*)' % regexp_dict, U)
99     reg_trail = compile('^(?P<root0>.*?)(?P<trail>%(trail)s+$)' % regexp_dict, U)
100
101     def __init__(self, fuzzstr):
102         """
103         """
104         self.orig = str(fuzzstr)
105         self.stripped = str(fuzzstr.strip())
106         # fuzzy computation
107         self._get_root()
108         self.remove_diacritics()
109
110     def __new__(cls, fuzzstr):
111         return super(SimaStr, cls).__new__(cls, fuzzstr)
112
113     def _get_root(self):
114         """
115         Remove all patterns in string.
116         """
117         sea = SimaStr.reg_lead.search(self.stripped)
118         if sea:
119             #print sea.groupdict()
120             self.stripped = sea.group('root0')
121
122         sea = SimaStr.reg_midl.search(self.stripped)
123         if sea:
124             #print sea.groupdict()
125             self.stripped = str().join([sea.group('root0'), ' ',
126                                         sea.group('root1')])
127
128         sea = SimaStr.reg_trail.search(self.stripped)
129         if sea:
130             #print sea.groupdict()
131             self.stripped = sea.group('root0')
132
133     def remove_diacritics(self):
134         self.stripped = ''.join(x for x in
135                                 unicodedata.normalize('NFKD', self.stripped)
136                                 if unicodedata.category(x) != 'Mn')
137
138     def __hash__(self):
139         return hash(self.stripped)
140
141     def __eq__(self, other):
142         if not isinstance(other, SimaStr):
143             other = SimaStr(other)
144         levenr = levenshtein_ratio(self.stripped.lower(),
145                                    other.stripped.lower())
146         if hash(self) == hash(other):
147             return True
148         return levenr >= 0.82
149
150     def __ne__(self, other):
151         if not isinstance(other, SimaStr):
152             return hash(self) != hash(SimaStr(other))
153         return hash(self) != hash(other)
154
155
156 # Script starts here
157 if __name__ == "__main__":
158     import time
159     print(SimaStr('Kétanoue'))
160     #from leven import levenshtein_ratio
161     CASES_LIST = list([
162         dict({
163                     'got': 'Guns N\' Roses (live)!! !',
164                 'look for': 'Guns And Roses'}),
165         dict({
166                      'got': 'Jesus & Mary Chains',
167                 'look for': 'The Jesus and Mary Chains - live'}),
168         dict({
169                          'got': 'Desert sessions',
170                     'look for': 'The Desert Sessions'}),
171         dict({
172                          'got': 'Têtes Raides',
173                     'look for': 'Les Têtes Raides'}),
174         dict({
175                          'got': 'Noir Désir',
176                     'look for': 'Noir Désir'}),
177         dict({
178                          'got': 'No Future',
179                     'look for': 'Future'})])
180
181     for case in CASES_LIST[:]:
182         str0 = case.get('got')
183         str1 = case.get('look for')
184         fz_str0 = SimaStr(str0)
185         fz_str1 = SimaStr(str1)
186         print(fz_str0, '\n', fz_str1)
187         print(fz_str0.stripped == fz_str1.stripped)
188         #print levenshtein_ratio(fz_str0.lower(), fz_str1.lower())
189         time.sleep(1)
190
191 # VIM MODLINE
192 # vim: ai ts=4 sw=4 sts=4 expandtab