es-normalizer-py 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- es_normalizer/__init__.py +243 -0
- es_normalizer/date_of_birth.py +128 -0
- es_normalizer/dictionary/__init__.py +48 -0
- es_normalizer/dictionary/address.py +110 -0
- es_normalizer/dictionary/firstname.py +229 -0
- es_normalizer/dictionary/title.py +41 -0
- es_normalizer_py-0.2.4.dist-info/METADATA +112 -0
- es_normalizer_py-0.2.4.dist-info/RECORD +11 -0
- es_normalizer_py-0.2.4.dist-info/WHEEL +5 -0
- es_normalizer_py-0.2.4.dist-info/licenses/LICENSE +21 -0
- es_normalizer_py-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contact data normalization adapted from the Empreinte Sociométrique's normalizers, the Python port of es-normalizer
|
|
3
|
+
|
|
4
|
+
Every normalizer takes a string and returns the normalized string, or `None` when nothing usable is left.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import unicodedata
|
|
9
|
+
from typing import Callable, Optional
|
|
10
|
+
|
|
11
|
+
from .dictionary import Dictionary, address_dictionary, first_name_dictionary, get_set, title_dictionary
|
|
12
|
+
|
|
13
|
+
Normalizer = Callable[[str], Optional[str]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize(data: str, normalizer: Normalizer) -> Optional[str]:
|
|
17
|
+
return normalizer(data)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def uniformize(data: str) -> Optional[str]:
|
|
21
|
+
"""
|
|
22
|
+
Upper-cases the passed string without its accents, any run of other characters than letters, digits and dashes becoming a single space
|
|
23
|
+
"""
|
|
24
|
+
normalized = "".join(c for c in unicodedata.normalize("NFD", data.strip().lower()) if not "̀" <= c <= "ͯ")
|
|
25
|
+
if normalized == "":
|
|
26
|
+
return None
|
|
27
|
+
uniformized = normalized.replace("ß", "ss").replace("ø", "o")
|
|
28
|
+
uniformized = re.sub(r"[^a-zA-Z0-9-]+", "-", uniformized)
|
|
29
|
+
uniformized = re.sub(r"-+", "-", uniformized).replace("-", " ").strip().upper()
|
|
30
|
+
return uniformized or None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def Any(data: str) -> Optional[str]: # noqa: N802
|
|
34
|
+
"""
|
|
35
|
+
Normalizer to use for any kind of data when no specific normalizer exists
|
|
36
|
+
"""
|
|
37
|
+
return uniformize(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def AddressLine(transform_title: bool = False) -> Normalizer: # noqa: N802
|
|
41
|
+
"""
|
|
42
|
+
Normalizer of an address line, eg. `8, rue Henner` becomes `8 RUE HENNER`; with `transform_title`, a French address
|
|
43
|
+
line 1 gets its title coded, eg. `Monsieur Cyril Dever` becomes `1 CYRIL DEVER`
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def normalizer(data: str) -> Optional[str]:
|
|
47
|
+
uniformized = uniformize(data)
|
|
48
|
+
if uniformized is None:
|
|
49
|
+
return None
|
|
50
|
+
return (title_dictionary if transform_title else address_dictionary).translate_text(uniformized)
|
|
51
|
+
|
|
52
|
+
return normalizer
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_AL6 = re.compile(r"^((0[1-9]|[1-8][0-9]|9[0-5]|2[AB]|97[1-8]|98[46-9])([0-9]{3}))(.*)$")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def AddressLine6(use_department_france: bool = True) -> Normalizer: # noqa: N802
|
|
59
|
+
"""
|
|
60
|
+
Normalizer of the French address line 6, the département inserted between the postal code and the town, eg. `75009 75 PARIS`
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def normalizer(data: str) -> Optional[str]:
|
|
64
|
+
line = AddressLine(False)(data)
|
|
65
|
+
if line is None:
|
|
66
|
+
return None
|
|
67
|
+
matches = _AL6.match(line)
|
|
68
|
+
if not matches or not matches.group(1):
|
|
69
|
+
return None
|
|
70
|
+
cp, dpt, city = matches.group(1), matches.group(2), matches.group(4)
|
|
71
|
+
if city.strip().startswith(dpt):
|
|
72
|
+
parts = [cp, city.strip()] if use_department_france else [cp, city.replace(dpt, "", 1).strip()]
|
|
73
|
+
else:
|
|
74
|
+
parts = [cp, dpt, city.strip()] if use_department_france else [cp, city.strip()]
|
|
75
|
+
return " ".join(parts).strip()
|
|
76
|
+
|
|
77
|
+
return normalizer
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def City(data: str) -> Optional[str]: # noqa: N802
|
|
81
|
+
"""
|
|
82
|
+
Normalizer of a town's name, its CEDEX mention removed
|
|
83
|
+
"""
|
|
84
|
+
uniformized = uniformize(data)
|
|
85
|
+
if uniformized is None:
|
|
86
|
+
return None
|
|
87
|
+
found = re.sub(r"CDX(\s+[0-9]+)*", "", address_dictionary.translate_text(uniformized), count=1)
|
|
88
|
+
return re.sub(r"\s+", " ", found).strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_CP = re.compile(r"^((0[1-9][0-9]|[1-8][0-9]{2})|(9[0-5][0-9])|(2[AB][0-9])|(97[1-6]))[0-9]{2}$")
|
|
92
|
+
_CORSE = re.compile(r"^2[AB][0-9]{3}$")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def CodePostalFrance(data: str) -> Optional[str]: # noqa: N802
|
|
96
|
+
"""
|
|
97
|
+
Normalizer of a French postal code, a Corsican one written with its département (`2A165`) becoming `20165`
|
|
98
|
+
"""
|
|
99
|
+
uniformized = uniformize(data)
|
|
100
|
+
if uniformized is None:
|
|
101
|
+
return None
|
|
102
|
+
words = re.split(r"\s", uniformized)
|
|
103
|
+
candidate = words[0] if len(words) > 1 else uniformized
|
|
104
|
+
if not _CP.search(candidate):
|
|
105
|
+
return None
|
|
106
|
+
return re.sub(r"[AB]", "0", candidate, count=1) if _CORSE.search(candidate) else candidate
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_DPT = re.compile(r"^(0[1-9]|[1-8][0-9]|9[0-5]|2[AB]|97[1-8]|98[46-9])([0-9]*)$")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def DepartementFrance(data: str) -> Optional[str]: # noqa: N802
|
|
113
|
+
"""
|
|
114
|
+
Normalizer extracting the French département out of a postal code, eg. `2A` out of `20167`
|
|
115
|
+
"""
|
|
116
|
+
uniformized = uniformize(data)
|
|
117
|
+
if not uniformized:
|
|
118
|
+
return None
|
|
119
|
+
matches = _DPT.match(uniformized)
|
|
120
|
+
if not matches or not matches.group(1):
|
|
121
|
+
return None
|
|
122
|
+
dpt = matches.group(1)
|
|
123
|
+
if dpt != "20":
|
|
124
|
+
return dpt
|
|
125
|
+
cp = CodePostalFrance(data)
|
|
126
|
+
if cp is None:
|
|
127
|
+
return None
|
|
128
|
+
return "2A" if 19999 < int(cp) < 20200 else "2B"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
_EMAIL = re.compile(
|
|
132
|
+
r"^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f]))"
|
|
133
|
+
r"@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:[a-z]{2,}[a-z0-9-]*|([a-z][a-z0-9-]+))|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.?){4})$"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def Email(data: str) -> Optional[str]: # noqa: N802
|
|
138
|
+
"""
|
|
139
|
+
Normalizer validating an e-mail address and lower-casing it
|
|
140
|
+
"""
|
|
141
|
+
processed = data.strip().lower()
|
|
142
|
+
if len(processed) > 255 or not _EMAIL.search(processed):
|
|
143
|
+
return None
|
|
144
|
+
return processed
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def FirstName(data: str) -> Optional[str]: # noqa: N802
|
|
148
|
+
"""
|
|
149
|
+
Normalizer of a first name, through a French dictionary of first names, eg. `J Louis` becomes `JEAN LOUIS`
|
|
150
|
+
"""
|
|
151
|
+
uniformized = uniformize(data)
|
|
152
|
+
return None if uniformized is None else first_name_dictionary.translate_word(uniformized)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_MOBILE = re.compile(r"^(((00)?(33))|0)?([0]?)([0-9])([0-9]{2})([0-9]{3})([0-9]{3})$")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def Mobile(data: str) -> Optional[str]: # noqa: N802
|
|
159
|
+
"""
|
|
160
|
+
Normalizer of a French mobile phone number in the international format, eg. `+33 (0) 623 456 789`
|
|
161
|
+
"""
|
|
162
|
+
uniformized = uniformize(data)
|
|
163
|
+
if uniformized is None:
|
|
164
|
+
return None
|
|
165
|
+
m = _MOBILE.match(re.sub(r"\s", "", uniformized))
|
|
166
|
+
if not m:
|
|
167
|
+
return None
|
|
168
|
+
if not (m.group(7) and m.group(8) and m.group(9)) or (m.group(7) + m.group(8) + m.group(9)).endswith("00000000"):
|
|
169
|
+
return None
|
|
170
|
+
if not ((m.group(6) and m.group(6) == "6") or m.group(6) == "7"):
|
|
171
|
+
return None
|
|
172
|
+
international = f"+{m.group(4)}" if m.group(4) else "+33"
|
|
173
|
+
prefix = f"({m.group(5)})" if m.group(5) else "(0)"
|
|
174
|
+
return " ".join([international, prefix, m.group(6) + m.group(7), m.group(8), m.group(9)])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_PHONE = re.compile(r"^(((00)?(33)|0?(262)|0?(590)|0?(594)|0?(596))([^1-9]*)|0)?([^0-9]*)([0-9]{3})([0-9]{3})([0-9]{3})$")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def PhoneNumber(data: str) -> Optional[str]: # noqa: N802
|
|
181
|
+
"""
|
|
182
|
+
Normalizer of a French phone or fax number in the international format, eg. `+33 (0) 123 456 789`
|
|
183
|
+
"""
|
|
184
|
+
uniformized = uniformize(data)
|
|
185
|
+
if uniformized is None:
|
|
186
|
+
return None
|
|
187
|
+
m = _PHONE.match(re.sub(r"\s", "", uniformized))
|
|
188
|
+
if not m:
|
|
189
|
+
return None
|
|
190
|
+
if not (m.group(11) and m.group(12) and m.group(13)) or (m.group(11) + m.group(12) + m.group(13)).endswith("00000000"):
|
|
191
|
+
return None
|
|
192
|
+
international = f"+{m.group(4)}" if m.group(4) else "+33"
|
|
193
|
+
prefix = f"({m.group(9)})" if m.group(9) else "(0)"
|
|
194
|
+
return " ".join([international, prefix, m.group(11), m.group(12), m.group(13)])
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_STREET_NUMBER = re.compile(r"^([0-9]*)\s*(.*)$")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def StreetNumber(data: str) -> Optional[str]: # noqa: N802
|
|
201
|
+
"""
|
|
202
|
+
Normalizer of a street number, eg. `1bis` or `1 bis` becomes `1B`
|
|
203
|
+
"""
|
|
204
|
+
m = _STREET_NUMBER.match(data.strip())
|
|
205
|
+
if not m:
|
|
206
|
+
return None
|
|
207
|
+
if not m.group(2):
|
|
208
|
+
return m.group(1)
|
|
209
|
+
uniformized = uniformize(m.group(2))
|
|
210
|
+
if uniformized is None:
|
|
211
|
+
return None
|
|
212
|
+
return m.group(1) + address_dictionary.translate_text(uniformized).split(" ")[0]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_CODE_F, _CODE_M, _CODE_U = "2", "1", "0"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def Title(data: str) -> Optional[str]: # noqa: N802
|
|
219
|
+
"""
|
|
220
|
+
Normalizer of a title into a code: 1 for gentlemen, 2 for ladies, 0 when undefined or unknown
|
|
221
|
+
"""
|
|
222
|
+
translated = uniformize(data)
|
|
223
|
+
if translated is None:
|
|
224
|
+
return None
|
|
225
|
+
if translated in ("H", "M"):
|
|
226
|
+
return _CODE_M
|
|
227
|
+
if translated == "F":
|
|
228
|
+
return _CODE_F
|
|
229
|
+
if translated == "U":
|
|
230
|
+
return _CODE_U
|
|
231
|
+
found = re.findall(r"[0-9]+", title_dictionary.translate_text(translated))
|
|
232
|
+
if not found:
|
|
233
|
+
return None
|
|
234
|
+
return found[0] if len(found) == 1 else _CODE_U
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
from .date_of_birth import FRENCH_DATE, ISO_DATE, DateOfBirth, Milliseconds, Timestamp # noqa: E402
|
|
238
|
+
|
|
239
|
+
__all__ = [
|
|
240
|
+
"Dictionary", "get_set", "normalize", "uniformize", "Any", "AddressLine", "AddressLine6", "City", "CodePostalFrance",
|
|
241
|
+
"DateOfBirth", "DepartementFrance", "Email", "FirstName", "Mobile", "PhoneNumber", "StreetNumber", "Title",
|
|
242
|
+
"FRENCH_DATE", "ISO_DATE", "Timestamp", "Milliseconds",
|
|
243
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The date of birth normalizer, which reads and writes dates the way moment.js does in its forgiving mode, as es-normalizer relies on it
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import calendar
|
|
6
|
+
import re
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
FRENCH_DATE = "DD/MM/YYYY"
|
|
11
|
+
ISO_DATE = "YYYYMMDD"
|
|
12
|
+
Timestamp = "timestamp"
|
|
13
|
+
Milliseconds = "timestamp_millis"
|
|
14
|
+
|
|
15
|
+
_SEPARATORS = re.compile(r"[-:/\s]+")
|
|
16
|
+
_TOKENS = re.compile(r"YYYY|YY|MMMM|MMM|MM|M|DD|D|HH|H|SS|S")
|
|
17
|
+
_MONTHS = [calendar.month_name[i] for i in range(1, 13)]
|
|
18
|
+
_SHORT_MONTHS = [calendar.month_abbr[i] for i in range(1, 13)]
|
|
19
|
+
_MONTH_NAME = re.compile("|".join(_MONTHS + _SHORT_MONTHS), re.IGNORECASE)
|
|
20
|
+
# What each token matches, searched anywhere in what is left of the input
|
|
21
|
+
_PARSE = {
|
|
22
|
+
"YYYY": re.compile(r"\d{1,4}"),
|
|
23
|
+
"YY": re.compile(r"\d\d?"),
|
|
24
|
+
"MMMM": _MONTH_NAME,
|
|
25
|
+
"MMM": _MONTH_NAME,
|
|
26
|
+
"MM": re.compile(r"\d\d?"),
|
|
27
|
+
"M": re.compile(r"\d\d?"),
|
|
28
|
+
"DD": re.compile(r"\d\d?"),
|
|
29
|
+
"D": re.compile(r"\d\d?"),
|
|
30
|
+
"HH": re.compile(r"\d\d?"),
|
|
31
|
+
"H": re.compile(r"\d\d?"),
|
|
32
|
+
"SS": re.compile(r"\d+"),
|
|
33
|
+
"S": re.compile(r"\d+"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _month_of(name: str) -> int:
|
|
38
|
+
name = name.lower()
|
|
39
|
+
for i, (full, short) in enumerate(zip(_MONTHS, _SHORT_MONTHS), start=1):
|
|
40
|
+
if name == full.lower() or name == short.lower():
|
|
41
|
+
return i
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _two_digit_year(value: str) -> int:
|
|
46
|
+
return int(value) + (1900 if int(value) > 68 else 2000)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse(data: str, fmt: str) -> Optional[date]:
|
|
50
|
+
year, month, day = date.today().year, 1, 1
|
|
51
|
+
rest = data
|
|
52
|
+
parsed = False
|
|
53
|
+
for token in _TOKENS.findall(fmt):
|
|
54
|
+
found = _PARSE[token].search(rest)
|
|
55
|
+
if not found:
|
|
56
|
+
continue
|
|
57
|
+
rest = rest[found.end():]
|
|
58
|
+
parsed = True
|
|
59
|
+
value = found.group(0)
|
|
60
|
+
if token == "YYYY":
|
|
61
|
+
# As moment.js: a year of two digits is read as one of `YY`
|
|
62
|
+
year = _two_digit_year(value) if len(value) == 2 else int(value)
|
|
63
|
+
elif token == "YY":
|
|
64
|
+
year = _two_digit_year(value)
|
|
65
|
+
elif token in ("MMMM", "MMM"):
|
|
66
|
+
month = _month_of(value)
|
|
67
|
+
elif token in ("MM", "M"):
|
|
68
|
+
month = int(value)
|
|
69
|
+
elif token in ("DD", "D"):
|
|
70
|
+
day = int(value)
|
|
71
|
+
if not parsed:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
return date(year, month, day)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _format(d: date, fmt: str) -> str:
|
|
80
|
+
def token(m: re.Match) -> str:
|
|
81
|
+
t = m.group(0)
|
|
82
|
+
return {
|
|
83
|
+
"YYYY": f"{d.year:04d}",
|
|
84
|
+
"YY": f"{d.year % 100:02d}",
|
|
85
|
+
"MMMM": _MONTHS[d.month - 1],
|
|
86
|
+
"MMM": _SHORT_MONTHS[d.month - 1],
|
|
87
|
+
"MM": f"{d.month:02d}",
|
|
88
|
+
"M": str(d.month),
|
|
89
|
+
"DD": f"{d.day:02d}",
|
|
90
|
+
"D": str(d.day),
|
|
91
|
+
"HH": "00",
|
|
92
|
+
"H": "0",
|
|
93
|
+
"SS": "00",
|
|
94
|
+
"S": "0",
|
|
95
|
+
}[t]
|
|
96
|
+
|
|
97
|
+
return _TOKENS.sub(token, fmt)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _leading_int(data: str) -> Optional[int]:
|
|
101
|
+
# As JavaScript's `parseInt`: the leading digits, sign included
|
|
102
|
+
found = re.match(r"\s*([+-]?\d+)", data)
|
|
103
|
+
return int(found.group(1)) if found else None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def DateOfBirth(input_format: str = None, output_format: str = None) -> Callable[[str], Optional[str]]: # noqa: N802
|
|
107
|
+
"""
|
|
108
|
+
Normalizer of a date of birth, read in the input format (ISO by default) and written in the output one (French by
|
|
109
|
+
default), eg. `24/04/2010` read as `DD/MM/YYYY` and written as `ISO_DATE` becomes `20100424`
|
|
110
|
+
"""
|
|
111
|
+
inp = ISO_DATE if not input_format else input_format
|
|
112
|
+
out = FRENCH_DATE if not output_format else output_format
|
|
113
|
+
|
|
114
|
+
def normalizer(data: str) -> Optional[str]:
|
|
115
|
+
if inp.lower() in (Timestamp, Milliseconds):
|
|
116
|
+
value = _leading_int(data)
|
|
117
|
+
if value is None:
|
|
118
|
+
return None
|
|
119
|
+
d = datetime.fromtimestamp(value if inp.lower() == Timestamp else value / 1000).date()
|
|
120
|
+
else:
|
|
121
|
+
d = _parse(data, _SEPARATORS.sub("-", inp).upper())
|
|
122
|
+
if d is None:
|
|
123
|
+
return None
|
|
124
|
+
written = _format(d, _SEPARATORS.sub("-", out).upper())
|
|
125
|
+
separator = _SEPARATORS.search(out)
|
|
126
|
+
return written if separator is None else written.replace("-", separator.group(0))
|
|
127
|
+
|
|
128
|
+
return normalizer
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from .address import ADDRESS
|
|
2
|
+
from .firstname import FIRST_NAME
|
|
3
|
+
from .title import TITLE
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Dictionary:
|
|
7
|
+
"""
|
|
8
|
+
Translates words through a set of `word -> translation` pairs, an empty translation dropping the word; a key of several words translates them together, the longest first
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, data_set: dict):
|
|
12
|
+
self.set = data_set
|
|
13
|
+
self.phrases = sorted((tuple(key.split(" ")) for key in data_set if " " in key), key=len, reverse=True)
|
|
14
|
+
|
|
15
|
+
def translate_text(self, text: str) -> str:
|
|
16
|
+
words = text.split(" ")
|
|
17
|
+
translated = []
|
|
18
|
+
i = 0
|
|
19
|
+
while i < len(words):
|
|
20
|
+
phrase = next((p for p in self.phrases if tuple(words[i : i + len(p)]) == p), None)
|
|
21
|
+
if phrase is not None and self.set[" ".join(phrase)] is not None:
|
|
22
|
+
translated.append(self.set[" ".join(phrase)])
|
|
23
|
+
i += len(phrase)
|
|
24
|
+
continue
|
|
25
|
+
translated.append(self.translate_word(words[i]))
|
|
26
|
+
i += 1
|
|
27
|
+
return " ".join(word for word in translated if word != "")
|
|
28
|
+
|
|
29
|
+
def translate_word(self, word: str) -> str:
|
|
30
|
+
translation = self.set.get(word)
|
|
31
|
+
return word if translation is None else translation
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_set(data: str) -> dict:
|
|
35
|
+
"""
|
|
36
|
+
Reads a dictionary written one `word<TAB>translation` pair per line; a line without a tab translates nothing
|
|
37
|
+
"""
|
|
38
|
+
data_set = {}
|
|
39
|
+
for line in data.split("\n"):
|
|
40
|
+
# As JavaScript's `split('\t', 2)`: anything after a second tab is dropped
|
|
41
|
+
parts = line.split("\t")[:2]
|
|
42
|
+
data_set[parts[0]] = parts[1] if len(parts) > 1 else None
|
|
43
|
+
return data_set
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
address_dictionary = Dictionary(get_set(ADDRESS))
|
|
47
|
+
first_name_dictionary = Dictionary(get_set(FIRST_NAME))
|
|
48
|
+
title_dictionary = Dictionary(get_set(TITLE))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
ADDRESS = """ALLEE ALL
|
|
2
|
+
ALLEES ALL
|
|
3
|
+
AVENUE AV
|
|
4
|
+
AVENU AV
|
|
5
|
+
AVE AV
|
|
6
|
+
AVN AV
|
|
7
|
+
BOUL BD
|
|
8
|
+
BOULEV BD
|
|
9
|
+
BOULEVARD BD
|
|
10
|
+
BOULEVAR BD
|
|
11
|
+
BLVD BD
|
|
12
|
+
BVD BD
|
|
13
|
+
BLD BD
|
|
14
|
+
BRD BD
|
|
15
|
+
CHE CH
|
|
16
|
+
CHEMIN CH
|
|
17
|
+
CHM CH
|
|
18
|
+
COUR CRS
|
|
19
|
+
COURS CRS
|
|
20
|
+
DOMAINE DOM
|
|
21
|
+
DOMAINES DOM
|
|
22
|
+
ESCALIER ESC
|
|
23
|
+
ESPLANADE ESP
|
|
24
|
+
IMPAS IMP
|
|
25
|
+
IMPASSE IMP
|
|
26
|
+
IMPASSES IMP
|
|
27
|
+
LOTISSEMENT LOT
|
|
28
|
+
LOTISSEMENTS LOT
|
|
29
|
+
PASSAGE PASS
|
|
30
|
+
PCE PL
|
|
31
|
+
PLACE PL
|
|
32
|
+
PLC PL
|
|
33
|
+
PORTE PTE
|
|
34
|
+
PRTE PTE
|
|
35
|
+
QUAI QU
|
|
36
|
+
QUAIS QU
|
|
37
|
+
RESIDENCE RES
|
|
38
|
+
RESIDENCES RES
|
|
39
|
+
ROND POINT RPT
|
|
40
|
+
RONDPOINT RPT
|
|
41
|
+
RDPT RPT
|
|
42
|
+
ROUTE RTE
|
|
43
|
+
ROUTES RTE
|
|
44
|
+
R RUE
|
|
45
|
+
RU RUE
|
|
46
|
+
RUELLE RLE
|
|
47
|
+
RUELLES RLE
|
|
48
|
+
RUES RUE
|
|
49
|
+
SENT SENTE
|
|
50
|
+
SENTE SENTE
|
|
51
|
+
SQUA SQ
|
|
52
|
+
SQUARE SQ
|
|
53
|
+
VILLA VLA
|
|
54
|
+
VILLAS VLA
|
|
55
|
+
VOIE VOI
|
|
56
|
+
VOIES VOI
|
|
57
|
+
BIS B
|
|
58
|
+
BI B
|
|
59
|
+
BS B
|
|
60
|
+
TER T
|
|
61
|
+
TR T
|
|
62
|
+
QUATER Q
|
|
63
|
+
QUAT Q
|
|
64
|
+
QUINQUIES C
|
|
65
|
+
QUI C
|
|
66
|
+
SEXIES S
|
|
67
|
+
SEX S
|
|
68
|
+
SEPTIES Z
|
|
69
|
+
SEP Z
|
|
70
|
+
OCTIES O
|
|
71
|
+
OCT O
|
|
72
|
+
NOVIES N
|
|
73
|
+
NOV N
|
|
74
|
+
DECIES D
|
|
75
|
+
DEC D
|
|
76
|
+
DU
|
|
77
|
+
DE
|
|
78
|
+
DES
|
|
79
|
+
D
|
|
80
|
+
LE
|
|
81
|
+
LA
|
|
82
|
+
LES
|
|
83
|
+
L
|
|
84
|
+
ET
|
|
85
|
+
GENERAL GAL
|
|
86
|
+
GNRL GAL
|
|
87
|
+
GL GAL
|
|
88
|
+
PRESIDENT PDT
|
|
89
|
+
MARECHAL MAL
|
|
90
|
+
DOCTEUR DR
|
|
91
|
+
GRANDE GDE
|
|
92
|
+
GRAND GD
|
|
93
|
+
PETITE PTE
|
|
94
|
+
PETIT PT
|
|
95
|
+
SAINT ST
|
|
96
|
+
SAINTE STE
|
|
97
|
+
CEDEX CDX
|
|
98
|
+
CEDX CDX
|
|
99
|
+
CDEX CDX
|
|
100
|
+
CX CDX
|
|
101
|
+
FAUBOURG FBG
|
|
102
|
+
CENTRE COMMERCIAL CCAL
|
|
103
|
+
CENTRE CIAL CCAL
|
|
104
|
+
CENTRE CCIAL CCAL
|
|
105
|
+
CTRE COMMERCIAL CCAL
|
|
106
|
+
CTRE CIAL CCAL
|
|
107
|
+
CTRE CCIAL CCAL
|
|
108
|
+
C COMMERCIAL CCAL
|
|
109
|
+
C CIAL CCAL
|
|
110
|
+
CCIAL CCAL"""
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
FIRST_NAME = """J ROGER JEAN ROGER
|
|
2
|
+
J ARNAUD JEAN ARNAUD
|
|
3
|
+
J AUGUSTE JEAN AUGUSTE
|
|
4
|
+
J SYLVESTRE JEAN SYLVESTRE
|
|
5
|
+
J SULLY JEAN SULLY
|
|
6
|
+
J GUY JEAN GUY
|
|
7
|
+
J MAXIME JEAN MAXIME
|
|
8
|
+
J RENE JEAN RENE
|
|
9
|
+
J MARTIN JEAN MARTIN
|
|
10
|
+
J ARMEL JEAN ARMEL
|
|
11
|
+
J ADRIEN JEAN ADRIEN
|
|
12
|
+
J MATTHIEU JEAN MATTHIEU
|
|
13
|
+
J GAEL JEAN GAEL
|
|
14
|
+
J RAOUL JEAN RAOUL
|
|
15
|
+
J STEPHAN JEAN STEPHAN
|
|
16
|
+
J VALERE JEAN VALERE
|
|
17
|
+
J NORBERT JEAN NORBERT
|
|
18
|
+
J YANNICK JEAN YANNICK
|
|
19
|
+
J FREDERIC JEAN FREDERIC
|
|
20
|
+
J EUDE JEAN EUDE
|
|
21
|
+
J AURELIEN JEAN AURELIEN
|
|
22
|
+
J YVON JEAN YVON
|
|
23
|
+
J LUCAS JEAN LUCAS
|
|
24
|
+
J THIERRY JEAN THIERRY
|
|
25
|
+
J MAX JEAN MAX
|
|
26
|
+
J LUDOVIC JEAN LUDOVIC
|
|
27
|
+
J ANDRE JEAN ANDRE
|
|
28
|
+
J ROCH JEAN ROCH
|
|
29
|
+
J ELIE JEAN ELIE
|
|
30
|
+
J ELOI JEAN ELOI
|
|
31
|
+
J SERGE JEAN SERGE
|
|
32
|
+
J PHILIPPE JEAN PHILIPPE
|
|
33
|
+
J DANY JEAN DANY
|
|
34
|
+
J MARIN JEAN MARIN
|
|
35
|
+
J YANN JEAN YANN
|
|
36
|
+
J HUGUES JEAN HUGUES
|
|
37
|
+
J CONSTANT JEAN CONSTANT
|
|
38
|
+
J JACK JEAN JACK
|
|
39
|
+
J ALIX JEAN ALIX
|
|
40
|
+
J MARK JEAN MARK
|
|
41
|
+
J CARL JEAN CARL
|
|
42
|
+
J GREGOIRE JEAN GREGOIRE
|
|
43
|
+
J EUGENE JEAN EUGENE
|
|
44
|
+
J BRIEUC JEAN BRIEUC
|
|
45
|
+
J DENYS JEAN DENYS
|
|
46
|
+
J EMILE JEAN EMILE
|
|
47
|
+
J NICOLAS JEAN NICOLAS
|
|
48
|
+
J FRANCOIS JEAN FRANCOIS
|
|
49
|
+
J RICHARD JEAN RICHARD
|
|
50
|
+
J THEOPHANE JEAN THEOPHANE
|
|
51
|
+
J DAVY JEAN DAVY
|
|
52
|
+
J BENOIT JEAN BENOIT
|
|
53
|
+
J PAUL JEAN PAUL
|
|
54
|
+
J RODOLPHE JEAN RODOLPHE
|
|
55
|
+
J QUENTIN JEAN QUENTIN
|
|
56
|
+
J MICKAEL JEAN MICKAEL
|
|
57
|
+
J BRIAC JEAN BRIAC
|
|
58
|
+
J DAVID JEAN DAVID
|
|
59
|
+
J MARC JEAN MARC
|
|
60
|
+
J VIANNEY JEAN VIANNEY
|
|
61
|
+
J AIME JEAN AIME
|
|
62
|
+
J DOMINIQUE JEAN DOMINIQUE
|
|
63
|
+
J EDDY JEAN EDDY
|
|
64
|
+
J BRICE JEAN BRICE
|
|
65
|
+
J FABIEN JEAN FABIEN
|
|
66
|
+
J CLAUDE JEAN CLAUDE
|
|
67
|
+
J WILFRIED JEAN WILFRIED
|
|
68
|
+
J DAMIEN JEAN DAMIEN
|
|
69
|
+
J WILLIAM JEAN WILLIAM
|
|
70
|
+
J EDOUARD JEAN EDOUARD
|
|
71
|
+
J RUDY JEAN RUDY
|
|
72
|
+
J LEON JEAN LEON
|
|
73
|
+
J NOEL JEAN NOEL
|
|
74
|
+
J BATISTE JEAN BATISTE
|
|
75
|
+
J REMY JEAN REMY
|
|
76
|
+
J OLIVIER JEAN OLIVIER
|
|
77
|
+
J BENJAMIN JEAN BENJAMIN
|
|
78
|
+
J MARY JEAN MARY
|
|
79
|
+
J BERNARD JEAN BERNARD
|
|
80
|
+
J CHARLES JEAN CHARLES
|
|
81
|
+
J CHRISTOPHER JEAN CHRISTOPHER
|
|
82
|
+
J MATHIS JEAN MATHIS
|
|
83
|
+
J MARCEL JEAN MARCEL
|
|
84
|
+
J CHRISTIAN JEAN CHRISTIAN
|
|
85
|
+
J STANISLAS JEAN STANISLAS
|
|
86
|
+
J ERNEST JEAN ERNEST
|
|
87
|
+
J BASTIEN JEAN BASTIEN
|
|
88
|
+
J FREDDY JEAN FREDDY
|
|
89
|
+
J ERICK JEAN ERICK
|
|
90
|
+
J VITAL JEAN VITAL
|
|
91
|
+
J FRANCO JEAN FRANCO
|
|
92
|
+
J ROMAIN JEAN ROMAIN
|
|
93
|
+
J RENAUD JEAN RENAUD
|
|
94
|
+
J PATRICE JEAN PATRICE
|
|
95
|
+
J PATRICK JEAN PATRICK
|
|
96
|
+
J EMMANUEL JEAN EMMANUEL
|
|
97
|
+
J STEPHANE JEAN STEPHANE
|
|
98
|
+
J XAVIER JEAN XAVIER
|
|
99
|
+
J BRUNO JEAN BRUNO
|
|
100
|
+
J MOISE JEAN MOISE
|
|
101
|
+
J LUC JEAN LUC
|
|
102
|
+
J VICTOR JEAN VICTOR
|
|
103
|
+
J VIVIEN JEAN VIVIEN
|
|
104
|
+
J CYRILLE JEAN CYRILLE
|
|
105
|
+
J KARL JEAN KARL
|
|
106
|
+
J JOSEPH JEAN JOSEPH
|
|
107
|
+
J MICHAEL JEAN MICHAEL
|
|
108
|
+
J AUGUSTIN JEAN AUGUSTIN
|
|
109
|
+
J THOMAS JEAN THOMAS
|
|
110
|
+
J LOUIS JEAN LOUIS
|
|
111
|
+
J ALEXANDRE JEAN ALEXANDRE
|
|
112
|
+
J SYLVAIN JEAN SYLVAIN
|
|
113
|
+
J LOIC JEAN LOIC
|
|
114
|
+
J PHILIP JEAN PHILIP
|
|
115
|
+
J FERNAND JEAN FERNAND
|
|
116
|
+
J MALO JEAN MALO
|
|
117
|
+
J ALBERT JEAN ALBERT
|
|
118
|
+
J FLORENT JEAN FLORENT
|
|
119
|
+
J POL JEAN POL
|
|
120
|
+
J ARMAND JEAN ARMAND
|
|
121
|
+
J GERARD JEAN GERARD
|
|
122
|
+
J AXEL JEAN AXEL
|
|
123
|
+
J FABRICE JEAN FABRICE
|
|
124
|
+
J ARTHUR JEAN ARTHUR
|
|
125
|
+
J LUCIEN JEAN LUCIEN
|
|
126
|
+
J LAURENT JEAN LAURENT
|
|
127
|
+
J JUNIOR JEAN JUNIOR
|
|
128
|
+
J LUCE JEAN LUCE
|
|
129
|
+
J CLEMENT JEAN CLEMENT
|
|
130
|
+
J COME JEAN COME
|
|
131
|
+
J VALERY JEAN VALERY
|
|
132
|
+
J HERVE JEAN HERVE
|
|
133
|
+
J BART JEAN BART
|
|
134
|
+
J FELIX JEAN FELIX
|
|
135
|
+
J REMI JEAN REMI
|
|
136
|
+
J CYRIL JEAN CYRIL
|
|
137
|
+
J MATHIAS JEAN MATHIAS
|
|
138
|
+
J BLAISE JEAN BLAISE
|
|
139
|
+
J MARIE JEAN MARIE
|
|
140
|
+
J CAMILLE JEAN CAMILLE
|
|
141
|
+
J LOU JEAN LOU
|
|
142
|
+
J DANIEL JEAN DANIEL
|
|
143
|
+
J JACQUES JEAN JACQUES
|
|
144
|
+
J FLORIAN JEAN FLORIAN
|
|
145
|
+
J GUILLAUME JEAN GUILLAUME
|
|
146
|
+
J SEBASTIEN JEAN SEBASTIEN
|
|
147
|
+
J HENRI JEAN HENRI
|
|
148
|
+
J CYPRIEN JEAN CYPRIEN
|
|
149
|
+
J LEO JEAN LEO
|
|
150
|
+
J ERIC JEAN ERIC
|
|
151
|
+
J SAMUEL JEAN SAMUEL
|
|
152
|
+
J GILLES JEAN GILLES
|
|
153
|
+
J ETIENNE JEAN ETIENNE
|
|
154
|
+
J VINCENT JEAN VINCENT
|
|
155
|
+
J ROLAND JEAN ROLAND
|
|
156
|
+
J EUDES JEAN EUDES
|
|
157
|
+
J JOEL JEAN JOEL
|
|
158
|
+
J GIL JEAN GIL
|
|
159
|
+
J TRISTAN JEAN TRISTAN
|
|
160
|
+
J ALEXIS JEAN ALEXIS
|
|
161
|
+
J CHRISTOPHE JEAN CHRISTOPHE
|
|
162
|
+
J BOSCO JEAN BOSCO
|
|
163
|
+
J DESIRE JEAN DESIRE
|
|
164
|
+
J LOUP JEAN LOUP
|
|
165
|
+
J PIERROT JEAN PIERROT
|
|
166
|
+
J GREGORY JEAN GREGORY
|
|
167
|
+
J KEVIN JEAN KEVIN
|
|
168
|
+
J HUGO JEAN HUGO
|
|
169
|
+
J YAN JEAN YAN
|
|
170
|
+
J BAPTISTE JEAN BAPTISTE
|
|
171
|
+
J JULES JEAN JULES
|
|
172
|
+
J ROBERT JEAN ROBERT
|
|
173
|
+
J GUILHEM JEAN GUILHEM
|
|
174
|
+
J JULIEN JEAN JULIEN
|
|
175
|
+
J MATHIEU JEAN MATHIEU
|
|
176
|
+
J ALEX JEAN ALEX
|
|
177
|
+
J PIERRE JEAN PIERRE
|
|
178
|
+
J GILBERT JEAN GILBERT
|
|
179
|
+
J GEORGES JEAN GEORGES
|
|
180
|
+
J HENRY JEAN HENRY
|
|
181
|
+
J WILFRID JEAN WILFRID
|
|
182
|
+
J LIN JEAN LIN
|
|
183
|
+
J BARTHELEMY JEAN BARTHELEMY
|
|
184
|
+
J ANTOINE JEAN ANTOINE
|
|
185
|
+
J EDMOND JEAN EDMOND
|
|
186
|
+
J ALAIN JEAN ALAIN
|
|
187
|
+
J DENIS JEAN DENIS
|
|
188
|
+
J GERALD JEAN GERALD
|
|
189
|
+
J WILSON JEAN WILSON
|
|
190
|
+
J LIONEL JEAN LIONEL
|
|
191
|
+
J RAPHAEL JEAN RAPHAEL
|
|
192
|
+
J ROBIN JEAN ROBIN
|
|
193
|
+
J YVES JEAN YVES
|
|
194
|
+
J PASCAL JEAN PASCAL
|
|
195
|
+
J FRANCIS JEAN FRANCIS
|
|
196
|
+
J TEDDY JEAN TEDDY
|
|
197
|
+
J AYMERIC JEAN AYMERIC
|
|
198
|
+
J GUSTAVE JEAN GUSTAVE
|
|
199
|
+
J MAEL JEAN MAEL
|
|
200
|
+
J ALFRED JEAN ALFRED
|
|
201
|
+
J MARIUS JEAN MARIUS
|
|
202
|
+
J REGIS JEAN REGIS
|
|
203
|
+
J ABEL JEAN ABEL
|
|
204
|
+
J HUBERT JEAN HUBERT
|
|
205
|
+
J KARIM JEAN KARIM
|
|
206
|
+
J CEDRIC JEAN CEDRIC
|
|
207
|
+
J NOE JEAN NOE
|
|
208
|
+
J FRANCK JEAN FRANCK
|
|
209
|
+
J JEROME JEAN JEROME
|
|
210
|
+
J SIMON JEAN SIMON
|
|
211
|
+
J EMILIEN JEAN EMILIEN
|
|
212
|
+
J MAXENCE JEAN MAXENCE
|
|
213
|
+
J FRED JEAN FRED
|
|
214
|
+
J VALENTIN JEAN VALENTIN
|
|
215
|
+
J JOSE JEAN JOSE
|
|
216
|
+
J MICHEL JEAN MICHEL
|
|
217
|
+
J RAYMOND JEAN RAYMOND
|
|
218
|
+
J MARTIAL JEAN MARTIAL
|
|
219
|
+
J MIKAEL JEAN MIKAEL
|
|
220
|
+
J ANTHONY JEAN ANTHONY
|
|
221
|
+
J CLAIR JEAN CLAIR
|
|
222
|
+
J TOUSSAINT JEAN TOUSSAINT
|
|
223
|
+
J WILLY JEAN WILLY
|
|
224
|
+
J FERDINAND JEAN FERDINAND
|
|
225
|
+
J GABRIEL JEAN GABRIEL
|
|
226
|
+
J MAURICE JEAN MAURICE
|
|
227
|
+
J DIDIER JEAN DIDIER
|
|
228
|
+
J BERTRAND JEAN BERTRAND
|
|
229
|
+
J GASTON JEAN GASTON"""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
TITLE = """MADAME 2
|
|
2
|
+
MME 2
|
|
3
|
+
MESDAMES 2
|
|
4
|
+
MMES 2
|
|
5
|
+
MADEMOISELLE 2
|
|
6
|
+
MLLE 2
|
|
7
|
+
MESDEMOISELLES 2
|
|
8
|
+
MLLES 2
|
|
9
|
+
MONSIEUR 1
|
|
10
|
+
MR 1
|
|
11
|
+
M 1
|
|
12
|
+
MESSIEURS 1
|
|
13
|
+
MM 1
|
|
14
|
+
VEUVE 2
|
|
15
|
+
VVE 2
|
|
16
|
+
DOCTEUR 0
|
|
17
|
+
DR 0
|
|
18
|
+
DOCTEURS 0
|
|
19
|
+
DRS 0
|
|
20
|
+
PROFESSEUR 0
|
|
21
|
+
PR 0
|
|
22
|
+
PROFESSEURS 0
|
|
23
|
+
PRS 0
|
|
24
|
+
MAITRE 0
|
|
25
|
+
ME 0
|
|
26
|
+
MAITRES 0
|
|
27
|
+
MES 0
|
|
28
|
+
MONSEIGNEUR 1
|
|
29
|
+
MGR 1
|
|
30
|
+
GENERAL 0
|
|
31
|
+
GAL 0
|
|
32
|
+
GNRL 0
|
|
33
|
+
MARECHAL 0
|
|
34
|
+
COLONEL 0
|
|
35
|
+
CEL 0
|
|
36
|
+
CAPITAINE 0
|
|
37
|
+
CNE 0
|
|
38
|
+
COMMANDANT 0
|
|
39
|
+
CDT 0
|
|
40
|
+
LIEUTENANT 0
|
|
41
|
+
LT 0"""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: es-normalizer-py
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Contact data normalization adapted from the Empreinte Sociométrique's normalizers, the Python port of es-normalizer
|
|
5
|
+
Author-email: Cyril Dever <cdever@pep-s.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cyrildever/es-normalizer-py
|
|
8
|
+
Keywords: data,contact,normalization,empreinte sociometrique
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10.2
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# es-normalizer-py
|
|
17
|
+
_Contact data normalization adapted from the Empreinte Sociométrique's normalizers (in Python)_
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
This is the Python port of [es-normalizer](https://github.com/cyrildever/es-normalizer), the TypeScript normalization library for contact data (address, phones, etc.) adapted from [Edgewhere](https://www.edgewhere.fr)'s Empreinte Sociométrique patented normalizers.
|
|
26
|
+
|
|
27
|
+
### Motivation
|
|
28
|
+
|
|
29
|
+
When it comes to hashing data, the necessary uniqueness of the source sometimes makes it hard to compare two hashed data, mostly if dealing with postal address. This library helps normalizing any contact data (the postal or e-mail address, the mobile or landline phone number, the title, names or date of birth of an individual) that would feed any hashing algorithm to make hash comparison always trustable.
|
|
30
|
+
It's based on the work for the Empreinte Sociométrique™ patented by Cyril Dever for Edgewhere.
|
|
31
|
+
|
|
32
|
+
### TypeScript version
|
|
33
|
+
|
|
34
|
+
The TypeScript library lives at [cyrildever/es-normalizer](https://github.com/cyrildever/es-normalizer) and is published on npm as `es-normalizer`.
|
|
35
|
+
Version `0.2.4` of this port matches its version `2.4.0`: both give the same outputs for the same inputs, the test suite of es-normalizer is ported here case by case, and their dictionaries are the same.
|
|
36
|
+
An address dictionary key may hold several words (eg. `CENTRE COMMERCIAL`), translated together before the single words, the longest first.
|
|
37
|
+
|
|
38
|
+
### Usage
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install es-normalizer-py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
*IMPORTANT*: as of this version, most normalizers are for use on French data.
|
|
45
|
+
|
|
46
|
+
To get a normalized string, pass the data and a normalizer function to `normalize()`: it returns the normalized string, or `None` when nothing usable is left.
|
|
47
|
+
There are currently eleven specific normalizer functions and a generic one:
|
|
48
|
+
* `Any`: the generic normalizer should be used if no specific normalizer exists;
|
|
49
|
+
* `AddressLine`: pass any address line through it to get a normalized address, eg. `8, rue Henner` becomes `8 RUE HENNER`, and eventually `True` as parameter when trying to normalize a French address line 1 to also transform the title, eg. `Monsieur Cyril Dever` should then become `1 CYRIL DEVER` when passed `True` instead of `MONSIEUR CYRIL DEVER` for the default behaviour;
|
|
50
|
+
* `AddressLine6`: the French address line 6 with the department inserted in between the zip code and the city as in the Empreinte Sociométrique implementation (eg. `75009 75 PARIS`);
|
|
51
|
+
* `City`: for normalizing city names (it removes any Cedex mention in French address, for instance);
|
|
52
|
+
* `CodePostalFrance`: for French zip code;
|
|
53
|
+
* `DateOfBirth`: pass a date and up to two parameters: the input format (eg. `YYYY-MM-DD`) and the output format wished (eg. `ISO_DATE`), the default values being respectively the ISO format and the French date format;
|
|
54
|
+
* `DepartementFrance`: extract the French departement number (out of a code postal, for instance);
|
|
55
|
+
* `Email`: validates the passed e-mail and returns it in lower-case;
|
|
56
|
+
* `FirstName`: pass a first name and get a normalized one (it uses an enlarged French dictionay of first names to process it making it possible to pass from `"J.-Louis"` to `"JEAN LOUIS"`);
|
|
57
|
+
* `Mobile`: to validate a French mobile phone;
|
|
58
|
+
* `PhoneNumber`: to normalize a French phone or fax number in the international format, eg. `+33 (0) 123 456 789`;
|
|
59
|
+
* `StreetNumber`: parses the passed field to normalize it the Empreinte Sociométrique™'s way, eg. `1bis` or `1 bis` becomes `1B`;
|
|
60
|
+
* `Title`: returns a code depending on the passed string (1 for gentlemen, 2 for ladies, 0 when undefined or unknown).
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from es_normalizer import *
|
|
64
|
+
|
|
65
|
+
print(normalize('#This is any String(). ', Any))
|
|
66
|
+
# THIS IS ANY STRING
|
|
67
|
+
|
|
68
|
+
print(normalize('Monsieur Cyril Dever', AddressLine(True)))
|
|
69
|
+
# 1 CYRIL DEVER
|
|
70
|
+
|
|
71
|
+
print(normalize('24, rué de Maubeuge', AddressLine()))
|
|
72
|
+
# 24 RUE MAUBEUGE
|
|
73
|
+
|
|
74
|
+
print(normalize('75009 .# Paris', AddressLine6()))
|
|
75
|
+
# 75009 75 PARIS
|
|
76
|
+
|
|
77
|
+
print(normalize('75009 .# Paris', AddressLine6(False)))
|
|
78
|
+
# 75009 PARIS
|
|
79
|
+
|
|
80
|
+
print(normalize('Paris Cedex 09', City))
|
|
81
|
+
# PARIS
|
|
82
|
+
|
|
83
|
+
print(normalize('70/12/01', DateOfBirth('YY/MM/DD', FRENCH_DATE)))
|
|
84
|
+
# 01/12/1970
|
|
85
|
+
|
|
86
|
+
print(normalize(' 75009 ', DepartementFrance))
|
|
87
|
+
# 75
|
|
88
|
+
|
|
89
|
+
print(normalize(' Contact@GMAIL.com', Email))
|
|
90
|
+
# contact@gmail.com
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`DateOfBirth` reads dates the way the forgiving parser of [moment.js](https://momentjs.com/) does, which es-normalizer relies on. Both give the same date for an input that fits its format; for an input that does not, eg. a numeric date read with a month name format (`DD MMM YYYY`), the guess of each may differ.
|
|
94
|
+
|
|
95
|
+
### Tests
|
|
96
|
+
|
|
97
|
+
```console
|
|
98
|
+
$ git clone https://github.com/cyrildever/es-normalizer-py.git
|
|
99
|
+
$ cd es-normalizer-py/
|
|
100
|
+
$ pip install -e .
|
|
101
|
+
$ python3 -m unittest discover
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
### License
|
|
106
|
+
|
|
107
|
+
This module is distributed under a MIT license. \
|
|
108
|
+
See the [LICENSE](LICENSE) file.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
<hr />
|
|
112
|
+
© 2026 Cyril Dever. All rights reserved.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
es_normalizer/__init__.py,sha256=tVXR6T2qo0Y9PoNy2mXbAlSdLoRU9Nf4ROpK2fRRzoo,8661
|
|
2
|
+
es_normalizer/date_of_birth.py,sha256=6beiq5KDvq_Gz8HL3nB6YYCuqxsLDPTeiIhSvfHzeFE,4237
|
|
3
|
+
es_normalizer/dictionary/__init__.py,sha256=RFC6a90ZzfstjRctQPNG1bf3A8LEqGY6v3GnpyXnZsc,1794
|
|
4
|
+
es_normalizer/dictionary/address.py,sha256=-zhoG_Gov3Bm5tOsRX_xVfk7HUKDenUvIIlvXPkg1GE,1089
|
|
5
|
+
es_normalizer/dictionary/firstname.py,sha256=5EYDLA1vcM1gnO3TQeUI6ss6Rs3l5oHqQMkePmZYQOY,4790
|
|
6
|
+
es_normalizer/dictionary/title.py,sha256=kCMb6w48AysAPoLq_x5g-hdfzfIvoUh3PdZysB9MYKI,370
|
|
7
|
+
es_normalizer_py-0.2.4.dist-info/licenses/LICENSE,sha256=PM4uNGV9-SG80q1m93WGtx-FQooSUK18WonpzGc8mJI,1068
|
|
8
|
+
es_normalizer_py-0.2.4.dist-info/METADATA,sha256=u_YPjKFpxKmI-fcNl1b7nz2SQ-7hfiBGFfl7RAz72O0,5878
|
|
9
|
+
es_normalizer_py-0.2.4.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
|
|
10
|
+
es_normalizer_py-0.2.4.dist-info/top_level.txt,sha256=cw6hHhElZgvzvBNuYLouQa8qliIEtZDE0MkB1MzOqQc,14
|
|
11
|
+
es_normalizer_py-0.2.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cyril Dever
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
es_normalizer
|