lam-arabicnumbers 2.0.2__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
7
+ .venv/
8
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mostafa Snowber
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,49 @@
1
+ Metadata-Version: 2.5
2
+ Name: lam-arabicnumbers
3
+ Version: 2.0.2
4
+ Summary: تحويل الأرقام إلى كلمات عربية بدقة نحوية (تفقيط) - Arabic Numbers to Words with high grammatical accuracy
5
+ Project-URL: Homepage, https://github.com/mostafasnowber2023-ai/lam-arabicnumbers
6
+ Project-URL: Repository, https://github.com/mostafasnowber2023-ai/lam-arabicnumbers
7
+ Project-URL: Issues, https://github.com/mostafasnowber2023-ai/lam-arabicnumbers/issues
8
+ Author-email: Mostafa Snowber <mostafa.snowber.2023@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: arabic,arabic-numbers,currency,date,number-to-words,numbers,ordinal,tafqeet,time,تفقيط,عربي
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: Arabic
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Text Processing :: Linguistic
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+
27
+ # lam-arabicnumbers
28
+
29
+ مكتبة Python لتحويل الأرقام إلى كلمات عربية بدقة نحوية.
30
+
31
+ [![PyPI version](https://badge.fury.io/py/lam-arabicnumbers.svg)](https://pypi.org/project/lam-arabicnumbers/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
33
+
34
+ ## المميزات
35
+
36
+ - ✅ تحويل الأعداد من 0 إلى 20 منزلة
37
+ - ✅ المذكر والمؤنث
38
+ - ✅ المثنى والجمع والتمييز النحوي
39
+ - ✅ العملات (ريال، جنيه، دينار، درهم، دولار)
40
+ - ✅ الأعداد الترتيبية (الأول، الحادي والعشرون...)
41
+ - ✅ التواريخ (ميلادي وهجري)
42
+ - ✅ الوقت (الساعة والدقائق والثواني)
43
+ - ✅ الأعداد السالبة
44
+ - ✅ بدون أي تبعيات خارجية
45
+
46
+ ## التثبيت
47
+
48
+ ```bash
49
+ pip install lam-arabicnumbers
@@ -0,0 +1,23 @@
1
+ # lam-arabicnumbers
2
+
3
+ مكتبة Python لتحويل الأرقام إلى كلمات عربية بدقة نحوية.
4
+
5
+ [![PyPI version](https://badge.fury.io/py/lam-arabicnumbers.svg)](https://pypi.org/project/lam-arabicnumbers/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## المميزات
9
+
10
+ - ✅ تحويل الأعداد من 0 إلى 20 منزلة
11
+ - ✅ المذكر والمؤنث
12
+ - ✅ المثنى والجمع والتمييز النحوي
13
+ - ✅ العملات (ريال، جنيه، دينار، درهم، دولار)
14
+ - ✅ الأعداد الترتيبية (الأول، الحادي والعشرون...)
15
+ - ✅ التواريخ (ميلادي وهجري)
16
+ - ✅ الوقت (الساعة والدقائق والثواني)
17
+ - ✅ الأعداد السالبة
18
+ - ✅ بدون أي تبعيات خارجية
19
+
20
+ ## التثبيت
21
+
22
+ ```bash
23
+ pip install lam-arabicnumbers
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lam-arabicnumbers"
7
+ version = "2.0.2"
8
+ description = "تحويل الأرقام إلى كلمات عربية بدقة نحوية (تفقيط) - Arabic Numbers to Words with high grammatical accuracy"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Mostafa Snowber", email = "mostafa.snowber.2023@gmail.com" }
14
+ ]
15
+ keywords = [
16
+ "arabic", "numbers", "tafqeet", "تفقيط", "عربي",
17
+ "currency", "ordinal", "date", "time",
18
+ "number-to-words", "arabic-numbers",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 5 - Production/Stable",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Natural Language :: Arabic",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.8",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Topic :: Text Processing :: Linguistic",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/mostafasnowber2023-ai/lam-arabicnumbers"
37
+ Repository = "https://github.com/mostafasnowber2023-ai/lam-arabicnumbers"
38
+ Issues = "https://github.com/mostafasnowber2023-ai/lam-arabicnumbers/issues"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/lam_arabicnumbers"]
@@ -0,0 +1,42 @@
1
+ """اختبار سريع لمكتبة lam_arabicnumbers."""
2
+ from lam_arabicnumbers import (
3
+ to_arabic, to_currency, to_ordinal,
4
+ to_date, to_ordinal_date, to_time,
5
+ )
6
+
7
+ print("=" * 60)
8
+ print(" اختبار سريع لمكتبة lam-arabicnumbers")
9
+ print("=" * 60)
10
+
11
+ # 1. الأعداد
12
+ print("\n--- to_arabic ---")
13
+ print(f" 1234 → {to_arabic(1234)}")
14
+ print(f" 200000 → {to_arabic(200000)}")
15
+ print(f" 3 (مؤنث) → {to_arabic(3, feminine=True)}")
16
+
17
+ # 2. العملات
18
+ print("\n--- to_currency ---")
19
+ print(f" 45 SAR → {to_currency(45, 'SAR')}")
20
+ print(f" 1234.56 SAR → {to_currency(1234.56, 'SAR')}")
21
+
22
+ # 3. الترتيب
23
+ print("\n--- to_ordinal ---")
24
+ print(f" 1 → {to_ordinal(1)}")
25
+ print(f" 21 → {to_ordinal(21)}")
26
+ print(f" 3 (مؤنث) → {to_ordinal(3, feminine=True)}")
27
+
28
+ # 4. التاريخ
29
+ print("\n--- to_date ---")
30
+ print(f" 2026/9/11 → {to_date(2026, 9, 11)}")
31
+
32
+ # 5. التاريخ الترتيبي
33
+ print("\n--- to_ordinal_date ---")
34
+ print(f" 2026/9/11 → {to_ordinal_date(2026, 9, 11)}")
35
+
36
+ # 6. الوقت
37
+ print("\n--- to_time ---")
38
+ print(f" 11:25:43 → {to_time(11, 25, 43, 'مساءً')}")
39
+
40
+ print("\n" + "=" * 60)
41
+ print(" ✅ كل الدوال تعمل بنجاح!")
42
+ print("=" * 60)
@@ -0,0 +1,25 @@
1
+ """مكتبة تحويل الأرقام إلى كلمات عربية بدقة نحوية."""
2
+ from .core import (
3
+ to_arabic,
4
+ to_currency,
5
+ to_ordinal,
6
+ to_ordinal_date,
7
+ to_date,
8
+ to_time,
9
+ CURRENCIES,
10
+ MONTHS_GREGORIAN,
11
+ MONTHS_HIJRI,
12
+ )
13
+
14
+ __version__ = "2.0.2"
15
+ __all__ = [
16
+ "to_arabic",
17
+ "to_currency",
18
+ "to_ordinal",
19
+ "to_ordinal_date",
20
+ "to_date",
21
+ "to_time",
22
+ "CURRENCIES",
23
+ "MONTHS_GREGORIAN",
24
+ "MONTHS_HIJRI",
25
+ ]
@@ -0,0 +1,610 @@
1
+ """
2
+ مكتبة تحويل الأرقام إلى كلمات عربية بدقة نحوية.
3
+ الإصدار: 2.0.2
4
+
5
+ تدعم:
6
+ - الأعداد من 0 إلى 20 منزلة
7
+ - المذكر والمؤنث
8
+ - العملات (ريال، جنيه، دينار، درهم، دولار)
9
+ - المثنى والجمع والتمييز
10
+ - الأعداد السالبة
11
+ - الأعداد الترتيبية (الأول، الثاني، الثالث...)
12
+ - التواريخ (ميلادي وهجري)
13
+ - الوقت (الساعة والدقائق والثواني)
14
+ """
15
+
16
+ __version__ = "2.0.2"
17
+ __all__ = [
18
+ "to_arabic",
19
+ "to_currency",
20
+ "to_ordinal",
21
+ "to_ordinal_date",
22
+ "to_date",
23
+ "to_time",
24
+ "CURRENCIES",
25
+ "MONTHS_GREGORIAN",
26
+ "MONTHS_HIJRI",
27
+ ]
28
+
29
+
30
+ # ============================================================
31
+ # قواميس الآحاد والعشرات والمئات (أصلية)
32
+ # ============================================================
33
+
34
+ ONES_MASCULINE = {
35
+ 0: "صفر", 1: "واحد", 2: "اثنان", 3: "ثلاثة", 4: "أربعة",
36
+ 5: "خمسة", 6: "ستة", 7: "سبعة", 8: "ثمانية", 9: "تسعة",
37
+ 10: "عشرة",
38
+ }
39
+
40
+ ONES_FEMININE = {
41
+ 0: "صفر", 1: "واحدة", 2: "اثنتان", 3: "ثلاث", 4: "أربع",
42
+ 5: "خمس", 6: "ست", 7: "سبع", 8: "ثمان", 9: "تسع",
43
+ 10: "عشر",
44
+ }
45
+
46
+ TEENS_MASCULINE = {
47
+ 11: "أحد عشر", 12: "اثنا عشر", 13: "ثلاثة عشر", 14: "أربعة عشر",
48
+ 15: "خمسة عشر", 16: "ستة عشر", 17: "سبعة عشر", 18: "ثمانية عشر",
49
+ 19: "تسعة عشر",
50
+ }
51
+
52
+ TEENS_FEMININE = {
53
+ 11: "إحدى عشرة", 12: "اثنتا عشرة", 13: "ثلاث عشرة", 14: "أربع عشرة",
54
+ 15: "خمس عشرة", 16: "ست عشرة", 17: "سبع عشرة", 18: "ثماني عشرة",
55
+ 19: "تسع عشرة",
56
+ }
57
+
58
+ TENS = {
59
+ 20: "عشرون", 30: "ثلاثون", 40: "أربعون", 50: "خمسون",
60
+ 60: "ستون", 70: "سبعون", 80: "ثمانون", 90: "تسعون",
61
+ }
62
+
63
+ HUNDREDS = {
64
+ 100: "مئة", 200: "مئتان", 300: "ثلاثمئة", 400: "أربعمئة",
65
+ 500: "خمسمئة", 600: "ستمئة", 700: "سبعمئة", 800: "ثمانمئة",
66
+ 900: "تسعمئة",
67
+ }
68
+
69
+ # المئات في حالة الإضافة (بحذف النون)
70
+ HUNDREDS_CONSTRUCT = {
71
+ 100: "مئة",
72
+ 200: "مئتا",
73
+ 300: "ثلاثمئة",
74
+ 400: "أربعمئة",
75
+ 500: "خمسمئة",
76
+ 600: "ستمئة",
77
+ 700: "سبعمئة",
78
+ 800: "ثمانمئة",
79
+ 900: "تسعمئة",
80
+ }
81
+
82
+
83
+ # ============================================================
84
+ # المقاييس الكبيرة
85
+ # ============================================================
86
+
87
+ SCALES = [
88
+ (10**18, "كوينتليون", "كوينتليونان", "كوينتليونات", "كوينتليونًا"),
89
+ (10**15, "كوادريليون", "كوادريليونان", "كوادريليونات", "كوادريليونًا"),
90
+ (10**12, "تريليون", "تريليونان", "تريليونات", "تريليونًا"),
91
+ (10**9, "مليار", "ملياران", "مليارات", "مليارًا"),
92
+ (10**6, "مليون", "مليونان", "ملايين", "مليونًا"),
93
+ (10**3, "ألف", "ألفان", "آلاف", "ألفًا"),
94
+ ]
95
+
96
+
97
+ # ============================================================
98
+ # العملات المدعومة
99
+ # ============================================================
100
+
101
+ CURRENCIES = {
102
+ "SAR": {
103
+ "name": "ريال", "dual": "ريالان", "plural": "ريالات",
104
+ "accusative": "ريالاً",
105
+ "fraction": "هللة", "fraction_plural": "هللات",
106
+ "fraction_accusative": "هللة",
107
+ },
108
+ "EGP": {
109
+ "name": "جنيه", "dual": "جنيهان", "plural": "جنيهات",
110
+ "accusative": "جنيهًا",
111
+ "fraction": "قرش", "fraction_plural": "قروش",
112
+ "fraction_accusative": "قرشًا",
113
+ },
114
+ "KWD": {
115
+ "name": "دينار", "dual": "ديناران", "plural": "دنانير",
116
+ "accusative": "دينارًا",
117
+ "fraction": "فلس", "fraction_plural": "فلوس",
118
+ "fraction_accusative": "فلسًا",
119
+ },
120
+ "AED": {
121
+ "name": "درهم", "dual": "درهمان", "plural": "دراهم",
122
+ "accusative": "درهمًا",
123
+ "fraction": "فلس", "fraction_plural": "فلوس",
124
+ "fraction_accusative": "فلسًا",
125
+ },
126
+ "USD": {
127
+ "name": "دولار", "dual": "دولاران", "plural": "دولارات",
128
+ "accusative": "دولارًا",
129
+ "fraction": "سنت", "fraction_plural": "سنتات",
130
+ "fraction_accusative": "سنتًا",
131
+ },
132
+ }
133
+
134
+
135
+ # ============================================================
136
+ # الأشهر الميلادية والهجرية
137
+ # ============================================================
138
+
139
+ MONTHS_GREGORIAN = {
140
+ 1: "يناير", 2: "فبراير", 3: "مارس", 4: "أبريل",
141
+ 5: "مايو", 6: "يونيو", 7: "يوليو", 8: "أغسطس",
142
+ 9: "سبتمبر", 10: "أكتوبر", 11: "نوفمبر", 12: "ديسمبر",
143
+ }
144
+
145
+ MONTHS_HIJRI = {
146
+ 1: "محرم", 2: "صفر", 3: "ربيع الأول", 4: "ربيع الآخر",
147
+ 5: "جمادى الأولى", 6: "جمادى الآخرة", 7: "رجب", 8: "شعبان",
148
+ 9: "رمضان", 10: "شوال", 11: "ذو القعدة", 12: "ذو الحجة",
149
+ }
150
+
151
+
152
+ # ============================================================
153
+ # الأعداد الترتيبية (Ordinal)
154
+ # ============================================================
155
+
156
+ ORDINAL_ONES_MASCULINE = {
157
+ 1: "الأول", 2: "الثاني", 3: "الثالث", 4: "الرابع",
158
+ 5: "الخامس", 6: "السادس", 7: "السابع", 8: "الثامن",
159
+ 9: "التاسع", 10: "العاشر",
160
+ }
161
+
162
+ ORDINAL_ONES_FEMININE = {
163
+ 1: "الأولى", 2: "الثانية", 3: "الثالثة", 4: "الرابعة",
164
+ 5: "الخامسة", 6: "السادسة", 7: "السابعة", 8: "الثامنة",
165
+ 9: "التاسعة", 10: "العاشرة",
166
+ }
167
+
168
+ ORDINAL_ONES_COMPOUND_MASCULINE = {
169
+ 1: "الحادي", 2: "الثاني", 3: "الثالث", 4: "الرابع",
170
+ 5: "الخامس", 6: "السادس", 7: "السابع", 8: "الثامن",
171
+ 9: "التاسع",
172
+ }
173
+
174
+ ORDINAL_ONES_COMPOUND_FEMININE = {
175
+ 1: "الحادية", 2: "الثانية", 3: "الثالثة", 4: "الرابعة",
176
+ 5: "الخامسة", 6: "السادسة", 7: "السابعة", 8: "الثامنة",
177
+ 9: "التاسعة",
178
+ }
179
+
180
+ ORDINAL_TEENS_MASCULINE = {
181
+ 11: "الحادي عشر", 12: "الثاني عشر", 13: "الثالث عشر",
182
+ 14: "الرابع عشر", 15: "الخامس عشر", 16: "السادس عشر",
183
+ 17: "السابع عشر", 18: "الثامن عشر", 19: "التاسع عشر",
184
+ }
185
+
186
+ ORDINAL_TEENS_FEMININE = {
187
+ 11: "الحادية عشرة", 12: "الثانية عشرة", 13: "الثالثة عشرة",
188
+ 14: "الرابعة عشرة", 15: "الخامسة عشرة", 16: "السادسة عشرة",
189
+ 17: "السابعة عشرة", 18: "الثامنة عشرة", 19: "التاسعة عشرة",
190
+ }
191
+
192
+ ORDINAL_TENS = {
193
+ 20: "العشرون", 30: "الثلاثون", 40: "الأربعون", 50: "الخمسون",
194
+ 60: "الستون", 70: "السبعون", 80: "الثمانون", 90: "التسعون",
195
+ }
196
+
197
+ ORDINAL_HUNDREDS = {
198
+ 100: "المئة", 200: "المئتان", 300: "الثلاثمئة", 400: "الأربعمئة",
199
+ 500: "الخمسمئة", 600: "الستمئة", 700: "السبعمئة", 800: "الثمانمئة",
200
+ 900: "التسعمئة",
201
+ }
202
+
203
+
204
+ # ============================================================
205
+ # الدوال المساعدة للأعداد الأصلية
206
+ # ============================================================
207
+
208
+ def _two_digits(number: int, feminine: bool = False) -> str:
209
+ """تحوّل عددًا من 0 إلى 99 إلى كلمات."""
210
+ if number <= 10:
211
+ return (ONES_FEMININE if feminine else ONES_MASCULINE)[number]
212
+ if 11 <= number <= 19:
213
+ return (TEENS_FEMININE if feminine else TEENS_MASCULINE)[number]
214
+ if number in TENS:
215
+ return TENS[number]
216
+
217
+ ones = number % 10
218
+ tens = number - ones
219
+ ones_word = (ONES_FEMININE if feminine else ONES_MASCULINE)[ones]
220
+ return f"{ones_word} و{TENS[tens]}"
221
+
222
+
223
+ def _three_digits(number: int, feminine: bool = False, construct: bool = False) -> str:
224
+ """تحوّل عددًا من 0 إلى 999 إلى كلمات.
225
+
226
+ construct: إذا كان True، نستخدم صيغة الإضافة (مئتا بدل مئتان).
227
+ """
228
+ if number < 100:
229
+ return _two_digits(number, feminine)
230
+
231
+ hundreds = (number // 100) * 100
232
+ remainder = number % 100
233
+
234
+ if remainder == 0:
235
+ return HUNDREDS_CONSTRUCT[hundreds] if construct else HUNDREDS[hundreds]
236
+
237
+ hundreds_word = HUNDREDS_CONSTRUCT[hundreds] if construct else HUNDREDS[hundreds]
238
+ return f"{hundreds_word} و{_two_digits(remainder, feminine)}"
239
+
240
+
241
+ def _effective_count(number: int) -> int:
242
+ """
243
+ ترجع العدد الفعلي الذي يحدد الصيغة النحوية للاسم التالي.
244
+ """
245
+ if number == 0:
246
+ return 0
247
+
248
+ temp = number
249
+ while temp > 0:
250
+ group = temp % 1000
251
+ if group != 0:
252
+ if group >= 100:
253
+ last_two = group % 100
254
+ return last_two if last_two != 0 else group
255
+ return group
256
+ temp //= 1000
257
+
258
+ return 0
259
+
260
+
261
+ def _scale_form(count: int, scale: tuple) -> str:
262
+ """ترجع الصيغة النحوية الصحيحة لاسم المقياس حسب العدد."""
263
+ _, singular, dual, plural, accusative = scale
264
+
265
+ if count == 1:
266
+ return singular
267
+ if count == 2:
268
+ return dual
269
+ if 3 <= count <= 10:
270
+ return plural
271
+ if 11 <= count <= 99:
272
+ return accusative
273
+ return singular
274
+
275
+
276
+ def _currency_form(count: int, currency: dict, is_fraction: bool = False) -> str:
277
+ """ترجع الصيغة النحوية الصحيحة لاسم العملة حسب العدد."""
278
+ if is_fraction:
279
+ scale = (
280
+ 0,
281
+ currency["fraction"],
282
+ currency["fraction"],
283
+ currency["fraction_plural"],
284
+ currency["fraction_accusative"],
285
+ )
286
+ else:
287
+ scale = (
288
+ 0,
289
+ currency["name"],
290
+ currency["dual"],
291
+ currency["plural"],
292
+ currency["accusative"],
293
+ )
294
+ return _scale_form(count, scale)
295
+
296
+
297
+ # ============================================================
298
+ # الدوال المساعدة للأعداد الترتيبية
299
+ # ============================================================
300
+
301
+ def _two_digits_ordinal(number: int, feminine: bool = False) -> str:
302
+ """تحوّل عددًا من 1 إلى 99 إلى ترتيبي."""
303
+ if number <= 10:
304
+ return (ORDINAL_ONES_FEMININE if feminine else ORDINAL_ONES_MASCULINE)[number]
305
+ if 11 <= number <= 19:
306
+ return (ORDINAL_TEENS_FEMININE if feminine else ORDINAL_TEENS_MASCULINE)[number]
307
+ if number in ORDINAL_TENS:
308
+ return ORDINAL_TENS[number]
309
+
310
+ ones = number % 10
311
+ tens = number - ones
312
+
313
+ if ones == 0:
314
+ return ORDINAL_TENS[tens]
315
+
316
+ ones_word = (
317
+ ORDINAL_ONES_COMPOUND_FEMININE if feminine
318
+ else ORDINAL_ONES_COMPOUND_MASCULINE
319
+ )[ones]
320
+
321
+ return f"{ones_word} و{ORDINAL_TENS[tens]}"
322
+
323
+
324
+ # ============================================================
325
+ # الدالة الرئيسية: to_arabic
326
+ # ============================================================
327
+
328
+ def to_arabic(number: int, feminine: bool = False) -> str:
329
+ """
330
+ تحوّل عددًا صحيحًا (حتى 20 منزلة) إلى كلمات عربية بدقة نحوية.
331
+
332
+ أمثلة:
333
+ >>> to_arabic(1234)
334
+ 'ألف ومئتان وأربعة وثلاثون'
335
+ >>> to_arabic(200000)
336
+ 'مئتا ألف'
337
+ >>> to_arabic(3, feminine=True)
338
+ 'ثلاث'
339
+ """
340
+ if not isinstance(number, int):
341
+ raise TypeError("يجب أن يكون المدخل عددًا صحيحًا")
342
+
343
+ MAX_20_DIGITS = 10**20 - 1
344
+ if number > MAX_20_DIGITS:
345
+ raise ValueError("الحد الأقصى هو 20 منزلة")
346
+
347
+ if number < 0:
348
+ return "سالب " + to_arabic(-number, feminine)
349
+
350
+ if number == 0:
351
+ return "صفر"
352
+
353
+ parts = []
354
+ remainder = number
355
+
356
+ for scale in SCALES:
357
+ scale_value = scale[0]
358
+ if remainder >= scale_value:
359
+ count = remainder // scale_value
360
+ remainder = remainder % scale_value
361
+
362
+ scale_name = _scale_form(count, scale)
363
+
364
+ if count == 1:
365
+ parts.append(scale_name)
366
+ elif count == 2:
367
+ parts.append(scale_name)
368
+ else:
369
+ # نمرر construct=True لأن العدد متبوع بمقياس
370
+ count_words = _three_digits(count, feminine, construct=True)
371
+ parts.append(f"{count_words} {scale_name}")
372
+
373
+ if remainder > 0:
374
+ parts.append(_three_digits(remainder, feminine, construct=False))
375
+
376
+ return " و".join(parts)
377
+
378
+
379
+ # ============================================================
380
+ # دالة العملات: to_currency
381
+ # ============================================================
382
+
383
+ def to_currency(amount: float, code: str = "SAR", feminine: bool = False) -> str:
384
+ """
385
+ تحوّل مبلغًا ماليًا إلى كلمات عربية مع اسم العملة.
386
+
387
+ أمثلة:
388
+ >>> to_currency(1, "SAR")
389
+ 'ريال'
390
+ >>> to_currency(200, "SAR")
391
+ 'مئتا ريال'
392
+ >>> to_currency(1234.56, "SAR")
393
+ 'ألف ومئتان وأربعة وثلاثون ريالاً وست وخمسون هللة'
394
+ """
395
+ if code not in CURRENCIES:
396
+ raise ValueError(f"العملة غير مدعومة: {code}. المدعوم: {list(CURRENCIES.keys())}")
397
+
398
+ if not isinstance(amount, (int, float)):
399
+ raise TypeError("يجب أن يكون المبلغ رقمًا")
400
+
401
+ if amount < 0:
402
+ return "سالب " + to_currency(-amount, code, feminine)
403
+
404
+ currency = CURRENCIES[code]
405
+
406
+ integer_part = int(amount)
407
+ fraction_part = round((amount - integer_part) * 100)
408
+
409
+ if fraction_part == 100:
410
+ integer_part += 1
411
+ fraction_part = 0
412
+
413
+ parts = []
414
+
415
+ if integer_part > 0:
416
+ effective = _effective_count(integer_part)
417
+ currency_word = _currency_form(effective, currency, is_fraction=False)
418
+
419
+ if integer_part == 1:
420
+ parts.append(currency_word)
421
+ elif integer_part == 2:
422
+ parts.append(currency_word)
423
+ else:
424
+ # نمرر construct=True لأن العدد متبوع باسم العملة
425
+ words = _to_arabic_construct(integer_part, feminine)
426
+ parts.append(f"{words} {currency_word}")
427
+
428
+ if fraction_part > 0:
429
+ effective = _effective_count(fraction_part)
430
+ fraction_word = _currency_form(effective, currency, is_fraction=True)
431
+
432
+ if fraction_part == 1:
433
+ parts.append(fraction_word)
434
+ elif fraction_part == 2:
435
+ parts.append(fraction_word)
436
+ else:
437
+ # الكسور مؤنثة دائمًا
438
+ words = _to_arabic_construct(fraction_part, feminine=True)
439
+ parts.append(f"{words} {fraction_word}")
440
+
441
+ if not parts:
442
+ return "صفر"
443
+
444
+ return " و".join(parts)
445
+
446
+
447
+ def _to_arabic_construct(number: int, feminine: bool = False) -> str:
448
+ """
449
+ نسخة من to_arabic تستخدم صيغة الإضافة للمئات.
450
+ تُستخدم عندما يكون العدد متبوعًا باسم (عملة، مقياس...).
451
+ """
452
+ if number < 100:
453
+ return _two_digits(number, feminine)
454
+
455
+ if number < 1000:
456
+ return _three_digits(number, feminine, construct=True)
457
+
458
+ # للأعداد الكبيرة، نستخدم to_arabic العادية
459
+ return to_arabic(number, feminine)
460
+
461
+
462
+ # ============================================================
463
+ # دالة الترتيب: to_ordinal
464
+ # ============================================================
465
+
466
+ def to_ordinal(number: int, feminine: bool = False) -> str:
467
+ """
468
+ تحوّل عددًا إلى ترتيبي عربي.
469
+
470
+ أمثلة:
471
+ >>> to_ordinal(1)
472
+ 'الأول'
473
+ >>> to_ordinal(21)
474
+ 'الحادي والعشرون'
475
+ >>> to_ordinal(100)
476
+ 'المئة'
477
+ """
478
+ if not isinstance(number, int):
479
+ raise TypeError("يجب أن يكون المدخل عددًا صحيحًا")
480
+ if number < 1:
481
+ raise ValueError("الترتيب يبدأ من 1")
482
+
483
+ if number < 100:
484
+ return _two_digits_ordinal(number, feminine)
485
+
486
+ if number < 1000:
487
+ hundreds = (number // 100) * 100
488
+ remainder = number % 100
489
+
490
+ if remainder == 0:
491
+ return ORDINAL_HUNDREDS[hundreds]
492
+
493
+ return f"{ORDINAL_HUNDREDS[hundreds]} و{_two_digits_ordinal(remainder, feminine)}"
494
+
495
+ return "ال" + to_arabic(number, feminine)
496
+
497
+
498
+ # ============================================================
499
+ # دالة التاريخ: to_date
500
+ # ============================================================
501
+
502
+ def to_date(year: int, month: int, day: int, hijri: bool = False) -> str:
503
+ """
504
+ تحوّل تاريخًا إلى كلمات عربية.
505
+
506
+ أمثلة:
507
+ >>> to_date(2026, 9, 11)
508
+ 'الحادي عشر من سبتمبر عام ألفان وستة وعشرون'
509
+ >>> to_date(1448, 3, 1, hijri=True)
510
+ 'الأول من ربيع الأول عام ألف وأربعمئة وثمانية وأربعين'
511
+ """
512
+ if not (1 <= month <= 12):
513
+ raise ValueError("الشهر يجب أن يكون بين 1 و 12")
514
+ if not (1 <= day <= 31):
515
+ raise ValueError("اليوم يجب أن يكون بين 1 و 31")
516
+
517
+ months = MONTHS_HIJRI if hijri else MONTHS_GREGORIAN
518
+ month_name = months[month]
519
+ day_word = to_ordinal(day)
520
+ year_word = to_arabic(year)
521
+
522
+ return f"{day_word} من {month_name} عام {year_word}"
523
+
524
+
525
+ # ============================================================
526
+ # دالة التاريخ الترتيبي: to_ordinal_date
527
+ # ============================================================
528
+
529
+ def to_ordinal_date(year: int, month: int, day: int, hijri: bool = False) -> str:
530
+ """
531
+ تحوّل تاريخًا إلى صيغة ترتيبية عربية (بدون السنة).
532
+
533
+ أمثلة:
534
+ >>> to_ordinal_date(2026, 9, 11)
535
+ 'الحادي عشر من سبتمبر'
536
+ """
537
+ if not (1 <= month <= 12):
538
+ raise ValueError("الشهر يجب أن يكون بين 1 و 12")
539
+ if not (1 <= day <= 31):
540
+ raise ValueError("اليوم يجب أن يكون بين 1 و 31")
541
+
542
+ months = MONTHS_HIJRI if hijri else MONTHS_GREGORIAN
543
+ month_name = months[month]
544
+ day_word = to_ordinal(day)
545
+
546
+ return f"{day_word} من {month_name}"
547
+
548
+
549
+ # ============================================================
550
+ # دالة الوقت: to_time
551
+ # ============================================================
552
+
553
+ def to_time(hour: int, minute: int = 0, second: int = 0, period: str = None) -> str:
554
+ """
555
+ تحوّل وقتًا إلى كلمات عربية.
556
+
557
+ المعاملات:
558
+ hour: الساعة (0-23).
559
+ minute: الدقائق (0-59).
560
+ second: الثواني (0-59).
561
+ period: "صباحًا" أو "مساءً" (اختياري، يُحدد تلقائيًا).
562
+
563
+ أمثلة:
564
+ >>> to_time(11, 25, 43, "مساءً")
565
+ 'الحادية عشرة مساءً وخمسة وعشرون دقيقة وثلاثة وأربعون ثانية'
566
+ >>> to_time(9, 0, 0, "صباحًا")
567
+ 'التاسعة صباحًا'
568
+ """
569
+ if not (0 <= hour <= 23):
570
+ raise ValueError("الساعة يجب أن تكون بين 0 و 23")
571
+ if not (0 <= minute <= 59):
572
+ raise ValueError("الدقائق يجب أن تكون بين 0 و 59")
573
+ if not (0 <= second <= 59):
574
+ raise ValueError("الثواني يجب أن تكون بين 0 و 59")
575
+
576
+ if period is None:
577
+ if 0 <= hour < 12:
578
+ period = "صباحًا"
579
+ elif hour == 12:
580
+ period = "ظهرًا"
581
+ else:
582
+ period = "مساءً"
583
+
584
+ hour_12 = hour % 12
585
+ if hour_12 == 0:
586
+ hour_12 = 12
587
+
588
+ hour_word = to_ordinal(hour_12, feminine=True)
589
+
590
+ parts = [f"{hour_word} {period}"]
591
+
592
+ if minute > 0:
593
+ if minute == 1:
594
+ parts.append("دقيقة واحدة")
595
+ elif minute == 2:
596
+ parts.append("دقيقتان")
597
+ else:
598
+ minute_word = to_arabic(minute)
599
+ parts.append(f"{minute_word} دقيقة")
600
+
601
+ if second > 0:
602
+ if second == 1:
603
+ parts.append("ثانية واحدة")
604
+ elif second == 2:
605
+ parts.append("ثانيتان")
606
+ else:
607
+ second_word = to_arabic(second)
608
+ parts.append(f"{second_word} ثانية")
609
+
610
+ return " و".join(parts)
@@ -0,0 +1,478 @@
1
+ """
2
+ اختبارات شاملة لمكتبة arabicnumbers.
3
+ تشغيل: pytest test_arabicnumbers.py -v
4
+ """
5
+ import pytest
6
+ from lam_arabicnumbers import (
7
+ to_arabic, to_currency, to_ordinal,
8
+ to_ordinal_date, to_date, to_time,
9
+ CURRENCIES, MONTHS_GREGORIAN, MONTHS_HIJRI,
10
+ )
11
+
12
+ # ============================================================
13
+ # 1. اختبارات to_arabic — الأساسيات (0-99)
14
+ # ============================================================
15
+
16
+ def test_zero():
17
+ assert to_arabic(0) == "صفر"
18
+
19
+
20
+ def test_ones():
21
+ assert to_arabic(1) == "واحد"
22
+ assert to_arabic(2) == "اثنان"
23
+ assert to_arabic(3) == "ثلاثة"
24
+ assert to_arabic(5) == "خمسة"
25
+ assert to_arabic(9) == "تسعة"
26
+ assert to_arabic(10) == "عشرة"
27
+
28
+
29
+ def test_teens():
30
+ assert to_arabic(11) == "أحد عشر"
31
+ assert to_arabic(12) == "اثنا عشر"
32
+ assert to_arabic(13) == "ثلاثة عشر"
33
+ assert to_arabic(19) == "تسعة عشر"
34
+
35
+
36
+ def test_tens():
37
+ assert to_arabic(20) == "عشرون"
38
+ assert to_arabic(30) == "ثلاثون"
39
+ assert to_arabic(40) == "أربعون"
40
+ assert to_arabic(50) == "خمسون"
41
+ assert to_arabic(60) == "ستون"
42
+ assert to_arabic(70) == "سبعون"
43
+ assert to_arabic(80) == "ثمانون"
44
+ assert to_arabic(90) == "تسعون"
45
+
46
+
47
+ def test_compound():
48
+ assert to_arabic(21) == "واحد وعشرون"
49
+ assert to_arabic(25) == "خمسة وعشرون"
50
+ assert to_arabic(44) == "أربعة وأربعون"
51
+ assert to_arabic(99) == "تسعة وتسعون"
52
+
53
+
54
+ # ============================================================
55
+ # 2. اختبارات to_arabic — المئات (100-999)
56
+ # ============================================================
57
+
58
+ def test_hundreds():
59
+ assert to_arabic(100) == "مئة"
60
+ assert to_arabic(200) == "مئتان"
61
+ assert to_arabic(300) == "ثلاثمئة"
62
+ assert to_arabic(900) == "تسعمئة"
63
+
64
+
65
+ def test_hundreds_with_remainder():
66
+ assert to_arabic(101) == "مئة وواحد"
67
+ assert to_arabic(250) == "مئتان وخمسون"
68
+ assert to_arabic(999) == "تسعمئة وتسعة وتسعون"
69
+
70
+
71
+ # ============================================================
72
+ # 3. اختبارات to_arabic — الآلاف
73
+ # ============================================================
74
+
75
+ def test_thousand_singular():
76
+ assert to_arabic(1000) == "ألف"
77
+
78
+
79
+ def test_thousand_dual():
80
+ assert to_arabic(2000) == "ألفان"
81
+
82
+
83
+ def test_thousand_plural():
84
+ assert to_arabic(3000) == "ثلاثة آلاف"
85
+ assert to_arabic(10000) == "عشرة آلاف"
86
+
87
+
88
+ def test_thousand_accusative():
89
+ assert to_arabic(11000) == "أحد عشر ألفًا"
90
+ assert to_arabic(99000) == "تسعة وتسعون ألفًا"
91
+
92
+
93
+ def test_thousand_hundreds():
94
+ assert to_arabic(100000) == "مئة ألف"
95
+ assert to_arabic(200000) == "مئتا ألف"
96
+ # 999 >= 100، إذن نستخدم المفرد المجرد "ألف" وليس "ألفًا"
97
+ assert to_arabic(999999) == "تسعمئة وتسعة وتسعون ألف وتسعمئة وتسعة وتسعون"
98
+
99
+ def test_thousand_compound():
100
+ assert to_arabic(1234) == "ألف ومئتان وأربعة وثلاثون"
101
+ assert to_arabic(1500) == "ألف وخمسمئة"
102
+ assert to_arabic(1001) == "ألف وواحد"
103
+
104
+
105
+ # ============================================================
106
+ # 4. اختبارات to_arabic — الملايين والمليارات
107
+ # ============================================================
108
+
109
+ def test_million():
110
+ assert to_arabic(10**6) == "مليون"
111
+ assert to_arabic(2 * 10**6) == "مليونان"
112
+ assert to_arabic(3 * 10**6) == "ثلاثة ملايين"
113
+ assert to_arabic(11 * 10**6) == "أحد عشر مليونًا"
114
+ assert to_arabic(100 * 10**6) == "مئة مليون"
115
+
116
+
117
+ def test_billion():
118
+ assert to_arabic(10**9) == "مليار"
119
+ assert to_arabic(2 * 10**9) == "ملياران"
120
+ assert to_arabic(3 * 10**9) == "ثلاثة مليارات"
121
+
122
+
123
+ def test_trillion_and_above():
124
+ assert to_arabic(10**12) == "تريليون"
125
+ assert to_arabic(10**15) == "كوادريليون"
126
+ assert to_arabic(10**18) == "كوينتليون"
127
+
128
+
129
+ def test_complex_large_numbers():
130
+ result = to_arabic(123456789)
131
+ assert "مليون" in result
132
+ assert "ألف" in result
133
+
134
+ result = to_arabic(10**20 - 1)
135
+ assert "كوينتليون" in result
136
+
137
+
138
+ # ============================================================
139
+ # 5. اختبارات to_arabic — المذكر والمؤنث
140
+ # ============================================================
141
+
142
+ def test_feminine_ones():
143
+ assert to_arabic(1, feminine=True) == "واحدة"
144
+ assert to_arabic(2, feminine=True) == "اثنتان"
145
+ assert to_arabic(3, feminine=True) == "ثلاث"
146
+ assert to_arabic(10, feminine=True) == "عشر"
147
+
148
+
149
+ def test_feminine_teens():
150
+ assert to_arabic(11, feminine=True) == "إحدى عشرة"
151
+ assert to_arabic(13, feminine=True) == "ثلاث عشرة"
152
+ assert to_arabic(15, feminine=True) == "خمس عشرة"
153
+
154
+
155
+ def test_feminine_compound():
156
+ assert to_arabic(21, feminine=True) == "واحدة وعشرون"
157
+ assert to_arabic(23, feminine=True) == "ثلاث وعشرون"
158
+
159
+
160
+ # ============================================================
161
+ # 6. اختبارات to_arabic — الأعداد السالبة
162
+ # ============================================================
163
+
164
+ def test_negative():
165
+ assert to_arabic(-1) == "سالب واحد"
166
+ assert to_arabic(-5) == "سالب خمسة"
167
+ assert to_arabic(-100) == "سالب مئة"
168
+ assert to_arabic(-1000) == "سالب ألف"
169
+
170
+
171
+ # ============================================================
172
+ # 7. اختبارات to_arabic — الأخطاء
173
+ # ============================================================
174
+
175
+ def test_type_error_string():
176
+ with pytest.raises(TypeError):
177
+ to_arabic("5")
178
+
179
+
180
+ def test_type_error_float():
181
+ with pytest.raises(TypeError):
182
+ to_arabic(5.5)
183
+
184
+
185
+ def test_value_error_too_large():
186
+ with pytest.raises(ValueError):
187
+ to_arabic(10**20)
188
+
189
+
190
+ # ============================================================
191
+ # 8. اختبارات to_currency — العملات
192
+ # ============================================================
193
+
194
+ def test_currency_singular():
195
+ assert to_currency(1, "SAR") == "ريال"
196
+ assert to_currency(1, "EGP") == "جنيه"
197
+ assert to_currency(1, "KWD") == "دينار"
198
+
199
+
200
+ def test_currency_dual():
201
+ assert to_currency(2, "SAR") == "ريالان"
202
+ assert to_currency(2, "EGP") == "جنيهان"
203
+
204
+
205
+ def test_currency_plural():
206
+ assert to_currency(3, "SAR") == "ثلاثة ريالات"
207
+ assert to_currency(10, "SAR") == "عشرة ريالات"
208
+
209
+
210
+ def test_currency_accusative():
211
+ assert to_currency(11, "SAR") == "أحد عشر ريالاً"
212
+ assert to_currency(45, "SAR") == "خمسة وأربعون ريالاً"
213
+
214
+
215
+ def test_currency_hundreds():
216
+ assert to_currency(100, "SAR") == "مئة ريال"
217
+ assert to_currency(200, "SAR") == "مئتا ريال"
218
+ assert to_currency(1000, "SAR") == "ألف ريال"
219
+
220
+
221
+ def test_currency_with_fraction():
222
+ assert to_currency(1.50, "SAR") == "ريال وخمسون هللة"
223
+ # "خمس" وليس "خمسة" لأن هللة مؤنثة
224
+ assert to_currency(2.25, "SAR") == "ريالان وخمس وعشرون هللة"
225
+ assert to_currency(1234.56, "SAR") == (
226
+ "ألف ومئتان وأربعة وثلاثون ريالاً وست وخمسون هللة"
227
+ )
228
+
229
+
230
+ def test_currency_zero():
231
+ assert to_currency(0, "SAR") == "صفر"
232
+
233
+
234
+ def test_currency_negative():
235
+ assert to_currency(-45, "SAR") == "سالب خمسة وأربعون ريالاً"
236
+
237
+
238
+ def test_currency_unsupported():
239
+ with pytest.raises(ValueError):
240
+ to_currency(100, "XYZ")
241
+
242
+
243
+ def test_currency_type_error():
244
+ with pytest.raises(TypeError):
245
+ to_currency("100", "SAR")
246
+
247
+
248
+ # ============================================================
249
+ # 9. اختبارات to_ordinal — الأعداد الترتيبية
250
+ # ============================================================
251
+
252
+ def test_ordinal_ones():
253
+ assert to_ordinal(1) == "الأول"
254
+ assert to_ordinal(2) == "الثاني"
255
+ assert to_ordinal(3) == "الثالث"
256
+ assert to_ordinal(10) == "العاشر"
257
+
258
+
259
+ def test_ordinal_feminine():
260
+ assert to_ordinal(1, feminine=True) == "الأولى"
261
+ assert to_ordinal(3, feminine=True) == "الثالثة"
262
+
263
+
264
+ def test_ordinal_teens():
265
+ assert to_ordinal(11) == "الحادي عشر"
266
+ assert to_ordinal(12) == "الثاني عشر"
267
+ assert to_ordinal(15, feminine=True) == "الخامسة عشرة"
268
+
269
+
270
+ def test_ordinal_tens():
271
+ assert to_ordinal(20) == "العشرون"
272
+ assert to_ordinal(30) == "الثلاثون"
273
+
274
+
275
+ def test_ordinal_compound():
276
+ assert to_ordinal(21) == "الحادي والعشرون"
277
+ assert to_ordinal(22) == "الثاني والعشرون"
278
+ assert to_ordinal(31) == "الحادي والثلاثون"
279
+ assert to_ordinal(99) == "التاسع والتسعون"
280
+
281
+
282
+ def test_ordinal_compound_feminine():
283
+ assert to_ordinal(21, feminine=True) == "الحادية والعشرون"
284
+
285
+
286
+ def test_ordinal_hundreds():
287
+ assert to_ordinal(100) == "المئة"
288
+ assert to_ordinal(200) == "المئتان"
289
+
290
+
291
+ def test_ordinal_hundreds_compound():
292
+ assert to_ordinal(101) == "المئة والأول"
293
+ assert to_ordinal(123) == "المئة والثالث والعشرون"
294
+
295
+
296
+ def test_ordinal_errors():
297
+ with pytest.raises(ValueError):
298
+ to_ordinal(0)
299
+ with pytest.raises(TypeError):
300
+ to_ordinal("1")
301
+
302
+
303
+ # ============================================================
304
+ # 10. اختبارات to_date — التواريخ
305
+ # ============================================================
306
+
307
+ def test_date_gregorian():
308
+ result = to_date(2026, 9, 11)
309
+ assert "الحادي عشر" in result
310
+ assert "سبتمبر" in result
311
+ assert "ألفان وستة وعشرون" in result
312
+
313
+
314
+ def test_date_hijri():
315
+ result = to_date(1448, 3, 1, hijri=True)
316
+ assert "الأول" in result
317
+ assert "ربيع الأول" in result
318
+
319
+
320
+ def test_date_first_day():
321
+ result = to_date(2026, 1, 1)
322
+ assert result.startswith("الأول من يناير")
323
+
324
+
325
+ def test_date_errors():
326
+ with pytest.raises(ValueError):
327
+ to_date(2026, 13, 1)
328
+ with pytest.raises(ValueError):
329
+ to_date(2026, 1, 32)
330
+
331
+
332
+ # ============================================================
333
+ # 11. اختبارات to_ordinal_date
334
+ # ============================================================
335
+
336
+ def test_ordinal_date_gregorian():
337
+ assert to_ordinal_date(2026, 9, 11) == "الحادي عشر من سبتمبر"
338
+ assert to_ordinal_date(2026, 1, 1) == "الأول من يناير"
339
+ assert to_ordinal_date(2026, 12, 31) == "الحادي والثلاثون من ديسمبر"
340
+
341
+
342
+ def test_ordinal_date_hijri():
343
+ result = to_ordinal_date(1448, 3, 1, hijri=True)
344
+ assert result == "الأول من ربيع الأول"
345
+
346
+
347
+ def test_ordinal_date_errors():
348
+ with pytest.raises(ValueError):
349
+ to_ordinal_date(2026, 13, 1)
350
+ with pytest.raises(ValueError):
351
+ to_ordinal_date(2026, 1, 32)
352
+
353
+
354
+ # ============================================================
355
+ # 12. اختبارات to_time — الوقت
356
+ # ============================================================
357
+
358
+ def test_time_full():
359
+ result = to_time(11, 25, 43, "مساءً")
360
+ assert result == "الحادية عشرة مساءً وخمسة وعشرون دقيقة وثلاثة وأربعون ثانية"
361
+
362
+
363
+ def test_time_hour_only():
364
+ assert to_time(9, 0, 0, "صباحًا") == "التاسعة صباحًا"
365
+
366
+
367
+ def test_time_with_minutes():
368
+ # 2 = 2 صباحًا (نظام 24 ساعة)
369
+ result = to_time(2, 30)
370
+ assert "الثانية صباحًا" in result
371
+ assert "ثلاثون دقيقة" in result
372
+
373
+ # 14 = 2 مساءً
374
+ result = to_time(14, 30)
375
+ assert "الثانية مساءً" in result
376
+
377
+
378
+ def test_time_with_seconds():
379
+ result = to_time(5, 0, 15, "صباحًا")
380
+ assert "الخامسة صباحًا" in result
381
+ assert "خمسة عشر ثانية" in result
382
+
383
+
384
+ def test_time_noon():
385
+ assert to_time(12, 0, 0) == "الثانية عشرة ظهرًا"
386
+
387
+
388
+ def test_time_midnight():
389
+ assert to_time(0, 0, 0) == "الثانية عشرة صباحًا"
390
+
391
+
392
+ def test_time_auto_period():
393
+ assert "صباحًا" in to_time(9, 0, 0)
394
+ assert "مساءً" in to_time(15, 0, 0)
395
+ assert "ظهرًا" in to_time(12, 0, 0)
396
+
397
+
398
+ def test_time_errors():
399
+ with pytest.raises(ValueError):
400
+ to_time(24, 0, 0)
401
+ with pytest.raises(ValueError):
402
+ to_time(12, 60, 0)
403
+ with pytest.raises(ValueError):
404
+ to_time(12, 0, 60)
405
+
406
+
407
+ # ============================================================
408
+ # 13. اختبارات القواميس الثابتة
409
+ # ============================================================
410
+
411
+ def test_currencies_count():
412
+ assert len(CURRENCIES) == 5
413
+ assert "SAR" in CURRENCIES
414
+ assert "USD" in CURRENCIES
415
+
416
+
417
+ def test_months_gregorian():
418
+ assert len(MONTHS_GREGORIAN) == 12
419
+ assert MONTHS_GREGORIAN[1] == "يناير"
420
+
421
+
422
+ def test_months_hijri():
423
+ assert len(MONTHS_HIJRI) == 12
424
+ assert MONTHS_HIJRI[9] == "رمضان"
425
+
426
+
427
+ # ============================================================
428
+ # 14. اختبارات التكامل
429
+ # ============================================================
430
+
431
+ def test_integration_invoice():
432
+ amount = to_currency(1234.56, "SAR")
433
+ date = to_date(2026, 9, 11)
434
+ time = to_time(14, 30, 0)
435
+
436
+ assert "ريال" in amount
437
+ assert "سبتمبر" in date
438
+ assert "الثانية مساءً" in time
439
+
440
+
441
+ def test_integration_consistency():
442
+ for _ in range(10):
443
+ assert to_arabic(1234) == "ألف ومئتان وأربعة وثلاثون"
444
+ assert to_ordinal(21) == "الحادي والعشرون"
445
+
446
+
447
+ # ============================================================
448
+ # 15. اختبارات الحالات الحدية
449
+ # ============================================================
450
+
451
+ def test_boundary_zero():
452
+ assert to_arabic(0) == "صفر"
453
+ assert to_currency(0, "SAR") == "صفر"
454
+
455
+
456
+ def test_boundary_one():
457
+ assert to_arabic(1) == "واحد"
458
+ assert to_ordinal(1) == "الأول"
459
+
460
+
461
+ def test_boundary_max():
462
+ result = to_arabic(10**20 - 1)
463
+ assert "كوينتليون" in result
464
+
465
+
466
+ def test_boundary_ten():
467
+ assert to_arabic(10) == "عشرة"
468
+ assert to_ordinal(10) == "العاشر"
469
+
470
+
471
+ def test_boundary_hundred():
472
+ assert to_arabic(100) == "مئة"
473
+ assert to_ordinal(100) == "المئة"
474
+
475
+
476
+ def test_boundary_thousand():
477
+ assert to_arabic(1000) == "ألف"
478
+ assert to_currency(1000, "SAR") == "ألف ريال"