Big update to 0.1.0. Improved error handling, syncing, the works...
[hermes] / package / src / trans.py
1 # coding: utf-8
2
3 ur"""
4
5 The **trans** module
6 ====================
7
8 This module translates national characters into similar sounding
9 latin characters (transliteration).
10 At the moment, Greek, Turkish, Russian, Ukrainian, Czech, Polish,
11 Latvian alphabets are supported (it covers 99% of needs).
12
13 .. contents::
14
15 Simple usage
16 ------------
17 It's very easy to use
18 ~~~~~~~~~~~~~~~~~~~~~
19   >>> # coding: utf-8
20   >>> import trans
21   >>> u'Hello World!'.encode('trans')
22   u'Hello World!'
23   >>> u'Привет, Мир!'.encode('trans')
24   u'Privet, Mir!'
25
26
27 Work only with unicode strings
28 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29   >>> 'Hello World!'.encode('trans')
30   Traceback (most recent call last):
31       ...
32   TypeError: trans codec support only unicode string, <type 'str'> given.
33
34 This is readability
35 ~~~~~~~~~~~~~~~~~~~
36   >>> s = u'''\
37   ...    -- Раскудрить твою через коромысло в бога душу мать
38   ...             триста тысяч раз едрену вошь тебе в крыло
39   ...             и кактус в глотку! -- взревел разъяренный Никодим.
40   ...    -- Аминь, -- робко добавил из склепа папа Пий.
41   ...                 (c) Г. Л. Олди, "Сказки дедушки вампира".'''
42   >>> 
43   >>> print s.encode('trans')
44      -- Raskudrit tvoyu cherez koromyslo v boga dushu mat
45               trista tysyach raz edrenu vosh tebe v krylo
46               i kaktus v glotku! -- vzrevel razyarennyy Nikodim.
47      -- Amin, -- robko dobavil iz sklepa papa Piy.
48                   (c) G. L. Oldi, "Skazki dedushki vampira".
49
50 Table "**id**"
51 ~~~~~~~~~~~~~~
52 Use the table "id", leaving only the Latin characters, digits and underscores:
53
54   >>> print u'1 2 3 4 5 \n6 7 8 9 0'.encode('trans')
55   1 2 3 4 5 
56   6 7 8 9 0
57   >>> print u'1 2 3 4 5 \n6 7 8 9 0'.encode('trans/id')
58   1_2_3_4_5__6_7_8_9_0
59   >>> s.encode('trans/id')[-42:-1]
60   u'_c__G__L__Oldi___Skazki_dedushki_vampira_'
61
62
63 Define user tables
64 ------------------
65 Simple variant
66 ~~~~~~~~~~~~~~
67   >>> u'1 2 3 4 5 6 7 8 9 0'.encode('trans/my')
68   Traceback (most recent call last):
69       ...
70   ValueError: Table "my" not found in tables!
71   >>> trans.tables['my'] = {u'1': u'A', u'2': u'B'}; 
72   >>> u'1 2 3 4 5 6 7 8 9 0'.encode('trans/my')
73   u'A_B________________'
74   >>> 
75
76 A little harder
77 ~~~~~~~~~~~~~~~
78 Table can consist of two parts - the map of diphthongs and map of characters.
79 First are processed diphthongs, by simple replacement on the substring.
80 Then according to the map of characters, replacing each character of string
81 by it's mapping. If character is absent in characters map, checked key None,
82 if not, then is used the default character u'_'.
83
84   >>> diphthongs = {u'11': u'AA', u'22': u'BB'}
85   >>> characters = {u'a': u'z', u'b': u'y', u'c': u'x',
86   ...               u'A': u'A', u'B': u'B', None: u'-'}
87   >>> trans.tables['test'] = (diphthongs, characters)
88   >>> u'11abc22cbaCC'.encode('trans/test')
89   u'AAzyxBBxyz--'
90
91 **The characters created by processing of diphthongs are also processed
92 by the map of the symbols:**
93
94   >>> diphthongs = {u'11': u'AA', u'22': u'BB'}
95   >>> characters = {u'a': u'z', u'b': u'y', u'c': u'x', None: u'-'}
96   >>> trans.tables['test'] = (diphthongs, characters)
97   >>> u'11abc22cbaCC'.encode('trans/test')
98   u'--zyx--xyz--'
99
100 Without the diphthongs
101 ~~~~~~~~~~~~~~~~~~~~~~
102 These two tables are equivalent:
103
104   >>> characters = {u'a': u'z', u'b': u'y', u'c': u'x', None: u'-'}
105   >>> trans.tables['t1'] = characters
106   >>> trans.tables['t2'] = ({}, characters)
107   >>> u'11abc22cbaCC'.encode('trans/t1') == u'11abc22cbaCC'.encode('trans/t2')
108   True
109
110
111 Finally
112 -------
113 + *Special thanks to Yuri Yurevich aka j2a for the kick in the right direction.*
114     - http://www.python.su/forum/viewtopic.php?pid=28965
115     - http://code.djangoproject.com/browser/django/trunk/django/contrib/admin/media/js/urlify.js
116 + *I please forgiveness for my bad English. I promise to be corrected.*
117
118 """
119
120 __version__ = '1.1a7'
121 __author__ = 'Zelenyak Aleksandr aka ZZZ <zzz.sochi@gmail.com>'
122
123 latin = {
124     u'à': u'a',  u'á': u'a',  u'â': u'a', u'ã': u'a', u'ä': u'a', u'å': u'a',
125     u'æ': u'ae', u'ç': u'c',  u'è': u'e', u'é': u'e', u'ê': u'e', u'ë': u'e',
126     u'ì': u'i',  u'í': u'i',  u'î': u'i', u'ï': u'i', u'ð': u'd', u'ñ': u'n',
127     u'ò': u'o',  u'ó': u'o',  u'ô': u'o', u'õ': u'o', u'ö': u'o', u'ő': u'o',
128     u'ø': u'o',  u'ù': u'u',  u'ú': u'u', u'û': u'u', u'ü': u'u', u'ű': u'u',
129     u'ý': u'y',  u'þ': u'th', u'ÿ': u'y',
130
131     u'À': u'A',  u'Á': u'A',  u'Â': u'A', u'Ã': u'A', u'Ä': u'A', u'Å': u'A',
132     u'Æ': u'AE', u'Ç': u'C',  u'È': u'E', u'É': u'E', u'Ê': u'E', u'Ë': u'E',
133     u'Ì': u'I',  u'Í': u'I',  u'Î': u'I', u'Ï': u'I', u'Ð': u'D', u'Ñ': u'N',
134     u'Ò': u'O',  u'Ó': u'O',  u'Ô': u'O', u'Õ': u'O', u'Ö': u'O', u'Ő': u'O',
135     u'Ø': u'O',  u'Ù': u'U',  u'Ú': u'U', u'Û': u'U', u'Ü': u'U', u'Ű': u'U',
136     u'Ý': u'Y',  u'Þ': u'TH', u'ß': u'ss'
137 }
138
139 greek = {
140     u'α': u'a', u'β': u'b', u'γ': u'g', u'δ': u'd', u'ε': u'e',  u'ζ': u'z',
141     u'η': u'h', u'θ': u'8', u'ι': u'i', u'κ': u'k', u'λ': u'l',  u'μ': u'm',
142     u'ν': u'n', u'ξ': u'3', u'ο': u'o', u'π': u'p', u'ρ': u'r',  u'σ': u's',
143     u'τ': u't', u'υ': u'y', u'φ': u'f', u'χ': u'x', u'ψ': u'ps', u'ω': u'w',
144     u'ά': u'a', u'έ': u'e', u'ί': u'i', u'ό': u'o', u'ύ': u'y',  u'ή': u'h',
145     u'ώ': u'w', u'ς': u's', u'ϊ': u'i', u'ΰ': u'y', u'ϋ': u'y',  u'ΐ': u'i',
146
147     u'Α': u'A', u'Β': u'B', u'Γ': u'G', u'Δ': u'D', u'Ε': u'E',  u'Ζ': u'Z',
148     u'Η': u'H', u'Θ': u'8', u'Ι': u'I', u'Κ': u'K', u'Λ': u'L',  u'Μ': u'M',
149     u'Ν': u'N', u'Ξ': u'3', u'Ο': u'O', u'Π': u'P', u'Ρ': u'R',  u'Σ': u'S',
150     u'Τ': u'T', u'Υ': u'Y', u'Φ': u'F', u'Χ': u'X', u'Ψ': u'PS', u'Ω': u'W',
151     u'Ά': u'A', u'Έ': u'E', u'Ί': u'I', u'Ό': u'O', u'Ύ': u'Y',  u'Ή': u'H',
152     u'Ώ': u'W', u'Ϊ': u'I', u'Ϋ': u'Y'
153 }
154
155 turkish = {
156     u'ş': u's', u'Ş': u'S', u'ı': u'i', u'İ': u'I', u'ç': u'c', u'Ç': u'C',
157     u'ü': u'u', u'Ü': u'U', u'ö': u'o', u'Ö': u'O', u'ğ': u'g', u'Ğ': u'G'
158 }
159
160 russian = (
161     {u'юй': u'yuy', u'ей': u'yay',
162      u'Юй': u'Yuy', u'Ей': u'Yay'},
163     {
164     u'а': u'a',  u'б': u'b',  u'в': u'v',  u'г': u'g', u'д': u'd', u'е': u'e',
165     u'ё': u'yo', u'ж': u'zh', u'з': u'z',  u'и': u'i', u'й': u'y', u'к': u'k',
166     u'л': u'l',  u'м': u'm',  u'н': u'n',  u'о': u'o', u'п': u'p', u'р': u'r',
167     u'с': u's',  u'т': u't',  u'у': u'u',  u'ф': u'f', u'х': u'h', u'ц': u'c',
168     u'ч': u'ch', u'ш': u'sh', u'щ': u'sh', u'ъ': u'',  u'ы': u'y', u'ь': u'',
169     u'э': u'e',  u'ю': u'yu', u'я': u'ya',
170
171     u'А': u'A',  u'Б': u'B',  u'В': u'V',  u'Г': u'G', u'Д': u'D', u'Е': u'E',
172     u'Ё': u'Yo', u'Ж': u'Zh', u'З': u'Z',  u'И': u'I', u'Й': u'Y', u'К': u'K',
173     u'Л': u'L',  u'М': u'M',  u'Н': u'N',  u'О': u'O', u'П': u'P', u'Р': u'R',
174     u'С': u'S',  u'Т': u'T',  u'У': u'U',  u'Ф': u'F', u'Х': u'H', u'Ц': u'C',
175     u'Ч': u'Ch', u'Ш': u'Sh', u'Щ': u'Sh', u'Ъ': u'',  u'Ы': u'Y', u'Ь': u'',
176     u'Э': u'E',  u'Ю': u'Yu', u'Я': u'Ya'
177 })
178
179 ukrainian = (russian[0].copy(), {
180     u'Є': u'Ye', u'І': u'I', u'Ї': u'Yi', u'Ґ': u'G',
181     u'є': u'ye', u'і': u'i', u'ї': u'yi', u'ґ': u'g'
182 })
183 ukrainian[1].update(russian[1])
184
185 czech = {
186     u'č': u'c', u'ď': u'd', u'ě': u'e', u'ň': u'n', u'ř': u'r', u'š': u's',
187     u'ť': u't', u'ů': u'u', u'ž': u'z',
188     u'Č': u'C', u'Ď': u'D', u'Ě': u'E', u'Ň': u'N', u'Ř': u'R', u'Š': u'S',
189     u'Ť': u'T', u'Ů': u'U', u'Ž': u'Z'
190 }
191
192 polish = {
193     u'ą': u'a', u'ć': u'c', u'ę': u'e', u'ł': u'l', u'ń': u'n', u'ó': u'o',
194     u'ś': u's', u'ź': u'z', u'ż': u'z',
195     u'Ą': u'A', u'Ć': u'C', u'Ę': u'e', u'Ł': u'L', u'Ń': u'N', u'Ó': u'o',
196     u'Ś': u'S', u'Ź': u'Z', u'Ż': u'Z'
197 }
198
199 latvian = {
200     u'ā': u'a', u'č': u'c', u'ē': u'e', u'ģ': u'g', u'ī': u'i', u'ķ': u'k',
201     u'ļ': u'l', u'ņ': u'n', u'š': u's', u'ū': u'u', u'ž': u'z',
202     u'Ā': u'A', u'Č': u'C', u'Ē': u'E', u'Ģ': u'G', u'Ī': u'i', u'Ķ': u'k',
203     u'Ļ': u'L', u'Ņ': u'N', u'Š': u'S', u'Ū': u'u', u'Ž': u'Z'
204 }
205
206
207
208 ascii_str = u'''_0123456789
209 abcdefghijklmnopqrstuvwxyz
210 ABCDEFGHIJKLMNOPQRSTUVWXYZ
211 !"#$%&'()*+,_-./:;<=>?@[\\]^`{|}~ \t\n\r\x0b\x0c'''
212
213 ascii = ({}, dict(zip(ascii_str, ascii_str)))
214 for t in [latin, greek, turkish, russian, ukrainian, czech, polish, latvian]:
215     if isinstance(t, dict):
216         t = ({}, t)
217     ascii[0].update(t[0])
218     ascii[1].update(t[1])
219 ascii[1][None] = u'_'
220 del t
221
222
223 id = (ascii[0].copy(), ascii[1].copy())
224 for c in '''!"#$%&'()*+,_-./:;<=>?@[\\]^`{|}~ \t\n\r\x0b\x0c''':
225     del id[1][c]
226
227
228 def trans(input, table=ascii):
229     '''Translate unicode string, using 'table'.
230        Table may be tuple (diphthongs, other) or dict (other).'''
231     if not isinstance(input, unicode):
232         raise TypeError, 'trans codec support only unicode string, %r given.' \
233                                                         % type(input)
234     if isinstance(table, dict):
235         table = ({}, table)
236
237     first = input
238     for diphthong, value in table[0].items():
239         first = first.replace(diphthong, value)
240
241     second = u''
242     for char in first:
243         second += table[1].get(char, table[1].get(None, u'_'))
244
245     return second, len(second)
246
247 tables = {'ascii': ascii, 'text': ascii, 'id': id}
248
249 import codecs
250
251 def encode(input, errors='strict', table_name='ascii'):
252     try:
253         table = tables[table_name]
254     except KeyError:
255         raise ValueError, 'Table "%s" not found in tables!' % table_name
256     else:
257         return trans(input, table)
258
259 def no_decode(input, errors='strict'):
260     raise TypeError("trans codec does not support decode.")
261
262 def trans_codec(enc):
263     if enc == 'trans':
264         return codecs.CodecInfo(encode, no_decode)
265 #    else:
266     try:
267         enc_name, table_name = enc.split('/', 1)
268     except ValueError:
269         return None
270     if enc_name != 'trans':
271         return None
272     if table_name not in tables:
273         raise ValueError, 'Table "%s" not found in tables!' % table_name
274
275     return codecs.CodecInfo(lambda i, e='strict': encode(i, e, table_name),
276                             no_decode)
277
278 codecs.register(trans_codec)