Metadata-Version: 2.4
Name: umt_python
Version: 0.4.0
Summary: A Python port of the UMT utility library (array, date, math, string, validate, and related helpers).
Author-email: riya-amemiya <riyaamemiya@gmail.com>
License-Expression: MIT
Project-URL: Bug Tracker, https://github.com/riya-amemiya/UMT/issues
Project-URL: Homepage, https://github.com/riya-amemiya/UMT
Project-URL: Repository, https://github.com/riya-amemiya/UMT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# UMT Python Package

UMT Python Package is a Python port of the TypeScript [`umt`](https://github.com/riya-amemiya/UMT/tree/main/package/main) utility library. `package/main` is the source of truth; this package mirrors those modules (`array`, `date`, `math`, `string`, `validate`, …) with `snake_case` names.

## Install

```bash
pip install umt-python

# or using uv
uv add umt-python

# or using poetry
poetry add umt-python
```

## Quick Start

```python
from umt_python import (
    add_business_days,
    format_string,
    is_between,
    random_string,
    to_base64,
)

random_string(10)  # e.g. "aBcD3fGh1j"
format_string("Hello, {0}! Today is {1}.", "World", "Monday")
to_base64("Hello World")  # "SGVsbG8gV29ybGQ="

from datetime import datetime

is_between(datetime(2025, 4, 15), datetime(2025, 4, 10), datetime(2025, 4, 20))
add_business_days(datetime(2025, 4, 18, 9, 30), 1)  # 2025-04-21 09:30
```

Import the published package as `umt_python`. Unit tests in this repo import from `src` (for example `from src.date import is_between`).

## Modules

Public names are re-exported from `src/__init__.py`. Grouped the same way as TypeScript:

| Module | Examples |
| --- | --- |
| `advance` | `range_advance` |
| `array` | `chunk`, `unique`, `ultra_number_sort`, `zip_arrays` |
| `async_util` | `sleep`, `parallel`, `timeout`, `debounce_async` |
| `color` | `hexa_to_rgba`, `rgba_to_hsla` (see below) |
| `consts` | `ONE_DAY_MS`, `HttpStatus` |
| `crypto` | `encode_base32`, `decode_base32`, `decode_base58` (see below) |
| `data_structure` | `LRUCache`, `TTLCache`, `PriorityQueue` |
| `date` | `is_between`, `add_business_days`, `from_unix` (see below) |
| `error` | `safe_execute`, `match_result` |
| `function` | `debounce`, `memoize`, `throttle` |
| `ip` | `ip_to_long`, `cidr_to_long`, `is_in_range`, `is_private_ip` (see below) |
| `iterator` | `lazy_map`, `lazy_filter`, `lazy_take` |
| `map` | `group_by_to_map`, `zip_to_map` |
| `math` | `gcd`, `n_cr`, `sum_precise`, `calculator`, `math_converter`, `to_base_n` (see below) |
| `number` | `format_number`, `to_ordinal`, `to_percentage` |
| `object` | `object_get`, `deep_clone`, `flatten_object` |
| `predicate` | `every`, `some`, `is_nullish` |
| `random` | `random_choice`, `seeded_random` |
| `simple` | `birthday_simple`, `now_simple` |
| `string` | `format_string`, `slugify`, `strip_ansi`, `strip_tags`, `words`, `normalize_whitespace`, `unescape_html`, `escape_html`, `camel_case`, `kebab_case`, `sanitize_string`, `delete_spaces`, `to_full_width`, `to_half_width` (see below) |
| `time` | `convert_time` |
| `tool` | `pipe`, `unwrap`, `parse_json`, `escape_regexp` (see below) |
| `ua` | `parse_user_agent`, `extract_browser_from_user_agent`, `extract_device_from_user_agent`, `extract_os_from_user_agent` (see below) |
| `unit` | `to_celsius`, `to_kelvin` |
| `url` | `build_url`, `parse_query_string`, `is_absolute_url`, `join_path` (see below) |
| `validate` | `is_number`, `array_of`, `parse_email` (see below) |

Not every TypeScript helper is ported yet. There is no `is_same` (use `is_same_day` or compare truncated values with `start_of`). There is no date `format` (TypeScript `format` / Rust `umt_format` / Go `FormatDate`). There is also no `count_by`, `partition`, `sliding`, `map_series`, or `safe_execute_async`.

## Date helpers

Local-time calendar helpers aligned with TypeScript `package/main/src/Date`. Week boundaries are Sunday-start (JavaScript `Date#getDay()` 0, not Python `weekday()` Monday=0). There are no UTC variants.

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `is_between` | `(date, start, end, unit=None, inclusivity="()") -> bool` | Range check. Default inclusivity is exclusive on both ends. Ranges are **not** swapped. Omit `unit` for exact timestamps; with a unit, all three dates are truncated with `start_of` first. | `is_between(datetime(2025, 4, 15), datetime(2025, 4, 10), datetime(2025, 4, 20))  # True` / `is_between(datetime(2025, 4, 10), datetime(2025, 4, 10), datetime(2025, 4, 20))  # False` / `is_between(datetime(2025, 4, 10), datetime(2025, 4, 10), datetime(2025, 4, 20), None, "[]")  # True` |
| `add_business_days` | `(date, amount, holidays=None) -> datetime` | Walk calendar days until `amount` weekdays (minus optional holidays compared with `is_same_day`) have been counted. Start date is not counted: Friday + 1 is Monday. `0` returns a clone and does not snap to a business day. | `add_business_days(datetime(2025, 4, 18, 9, 30), 1)  # datetime(2025, 4, 21, 9, 30)` |
| `sub_business_days` | `(date, amount, holidays=None) -> datetime` | `add_business_days(date, -amount, holidays)` | `sub_business_days(datetime(2025, 4, 21, 9, 30), 1)  # datetime(2025, 4, 18, 9, 30)` |
| `is_business_day` | `(date, holidays=None) -> bool` | Weekday and not in `holidays` | `is_business_day(datetime(2025, 4, 21))  # True` |
| `get_quarter` | `(date) -> int` | Local month → 1–4 (Jan–Mar = 1) | `get_quarter(datetime(2025, 4, 15))  # 2` |
| `week_of_year` | `(date) -> int` | Sunday-start week index. Week 1 contains January 1 of that year. Not ISO-8601. | `week_of_year(datetime(2025, 1, 1))  # 1` / `week_of_year(datetime(2025, 1, 5))  # 2` |
| `from_unix` | `(value, unit="s") -> datetime` | Epoch to naive local datetime whose timestamp matches JavaScript `Date.getTime()` | `from_unix(0).timestamp()  # 0.0` |
| `to_unix` | `(date, unit="s") -> float` | Default seconds, floored | `to_unix(datetime(1970, 1, 1, tzinfo=timezone.utc))  # 0.0` |
| `start_of` / `end_of` | `(date, unit) -> datetime` | Boundary truncation. `unit` is `second` \| `minute` \| `hour` \| `day` \| `week` \| `month` \| `quarter` \| `year` | `start_of(datetime(2025, 4, 15, 10, 30), "day")  # datetime(2025, 4, 15, 0, 0)` |

`DateInclusivity` is `"()"` \| `"[]"` \| `"[)"` \| `"(]"`. `UnixTimeUnit` is `"s"` \| `"ms"`.

## String helpers

Unicode-aware helpers aligned with TypeScript `package/main/src/String`.

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `strip_ansi` | `(string_: str) -> str` | Removes CSI (colors, cursor, including C1 `U+009B`) and OSC sequences. Incomplete CSI is left unchanged. Unlike `sanitize_string`, other non-printables stay. | `strip_ansi("\x1b[31mred\x1b[0m")  # "red"` |
| `strip_tags` | `(string_: str) -> str` | Deletes HTML/XML tags, repeating until stable so `"<sc<script>ript>"` becomes `""`. Self-closing tags leave no separator. | `strip_tags("<p>Hello <b>World</b></p>")  # "Hello World"` |
| `words` | `(string_: str, pattern: re.Pattern[str] \| None = None) -> list[str]` | Default: camelCase / acronym split, then non-letter/number separators. Pass a compiled pattern to return `findall` matches (`[]` if none). | `words("XMLHttpRequest")  # ["XML", "Http", "Request"]` |
| `normalize_whitespace` | `(string_: str) -> str` | Collapses Unicode `\s+` to a single space and trims. Unlike `delete_spaces`, words stay separated. | `normalize_whitespace("hello   world")  # "hello world"` |
| `delete_spaces` | `(string_: str) -> str` | Removes every Unicode `\s` character (spaces, tabs, newlines, em space, ideographic space). | `delete_spaces("Hello World")  # "HelloWorld"` / `delete_spaces("  tab\t space ")  # "tabspace"` |
| `sanitize_string` | `(string_: str) -> str` | Keeps printable ASCII (space to tilde) plus TAB / LF / CR. Drops NULL, other C0, DEL, ANSI ESC, and **all non-ASCII** (CJK, emoji, `"é"`). Unlike `strip_ansi`, CSI leftovers remain. | `sanitize_string("\x1b[31mred\x1b[0m")  # "[31mred[0m"` / `sanitize_string("héllo")  # "hllo"` / `sanitize_string("日本語")  # ""` |
| `unescape_html` | `(string_: str) -> str` | Named entities plus decimal / hex numeric references. `&#X41;` (uppercase `X`) is left unchanged. Decodes via `chr()` — it does **not** apply TypeScript's extra rejection of NULL / C0 / DEL / C1 / surrogates. | `unescape_html("Tom &amp; Jerry")  # "Tom & Jerry"` / `unescape_html("&#65;")  # "A"` |
| `escape_html` | `(string_: str) -> str` | Encodes `& < > " '`. Does **not** encode backtick or `/`. TypeScript `escapeHtml` also maps backtick → `&#x60;` and `/` → `&#x2F;`. Cached `re.compile(r"[&<>\"']")`. | `escape_html("<script></script>")  # "&lt;script&gt;&lt;/script&gt;"` / `escape_html("Tom & Jerry")  # "Tom &amp; Jerry"` |
| `to_full_width` | `(string_: str) -> str` | ASCII digits and letters only (`0-9A-Za-z`), each code point `+ 0xFEE0`. Punctuation and space stay. `str.maketrans` / `translate`, not regex. | `to_full_width("Abc123")  # "Ａｂｃ１２３"` / `to_full_width("a-b!")  # "ａ-ｂ!"` |
| `to_half_width` | `(string_: str) -> str` | **Broader than TypeScript / Rust / Go**: `U+FF01`–`U+FF5E` (full-width `!` through `~`, including digits and letters) plus ideographic space `U+3000` → `" "`. Those ports convert only `０-９Ａ-Ｚａ-ｚ`. `str.maketrans` / `translate`, not regex. | `to_half_width("ＡＢＣ１２３")  # "ABC123"` / `to_half_width("！　")  # "! "` |
| `camel_case` | `(string_: str) -> str` | Replaces non-alphanumeric runs, then lowercases only the first character. Does not split acronyms. | `camel_case("hello-world")  # "helloWorld"` / `camel_case("HELLO")  # "hELLO"` |
| `kebab_case` | `(string_: str) -> str` | Inserts dashes on case boundaries and replaces separators. Splits acronyms. | `kebab_case("XMLHttpRequest")  # "xml-http-request"` |
| `slugify` | `(string_: str) -> str` | NFD, strip combining marks, lowercase, hyphenate. Python `\w` is Unicode, so CJK stays: `"Japanese: こんにちは"` → `"japanese-こんにちは"`. TypeScript / Go drop those letters (`"japanese"`). | `slugify("Hello World!")  # "hello-world"` / `slugify("Café")  # "cafe"` |

## IP helpers

IPv4 dotted-decimal only, aligned with TypeScript `package/main/src/IP`. Unlike TypeScript, these functions validate input and raise `ValueError` (or `TypeError` from `get_network_address`) on malformed values.

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `ip_to_long` / `long_to_ip` | `(ip: str) -> int` / `(long: int \| float) -> str` | Pack or unpack four octets. Rejects leading zeros (`"192.168.01.1"`). | `ip_to_long("192.168.1.1")  # 3232235777` / `long_to_ip(3232235777)  # "192.168.1.1"` |
| `cidr_to_long` / `cidr_to_subnet_mask` | `(cidr: int) -> int` / `(cidr: int) -> str` | Prefix `0`–`32`. Outside that range raises `ValueError`. | `cidr_to_long(24)  # 4294967040` / `cidr_to_subnet_mask(24)  # "255.255.255.0"` |
| `subnet_mask_to_cidr` | `(subnet_mask: str) -> int` | Requires contiguous `1` bits then `0` bits. `"255.0.255.0"` raises. TypeScript `subnetMaskToCidr` only counts bits. | `subnet_mask_to_cidr("255.255.255.0")  # 24` |
| `is_in_range` | `(remote_ip, network_ip, cidr) -> bool` | `(ip & mask) == (network & mask)`. CIDR `0` matches every IPv4 address. | `is_in_range("192.168.1.2", "192.168.1.0", 24)  # True` |
| `is_private_ip` | `(ip: str) -> bool` | RFC 1918 only: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. Loopback and link-local are not private. The three `(network, mask)` pairs are computed once at module load (TypeScript / Rust / Go do the same). Invalid input raises `ValueError`. | `is_private_ip("10.0.0.1")  # True` / `is_private_ip("127.0.0.1")  # False` |
| `get_ip_class` | `(ip: str) -> str` | Classful first-octet lookup (`A`–`E`). `0.0.0.0` and malformed input return `""`. | `get_ip_class("10.0.0.1")  # "A"` |
| `get_network_address` | `(ip: str, subnet_mask: str) -> int` | Returns an unsigned 32-bit **int**, not a dotted string. | `get_network_address("192.168.1.1", "255.255.255.0")  # 3232235776` |
| `ip_to_binary_string` | `(ip: str) -> str` | 32-character `0`/`1` string, eight bits per octet. | `ip_to_binary_string("192.168.0.1")  # "11000000101010000000000000000001"` |

## Color and URL helpers

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `hexa_to_rgba` | `(hex_code: str) -> dict[str, float]` | `#` plus 3, 6, or 8 hex digits → `{r, g, b, a}`. `a` is `0`–`1` rounded to 2 decimals. Invalid input raises `ValueError`. TypeScript `hexaToRgba` does not validate. | `hexa_to_rgba("#FF0000")  # {'r': 255, 'g': 0, 'b': 0, 'a': 1.0}` / `hexa_to_rgba("#fff")  # {'r': 255, 'g': 255, 'b': 255, 'a': 1.0}` |
| `is_absolute_url` | `(url: str) -> bool` | RFC 3986 scheme check (letter, then letters / digits / `+` / `.` / `-`, then `:`). Protocol-relative URLs are not absolute. | `is_absolute_url("https://example.com")  # True` / `is_absolute_url("//example.com")  # False` |
| `join_path` | `(*segments: str) -> str` | Slash-normalizing string join, not `os.path.join`. Keeps the first segment's leading `/` and the last segment's trailing `/`. Empty pieces are dropped. No `.` / `..` resolution. No segments → `""`. A single `"///"` stays `"///"`. | `join_path("https://example.com/", "/api/", "/users")  # "https://example.com/api/users"` / `join_path("a", "", "b")  # "a/b"` |

`escape_regexp(string)` (in `tool`) prefixes `$ ( ) * + . ? [ \ ] ^ { | }` with a backslash. Hyphen and `/` are not escaped. `calculator` builds its currency-symbol regex with this helper at call time.

```python
from umt_python import escape_regexp, join_path

escape_regexp("a.b")  # "a\\.b"
escape_regexp("foo-bar")  # "foo-bar"
escape_regexp("path/to/file.txt")  # "path/to/file\\.txt"
join_path("https://example.com/", "/api/", "/users")  # "https://example.com/api/users"
```

## Crypto helpers

RFC 4648 Base32 (`A-Z` + `2-7`, `=` padding), aligned with TypeScript `package/main/src/Crypto`. Encoding, not encryption.

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `encode_base32` | `(input_data: str \| bytes) -> str` | UTF-8 encode a string first. Empty input → `""`. Pads to a multiple of 8 with `=`. | `encode_base32("Hello")  # "JBSWY3DP"` / `encode_base32("f")  # "MY======"` |
| `decode_base32` | `(input_data: str) -> bytes` | Skips `=` anywhere. **Raises `ValueError`** on invalid / lowercase characters (`"1A"`, `"jbswy3dp"`). TypeScript `decodeBase32` maps those to value `0` and does not throw. | `decode_base32("JBSWY3DP")  # b"Hello"` / `decode_base32("JB=SWY3DP")  # b"Hello"` |
| `decode_base32_to_string` | `(input_data: str) -> str` | UTF-8 decode of `decode_base32`. | `decode_base32_to_string("JBSWY3DP")  # "Hello"` |

## parse_email

`parse_email(email, ParseEmailOptions(level=...))` returns `ParseEmailResult(valid, parts)`. `level` is required: `"basic"` \| `"rfc822"` \| `"rfc2822"` \| `"rfc5321"` \| `"rfc5322"`. There is **no** TypeScript 320-character pre-check.

```python
from umt_python import ParseEmailOptions, parse_email

parse_email("user@example.com", ParseEmailOptions(level="basic")).valid  # True
parse_email("user@localhost", ParseEmailOptions(level="basic")).valid  # True
parse_email("user@localhost", ParseEmailOptions(level="rfc5321")).valid  # False
```

`rfc822` allows single-label domains and comments. `rfc2822` requires a TLD and rejects `..`. `rfc5321` is the SMTP path (max 256 / local 64, domain literals allowed). `rfc5322` adds comments and quoted strings.

## Math helpers

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `calculator` | `(expression: str, exchange=None) -> str` | Strips whitespace. `=` solves a single-variable equation (`"x=5"` → `"5"`). Otherwise evaluates `+ - * / ^` and parentheses. Incomplete input is returned as-is. `^` reduces the last pair first (`"2^2^2"` → `"16"`). | `calculator("1+2")  # "3"` / `calculator("$10*2", {"$": 100})  # "2000"` |
| `math_converter` | `(equation: str) -> str` | Rewrites `n*n` or `n^2` as a sum of simpler products. Uses `float` in the rewritten terms, so the string is `"1500.0*1000+…"` rather than TypeScript / Go `"1500*1000+…"`. | `math_converter("1250*1250")  # "1500.0*1000+400.0*100+200.0*100+50*50"` |
| `to_base_n` | `(value: int, radix: int = 2) -> str` | Default radix is **2** (`to_base_n(10)` → `"1010"`). Hex digits are lowercase. Radix 2 / 8 / 10 / 16 use format strings (`f"{value:d}"` for 10). `bool` is accepted as `int` (`True` → `"1"`). Rust returns `"Invalid radix"` and Go panics on a base outside `2`–`36`; this function does not check that range. | `to_base_n(15, 16)  # "f"` / `to_base_n(255, 16)  # "ff"` / `to_base_n(-10, 10)  # "-10"` |

## UA helpers

Coarse User-Agent labels aligned with TypeScript `package/main/src/UA`. Matching is case-insensitive. `parse_user_agent` lowercases the input, then calls the three extractors. Not a full UA parser (no versions).

| Function | Type | Description | Example |
| --- | --- | --- | --- |
| `extract_browser_from_user_agent` | `(ua: str) -> BrowserType` | Order: Edge → IE → Firefox → Opera (`opr/`, returned as **`"other"`**) → Chrome (`chrome` / `crios`) → Safari → `"other"`. | `extract_browser_from_user_agent("… Chrome/91 … OPR/77")  # "other"` |
| `extract_device_from_user_agent` | `(ua: str) -> DeviceType` | Bots first, then iPhone / iPod / webOS / BlackBerry / Opera Mini as `"mobile"`. Android with `mobile` → `"mobile"`; Android without `mobile` → `"tablet"`. iPad is `"tablet"`. | `extract_device_from_user_agent("Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X)")  # "tablet"` |
| `extract_os_from_user_agent` | `(ua: str) -> OsType` | iPhone / iPad / iPod → `"ios"` even when the string contains `like Mac OS X`. Android wins over the `Linux` token. | `extract_os_from_user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")  # "macos"` |
| `parse_user_agent` | `(user_agent: str) -> UserAgentInfo` | TypedDict `{browser, device, os}`. | `parse_user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/91.0")  # {'browser': 'chrome', 'device': 'desktop', 'os': 'macos'}` |

Fixed regexes in this package are compiled once at module level (`re.compile`): UA extractors, `hexa_to_rgba`, `is_absolute_url`, `join_path`, `math_converter`, `parse_email`, `strip_ansi` / `strip_tags`, `unescape_html`, `escape_html`, `camel_case`, `kebab_case`, `slugify`, `normalize_whitespace`, `sanitize_string`, `delete_spaces`, `escape_regexp`, calculator / `calculator_core` (except the currency-symbol pattern), `literal_expression`, `apply_formatter`, `get_value`. Caller-built patterns (`format_string`, `regex_match`, optional `words` pattern, `template_literal`, calculator currency symbol) stay inline. `to_full_width` / `to_half_width` use module-level `str.maketrans` tables, not regex.

## Function List

### String Manipulation Functions

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `delete_spaces` | `(string_: str) -> str` | Remove all whitespace characters from a string | `delete_spaces("Hello World")  # "HelloWorld"` |
| `format_string` | `(template: str, *values: object) -> str` | Replace placeholders {0}, {1}, etc. in a template string | `format_string("Sum of {0} and {1} is {2}", 1, 2, 3)  # "Sum of 1 and 2 is 3"` |
| `reverse_string` | `(char: str) -> str` | Reverse a string | `reverse_string("Hello")  # "olleH"` |
| `escape_html` | `(string_: str) -> str` | Encode `& < > " '` (not backtick or `/`) | `escape_html("<b>")  # "&lt;b&gt;"` |
| `to_full_width` | `(string_: str) -> str` | Convert ASCII digits and letters to full-width | `to_full_width("Abc123")  # "Ａｂｃ１２３"` |
| `to_half_width` | `(string_: str) -> str` | Convert `U+FF01`–`U+FF5E` and ideographic space to half-width | `to_half_width("Ｈｅｌｌｏ １２３")  # "Hello 123"` |

### Base64 Encoding Functions

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `to_base64` | `(char: str) -> str` | Encode a string to Base64 | `to_base64("Hello")  # "SGVsbG8="` |
| `from_base64` | `(base64_string: str) -> str` | Decode a Base64 string | `from_base64("SGVsbG8=")  # "Hello"` |

### String Padding Functions

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `pad_start` | `(string_: str, target_length: int, pad_string: str) -> str` | Pad string from the start to reach target length | `pad_start("123", 5, "0")  # "00123"` |
| `pad_end` | `(string_: str, target_length: int, pad_string: str) -> str` | Pad string from the end to reach target length | `pad_end("abc", 5, "0")  # "abc00"` |

### String Trimming Functions

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `trim_characters` | `(string_: str, chars: str) -> str` | Remove specified characters from both ends | `trim_characters("-.-hello-.-", "-.")  # "hello"` |
| `trim_start_characters` | `(string_: str, chars: str) -> str` | Remove specified characters from the start | `trim_start_characters("!!!hello", "!")  # "hello"` |
| `trim_end_characters` | `(string_: str, chars: str) -> str` | Remove specified characters from the end | `trim_end_characters("hello!!!", "!")  # "hello"` |

### Random String Generation

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `random_string` | `(size: int = 8, char_pool: str = DEFAULT_RANDOM_STRING_CHARS) -> str` | Generate a random string of specified length | `random_string(10)  # "aBcD3fGh1j"` |
| `random_string_initialization` | `(char_pool: str = DEFAULT_RANDOM_STRING_CHARS) -> Callable[[int], str]` | Create a custom random string generator with specific character pool | `custom_random = random_string_initialization("xyz")` → `custom_random(3)  # "xyx"` |

### String Validation Functions

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `has_no_letters` | `(text: str) -> bool` | Check if string contains no letters (only numbers, emojis, special chars) | `has_no_letters("123!@#")  # True` / `has_no_letters("abc123")  # False` |

### Number Functions

| Function | Type | Description | Example |
|----------|------|-------------|---------|
| `format_number` | `(value: float \| int, *, locale: str \| None = None, minimum_fraction_digits: int \| None = None, maximum_fraction_digits: int \| None = None, style: Literal["decimal", "currency", "percent"] = "decimal", currency: str \| None = None) -> str` | Format a number with locale-aware separators, fraction-digit controls, and decimal/percent/currency styles. Mirrors `Intl.NumberFormat` from the TypeScript source. | `format_number(1234567.89)  # "1,234,567.89"` / `format_number(1234567.89, locale="de-DE")  # "1.234.567,89"` / `format_number(0.75, style="percent")  # "75%"` / `format_number(1234.5, style="currency", currency="USD")  # "$1,234.50"` |
| `to_ordinal` | `(value: int \| float) -> str` | Convert a number to its English ordinal string (handles 11th/12th/13th specially) | `to_ordinal(1)  # "1st"` / `to_ordinal(11)  # "11th"` / `to_ordinal(21)  # "21st"` |
| `to_percentage` | `(value: float \| int, total: float \| int, decimals: int = 2) -> float` | Calculate the percentage of a value relative to a total (returns 0 when total is 0) | `to_percentage(1, 3)  # 33.33` / `to_percentage(25, 100)  # 25.0` |

## Constants

- `DEFAULT_RANDOM_STRING_CHARS`: Default character pool for random string generation (ASCII letters + digits)

## Constraints

- **Python 3.10+** (`requires-python = ">=3.10"`). CI currently runs 3.10–3.15.
- `bool` is a subclass of `int` in Python. Numeric validators must reject `True` / `False` explicitly so they do not treat them as `1` / `0`.
- Exact arithmetic in math helpers uses `decimal.Decimal` constructed from strings, to match JavaScript number-string behavior.
- Date week math uses Sunday-start weeks. Do not assume ISO weeks (`datetime.isocalendar()`).
- IP helpers are IPv4 only. They validate input (TypeScript does not). `subnet_mask_to_cidr` requires a contiguous mask; TypeScript `subnetMaskToCidr` only counts set bits. `is_private_ip` is RFC 1918 only (not loopback or link-local). The three RFC 1918 `(network, mask)` pairs are hoisted at module load, matching TypeScript / Rust / Go.
- `decode_base32` raises `ValueError` on invalid / lowercase characters. TypeScript `decodeBase32` maps those to `0`.
- `to_base_n` defaults to radix 2. `True` / `False` are `1` / `0` because `bool` is an `int`.
- `hexa_to_rgba` requires `#` plus 3, 6, or 8 hex digits. TypeScript `hexaToRgba` does not validate.
- `unescape_html` decodes numeric entities with `chr()`. TypeScript `unescapeHtml` additionally rejects NULL, most C0 controls, DEL, C1, surrogates, and out-of-range values.
- `escape_html` encodes `& < > " '` only. TypeScript `escapeHtml` also encodes backtick (`&#x60;`) and `/` (`&#x2F;`).
- `to_full_width` converts ASCII alnum only. `to_half_width` also converts full-width punctuation (`U+FF01`–`U+FF5E`) and ideographic space; TypeScript / Rust / Go `toHalfWidth` convert digits and letters only.
- `slugify` keeps Unicode letters (Python `\w`). TypeScript / Go `slugify` use ASCII `\w` and drop CJK.
- `parse_email` requires `ParseEmailOptions`. It does not apply TypeScript's 320-character ReDoS cap.
- `math_converter` formats split terms with `float` (`"1500.0*1000+…"`). TypeScript / Rust / Go emit integers (`"1500*1000+…"`).
- UA helpers are coarse labels. Opera (`OPR/`) is `"other"`. Android without a `mobile` token is `"tablet"`. iPhone UAs that contain `like Mac OS X` are `"ios"`.
- `sanitize_string` is ASCII-only. CJK, emoji, and accented letters are dropped. It is not `strip_ansi`: `"\x1b[31mred\x1b[0m"` becomes `"[31mred[0m"`.
- `join_path` does not resolve `.` / `..` and does not use OS separators.
- `escape_regexp` does not escape `-` or `/`.

## Development

This project uses `uv`. Prefer the Makefile targets (same commands as CI):

```bash
make install      # uv sync
make test         # uv run pytest
make lint         # ruff format --check && ruff check
make format       # uv run ruff format
make typecheck    # uv run pyright
make all          # format, lint, typecheck, test
```

Tests live under `tests/unit/` (mirroring `src/`) and import from `src`:

```python
from src.date import is_between
```

## License

MIT License
