frznkshn commited on
Commit
0a4e172
·
verified ·
1 Parent(s): d64688d

Upload 5 files

Browse files
Files changed (5) hide show
  1. normalizer.py +65 -0
  2. requirement.txt +5 -0
  3. simple_test.py +225 -0
  4. tests.py +0 -0
  5. utils.py +51 -0
normalizer.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from rules.base import BaseProcessor
2
+ from rules.currency import CurrencyProcessor
3
+ from rules.dates import DateProcessor
4
+ from rules.fraction import FractionProcessor
5
+ from rules.cards import CardNumberProcessor
6
+ from rules.postal_national_id import PostalCodeAndNationalIdProcessor
7
+ from rules.numbers import NumberProcessor
8
+ from rules.phone import PhoneNumberProcessor
9
+ from rules.spaces import CurrencySpaceProcessor
10
+ from rules.symbols import (
11
+ AmpersandProcessor,
12
+ MathOperatorProcessor,
13
+ NegativeSignProcessor,
14
+ RangeProcessor,
15
+ RomanNumeralProcessor,
16
+ )
17
+ from rules.times import TimeProcessor
18
+ from rules.units import UnitProcessor
19
+ from utils import detect_language
20
+
21
+
22
+ class Normalizer(BaseProcessor):
23
+ def __init__(self):
24
+ super(Normalizer, self).__init__()
25
+ self.text_processors = [
26
+ CurrencySpaceProcessor(),
27
+ CardNumberProcessor(),
28
+ PostalCodeAndNationalIdProcessor(),
29
+ PhoneNumberProcessor(),
30
+ NegativeSignProcessor(),
31
+ RangeProcessor(),
32
+ UnitProcessor(),
33
+ FractionProcessor(),
34
+ RomanNumeralProcessor(),
35
+ AmpersandProcessor(),
36
+ MathOperatorProcessor()
37
+ ]
38
+ self.processors = [
39
+ DateProcessor(),
40
+ TimeProcessor(),
41
+ CurrencyProcessor()
42
+ ]
43
+ # Runs last, on whatever number is still left as digits.
44
+ self.number_processor = NumberProcessor()
45
+
46
+ def process(self, text:str) -> str:
47
+ language = detect_language(text)
48
+ for text_processor in self.text_processors:
49
+ text = text_processor.process(text, language=language)
50
+ # A processor may have rewritten symbols into words (`5 km`).
51
+ language = detect_language(text)
52
+ for processor in self.processors:
53
+ text = self._apply_a_processor(text=text, processor=processor, language=language)
54
+ return self.number_processor.process(text, language=detect_language(text))
55
+
56
+ @staticmethod
57
+ def _apply_a_processor(text:str, processor:BaseProcessor, language:str) -> str:
58
+ words = text.split()
59
+ processed = []
60
+
61
+ for word in words:
62
+ word = word.strip()
63
+ processed.append(processor.process(word, language=language))
64
+
65
+ return ' '.join(processed)
requirement.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ num2words==0.5.14
2
+ persiantools==6.2.0
3
+ num2fawords==1.1
4
+ dataparser==0.0.2
5
+ dateparser==1.4.3
simple_test.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from normalizer import Normalizer
2
+
3
+ def test_currency_normalization():
4
+ n = Normalizer()
5
+
6
+ assert n.process("من امروز ۳$ دارم") == "من امروز سه دلار دارم"
7
+ assert n.process("من امروز 3$ دارم") == "من امروز سه دلار دارم"
8
+ assert n.process("من امروز 1 $ دارم") == "من امروز یک دلار دارم"
9
+ assert n.process("من امروز 12.4$ دارم") == "من امروز دوازده دلار و چهل سنت دارم"
10
+ assert n.process("من امروز 12.۴$ دارم") == "من امروز دوازده دلار و چهل سنت دارم"
11
+ assert n.process("من امروز 125.۴3$ دارم") == "من امروز یکصد و بیست و پنج دلار و چهل و سه سنت دارم"
12
+ assert n.process("من امروز 12.۴€ دارم") == "من امروز دوازده یورو و چهل سنت دارم"
13
+ assert n.process("من امروز 1001.۴€ و 5۲.4$ دارم") == "من امروز یک هزار و یک یورو و چهل سنت و پنجاه و دو دلار و چهل سنت دارم"
14
+
15
+ assert n.process("I have 3$") == "I have three dollars"
16
+ assert n.process("I have ۳$.") == "I have three dollars."
17
+ assert n.process("I have 1$") == "I have one dollar"
18
+ assert n.process("I have 1 $") == "I have one dollar"
19
+ assert n.process("I have 2.5$") == "I have two dollars and fifty cents"
20
+ assert n.process("I have ۲.۵$") == "I have two dollars and fifty cents"
21
+ assert n.process("I have 2.۵$") == "I have two dollars and fifty cents"
22
+ assert n.process("I have ۱.1$.") == "I have one dollar and ten cents."
23
+ assert n.process("I have 3$ and 1 €") == "I have three dollars and one euro"
24
+ assert n.process("I have 3.9$ and 1.3€ and 12.۳3 £.") == (
25
+ "I have three dollars and ninety cents and one euro and thirty cents "
26
+ "and twelve pounds and thirty-three pence."
27
+ )
28
+ assert n.process("1000$") == "one thousand dollars"
29
+ assert n.process("1,000$") == "one thousand dollars"
30
+ assert n.process("1000 $") == "one thousand dollars"
31
+ assert n.process("1,000,001 €") == "one million and one euros"
32
+ assert n.process("۱۰۰۰$") == "یک هزار دلار"
33
+ assert n.process("۱۰۰۱ $") == "یک هزار و یک دلار"
34
+
35
+ print("✅ All currency normalization tests passed successfully!")
36
+ #----------------------------------------------------------------------------------------------------------------------------
37
+ def test_unit_normalization():
38
+ n = Normalizer()
39
+
40
+ assert n.process("وزن او ۷۵kg است") == "وزن او هفتاد و پنج کیلوگرم است"
41
+ assert n.process("وزن او 75kg است") == "وزن او هفتاد و پنج کیلوگرم است"
42
+ assert n.process("دمای هوا ۷۷°F است") == "دمای هوا هفتاد و هفت درجه فارنهایت است"
43
+ assert n.process("از اینجا تا آنجا ۱۰۰۰.۵m فاصله است") == "از اینجا تا آنجا یک هزار متر و پنجاه سانتیمتر فاصله است"
44
+ assert n.process("I ran 5km today") == "I ran five kilometres today"
45
+ assert n.process("It is 2۵°C outside") == "It is twenty-five degrees Celsius outside"
46
+ assert n.process("The car goes 60 km/h") == "The car goes sixty kilometres per hour"
47
+ assert n.process("1000m") == "one thousand metres"
48
+ assert n.process("۲۳۰۰m") == "دو هزار و سیصد متر"
49
+ assert n.process("۱.۲۳۴ m") == "یک متر و بیست و سه سانتیمتر و چهار میلیمتر"
50
+ assert n.process("26°C") == "twenty-six degrees Celsius"
51
+ assert n.process("۲۶ °C") == "بیست و شش درجه سانتیگراد"
52
+ assert n.process("۲۴ °F") == "بیست و چهار درجه فارنهایت"
53
+ assert n.process("۷۰ km/h") == "هفتاد کیلومتر بر ساعت"
54
+ assert n.process("۳.۵ L") == "سه لیتر و پانصد میلی‌لیتر"
55
+ assert n.process("۸ oz") == "هشت اونس"
56
+ assert n.process("۳.۵ sec") == "سه ثانیه و پانصد میلی‌ثانیه"
57
+ assert n.process("۳.۵ min") == "سه دقیقه و سی ثانیه"
58
+ assert n.process("3.5hour") == "three hours and thirty minutes"
59
+ assert n.process("3.51 hour") == "three hours, thirty minutes and thirty-six seconds"
60
+ assert n.process("3.512 hour") == "three hours, thirty minutes, forty-three seconds and two hundred milliseconds"
61
+ assert n.process("۳.۵۱۲ hour") == "سه ساعت و سی دقیقه و چهل و سه ثانیه و دویست میلی‌ثانیه"
62
+ assert n.process("10.5 days") == "ten days and twelve hours"
63
+ assert n.process("۱,۰۰۰,۰۰۰ hour") == "یک میلیون ساعت"
64
+ assert n.process("5 %") == "five percent"
65
+ assert n.process("%5") == "five percent"
66
+ assert n.process("٪۲۰") == "بیست درصد"
67
+ assert n.process("20 % cm") == "twenty percent centimetre"
68
+ assert n.process("۲۰% cm") == "بیست درصد سانتیمتر"
69
+ assert n.process("۲۰٪ cm") == "بیست درصد سانتیمتر"
70
+ assert n.process("٪۲۰ cm") == "بیست درصد سانتیمتر"
71
+ assert n.process("٪ ۲۰ cm.") == "بیست درصد سانتیمتر."
72
+ assert n.process("% ۲۰ cm.") == "بیست درصد سانتیمتر."
73
+
74
+
75
+ print("✅ All unit normalization tests passed successfully!")
76
+ #----------------------------------------------------------------------------------------------------------------------------
77
+ def test_fraction_normalization():
78
+ n = Normalizer()
79
+
80
+ assert n.process("1/3") == "one third"
81
+ assert n.process("1000000/3") == "one million thirds"
82
+ assert n.process("۱/۳") == "یک سوم"
83
+ assert n.process("۱ ۱/۳") == "یک و یک سوم"
84
+ assert n.process("1/2 kg") == "one half kilogram"
85
+ assert n.process("1000/2 kg") == "one thousand halves kilograms"
86
+ assert n.process("1,000,000/2 sec") == "one million halves seconds"
87
+ assert n.process("1,000,000/3 sec") == "one million thirds seconds"
88
+ assert n.process("۱,۵۰۰,۰۰۰/۳ hour") == "یک میلیون و پانصد هزار سوم ساعت"
89
+ assert n.process("۶/۵ sec") == "شش پنجم ثانیه"
90
+ assert n.process("۷/۹۰ km/h") == "هفت نودم کیلومتر بر ساعت"
91
+ assert n.process("۷.۹۰ km/h") == "هفت و نه دهم کیلومتر بر ساعت"
92
+ assert n.process("۱۵ ۱۲/۲۳ min") == "پانزده و دوازده بیست و سوم دقیقه"
93
+ assert n.process("1 100/2 %") == "one and one hundred halves percent"
94
+ assert n.process("۱/۲ ٪") == "یک دوم درصد"
95
+ assert n.process("1 5,000/2000% cm") == "one and five thousand two thousandths percent centimetre"
96
+ assert n.process("۲/۱۰٪ cm") == "دو دهم درصد سانتیمتر"
97
+ assert n.process("3/4 km/h") == "three quarters kilometre per hour"
98
+ assert n.process("1½ kg") == "one and one half kilograms"
99
+ assert n.process("۵½ kg") == "پنج و یک دوم کیلوگرم"
100
+
101
+ print("✅ All fraction normalization tests passed successfully!")
102
+ #----------------------------------------------------------------------------------------------------------------------------
103
+ def test_symbol_normalization():
104
+ n = Normalizer()
105
+
106
+ assert n.process("علی & محسن") == "علی و محسن"
107
+ assert n.process("Ali & Mohsen") == "Ali and Mohsen"
108
+ assert n.process("R&D") == "R and D"
109
+ assert n.process("Tom &Jerry") == "Tom and Jerry"
110
+ assert n.process("4 & 5") == "four and five"
111
+ assert n.process("۴ & ۵") == "چهار و پنج"
112
+ assert n.process("3/4 & 7/3") == "three quarters and seven thirds"
113
+ assert n.process("۱/۲ & ۳/۲") == "یک دوم و سه دوم"
114
+ assert n.process("Farzin & 2 friends") == "Farzin and two friends"
115
+ assert n.process("Chapter V.") == "Chapter five."
116
+ assert n.process("Mohsen V.") == "Mohsen V."
117
+ assert n.process("II & IV") == "two and four"
118
+ assert n.process("3$ & 4€") == "three dollars and four euros"
119
+ assert n.process("بخش V را بخوان") == "بخش پنج را بخوان"
120
+
121
+ assert n.process("- 2 kg") == "negative two kilograms"
122
+ assert n.process("3+4") == "three plus four"
123
+ assert n.process("3×4") == "three times four"
124
+ assert n.process("3 × 4=12.") == "three times four equals twelve."
125
+ assert n.process("8 ÷ 2") == "eight divided by two"
126
+ assert n.process("۸ ÷ ۲") == "هشت تقسیم بر دو"
127
+ assert n.process("۸ × ۲ = 16") == "هشت ضربدر دو مساوی است با شانزده"
128
+ assert n.process("۸ * ۲ = 16") == "هشت ضربدر دو مساوی است با شانزده"
129
+ assert n.process("5 - 3 = 2") == "five minus three equals two"
130
+ assert n.process("5,000 - 3000 = 2000") == "five thousand minus three thousand equals two thousand"
131
+ assert n.process("۵ - ۳ = ۲") == "پنج منهای سه مساوی است با دو"
132
+ assert n.process("5kg + 3kg = 8kg") == "five kilograms plus three kilograms equals eight kilograms"
133
+ assert n.process("Tom + Jerry") == "Tom plus Jerry"
134
+
135
+ assert n.process("5 - 10") == "five to ten"
136
+ assert n.process("1/2 - 3/4") == "one half to three quarters"
137
+ assert n.process("5kg - 10kg") == "five kilograms to ten kilograms"
138
+ assert n.process("5% - 10%") == "five percent to ten percent"
139
+ assert n.process("1990 - 2000") == "one thousand, nine hundred and ninety to two thousand"
140
+
141
+ assert n.process('3 "m"') == 'three "metres"'
142
+ assert n.process('3 (kg)') == 'three (kilograms)'
143
+ assert n.process('3 {kg}') == 'three {kilograms}'
144
+ assert n.process('3 [kg]') == 'three [kilograms]'
145
+ assert n.process('(3) + (4)') == '(three) plus (four)'
146
+ assert n.process('3 $ + [4 oz]') == 'three dollars plus [four ounces]'
147
+ assert n.process('[3]-[5]') == '[three] to [five]'
148
+ assert n.process('[۳] - ۵') == '[سه] تا پنج'
149
+
150
+ print("✅ All symbol normalization tests passed successfully!")
151
+ #----------------------------------------------------------------------------------------------------------------------------
152
+ def test_phone_normalization():
153
+ n = Normalizer()
154
+
155
+ assert n.process("۰۹۱۵۶۴۸۶۳۲۰") == "صفر نهصد و پانزده، ششصد و چهل و هشت، شصت و سه، بیست"
156
+ assert n.process("۰۹۱۵-۶۴۸۶۳۲۰") == "صفر نهصد و پانزده، ششصد و چهل و هشت، شصت و سه، بیست"
157
+ assert n.process("+۹۸۹۱۵۶۴۸۶۳۲۰") == "مثبت نود و هشت، نهصد و پانزده، ششصد و چهل و هشت، شصت و سه، بیست"
158
+ assert n.process("(+۹۸۹۱۵) ۶۴۸۶۳۲۰") == "(مثبت نود و هشت، نهصد و پانزده)، ششصد و چهل و هشت، شصت و سه، بیست"
159
+ assert n.process("۰۲۱-۸۸۵۲۳۰۰۶") == "صفر بیست و یک، هشتاد و هشت، پنجاه و دو، سی، صفر شش"
160
+ assert n.process("(۰۲۱) ۸۸۵۲۳۰۰۶") == "(صفر بیست و یک)، هشتاد و هشت، پنجاه و دو، سی، صفر شش"
161
+ assert n.process("(۰۲۱)۸۸۵۲۳۰۰۶") == "(صفر بیست و یک)، هشتاد و هشت، پنجاه و دو، سی، صفر شش"
162
+ assert n.process("۰۰۱-۸۸۵۲۳۰۰۶") == "دو صفر یک، هشتاد و هشت، پنجاه و دو، سی، صفر شش"
163
+ assert n.process("09156486320") == "zero nine one five, six four eight, six three, two zero"
164
+ assert n.process("0915-6486320") == "zero nine one five, six four eight, six three, two zero"
165
+ assert n.process("(0915) 6486320") == "(zero nine one five), six four eight, six three, two zero"
166
+ assert n.process("(0915)6486320") == "(zero nine one five), six four eight, six three, two zero"
167
+ assert n.process("+989156486320") == "plus nine eight, nine one five, six four eight, six three, two zero"
168
+ assert n.process("(+98915) 6486320") == "(plus nine eight, nine one five), six four eight, six three, two zero"
169
+ assert n.process("02188523006") == "zero two one, eight eight, five two, three zero, zero six"
170
+ assert n.process("021-88523006") == "zero two one, eight eight, five two, three zero, zero six"
171
+ assert n.process("Call 1-800-555-1234") == "Call one, eight zero zero, five five five, one two three four"
172
+ assert n.process("زنگ بزن به 09356486320") == "زنگ بزن به صفر نهصد و سی و پنج، ششصد و چهل و هشت، شصت و سه، بیست"
173
+
174
+ print("✅ All phone normalization tests passed successfully!")
175
+ #----------------------------------------------------------------------------------------------------------------------------
176
+ def test_card_normalization():
177
+ n = Normalizer()
178
+
179
+ assert n.process("6219-8619-1635-6362") == "sixty-two, nineteen - eighty-six, nineteen - sixteen, thirty-five - sixty-three, sixty-two"
180
+ assert n.process("6200-8619-1635-6305") == "sixty-two, double zero - eighty-six, nineteen - sixteen, thirty-five - sixty-three, oh five"
181
+ assert n.process("6200861916356305") == "sixty-two, double zero - eighty-six, nineteen - sixteen, thirty-five - sixty-three, oh five"
182
+ assert n.process("۶۲۱۹-۸۶۱۹-۱۶۳۵-۶۳۶۲") == "شصت و دو، نوزده - هشتاد و شش، نوزده - شانزده، سی و پنج - شصت و سه، شصت و دو"
183
+ assert n.process("۶۲۰۰-۸۶۱۹-۱۶۳۵-۶۳۰۵") == "شصت و دو، دو صفر - هشتاد و شش، نوزده - شانزده، سی و پنج - شصت و سه، صفر پنج"
184
+ assert n.process("۶۲۰۰۸۶۱۹۱۶۳۵۶۳۰۵") == "شصت و دو، دو صفر - هشتاد و شش، نوزده - شانزده، سی و پنج - شصت و سه، صفر پنج"
185
+ assert n.process("شماره کارت من رو یادداشت کنین: 6200861916356305") == "شماره کارت من رو یادداشت کنین: شصت و دو، دو صفر - هشتاد و شش، نوزده - شانزده، سی و پنج - شصت و سه، صفر پنج"
186
+
187
+ print("✅ All card normalization tests passed successfully!")
188
+ #----------------------------------------------------------------------------------------------------------------------------
189
+ def test_postal_nationalId_normalization():
190
+ n = Normalizer()
191
+
192
+ assert n.process("1514717118") == "fifteen, fourteen, seventy-one, seventy-one, eighteen"
193
+ assert n.process("15147-17118") == "fifteen, fourteen, seventy-one, seventy-one, eighteen"
194
+ assert n.process("۱۵۱۴۷۱۷۱۱۸") == "پانزده، چهارده، هفتاد و یک، هفتاد و یک، هجده"
195
+ assert n.process("۱۵۱۴۷-۱۷۱۱۸") == "پانزده، چهارده، هفتاد و یک، هفتاد و یک، هجده"
196
+
197
+ assert n.process("0010062637") == "double zero, ten, oh six, twenty-six, thirty-seven"
198
+ assert n.process("[0010062637]") == "[double zero, ten, oh six, twenty-six, thirty-seven]"
199
+ assert n.process("۰۰۱۰۰۶۲۶۳۷") == "دو صفر، ده، صفر شش، بیست و شش، سی و هفت"
200
+
201
+ print("✅ All postal and national id normalization tests passed successfully!")
202
+
203
+ #----------------------------------------------------------------------------------------------------------------------------
204
+ def test_plain_number_normalization():
205
+ n = Normalizer()
206
+
207
+ assert n.process("۳.۵") == "سه و پنج دهم"
208
+ assert n.process("3.5") == "three point five"
209
+ assert n.process("من ۲۰ سیب دارم") == "من بیست سیب دارم"
210
+ assert n.process("۱۴۰۳") == "یک هزار و چهارصد و سه"
211
+ assert n.process("سال ۱۴۰۳") == "سال هزار و چهارصد و سه"
212
+ assert n.process("4338135") == "four million, three hundred and thirty-eight thousand, one hundred and thirty-five"
213
+
214
+ print("✅ All plain number normalization tests passed successfully!")
215
+
216
+ #----------------------------------------------------------------------------------------------------------------------------
217
+ if __name__ == "__main__":
218
+ test_currency_normalization()
219
+ test_unit_normalization()
220
+ test_fraction_normalization()
221
+ test_symbol_normalization()
222
+ test_phone_normalization()
223
+ test_card_normalization()
224
+ test_postal_nationalId_normalization()
225
+ test_plain_number_normalization()
tests.py ADDED
The diff for this file is too large to render. See raw diff
 
utils.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ NO_LETTERS = "Unknown / No letters found"
4
+
5
+
6
+ def detect_language(text: str) -> str:
7
+ cleaned_text = re.sub(r'[\s\d\W_]+', '', text)
8
+
9
+ if not cleaned_text:
10
+ return NO_LETTERS
11
+
12
+ persian_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]')
13
+ english_pattern = re.compile(r'[a-zA-Z]')
14
+
15
+ persian_count = len(persian_pattern.findall(cleaned_text))
16
+ english_count = len(english_pattern.findall(cleaned_text))
17
+
18
+ if persian_count > english_count:
19
+ return "fa"
20
+ elif english_count > persian_count:
21
+ return "en"
22
+ else:
23
+ return "mixed"
24
+
25
+
26
+ def detect_digit_language(text: str):
27
+ """Guesses the language from the digits alone: Persian digits -> "fa",
28
+ ASCII digits -> "en", no digits -> None. Used when a text has no letters."""
29
+ if re.search(r'[\u06F0-\u06F9\u0660-\u0669]', text):
30
+ return "fa"
31
+ if re.search(r'[0-9]', text):
32
+ return "en"
33
+ return None
34
+
35
+
36
+ def convert_fa_num_to_en(text:str) -> str:
37
+ fa_to_en = {
38
+ '۰':'0',
39
+ '۱':'1',
40
+ '۲':'2',
41
+ '۳':'3',
42
+ '۴':'4',
43
+ '۵':'5',
44
+ '۶':'6',
45
+ '۷':'7',
46
+ '۸':'8',
47
+ '۹':'9',
48
+ }
49
+ for k, v in fa_to_en.items():
50
+ text = text.replace(k, v)
51
+ return text