a few more fixes here and there
[smewt:guessit.git] / guessit / language.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # GuessIt - A library for guessing information from filenames
5 # Copyright (c) 2011 Nicolas Wack <wackou@gmail.com>
6 #
7 # GuessIt is free software; you can redistribute it and/or modify it under
8 # the terms of the Lesser GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # GuessIt is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # Lesser GNU General Public License for more details.
16 #
17 # You should have received a copy of the Lesser GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20
21 from guessit import fileutils
22 import os.path
23 import re
24 import logging
25
26 log = logging.getLogger('guessit.language')
27
28
29 _reverse_language_map = { 'English': [ 'english', 'eng', 'en' ],
30                           'French': [ 'french', 'fr', 'francais', u'français' ],
31                           'Spanish': [ 'spanish', 'es', 'esp', 'espanol', u'español' ], # should we remove 'es'? (very common in spanish)
32                           'Italian': [ 'italian', 'italiano', 'ita' ]  # no 'it', too common a word
33                           }
34
35 _language_map = {}
36 for lang, langs in _reverse_language_map.items():
37     for l in langs:
38         _language_map[l] = lang
39
40
41 # downloaded from http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
42 #
43 # Description of the fields:
44 # "An alpha-3 (bibliographic) code, an alpha-3 (terminologic) code (when given),
45 # an alpha-2 code (when given), an English name, and a French name of a language
46 # are all separated by pipe (|) characters."
47 language_matrix = [ l.strip().decode('utf-8').split('|') for l in open(fileutils.file_in_same_dir(__file__, 'ISO-639-2_utf-8.txt')) ]
48
49 lng3        = frozenset(filter(bool, (l[0] for l in language_matrix)))
50 lng3term    = frozenset(filter(bool, (l[1] for l in language_matrix)))
51 lng2        = frozenset(filter(bool, (l[2] for l in language_matrix)))
52 lng_en_name = frozenset(filter(bool, (lng for l in language_matrix for lng in l[3].lower().split('; '))))
53 lng_fr_name = frozenset(filter(bool, (lng for l in language_matrix for lng in l[4].lower().split('; '))))
54 lng_all_names = lng3 | lng3term | lng2 | lng_en_name | lng_fr_name
55
56 lng3_to_lng3term = dict((l[0], l[1]) for l in language_matrix if l[1])
57 lng3term_to_lng3 = dict((l[1], l[0]) for l in language_matrix if l[1])
58
59 lng3_to_lng2 = dict((l[0], l[2]) for l in language_matrix if l[2])
60 lng2_to_lng3 = dict((l[2], l[0]) for l in language_matrix if l[2])
61
62 # we only return the first given english name, hoping it is the most used one
63 lng3_to_lng_en_name = dict((l[0], l[3].split('; ')[0]) for l in language_matrix if l[3])
64 lng_en_name_to_lng3 = dict((en_name.lower(), l[0]) for l in language_matrix if l[3] for en_name in l[3].split('; '))
65
66 # we only return the first given french name, hoping it is the most used one
67 lng3_to_lng_fr_name = dict((l[0], l[4].split('; ')[0]) for l in language_matrix if l[4])
68 lng_fr_name_to_lng3 = dict((fr_name.lower(), l[0]) for l in language_matrix if l[4] for fr_name in l[4].split('; '))
69
70
71 def is_language(language):
72     return language.lower() in lng_all_names
73
74 class Language(object):
75     def __init__(self, language):
76         lang = None
77         language = language.lower()
78         if len(language) == 2:
79             lang = lng2_to_lng3.get(language)
80         elif len(language) == 3:
81             lang = language if language in lng3 else lng3term_to_lng3.get(language)
82         else:
83             lang = lng_en_name_to_lng3.get(language) or lng_fr_name_to_lng3.get(language)
84
85         if lang is None:
86             raise ValueError, 'The given string "%s" could not be identified as a language' % language
87
88         self.lang = lang
89
90     def __hash__(self):
91         return hash(self.lang)
92
93     def __eq__(self, other):
94         if isinstance(other, Language):
95             return self.lang == other.lang
96
97         if isinstance(other, basestring):
98             try:
99                 return self == Language(other)
100             except ValueError:
101                 return False
102
103         return False
104
105     def __ne__(self, other):
106         return not self == other
107
108     def __unicode__(self):
109         return lng3_to_lng_en_name[self.lang]
110
111     def __str__(self):
112         return unicode(self).encode('utf-8')
113
114     def __repr__(self):
115         return 'Language(%s)' % self
116
117
118
119 def search_language(string):
120     """Looks for language patterns, and if found return the language object,
121     its group span and an associated confidence.
122     Assumes there are sentinels at the beginning and end of the string that
123     always allow matching a non-letter delimiting the language."""
124
125     # list of common words which could be interpreted as languages, but which
126     # are far too common to be able to say they represent a language in the
127     # middle of a string (where they most likely carry their commmon meaning)
128     lng_common_words = frozenset([ # english words
129                                    'is', 'it', 'am', 'mad', 'men', 'run', 'sin', 'st',
130                                    # french words
131                                    'bas', 'de', 'le', 'son', 'vo', 'vf',
132                                    # spanish words
133                                    'la', 'el',
134                                    # other
135                                    'ind',
136                                    ])
137     sep = r'[](){} \._-+'
138
139     slow = string.lower()
140     confidence = 1.0 # for all of them
141     for lang in lng_all_names:
142         if lang in lng_common_words:
143             continue
144
145         pos = slow.find(lang)
146
147         if pos != -1:
148             end = pos + len(lang)
149             # make sure our word is always surrounded by separators
150             if slow[pos-1] not in sep or slow[end] not in sep:
151                 continue
152
153             # confidence depends on lng2, lng3, english name, ...
154             if len(lang) == 2:
155                 confidence = 0.8
156             elif len(lang) == 3:
157                 confidence = 0.9
158             else:
159                 # Note: we could either be really confident that we found a language
160                 #       or assume that full language names are too common words
161                 confidence = 0.3 # going with the low-confidence route here
162
163             return Language(slow[pos:end]), (pos, end), confidence
164
165     return None, None, None