umt-python 0.2.0__tar.gz → 0.4.0__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,317 @@
1
+ Metadata-Version: 2.4
2
+ Name: umt_python
3
+ Version: 0.4.0
4
+ Summary: A Python port of the UMT utility library (array, date, math, string, validate, and related helpers).
5
+ Author-email: riya-amemiya <riyaamemiya@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Bug Tracker, https://github.com/riya-amemiya/UMT/issues
8
+ Project-URL: Homepage, https://github.com/riya-amemiya/UMT
9
+ Project-URL: Repository, https://github.com/riya-amemiya/UMT
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # UMT Python Package
25
+
26
+ 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.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install umt-python
32
+
33
+ # or using uv
34
+ uv add umt-python
35
+
36
+ # or using poetry
37
+ poetry add umt-python
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from umt_python import (
44
+ add_business_days,
45
+ format_string,
46
+ is_between,
47
+ random_string,
48
+ to_base64,
49
+ )
50
+
51
+ random_string(10) # e.g. "aBcD3fGh1j"
52
+ format_string("Hello, {0}! Today is {1}.", "World", "Monday")
53
+ to_base64("Hello World") # "SGVsbG8gV29ybGQ="
54
+
55
+ from datetime import datetime
56
+
57
+ is_between(datetime(2025, 4, 15), datetime(2025, 4, 10), datetime(2025, 4, 20))
58
+ add_business_days(datetime(2025, 4, 18, 9, 30), 1) # 2025-04-21 09:30
59
+ ```
60
+
61
+ Import the published package as `umt_python`. Unit tests in this repo import from `src` (for example `from src.date import is_between`).
62
+
63
+ ## Modules
64
+
65
+ Public names are re-exported from `src/__init__.py`. Grouped the same way as TypeScript:
66
+
67
+ | Module | Examples |
68
+ | --- | --- |
69
+ | `advance` | `range_advance` |
70
+ | `array` | `chunk`, `unique`, `ultra_number_sort`, `zip_arrays` |
71
+ | `async_util` | `sleep`, `parallel`, `timeout`, `debounce_async` |
72
+ | `color` | `hexa_to_rgba`, `rgba_to_hsla` (see below) |
73
+ | `consts` | `ONE_DAY_MS`, `HttpStatus` |
74
+ | `crypto` | `encode_base32`, `decode_base32`, `decode_base58` (see below) |
75
+ | `data_structure` | `LRUCache`, `TTLCache`, `PriorityQueue` |
76
+ | `date` | `is_between`, `add_business_days`, `from_unix` (see below) |
77
+ | `error` | `safe_execute`, `match_result` |
78
+ | `function` | `debounce`, `memoize`, `throttle` |
79
+ | `ip` | `ip_to_long`, `cidr_to_long`, `is_in_range`, `is_private_ip` (see below) |
80
+ | `iterator` | `lazy_map`, `lazy_filter`, `lazy_take` |
81
+ | `map` | `group_by_to_map`, `zip_to_map` |
82
+ | `math` | `gcd`, `n_cr`, `sum_precise`, `calculator`, `math_converter`, `to_base_n` (see below) |
83
+ | `number` | `format_number`, `to_ordinal`, `to_percentage` |
84
+ | `object` | `object_get`, `deep_clone`, `flatten_object` |
85
+ | `predicate` | `every`, `some`, `is_nullish` |
86
+ | `random` | `random_choice`, `seeded_random` |
87
+ | `simple` | `birthday_simple`, `now_simple` |
88
+ | `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) |
89
+ | `time` | `convert_time` |
90
+ | `tool` | `pipe`, `unwrap`, `parse_json`, `escape_regexp` (see below) |
91
+ | `ua` | `parse_user_agent`, `extract_browser_from_user_agent`, `extract_device_from_user_agent`, `extract_os_from_user_agent` (see below) |
92
+ | `unit` | `to_celsius`, `to_kelvin` |
93
+ | `url` | `build_url`, `parse_query_string`, `is_absolute_url`, `join_path` (see below) |
94
+ | `validate` | `is_number`, `array_of`, `parse_email` (see below) |
95
+
96
+ 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`.
97
+
98
+ ## Date helpers
99
+
100
+ 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.
101
+
102
+ | Function | Type | Description | Example |
103
+ | --- | --- | --- | --- |
104
+ | `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` |
105
+ | `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)` |
106
+ | `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)` |
107
+ | `is_business_day` | `(date, holidays=None) -> bool` | Weekday and not in `holidays` | `is_business_day(datetime(2025, 4, 21)) # True` |
108
+ | `get_quarter` | `(date) -> int` | Local month → 1–4 (Jan–Mar = 1) | `get_quarter(datetime(2025, 4, 15)) # 2` |
109
+ | `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` |
110
+ | `from_unix` | `(value, unit="s") -> datetime` | Epoch to naive local datetime whose timestamp matches JavaScript `Date.getTime()` | `from_unix(0).timestamp() # 0.0` |
111
+ | `to_unix` | `(date, unit="s") -> float` | Default seconds, floored | `to_unix(datetime(1970, 1, 1, tzinfo=timezone.utc)) # 0.0` |
112
+ | `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)` |
113
+
114
+ `DateInclusivity` is `"()"` \| `"[]"` \| `"[)"` \| `"(]"`. `UnixTimeUnit` is `"s"` \| `"ms"`.
115
+
116
+ ## String helpers
117
+
118
+ Unicode-aware helpers aligned with TypeScript `package/main/src/String`.
119
+
120
+ | Function | Type | Description | Example |
121
+ | --- | --- | --- | --- |
122
+ | `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"` |
123
+ | `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"` |
124
+ | `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"]` |
125
+ | `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"` |
126
+ | `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"` |
127
+ | `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("日本語") # ""` |
128
+ | `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"` |
129
+ | `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"` |
130
+ | `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") # "Abc123"` / `to_full_width("a-b!") # "a-b!"` |
131
+ | `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 `0-9A-Za-z`. `str.maketrans` / `translate`, not regex. | `to_half_width("ABC123") # "ABC123"` / `to_half_width("! ") # "! "` |
132
+ | `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"` |
133
+ | `kebab_case` | `(string_: str) -> str` | Inserts dashes on case boundaries and replaces separators. Splits acronyms. | `kebab_case("XMLHttpRequest") # "xml-http-request"` |
134
+ | `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"` |
135
+
136
+ ## IP helpers
137
+
138
+ 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.
139
+
140
+ | Function | Type | Description | Example |
141
+ | --- | --- | --- | --- |
142
+ | `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"` |
143
+ | `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"` |
144
+ | `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` |
145
+ | `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` |
146
+ | `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` |
147
+ | `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"` |
148
+ | `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` |
149
+ | `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"` |
150
+
151
+ ## Color and URL helpers
152
+
153
+ | Function | Type | Description | Example |
154
+ | --- | --- | --- | --- |
155
+ | `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}` |
156
+ | `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` |
157
+ | `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"` |
158
+
159
+ `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.
160
+
161
+ ```python
162
+ from umt_python import escape_regexp, join_path
163
+
164
+ escape_regexp("a.b") # "a\\.b"
165
+ escape_regexp("foo-bar") # "foo-bar"
166
+ escape_regexp("path/to/file.txt") # "path/to/file\\.txt"
167
+ join_path("https://example.com/", "/api/", "/users") # "https://example.com/api/users"
168
+ ```
169
+
170
+ ## Crypto helpers
171
+
172
+ RFC 4648 Base32 (`A-Z` + `2-7`, `=` padding), aligned with TypeScript `package/main/src/Crypto`. Encoding, not encryption.
173
+
174
+ | Function | Type | Description | Example |
175
+ | --- | --- | --- | --- |
176
+ | `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======"` |
177
+ | `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"` |
178
+ | `decode_base32_to_string` | `(input_data: str) -> str` | UTF-8 decode of `decode_base32`. | `decode_base32_to_string("JBSWY3DP") # "Hello"` |
179
+
180
+ ## parse_email
181
+
182
+ `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.
183
+
184
+ ```python
185
+ from umt_python import ParseEmailOptions, parse_email
186
+
187
+ parse_email("user@example.com", ParseEmailOptions(level="basic")).valid # True
188
+ parse_email("user@localhost", ParseEmailOptions(level="basic")).valid # True
189
+ parse_email("user@localhost", ParseEmailOptions(level="rfc5321")).valid # False
190
+ ```
191
+
192
+ `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.
193
+
194
+ ## Math helpers
195
+
196
+ | Function | Type | Description | Example |
197
+ | --- | --- | --- | --- |
198
+ | `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"` |
199
+ | `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"` |
200
+ | `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"` |
201
+
202
+ ## UA helpers
203
+
204
+ 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).
205
+
206
+ | Function | Type | Description | Example |
207
+ | --- | --- | --- | --- |
208
+ | `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"` |
209
+ | `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"` |
210
+ | `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"` |
211
+ | `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'}` |
212
+
213
+ 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.
214
+
215
+ ## Function List
216
+
217
+ ### String Manipulation Functions
218
+
219
+ | Function | Type | Description | Example |
220
+ |----------|------|-------------|---------|
221
+ | `delete_spaces` | `(string_: str) -> str` | Remove all whitespace characters from a string | `delete_spaces("Hello World") # "HelloWorld"` |
222
+ | `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"` |
223
+ | `reverse_string` | `(char: str) -> str` | Reverse a string | `reverse_string("Hello") # "olleH"` |
224
+ | `escape_html` | `(string_: str) -> str` | Encode `& < > " '` (not backtick or `/`) | `escape_html("<b>") # "&lt;b&gt;"` |
225
+ | `to_full_width` | `(string_: str) -> str` | Convert ASCII digits and letters to full-width | `to_full_width("Abc123") # "Abc123"` |
226
+ | `to_half_width` | `(string_: str) -> str` | Convert `U+FF01`–`U+FF5E` and ideographic space to half-width | `to_half_width("Hello 123") # "Hello 123"` |
227
+
228
+ ### Base64 Encoding Functions
229
+
230
+ | Function | Type | Description | Example |
231
+ |----------|------|-------------|---------|
232
+ | `to_base64` | `(char: str) -> str` | Encode a string to Base64 | `to_base64("Hello") # "SGVsbG8="` |
233
+ | `from_base64` | `(base64_string: str) -> str` | Decode a Base64 string | `from_base64("SGVsbG8=") # "Hello"` |
234
+
235
+ ### String Padding Functions
236
+
237
+ | Function | Type | Description | Example |
238
+ |----------|------|-------------|---------|
239
+ | `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"` |
240
+ | `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"` |
241
+
242
+ ### String Trimming Functions
243
+
244
+ | Function | Type | Description | Example |
245
+ |----------|------|-------------|---------|
246
+ | `trim_characters` | `(string_: str, chars: str) -> str` | Remove specified characters from both ends | `trim_characters("-.-hello-.-", "-.") # "hello"` |
247
+ | `trim_start_characters` | `(string_: str, chars: str) -> str` | Remove specified characters from the start | `trim_start_characters("!!!hello", "!") # "hello"` |
248
+ | `trim_end_characters` | `(string_: str, chars: str) -> str` | Remove specified characters from the end | `trim_end_characters("hello!!!", "!") # "hello"` |
249
+
250
+ ### Random String Generation
251
+
252
+ | Function | Type | Description | Example |
253
+ |----------|------|-------------|---------|
254
+ | `random_string` | `(size: int = 8, char_pool: str = DEFAULT_RANDOM_STRING_CHARS) -> str` | Generate a random string of specified length | `random_string(10) # "aBcD3fGh1j"` |
255
+ | `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"` |
256
+
257
+ ### String Validation Functions
258
+
259
+ | Function | Type | Description | Example |
260
+ |----------|------|-------------|---------|
261
+ | `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` |
262
+
263
+ ### Number Functions
264
+
265
+ | Function | Type | Description | Example |
266
+ |----------|------|-------------|---------|
267
+ | `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"` |
268
+ | `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"` |
269
+ | `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` |
270
+
271
+ ## Constants
272
+
273
+ - `DEFAULT_RANDOM_STRING_CHARS`: Default character pool for random string generation (ASCII letters + digits)
274
+
275
+ ## Constraints
276
+
277
+ - **Python 3.10+** (`requires-python = ">=3.10"`). CI currently runs 3.10–3.15.
278
+ - `bool` is a subclass of `int` in Python. Numeric validators must reject `True` / `False` explicitly so they do not treat them as `1` / `0`.
279
+ - Exact arithmetic in math helpers uses `decimal.Decimal` constructed from strings, to match JavaScript number-string behavior.
280
+ - Date week math uses Sunday-start weeks. Do not assume ISO weeks (`datetime.isocalendar()`).
281
+ - 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.
282
+ - `decode_base32` raises `ValueError` on invalid / lowercase characters. TypeScript `decodeBase32` maps those to `0`.
283
+ - `to_base_n` defaults to radix 2. `True` / `False` are `1` / `0` because `bool` is an `int`.
284
+ - `hexa_to_rgba` requires `#` plus 3, 6, or 8 hex digits. TypeScript `hexaToRgba` does not validate.
285
+ - `unescape_html` decodes numeric entities with `chr()`. TypeScript `unescapeHtml` additionally rejects NULL, most C0 controls, DEL, C1, surrogates, and out-of-range values.
286
+ - `escape_html` encodes `& < > " '` only. TypeScript `escapeHtml` also encodes backtick (`&#x60;`) and `/` (`&#x2F;`).
287
+ - `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.
288
+ - `slugify` keeps Unicode letters (Python `\w`). TypeScript / Go `slugify` use ASCII `\w` and drop CJK.
289
+ - `parse_email` requires `ParseEmailOptions`. It does not apply TypeScript's 320-character ReDoS cap.
290
+ - `math_converter` formats split terms with `float` (`"1500.0*1000+…"`). TypeScript / Rust / Go emit integers (`"1500*1000+…"`).
291
+ - 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"`.
292
+ - `sanitize_string` is ASCII-only. CJK, emoji, and accented letters are dropped. It is not `strip_ansi`: `"\x1b[31mred\x1b[0m"` becomes `"[31mred[0m"`.
293
+ - `join_path` does not resolve `.` / `..` and does not use OS separators.
294
+ - `escape_regexp` does not escape `-` or `/`.
295
+
296
+ ## Development
297
+
298
+ This project uses `uv`. Prefer the Makefile targets (same commands as CI):
299
+
300
+ ```bash
301
+ make install # uv sync
302
+ make test # uv run pytest
303
+ make lint # ruff format --check && ruff check
304
+ make format # uv run ruff format
305
+ make typecheck # uv run pyright
306
+ make all # format, lint, typecheck, test
307
+ ```
308
+
309
+ Tests live under `tests/unit/` (mirroring `src/`) and import from `src`:
310
+
311
+ ```python
312
+ from src.date import is_between
313
+ ```
314
+
315
+ ## License
316
+
317
+ MIT License