aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-11 16:50:55 +0530
committernatsuoto <[email protected]>2026-07-11 16:50:55 +0530
commitdf4322c63b2937f803d1f35705f73db200b6dec7 (patch)
tree4b150a08fd658c6a2d99d9e34ef449ca845106e7
parent1d751146b2435fc5baf8cd903d2f88d2ac9ebd83 (diff)
downloadedify-df4322c63b2937f803d1f35705f73db200b6dec7.tar.xz
edify-df4322c63b2937f803d1f35705f73db200b6dec7.zip
refactor(library): rewrite temporal/, text/ validators to fluent Pattern() chain
-rw-r--r--edify/library/temporal/cron.py48
-rw-r--r--edify/library/temporal/date.py25
-rw-r--r--edify/library/temporal/datetime.py47
-rw-r--r--edify/library/temporal/duration.py43
-rw-r--r--edify/library/temporal/epoch.py12
-rw-r--r--edify/library/temporal/interval.py71
-rw-r--r--edify/library/temporal/offset.py23
-rw-r--r--edify/library/temporal/time.py36
-rw-r--r--edify/library/temporal/timestamp.py12
-rw-r--r--edify/library/temporal/timezone.py42
-rw-r--r--edify/library/text/alpha.py10
-rw-r--r--edify/library/text/alphanumeric.py10
-rw-r--r--edify/library/text/ascii.py14
-rw-r--r--edify/library/text/base.py71
-rw-r--r--edify/library/text/emoji.py15
-rw-r--r--edify/library/text/numeric.py10
-rw-r--r--edify/library/text/printable.py28
-rw-r--r--edify/library/text/script.py42
-rw-r--r--edify/library/text/slug.py22
-rw-r--r--edify/library/text/unicode.py26
-rw-r--r--edify/library/text/word.py14
21 files changed, 514 insertions, 107 deletions
diff --git a/edify/library/temporal/cron.py b/edify/library/temporal/cron.py
index fc50172..ada3d89 100644
--- a/edify/library/temporal/cron.py
+++ b/edify/library/temporal/cron.py
@@ -2,11 +2,51 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-cron = RegexBackedPattern(
- r"^(?:@(?:annually|yearly|monthly|weekly|daily|hourly|reboot)"
- r"|(?:[*?\d/,\-]+\s+){4,5}[*?\d/,\-]+)$"
+_alias = (
+ Pattern()
+ .char("@")
+ .group()
+ .any_of()
+ .string("annually")
+ .string("yearly")
+ .string("monthly")
+ .string("weekly")
+ .string("daily")
+ .string("hourly")
+ .string("reboot")
+ .end()
+ .end()
+)
+_field = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .char("*")
+ .char("?")
+ .range("0", "9")
+ .char("/")
+ .char(",")
+ .char("-")
+ .end()
+)
+_expr = (
+ Pattern()
+ .between(4, 5)
+ .group()
+ .subexpression(_field)
+ .one_or_more()
+ .whitespace_char()
+ .end()
+ .subexpression(_field)
+)
+
+cron = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_alias, _expr))
+ .end_of_input()
)
"""Callable :class:`Pattern` for cron-expression shapes: shortcut aliases
(``@daily`` etc.) or 5-/6-field whitespace-separated expressions.
diff --git a/edify/library/temporal/date.py b/edify/library/temporal/date.py
index 07e64de..f92eaf9 100644
--- a/edify/library/temporal/date.py
+++ b/edify/library/temporal/date.py
@@ -2,18 +2,21 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-date = RegexBackedPattern(
- r"^(?:"
- r"\d{1,2}/\d{1,2}/\d{4}"
- r"|\d{4}-\d{2}-\d{2}"
- r"|\d{2}-\d{2}-\d{4}"
- r"|\d{4}/\d{2}/\d{2}"
- r"|\d{1,2}\.\d{1,2}\.\d{4}"
- r"|\d{4}\.\d{2}\.\d{2}"
- r"|\d{8}"
- r")$"
+_d1 = Pattern().between(1, 2).digit().char("/").between(1, 2).digit().char("/").exactly(4).digit()
+_d2 = Pattern().exactly(4).digit().char("-").exactly(2).digit().char("-").exactly(2).digit()
+_d3 = Pattern().exactly(2).digit().char("-").exactly(2).digit().char("-").exactly(4).digit()
+_d4 = Pattern().exactly(4).digit().char("/").exactly(2).digit().char("/").exactly(2).digit()
+_d5 = Pattern().between(1, 2).digit().char(".").between(1, 2).digit().char(".").exactly(4).digit()
+_d6 = Pattern().exactly(4).digit().char(".").exactly(2).digit().char(".").exactly(2).digit()
+_d7 = Pattern().exactly(8).digit()
+
+date = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_d1, _d2, _d3, _d4, _d5, _d6, _d7))
+ .end_of_input()
)
"""Callable :class:`Pattern` for calendar-date shapes:
``M/D/YYYY``, ``YYYY-MM-DD``, ``DD-MM-YYYY``, ``YYYY/MM/DD``,
diff --git a/edify/library/temporal/datetime.py b/edify/library/temporal/datetime.py
index a1aa6ec..1e659d3 100644
--- a/edify/library/temporal/datetime.py
+++ b/edify/library/temporal/datetime.py
@@ -2,14 +2,45 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
-
-datetime = RegexBackedPattern(
- r"^(?:"
- r"\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?"
- r"(?:[Zz]|[+-]\d{2}:?\d{2})?"
- r"|\d{4}\d{2}\d{2}[Tt]\d{2}\d{2}\d{2}(?:[Zz]|[+-]\d{4})?"
- r")$"
+from edify import Pattern, any_of
+
+
+def _iso_extended() -> Pattern:
+ return (
+ Pattern()
+ .exactly(4).digit().char("-").exactly(2).digit().char("-").exactly(2).digit()
+ .any_of_chars("Tt ")
+ .exactly(2).digit().char(":").exactly(2).digit()
+ .optional().group().char(":").exactly(2).digit()
+ .optional().group().char(".").one_or_more().digit().end()
+ .end()
+ .optional().group().any_of()
+ .any_of_chars("Zz")
+ .subexpression(
+ Pattern().any_of_chars("+-").exactly(2).digit().optional().char(":").exactly(2).digit()
+ )
+ .end().end()
+ )
+
+
+def _iso_basic() -> Pattern:
+ return (
+ Pattern()
+ .exactly(4).digit().exactly(2).digit().exactly(2).digit()
+ .any_of_chars("Tt")
+ .exactly(2).digit().exactly(2).digit().exactly(2).digit()
+ .optional().group().any_of()
+ .any_of_chars("Zz")
+ .subexpression(Pattern().any_of_chars("+-").exactly(4).digit())
+ .end().end()
+ )
+
+
+datetime = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_iso_extended(), _iso_basic()))
+ .end_of_input()
)
"""Callable :class:`Pattern` for combined date-time shapes: ISO 8601 /
RFC 3339 forms with ``T`` or space separator, optional fractional seconds,
diff --git a/edify/library/temporal/duration.py b/edify/library/temporal/duration.py
index 640812b..40129fe 100644
--- a/edify/library/temporal/duration.py
+++ b/edify/library/temporal/duration.py
@@ -2,13 +2,44 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-duration = RegexBackedPattern(
- r"^P(?!$)(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?"
- r"(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?"
- r"(?:T(?=\d)(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?"
- r"(?:\d+(?:\.\d+)?S)?)?$"
+
+def _num_with_letter(letter: str) -> Pattern:
+ return (
+ Pattern()
+ .optional()
+ .group()
+ .one_or_more().digit()
+ .optional().group().char(".").one_or_more().digit().end()
+ .char(letter)
+ .end()
+ )
+
+
+def _duration_body() -> Pattern:
+ return (
+ Pattern()
+ .char("P")
+ .assert_ahead().any_char().end()
+ .subexpression(_num_with_letter("Y"))
+ .subexpression(_num_with_letter("M"))
+ .subexpression(_num_with_letter("W"))
+ .subexpression(_num_with_letter("D"))
+ .optional().group()
+ .char("T").assert_ahead().digit().end()
+ .subexpression(_num_with_letter("H"))
+ .subexpression(_num_with_letter("M"))
+ .subexpression(_num_with_letter("S"))
+ .end()
+ )
+
+
+duration = (
+ Pattern()
+ .start_of_input()
+ .subexpression(_duration_body())
+ .end_of_input()
)
"""Callable :class:`Pattern` for the ISO 8601 duration shape:
``PnYnMnDTnHnMnS`` with optional fractional components.
diff --git a/edify/library/temporal/epoch.py b/edify/library/temporal/epoch.py
index 859dc01..fec49bb 100644
--- a/edify/library/temporal/epoch.py
+++ b/edify/library/temporal/epoch.py
@@ -2,9 +2,17 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-epoch = RegexBackedPattern(r"^-?\d{1,10}$")
+epoch = (
+ Pattern()
+ .start_of_input()
+ .optional()
+ .char("-")
+ .between(1, 10)
+ .digit()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a Unix epoch-seconds value: optional sign
followed by 1-10 digits (fits in a 32-bit signed integer).
"""
diff --git a/edify/library/temporal/interval.py b/edify/library/temporal/interval.py
index fddf317..e1397b6 100644
--- a/edify/library/temporal/interval.py
+++ b/edify/library/temporal/interval.py
@@ -2,19 +2,64 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
-
-interval = RegexBackedPattern(
- r"^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?"
- r"(?:[Zz]|[+-]\d{2}:?\d{2})?"
- r"/"
- r"(?:\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?"
- r"(?:[Zz]|[+-]\d{2}:?\d{2})?"
- r"|P(?!$)(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?"
- r"(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?"
- r"(?:T(?=\d)(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?"
- r"(?:\d+(?:\.\d+)?S)?)?"
- r")$"
+from edify import Pattern, any_of
+
+
+def _iso_extended() -> Pattern:
+ return (
+ Pattern()
+ .exactly(4).digit().char("-").exactly(2).digit().char("-").exactly(2).digit()
+ .any_of_chars("Tt ")
+ .exactly(2).digit().char(":").exactly(2).digit()
+ .optional().group().char(":").exactly(2).digit()
+ .optional().group().char(".").one_or_more().digit().end()
+ .end()
+ .optional().group().any_of()
+ .any_of_chars("Zz")
+ .subexpression(
+ Pattern().any_of_chars("+-").exactly(2).digit().optional().char(":").exactly(2).digit()
+ )
+ .end().end()
+ )
+
+
+def _num_with_letter(letter: str) -> Pattern:
+ return (
+ Pattern()
+ .optional()
+ .group()
+ .one_or_more().digit()
+ .optional().group().char(".").one_or_more().digit().end()
+ .char(letter)
+ .end()
+ )
+
+
+def _duration_body() -> Pattern:
+ return (
+ Pattern()
+ .char("P")
+ .assert_ahead().any_char().end()
+ .subexpression(_num_with_letter("Y"))
+ .subexpression(_num_with_letter("M"))
+ .subexpression(_num_with_letter("W"))
+ .subexpression(_num_with_letter("D"))
+ .optional().group()
+ .char("T").assert_ahead().digit().end()
+ .subexpression(_num_with_letter("H"))
+ .subexpression(_num_with_letter("M"))
+ .subexpression(_num_with_letter("S"))
+ .end()
+ )
+
+
+interval = (
+ Pattern()
+ .start_of_input()
+ .subexpression(_iso_extended())
+ .char("/")
+ .subexpression(any_of(_iso_extended(), _duration_body()))
+ .end_of_input()
)
"""Callable :class:`Pattern` for the ISO 8601 time-interval shape:
``start-datetime/end-datetime`` or ``start-datetime/duration``.
diff --git a/edify/library/temporal/offset.py b/edify/library/temporal/offset.py
index adcf642..b72bf40 100644
--- a/edify/library/temporal/offset.py
+++ b/edify/library/temporal/offset.py
@@ -2,9 +2,28 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-offset = RegexBackedPattern(r"^(?:Z|[+-](?:0\d|1[0-4]):?[0-5]\d)$")
+_hh = any_of(
+ Pattern().char("0").digit(),
+ Pattern().char("1").range("0", "4"),
+)
+
+offset = (
+ Pattern()
+ .start_of_input()
+ .any_of()
+ .char("Z")
+ .subexpression(
+ Pattern()
+ .any_of_chars("+-")
+ .subexpression(_hh)
+ .optional().char(":")
+ .range("0", "5").digit()
+ )
+ .end()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for the UTC-offset shape: ``Z`` for UTC or
``±HH:MM`` / ``±HHMM`` with range ``-14:00`` to ``+14:00``.
"""
diff --git a/edify/library/temporal/time.py b/edify/library/temporal/time.py
index 3389f4b..5b2b2f5 100644
--- a/edify/library/temporal/time.py
+++ b/edify/library/temporal/time.py
@@ -2,13 +2,37 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-time = RegexBackedPattern(
- r"^(?:"
- r"(?:2[0-3]|[01]?\d):[0-5]\d(?::[0-5]\d(?:\.\d{1,6})?)?"
- r"|(?:1[0-2]|0?[1-9]):[0-5]\d(?::[0-5]\d)?\s?[AaPp][Mm]"
- r")$"
+_h24 = (
+ Pattern()
+ .any_of()
+ .subexpression(Pattern().char("2").range("0", "3"))
+ .subexpression(Pattern().optional().any_of_chars("01").digit())
+ .end()
+ .char(":").range("0", "5").digit()
+ .optional().group().char(":").range("0", "5").digit()
+ .optional().group().char(".").between(1, 6).digit().end()
+ .end()
+)
+
+_h12 = (
+ Pattern()
+ .any_of()
+ .subexpression(Pattern().char("1").range("0", "2"))
+ .subexpression(Pattern().optional().char("0").range("1", "9"))
+ .end()
+ .char(":").range("0", "5").digit()
+ .optional().group().char(":").range("0", "5").digit().end()
+ .optional().whitespace_char()
+ .any_of_chars("AaPp").any_of_chars("Mm")
+)
+
+time = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_h24, _h12))
+ .end_of_input()
)
"""Callable :class:`Pattern` for clock-time shapes: 24-hour
``HH:MM[:SS[.ffffff]]`` or 12-hour ``H:MM[:SS] AM/PM``.
diff --git a/edify/library/temporal/timestamp.py b/edify/library/temporal/timestamp.py
index 8be8d7a..2cf4ed2 100644
--- a/edify/library/temporal/timestamp.py
+++ b/edify/library/temporal/timestamp.py
@@ -2,9 +2,17 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-timestamp = RegexBackedPattern(r"^-?\d{10,13}$")
+timestamp = (
+ Pattern()
+ .start_of_input()
+ .optional()
+ .char("-")
+ .between(10, 13)
+ .digit()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a Unix epoch timestamp in seconds or
milliseconds: optional sign followed by 10-13 digits.
"""
diff --git a/edify/library/temporal/timezone.py b/edify/library/temporal/timezone.py
index b582136..2bf5b54 100644
--- a/edify/library/temporal/timezone.py
+++ b/edify/library/temporal/timezone.py
@@ -2,14 +2,42 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-timezone = RegexBackedPattern(
- r"^(?:"
- r"[A-Z][a-zA-Z_+\-]+(?:/[A-Z][a-zA-Z_+\-]+)+"
- r"|UTC|GMT|UT|Z"
- r"|[A-Z]{2,5}"
- r")$"
+_iana_seg = (
+ Pattern()
+ .uppercase()
+ .one_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("A", "Z")
+ .char("_")
+ .char("+")
+ .char("-")
+ .end()
+)
+_iana = (
+ Pattern()
+ .subexpression(_iana_seg)
+ .one_or_more()
+ .group()
+ .char("/")
+ .subexpression(_iana_seg)
+ .end()
+)
+_short = any_of(
+ Pattern().string("UTC"),
+ Pattern().string("GMT"),
+ Pattern().string("UT"),
+ Pattern().string("Z"),
+)
+_abbrev = Pattern().between(2, 5).uppercase()
+
+timezone = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_iana, _short, _abbrev))
+ .end_of_input()
)
"""Callable :class:`Pattern` for the timezone shape:
IANA region/city (``America/Los_Angeles``), or short abbreviation
diff --git a/edify/library/text/alpha.py b/edify/library/text/alpha.py
index ea5f78f..538aff8 100644
--- a/edify/library/text/alpha.py
+++ b/edify/library/text/alpha.py
@@ -2,7 +2,13 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-alpha = RegexBackedPattern(r"^[A-Za-z]+$")
+alpha = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .letter()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a letters-only string."""
diff --git a/edify/library/text/alphanumeric.py b/edify/library/text/alphanumeric.py
index 7bc6fad..6d3cadc 100644
--- a/edify/library/text/alphanumeric.py
+++ b/edify/library/text/alphanumeric.py
@@ -2,7 +2,13 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-alphanumeric = RegexBackedPattern(r"^[A-Za-z0-9]+$")
+alphanumeric = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .alphanumeric()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a letters-and-digits-only string."""
diff --git a/edify/library/text/ascii.py b/edify/library/text/ascii.py
index 700963d..4412196 100644
--- a/edify/library/text/ascii.py
+++ b/edify/library/text/ascii.py
@@ -1,10 +1,16 @@
-"""``ascii`` — printable-ASCII-only string shape."""
+"""``ascii`` — printable-ASCII string shape."""
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-ascii = RegexBackedPattern(r"^[\x20-\x7E]+$")
+ascii = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .range("\x20", "\x7E")
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a printable-ASCII-only string
-(characters ``0x20``-``0x7E``).
+(``0x20``-``0x7E`` — space through tilde).
"""
diff --git a/edify/library/text/base.py b/edify/library/text/base.py
index 526d94e..959d7a0 100644
--- a/edify/library/text/base.py
+++ b/edify/library/text/base.py
@@ -2,16 +2,69 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-base = RegexBackedPattern(
- r"^(?:"
- r"[0-9A-Fa-f]+"
- r"|[A-Z2-7]+=*"
- r"|[1-9A-HJ-NP-Za-km-z]+"
- r"|[A-Za-z0-9+/]+=*"
- r"|[A-Za-z0-9_-]+"
- r")$"
+_base16 = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("0", "9")
+ .range("A", "F")
+ .range("a", "f")
+ .end()
+)
+_base32 = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("A", "Z")
+ .range("2", "7")
+ .end()
+ .zero_or_more()
+ .char("=")
+)
+_base58 = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("1", "9")
+ .range("A", "H")
+ .range("J", "N")
+ .range("P", "Z")
+ .range("a", "k")
+ .range("m", "z")
+ .end()
+)
+_base64 = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("A", "Z")
+ .range("a", "z")
+ .range("0", "9")
+ .char("+")
+ .char("/")
+ .end()
+ .zero_or_more()
+ .char("=")
+)
+_base64url = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("A", "Z")
+ .range("a", "z")
+ .range("0", "9")
+ .char("_")
+ .char("-")
+ .end()
+)
+
+base = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_base16, _base32, _base58, _base64, _base64url))
+ .end_of_input()
)
"""Callable :class:`Pattern` that accepts any of base16, base32, base58,
base64, or base64url encoded strings.
diff --git a/edify/library/text/emoji.py b/edify/library/text/emoji.py
index 7f55330..ec1caa8 100644
--- a/edify/library/text/emoji.py
+++ b/edify/library/text/emoji.py
@@ -1,8 +1,17 @@
-"""``emoji`` — one-or-more emoji-character shape."""
+"""``emoji`` — Unicode-emoji run shape."""
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-emoji = RegexBackedPattern(r"^[\U0001F300-\U0001FAFF☀-➿]+$")
+emoji = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .any_of()
+ .range("\U0001F300", "\U0001FAFF")
+ .range("☀", "➿")
+ .end()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a run of one or more emoji characters."""
diff --git a/edify/library/text/numeric.py b/edify/library/text/numeric.py
index 1bc5485..67b6bc5 100644
--- a/edify/library/text/numeric.py
+++ b/edify/library/text/numeric.py
@@ -2,7 +2,13 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-numeric = RegexBackedPattern(r"^\d+$")
+numeric = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .digit()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a digits-only string."""
diff --git a/edify/library/text/printable.py b/edify/library/text/printable.py
index f729e91..96e2382 100644
--- a/edify/library/text/printable.py
+++ b/edify/library/text/printable.py
@@ -1,10 +1,30 @@
-"""``printable`` — printable-character string shape (excludes control codes)."""
+"""``printable`` — printable-character string shape."""
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-printable = RegexBackedPattern(r"^[^\x00-\x1F\x7F]+$")
+
+def _not_ctrl() -> Pattern:
+ return (
+ Pattern()
+ .assert_not_ahead()
+ .any_of()
+ .range("\x00", "\x1F")
+ .char("\x7F")
+ .end()
+ .end()
+ .any_char()
+ )
+
+
+printable = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .subexpression(_not_ctrl())
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a printable-character string
-(excludes ASCII control codes).
+(anything except ASCII control codes ``0x00``-``0x1F`` and ``0x7F``).
"""
diff --git a/edify/library/text/script.py b/edify/library/text/script.py
index 0a5e1fd..d5e053d 100644
--- a/edify/library/text/script.py
+++ b/edify/library/text/script.py
@@ -2,18 +2,38 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern, any_of
-script = RegexBackedPattern(
- r"^(?:"
- r"[A-Za-zÀ-ɏ]+"
- r"|[Ѐ-ӿ]+"
- r"|[Ͱ-Ͽ]+"
- r"|[一-鿿぀-ゟ゠-ヿ가-힯]+" # noqa: RUF001
- r"|[؀-ۿ]+"
- r"|[֐-׿]+"
- r"|[ऀ-ॿ]+"
- r")$"
+_latin = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("A", "Z")
+ .range("a", "z")
+ .range("À", "ɏ")
+ .end()
+)
+_cyrillic = Pattern().one_or_more().range("Ѐ", "ӿ")
+_greek = Pattern().one_or_more().range("Ͱ", "Ͽ")
+_cjk = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("一", "鿿")
+ .range("぀", "ゟ")
+ .range("゠", "ヿ") # noqa: RUF001
+ .range("가", "힯")
+ .end()
+)
+_arabic = Pattern().one_or_more().range("؀", "ۿ")
+_hebrew = Pattern().one_or_more().range("֐", "׿")
+_devanagari = Pattern().one_or_more().range("ऀ", "ॿ")
+
+script = (
+ Pattern()
+ .start_of_input()
+ .subexpression(any_of(_latin, _cyrillic, _greek, _cjk, _arabic, _hebrew, _devanagari))
+ .end_of_input()
)
"""Callable :class:`Pattern` for text in a single common Unicode script:
Latin (with extensions), Cyrillic, Greek, CJK (Chinese/Japanese/Korean),
diff --git a/edify/library/text/slug.py b/edify/library/text/slug.py
index 7d45a76..0537a3b 100644
--- a/edify/library/text/slug.py
+++ b/edify/library/text/slug.py
@@ -2,9 +2,27 @@
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-slug = RegexBackedPattern(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
+slug = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("0", "9")
+ .end()
+ .zero_or_more()
+ .group()
+ .char("-")
+ .one_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("0", "9")
+ .end()
+ .end()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a URL-safe slug: lowercase alphanumerics
separated by single hyphens.
"""
diff --git a/edify/library/text/unicode.py b/edify/library/text/unicode.py
index cc1eacd..a6d95ae 100644
--- a/edify/library/text/unicode.py
+++ b/edify/library/text/unicode.py
@@ -1,8 +1,28 @@
-"""``unicode`` — any Unicode-letter-or-digit string shape."""
+"""``unicode`` — Unicode string free of control codes."""
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-unicode = RegexBackedPattern(r"^[^\x00-\x1F\x7F]+$")
+
+def _not_ctrl() -> Pattern:
+ return (
+ Pattern()
+ .assert_not_ahead()
+ .any_of()
+ .range("\x00", "\x1F")
+ .char("\x7F")
+ .end()
+ .end()
+ .any_char()
+ )
+
+
+unicode = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .subexpression(_not_ctrl())
+ .end_of_input()
+)
"""Callable :class:`Pattern` for any Unicode string containing no control codes."""
diff --git a/edify/library/text/word.py b/edify/library/text/word.py
index 44ee068..7fbaff6 100644
--- a/edify/library/text/word.py
+++ b/edify/library/text/word.py
@@ -1,10 +1,16 @@
-"""``word`` — Python word-character string shape (``\\w+``)."""
+"""``word`` — word-character string shape."""
from __future__ import annotations
-from edify.library._support.regex import RegexBackedPattern
+from edify import Pattern
-word = RegexBackedPattern(r"^\w+$")
+word = (
+ Pattern()
+ .start_of_input()
+ .one_or_more()
+ .word()
+ .end_of_input()
+)
"""Callable :class:`Pattern` for a word-character string:
-letters, digits, and underscores.
+letters, digits, and underscore.
"""