urdu-text-utils 0.3.0 → 0.4.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Added
6
+
7
+ - Add `formatUrduNumber`, which formats numbers with South Asian lakh/crore digit grouping (`1234567` -> `۱۲٬۳۴٬۵۶۷`) and Urdu digits by default, plus western and no-grouping modes and a `decimals` cap. Rounding is applied to the binary double identically in both ports, so JS and PHP stay byte-identical rather than diverging at ties.
8
+ - Add `formatUrduCurrency`, which picks `روپیہ` for exactly one and `روپے` otherwise, or spells the amount out with `style: "words"`.
9
+ - Add `toUrduOrdinal`, which spells ordinals and inflects for gender (`پہلا`/`پہلی`). 1-6 come from a table; larger ordinals suffix the last word of the cardinal, a word-final `و` contracts (`نواں`, `سواں`), and an exact scale value drops its `ایک` (`1000` -> `ہزارواں`).
10
+ - Add `parseUrduDate`, the inverse of `formatUrduDate`. It reads Urdu and English month names, Urdu/Arabic-Indic/ASCII digits, day-first numeric dates, ISO dates and day-period times, and returns the same wall-clock moment `formatUrduDate` formatted — verified over all 24 hours.
11
+ - Export the number word tables (`URDU_ONES`, `URDU_TEENS_AND_TENS`, `URDU_TENS_UNITS`, `URDU_SCALES`, `URDU_ORDINAL_IRREGULARS`) from the package root.
12
+ - Extend `scripts/verify-php-parity.mjs` to mirror `Numbers.php` and `Date.php`, so the no-PHP check covers 300 fixture cases instead of 89.
13
+ - Add `npm run diff:php` (`scripts/diff-php.mjs` with `php/tools/diff-runner.php`), which runs the same seeded randomized number and date inputs through the real TypeScript build and the real PHP port and fails on any disagreement. Unlike the fixtures it can cover inputs nobody wrote down, and unlike the parity mirror it executes PHP rather than re-describing it, so it observes PHP's own behaviour. Errors are compared too, so both languages must reject the same inputs.
14
+
15
+ ### Changed
16
+
17
+ - `numberToUrduWords` now spells 21-99 with the real irregular Urdu words instead of a composed `بیس ایک`-style fallback. `21` is `اکیس` and `1234567` is `بارہ لاکھ چونتیس ہزار پانچ سو سڑسٹھ`; every cardinal from 0 to 99 is now a single correct word, covered by a completeness test.
18
+ - Complete the South Asian scale with `ارب` (10^9) and `کھرب` (10^11), which the docstring had promised but the scale table did not implement. Values below 10^8 are unchanged.
19
+ - Flow the number word tables through `php/data/tables.json`, generated from `src/numbers.ts`, so `Numbers.php` no longer keeps a hand-copied second set of cardinals.
20
+
21
+ ### Fixed
22
+
23
+ - `formatUrduNumber` no longer emits exponential notation for large magnitudes. `String(1e21)` is `"1e+21"` in JavaScript, which reached the output verbatim — and digit grouping then inserted its separator inside the exponent (`۱e,+۲۱`, with western grouping `1e,+21`). Anything at or above 10^21 now prints as digits, matching the PHP port.
24
+ - Fix the PHP port so `parseUrduDate` works at all. `DateTime::createFromFormat` requires exactly two digits for the `i` and `s` tokens, so the single-digit minute and second the port built (`0:0:0`) made it return `false`, and every non-numeric date parsed to `null`.
25
+
26
+ ## 0.3.1
27
+
28
+ - Publish the PHP port to Packagist. A root composer.json (Packagist reads repo-root manifests only) exposes php/src/UrduTextUtils via PSR-4 autoload, with export-ignore rules in .gitattributes for a clean dist archive. The new Packagist workflow runs on the same version tags as the npm Release: a PHP 8.1-8.3 matrix re-verifies table sync, strict manifest validation and the 536-test parity suite, then notifies packagist.org's update API so every tag publishes to both registries. v0.3.1 is the first version indexed on Packagist (composer require urdu-text-utils/urdu-text-utils).
29
+
30
+
3
31
  ## 0.3.0
4
32
 
5
33
  - Complete the PHP port (stages 0.2-0.4): add Detect.php, StopWords.php, Words.php sentence/statistics APIs, Stemmer.php, Date.php and Names.php with full fixture parity against the TypeScript source
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # urdu-text-utils — Urdu Text Processing Toolkit for JavaScript & TypeScript
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/urdu-text-utils?style=flat-square&color=157f52&label=npm)](https://www.npmjs.com/package/urdu-text-utils)
4
+ [![Packagist version](https://img.shields.io/packagist/v/urdu-text-utils/urdu-text-utils?style=flat-square&color=157f52&label=packagist)](https://packagist.org/packages/urdu-text-utils/urdu-text-utils)
4
5
  [![jsDelivr](https://img.shields.io/jsdelivr/npm/hm/urdu-text-utils?style=flat-square&label=jsDelivr)](https://www.jsdelivr.com/package/npm/urdu-text-utils)
5
6
  [![unpkg](https://img.shields.io/badge/unpkg-urdu--text--utils-blue?style=flat-square)](https://unpkg.com/urdu-text-utils/)
6
7
  [![CI](https://img.shields.io/github/actions/workflow/status/Zaid-maker/urdu-text-utils/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Zaid-maker/urdu-text-utils/actions/workflows/ci.yml)
@@ -16,6 +17,25 @@ Zero runtime dependencies. ESM + CJS. Fully typed.
16
17
 
17
18
  **[Documentation and live playground →](https://zaid-maker.github.io/urdu-text-utils/)**
18
19
 
20
+ ## Also available for PHP
21
+
22
+ The complete toolkit ships as a native PHP package: [`urdu-text-utils/urdu-text-utils`](https://packagist.org/packages/urdu-text-utils/urdu-text-utils) on Packagist (PHP 8.1+, zero dependencies, no required extensions).
23
+
24
+ ```bash
25
+ composer require urdu-text-utils/urdu-text-utils
26
+ ```
27
+
28
+ ```php
29
+ use UrduTextUtils\Normalizer;
30
+ use UrduTextUtils\Search;
31
+
32
+ Normalizer::normalizeUrdu("كيا حال ہے"); // "کیا حال ہے"
33
+ Search::searchUrdu("محمد", ["مُحَمَّد علی", "احمد", "محمد خان"]);
34
+ // ["مُحَمَّد علی", "محمد خان"]
35
+ ```
36
+
37
+ The port is not a fork: the [TypeScript source stays the single source of truth](https://zaid-maker.github.io/urdu-text-utils/guide/php). Shared tables and parity fixtures are generated from it, and CI re-verifies both on every push on PHP 8.1–8.3, with and without `ext-intl`. Same functions, same output, byte for byte. See [`php/README.md`](./php/README.md) for the full API.
38
+
19
39
  ```bash
20
40
  npm install urdu-text-utils
21
41
  ```
@@ -53,7 +73,11 @@ import {
53
73
  isStopWord,
54
74
  removeStopWords,
55
75
  formatUrduDate,
76
+ parseUrduDate,
56
77
  timeAgoUrdu,
78
+ formatUrduNumber,
79
+ formatUrduCurrency,
80
+ toUrduOrdinal,
57
81
  stemUrdu,
58
82
  stemUrduText,
59
83
  isUrdu,
@@ -151,9 +175,17 @@ toArabicIndicDigits("123"); // "١٢٣"
151
175
 
152
176
  parseUrduNumber("۱٬۲۳۴"); // 1234 — handles ٬ and ٫
153
177
  parseUrduNumber("۳٫۱۴"); // 3.14
154
- numberToUrduWords(100000); // "ایک لاکھ" — South Asian scale, @experimental
178
+ numberToUrduWords(1234567); // "بارہ لاکھ چونتیس ہزار پانچ سو سڑسٹھ" — South Asian scale
179
+
180
+ formatUrduNumber(1234567); // "۱۲٬۳۴٬۵۶۷" — lakh/crore grouping, not 1,234,567
181
+ formatUrduCurrency(1500); // "۱٬۵۰۰ روپے"
182
+ formatUrduCurrency(1); // "۱ روپیہ" — singular for exactly one
183
+ toUrduOrdinal(21); // "اکیسواں"
184
+ toUrduOrdinal(1, { gender: "feminine" }); // "پہلی"
155
185
  ```
156
186
 
187
+ The word tables cover every cardinal from 0 to 99 (`21` is `اکیس`, never a composed `بیس ایک`) and the scale runs ہزار → لاکھ → کروڑ → ارب → کھرب. `numberToUrduWords`, `formatUrduCurrency`'s `style: "words"` and `toUrduOrdinal` are `@experimental`: several of the 21–99 words have recognised variant spellings.
188
+
157
189
  ## Urdu dates & relative time
158
190
 
159
191
  ```ts
@@ -173,8 +205,16 @@ timeAgoUrdu(Date.now() - 3 * 3600 * 1000); // "۳ گھنٹے پہلے"
173
205
  timeAgoUrdu(Date.now() - 86400 * 1000); // "کل"
174
206
  timeAgoUrdu(Date.now() - 2 * 86400 * 1000);// "پرسوں"
175
207
  timeAgoUrdu(Date.now() + 10 * 60 * 1000); // "۱۰ منٹ بعد"
208
+
209
+ // Parse it back — the inverse of formatUrduDate
210
+ parseUrduDate("۲۲ اگست ۲۰۲۶"); // Date(2026, 7, 22)
211
+ parseUrduDate("22/08/2026"); // day-first, Pakistani convention
212
+ parseUrduDate("۵ جنوری", { defaultYear: 1999 });
213
+ parseUrduDate("۳۱ فروری ۲۰۲۶"); // null — that date does not exist
176
214
  ```
177
215
 
216
+ Anything `formatUrduDate` can emit — including `صبح دوپہر شام رات` periods — parses back to the same wall-clock moment, hours included. Absolute instants (a trailing `Z` or offset) and Hijri month names return `null` rather than guessing.
217
+
178
218
  ## Urdu stemming
179
219
 
180
220
  Rule-based stemmer and affix stripper with morphological vowel/letter restorations:
package/dist/index.cjs CHANGED
@@ -193,8 +193,23 @@ function parseUrduNumber(input) {
193
193
  if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/u.test(ascii)) return NaN;
194
194
  return Number(ascii);
195
195
  }
196
- var ONES = ["", "\u0627\u06CC\u06A9", "\u062F\u0648", "\u062A\u06CC\u0646", "\u0686\u0627\u0631", "\u067E\u0627\u0646\u0686", "\u0686\u06BE", "\u0633\u0627\u062A", "\u0622\u0679\u06BE", "\u0646\u0648"];
197
- var TEENS_AND_TENS = {
196
+ var SEPARATORS = {
197
+ urdu: { group: "\u066C", decimal: "\u066B" },
198
+ english: { group: ",", decimal: "." }
199
+ };
200
+ var URDU_ONES = [
201
+ "",
202
+ "\u0627\u06CC\u06A9",
203
+ "\u062F\u0648",
204
+ "\u062A\u06CC\u0646",
205
+ "\u0686\u0627\u0631",
206
+ "\u067E\u0627\u0646\u0686",
207
+ "\u0686\u06BE",
208
+ "\u0633\u0627\u062A",
209
+ "\u0622\u0679\u06BE",
210
+ "\u0646\u0648"
211
+ ];
212
+ var URDU_TEENS_AND_TENS = {
198
213
  10: "\u062F\u0633",
199
214
  11: "\u06AF\u06CC\u0627\u0631\u06C1",
200
215
  12: "\u0628\u0627\u0631\u06C1",
@@ -215,20 +230,104 @@ var TEENS_AND_TENS = {
215
230
  90: "\u0646\u0648\u06D2",
216
231
  100: "\u0633\u0648"
217
232
  };
233
+ var URDU_TENS_UNITS = {
234
+ 21: "\u0627\u06A9\u06CC\u0633",
235
+ 22: "\u0628\u0627\u0626\u06CC\u0633",
236
+ 23: "\u062A\u0626\u06CC\u0633",
237
+ 24: "\u0686\u0648\u0628\u06CC\u0633",
238
+ 25: "\u067E\u0686\u06CC\u0633",
239
+ 26: "\u0686\u06BE\u0628\u06CC\u0633",
240
+ 27: "\u0633\u062A\u0627\u0626\u06CC\u0633",
241
+ 28: "\u0627\u0679\u06BE\u0627\u0626\u06CC\u0633",
242
+ 29: "\u0627\u0646\u062A\u06CC\u0633",
243
+ 31: "\u0627\u06A9\u062A\u06CC\u0633",
244
+ 32: "\u0628\u062A\u06CC\u0633",
245
+ 33: "\u062A\u06CC\u0646\u062A\u06CC\u0633",
246
+ 34: "\u0686\u0648\u0646\u062A\u06CC\u0633",
247
+ 35: "\u067E\u06CC\u0646\u062A\u06CC\u0633",
248
+ 36: "\u0686\u06BE\u062A\u06CC\u0633",
249
+ 37: "\u0633\u06CC\u0646\u062A\u06CC\u0633",
250
+ 38: "\u0627\u0691\u062A\u06CC\u0633",
251
+ 39: "\u0627\u0646\u062A\u0627\u0644\u06CC\u0633",
252
+ 41: "\u0627\u06A9\u062A\u0627\u0644\u06CC\u0633",
253
+ 42: "\u0628\u06CC\u0627\u0644\u06CC\u0633",
254
+ 43: "\u062A\u06CC\u0646\u062A\u0627\u0644\u06CC\u0633",
255
+ 44: "\u0686\u0648\u0627\u0644\u06CC\u0633",
256
+ 45: "\u067E\u06CC\u0646\u062A\u0627\u0644\u06CC\u0633",
257
+ 46: "\u0686\u06BE\u06CC\u0627\u0644\u06CC\u0633",
258
+ 47: "\u0633\u06CC\u0646\u062A\u0627\u0644\u06CC\u0633",
259
+ 48: "\u0627\u0691\u062A\u0627\u0644\u06CC\u0633",
260
+ 49: "\u0627\u0646\u0686\u0627\u0633",
261
+ 51: "\u0627\u06A9\u06CC\u0627\u0648\u0646",
262
+ 52: "\u0628\u0627\u0648\u0646",
263
+ 53: "\u062A\u0631\u067E\u0646",
264
+ 54: "\u0686\u0648\u0646",
265
+ 55: "\u067E\u0686\u067E\u0646",
266
+ 56: "\u0686\u06BE\u067E\u0646",
267
+ 57: "\u0633\u062A\u0627\u0648\u0646",
268
+ 58: "\u0627\u0679\u06BE\u0627\u0648\u0646",
269
+ 59: "\u0627\u0646\u0633\u0679\u06BE",
270
+ 61: "\u0627\u06A9\u0633\u0679\u06BE",
271
+ 62: "\u0628\u0627\u0633\u0679\u06BE",
272
+ 63: "\u062A\u0631\u0633\u0679\u06BE",
273
+ 64: "\u0686\u0648\u0646\u0633\u0679\u06BE",
274
+ 65: "\u067E\u06CC\u0646\u0633\u0679\u06BE",
275
+ 66: "\u0686\u06BE\u06CC\u0627\u0633\u0679\u06BE",
276
+ 67: "\u0633\u0691\u0633\u0679\u06BE",
277
+ 68: "\u0627\u0691\u0633\u0679\u06BE",
278
+ 69: "\u0627\u0646\u06C1\u062A\u0631",
279
+ 71: "\u0627\u06A9\u06C1\u062A\u0631",
280
+ 72: "\u0628\u06C1\u062A\u0631",
281
+ 73: "\u062A\u06C1\u062A\u0631",
282
+ 74: "\u0686\u0648\u06C1\u062A\u0631",
283
+ 75: "\u067E\u0686\u06C1\u062A\u0631",
284
+ 76: "\u0686\u06BE\u06C1\u062A\u0631",
285
+ 77: "\u0633\u062A\u062A\u0631",
286
+ 78: "\u0627\u0679\u06BE\u06C1\u062A\u0631",
287
+ 79: "\u0627\u0646\u0627\u0633\u06CC",
288
+ 81: "\u0627\u06A9\u06CC\u0627\u0633\u06CC",
289
+ 82: "\u0628\u06CC\u0627\u0633\u06CC",
290
+ 83: "\u062A\u0631\u0627\u0633\u06CC",
291
+ 84: "\u0686\u0648\u0631\u0627\u0633\u06CC",
292
+ 85: "\u067E\u0686\u0627\u0633\u06CC",
293
+ 86: "\u0686\u06BE\u06CC\u0627\u0633\u06CC",
294
+ 87: "\u0633\u062A\u0627\u0633\u06CC",
295
+ 88: "\u0627\u0679\u06BE\u0627\u0633\u06CC",
296
+ 89: "\u0646\u0648\u0627\u0633\u06CC",
297
+ 91: "\u0627\u06A9\u0627\u0646\u0648\u06D2",
298
+ 92: "\u0628\u0627\u0646\u0648\u06D2",
299
+ 93: "\u062A\u0631\u0627\u0646\u0648\u06D2",
300
+ 94: "\u0686\u0648\u0631\u0627\u0646\u0648\u06D2",
301
+ 95: "\u067E\u0686\u0627\u0646\u0648\u06D2",
302
+ 96: "\u0686\u06BE\u06CC\u0627\u0646\u0648\u06D2",
303
+ 97: "\u0633\u062A\u0627\u0646\u0648\u06D2",
304
+ 98: "\u0627\u0679\u06BE\u0627\u0646\u0648\u06D2",
305
+ 99: "\u0646\u0627\u0646\u0648\u06D2"
306
+ };
307
+ var URDU_SCALES = [
308
+ [1e11, "\u06A9\u06BE\u0631\u0628"],
309
+ [1e9, "\u0627\u0631\u0628"],
310
+ [1e7, "\u06A9\u0631\u0648\u0691"],
311
+ [1e5, "\u0644\u0627\u06A9\u06BE"],
312
+ [1e3, "\u06C1\u0632\u0627\u0631"],
313
+ [100, "\u0633\u0648"]
314
+ ];
315
+ var URDU_ORDINAL_IRREGULARS = {
316
+ 1: { masculine: "\u067E\u06C1\u0644\u0627", feminine: "\u067E\u06C1\u0644\u06CC" },
317
+ 2: { masculine: "\u062F\u0648\u0633\u0631\u0627", feminine: "\u062F\u0648\u0633\u0631\u06CC" },
318
+ 3: { masculine: "\u062A\u06CC\u0633\u0631\u0627", feminine: "\u062A\u06CC\u0633\u0631\u06CC" },
319
+ 4: { masculine: "\u0686\u0648\u062A\u06BE\u0627", feminine: "\u0686\u0648\u062A\u06BE\u06CC" },
320
+ 5: { masculine: "\u067E\u0627\u0646\u0686\u0648\u0627\u06BA", feminine: "\u067E\u0627\u0646\u0686\u0648\u06CC\u06BA" },
321
+ 6: { masculine: "\u0686\u06BE\u0679\u0627", feminine: "\u0686\u06BE\u0679\u06CC" }
322
+ };
218
323
  function numberToUrduWords(value) {
219
324
  if (!Number.isFinite(value)) return "";
220
325
  if (!Number.isInteger(value)) throw new TypeError("numberToUrduWords expects an integer");
221
326
  if (value < 0) return `\u0645\u0646\u0641\u06CC ${numberToUrduWords(-value)}`;
222
327
  if (value === 0) return "\u0635\u0641\u0631";
223
- const scales = [
224
- [1e7, "\u06A9\u0631\u0648\u0691"],
225
- [1e5, "\u0644\u0627\u06A9\u06BE"],
226
- [1e3, "\u06C1\u0632\u0627\u0631"],
227
- [100, "\u0633\u0648"]
228
- ];
229
328
  const parts = [];
230
329
  let rest = value;
231
- for (const [size, name] of scales) {
330
+ for (const [size, name] of URDU_SCALES) {
232
331
  if (rest >= size) {
233
332
  const count = Math.floor(rest / size);
234
333
  rest %= size;
@@ -236,17 +335,83 @@ function numberToUrduWords(value) {
236
335
  }
237
336
  }
238
337
  if (rest > 0) {
239
- if (TEENS_AND_TENS[rest]) {
240
- parts.push(TEENS_AND_TENS[rest]);
338
+ const irregular = URDU_TENS_UNITS[rest];
339
+ if (irregular !== void 0) {
340
+ parts.push(irregular);
341
+ } else if (URDU_TEENS_AND_TENS[rest]) {
342
+ parts.push(URDU_TEENS_AND_TENS[rest]);
241
343
  } else if (rest < 10) {
242
- parts.push(ONES[rest]);
344
+ parts.push(URDU_ONES[rest]);
243
345
  } else {
244
346
  const tens = Math.floor(rest / 10) * 10;
245
- parts.push(`${TEENS_AND_TENS[tens] ?? ""} ${ONES[rest % 10] ?? ""}`.trim());
347
+ parts.push(`${URDU_TEENS_AND_TENS[tens] ?? ""} ${URDU_ONES[rest % 10] ?? ""}`.trim());
246
348
  }
247
349
  }
248
350
  return parts.join(" ").replace(/\s+/gu, " ").trim();
249
351
  }
352
+ function toDigitString(n) {
353
+ const plain = String(n);
354
+ const exponential = /^(\d)(?:\.(\d+))?e\+(\d+)$/u.exec(plain);
355
+ if (!exponential) return plain;
356
+ const lead = exponential[1];
357
+ const fraction = exponential[2] ?? "";
358
+ const zeros = Number(exponential[3]) - fraction.length;
359
+ return `${lead}${fraction}${"0".repeat(zeros)}`;
360
+ }
361
+ function groupDigits(intStr, grouping, separator) {
362
+ if (grouping === "none" || separator === "") return intStr;
363
+ if (grouping === "western") {
364
+ return intStr.replace(/\B(?=(\d{3})+(?!\d))/gu, separator);
365
+ }
366
+ const lastThree = intStr.slice(-3);
367
+ const rest = intStr.slice(0, -3);
368
+ return rest === "" ? lastThree : `${rest.replace(/\B(?=(\d{2})+(?!\d))/gu, separator)}${separator}${lastThree}`;
369
+ }
370
+ function formatUrduNumber(value, options = {}) {
371
+ if (!Number.isFinite(value)) return "";
372
+ const digits = options.digits ?? "urdu";
373
+ const grouping = options.grouping ?? "southAsian";
374
+ const decimals = Math.min(15, Math.max(0, Math.floor(options.decimals ?? 2)));
375
+ const separators = SEPARATORS[digits];
376
+ const negative = value < 0;
377
+ const abs = Math.abs(value);
378
+ const scale = 10 ** decimals;
379
+ let intPart = Math.floor(abs);
380
+ let scaledFraction = Math.floor((abs - intPart) * scale + 0.5);
381
+ if (scaledFraction >= scale) {
382
+ intPart += 1;
383
+ scaledFraction -= scale;
384
+ }
385
+ const intStr = groupDigits(toDigitString(intPart), grouping, separators.group);
386
+ const fraction = String(scaledFraction).padStart(decimals, "0").replace(/0+$/u, "");
387
+ const body = fraction === "" ? intStr : `${intStr}${separators.decimal}${fraction}`;
388
+ const sign = negative ? "-" : "";
389
+ return digits === "urdu" ? `${sign}${toUrduDigits(body)}` : `${sign}${body}`;
390
+ }
391
+ function formatUrduCurrency(value, options = {}) {
392
+ if (!Number.isFinite(value)) return "";
393
+ const style = options.style ?? "digits";
394
+ const unit = options.unit ?? "\u0631\u0648\u067E\u06D2";
395
+ const singularUnit = options.singularUnit ?? "\u0631\u0648\u067E\u06CC\u06C1";
396
+ const amount = style === "words" ? numberToUrduWords(value) : formatUrduNumber(value, options);
397
+ if (amount === "") return "";
398
+ return `${amount} ${Math.abs(value) === 1 ? singularUnit : unit}`;
399
+ }
400
+ function toUrduOrdinal(value, options = {}) {
401
+ if (!Number.isFinite(value)) return "";
402
+ if (!Number.isInteger(value)) throw new TypeError("toUrduOrdinal expects an integer");
403
+ const gender = options.gender ?? "masculine";
404
+ if (value < 0) return `\u0645\u0646\u0641\u06CC ${toUrduOrdinal(-value, options)}`;
405
+ if (value === 0) return "\u0635\u0641\u0631";
406
+ const irregular = URDU_ORDINAL_IRREGULARS[value];
407
+ if (irregular) return irregular[gender];
408
+ const words = numberToUrduWords(value).split(" ");
409
+ const last = words.pop();
410
+ const stem = last.endsWith("\u0648") ? last.slice(0, -1) : last;
411
+ const ordinated = `${stem}${gender === "feminine" ? "\u0648\u06CC\u06BA" : "\u0648\u0627\u06BA"}`;
412
+ const prefix = words.length === 1 && words[0] === "\u0627\u06CC\u06A9" ? [] : words;
413
+ return [...prefix, ordinated].join(" ");
414
+ }
250
415
 
251
416
  // src/detect.ts
252
417
  function urduRatio(input) {
@@ -4078,6 +4243,186 @@ function timeAgoUrdu(date, relativeTo = /* @__PURE__ */ new Date(), options = {}
4078
4243
  if (!addSuffix) return `${count} \u0633\u0627\u0644`;
4079
4244
  return isPast ? `${count} \u0633\u0627\u0644 \u067E\u06C1\u0644\u06D2` : `${count} \u0633\u0627\u0644 \u0628\u0639\u062F`;
4080
4245
  }
4246
+ var ENGLISH_MONTH_NAMES = [
4247
+ "january",
4248
+ "february",
4249
+ "march",
4250
+ "april",
4251
+ "may",
4252
+ "june",
4253
+ "july",
4254
+ "august",
4255
+ "september",
4256
+ "october",
4257
+ "november",
4258
+ "december"
4259
+ ];
4260
+ var ENGLISH_WEEKDAY_NAMES = [
4261
+ "sunday",
4262
+ "monday",
4263
+ "tuesday",
4264
+ "wednesday",
4265
+ "thursday",
4266
+ "friday",
4267
+ "saturday"
4268
+ ];
4269
+ var MONTH_LOOKUP = (() => {
4270
+ const lookup = /* @__PURE__ */ Object.create(null);
4271
+ URDU_MONTHS_GREGORIAN.forEach((name, index) => {
4272
+ lookup[name] = index;
4273
+ });
4274
+ ENGLISH_MONTH_NAMES.forEach((name, index) => {
4275
+ lookup[name] = index;
4276
+ lookup[name.slice(0, 3)] = index;
4277
+ });
4278
+ return lookup;
4279
+ })();
4280
+ var WEEKDAY_TOKENS = /* @__PURE__ */ new Set();
4281
+ for (const name of URDU_WEEKDAYS) WEEKDAY_TOKENS.add(name);
4282
+ for (const name of ENGLISH_WEEKDAY_NAMES) {
4283
+ WEEKDAY_TOKENS.add(name);
4284
+ WEEKDAY_TOKENS.add(name.slice(0, 3));
4285
+ }
4286
+ var URDU_PERIODS = Object.assign(
4287
+ /* @__PURE__ */ Object.create(null),
4288
+ {
4289
+ \u0635\u0628\u062D: "morning",
4290
+ \u062F\u0648\u067E\u06C1\u0631: "afternoon",
4291
+ \u0634\u0627\u0645: "evening",
4292
+ \u0631\u0627\u062A: "night"
4293
+ }
4294
+ );
4295
+ function resolveHour(hour, period) {
4296
+ switch (period) {
4297
+ case "am":
4298
+ case "morning":
4299
+ return hour === 12 ? 0 : hour;
4300
+ case "pm":
4301
+ case "afternoon":
4302
+ case "evening":
4303
+ return hour < 12 ? hour + 12 : hour;
4304
+ case "night":
4305
+ if (hour === 12) return 0;
4306
+ return hour >= 8 && hour <= 11 ? hour + 12 : hour;
4307
+ default:
4308
+ return hour;
4309
+ }
4310
+ }
4311
+ function buildLocalDate(year, month, day, hour, minute, second) {
4312
+ if (!Number.isInteger(year) || year < 100) return null;
4313
+ if (!Number.isInteger(month) || month < 0 || month > 11) return null;
4314
+ if (!Number.isInteger(day) || day < 1 || day > 31) return null;
4315
+ if (!Number.isInteger(hour) || hour < 0 || hour > 23) return null;
4316
+ if (!Number.isInteger(minute) || minute < 0 || minute > 59) return null;
4317
+ if (!Number.isInteger(second) || second < 0 || second > 59) return null;
4318
+ const date = new Date(year, month, day, hour, minute, second);
4319
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day || date.getHours() !== hour) {
4320
+ return null;
4321
+ }
4322
+ return date;
4323
+ }
4324
+ function expandYear(year) {
4325
+ if (year >= 100) return year;
4326
+ return year < 70 ? 2e3 + year : 1900 + year;
4327
+ }
4328
+ var INVISIBLE_CHARS_RE = /[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/gu;
4329
+ function parseNumericDate(input) {
4330
+ const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/u.exec(input);
4331
+ if (iso) {
4332
+ return buildLocalDate(
4333
+ Number(iso[1]),
4334
+ Number(iso[2]) - 1,
4335
+ Number(iso[3]),
4336
+ iso[4] === void 0 ? 0 : Number(iso[4]),
4337
+ iso[5] === void 0 ? 0 : Number(iso[5]),
4338
+ iso[6] === void 0 ? 0 : Number(iso[6])
4339
+ );
4340
+ }
4341
+ const dayFirst = /^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2,4})(?:[ ,T]+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/u.exec(input);
4342
+ if (dayFirst) {
4343
+ return buildLocalDate(
4344
+ expandYear(Number(dayFirst[3])),
4345
+ Number(dayFirst[2]) - 1,
4346
+ Number(dayFirst[1]),
4347
+ dayFirst[4] === void 0 ? 0 : Number(dayFirst[4]),
4348
+ dayFirst[5] === void 0 ? 0 : Number(dayFirst[5]),
4349
+ dayFirst[6] === void 0 ? 0 : Number(dayFirst[6])
4350
+ );
4351
+ }
4352
+ return null;
4353
+ }
4354
+ function parseUrduDate(input, options = {}) {
4355
+ if (!input) return null;
4356
+ const normalized = toEnglishDigits(input).replace(INVISIBLE_CHARS_RE, "").replace(/[،۔]/gu, " ").replace(/\s+/gu, " ").trim();
4357
+ if (normalized === "") return null;
4358
+ const numeric = parseNumericDate(normalized);
4359
+ if (numeric) return numeric;
4360
+ let month = null;
4361
+ let hour = null;
4362
+ let minute = 0;
4363
+ let second = 0;
4364
+ let period;
4365
+ const numbers = [];
4366
+ for (const rawToken of normalized.split(" ")) {
4367
+ const token = rawToken.replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "");
4368
+ if (token === "") continue;
4369
+ const urduPeriod = URDU_PERIODS[token];
4370
+ if (urduPeriod) {
4371
+ period = urduPeriod;
4372
+ continue;
4373
+ }
4374
+ const lower = token.toLowerCase();
4375
+ if (lower === "am" || lower === "pm") {
4376
+ period = lower;
4377
+ continue;
4378
+ }
4379
+ if (WEEKDAY_TOKENS.has(token) || WEEKDAY_TOKENS.has(lower)) continue;
4380
+ const time = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/u.exec(token);
4381
+ if (time) {
4382
+ hour = Number(time[1]);
4383
+ minute = Number(time[2]);
4384
+ second = time[3] === void 0 ? 0 : Number(time[3]);
4385
+ continue;
4386
+ }
4387
+ const monthIndex = MONTH_LOOKUP[token] ?? MONTH_LOOKUP[lower];
4388
+ if (monthIndex !== void 0) {
4389
+ month = monthIndex;
4390
+ continue;
4391
+ }
4392
+ if (/^\d+$/u.test(token)) numbers.push(Number(token));
4393
+ }
4394
+ if (month === null || numbers.length === 0) return null;
4395
+ let day;
4396
+ let year;
4397
+ if (numbers.length === 1) {
4398
+ const only = numbers[0];
4399
+ if (only > 31) {
4400
+ day = 1;
4401
+ year = only;
4402
+ } else {
4403
+ day = only;
4404
+ year = options.defaultYear ?? (/* @__PURE__ */ new Date()).getFullYear();
4405
+ }
4406
+ } else {
4407
+ const first = numbers[0];
4408
+ const last = numbers[numbers.length - 1];
4409
+ if (first > 31 && last <= 31) {
4410
+ day = last;
4411
+ year = first;
4412
+ } else {
4413
+ day = first;
4414
+ year = last;
4415
+ }
4416
+ }
4417
+ return buildLocalDate(
4418
+ expandYear(year),
4419
+ month,
4420
+ day,
4421
+ hour === null ? 0 : resolveHour(hour, period),
4422
+ minute,
4423
+ second
4424
+ );
4425
+ }
4081
4426
 
4082
4427
  // src/stemmer.ts
4083
4428
  var URDU_PREFIXES = [
@@ -4721,9 +5066,14 @@ exports.URDU_FAMILY_NAMES = URDU_FAMILY_NAMES;
4721
5066
  exports.URDU_FIRST_NAMES = URDU_FIRST_NAMES;
4722
5067
  exports.URDU_MONTHS_GREGORIAN = URDU_MONTHS_GREGORIAN;
4723
5068
  exports.URDU_MONTHS_HIJRI = URDU_MONTHS_HIJRI;
5069
+ exports.URDU_ONES = URDU_ONES;
5070
+ exports.URDU_ORDINAL_IRREGULARS = URDU_ORDINAL_IRREGULARS;
4724
5071
  exports.URDU_PREFIXES = URDU_PREFIXES;
5072
+ exports.URDU_SCALES = URDU_SCALES;
4725
5073
  exports.URDU_STOP_WORDS = URDU_STOP_WORDS;
4726
5074
  exports.URDU_SUFFIXES = URDU_SUFFIXES;
5075
+ exports.URDU_TEENS_AND_TENS = URDU_TEENS_AND_TENS;
5076
+ exports.URDU_TENS_UNITS = URDU_TENS_UNITS;
4727
5077
  exports.URDU_WEEKDAYS = URDU_WEEKDAYS;
4728
5078
  exports.analyzeUrdu = analyzeUrdu;
4729
5079
  exports.compareUrdu = compareUrdu;
@@ -4734,7 +5084,9 @@ exports.editDistance = editDistance;
4734
5084
  exports.extractNameParts = extractNameParts;
4735
5085
  exports.filterStopWords = filterStopWords;
4736
5086
  exports.foldUrdu = foldUrdu;
5087
+ exports.formatUrduCurrency = formatUrduCurrency;
4737
5088
  exports.formatUrduDate = formatUrduDate;
5089
+ exports.formatUrduNumber = formatUrduNumber;
4738
5090
  exports.getAffixes = getAffixes;
4739
5091
  exports.getUrduMonthName = getUrduMonthName;
4740
5092
  exports.getUrduWeekdayName = getUrduWeekdayName;
@@ -4744,6 +5096,7 @@ exports.isStopWord = isStopWord;
4744
5096
  exports.isUrdu = isUrdu;
4745
5097
  exports.normalizeUrdu = normalizeUrdu;
4746
5098
  exports.numberToUrduWords = numberToUrduWords;
5099
+ exports.parseUrduDate = parseUrduDate;
4747
5100
  exports.parseUrduNumber = parseUrduNumber;
4748
5101
  exports.removeDiacritics = removeDiacritics;
4749
5102
  exports.removeStopWords = removeStopWords;
@@ -4760,6 +5113,7 @@ exports.timeAgoUrdu = timeAgoUrdu;
4760
5113
  exports.toArabicIndicDigits = toArabicIndicDigits;
4761
5114
  exports.toEnglishDigits = toEnglishDigits;
4762
5115
  exports.toUrduDigits = toUrduDigits;
5116
+ exports.toUrduOrdinal = toUrduOrdinal;
4763
5117
  exports.transliterateNameToEnglish = transliterateNameToEnglish;
4764
5118
  exports.transliterateNameToUrdu = transliterateNameToUrdu;
4765
5119
  exports.urduRatio = urduRatio;