Import U :: #import "std/unicode";. The checked Ziran implementation and
its generated tables run on native C, Go and the portable VM. There are no
GLib, ICU, locale, Python or platform Unicode dependencies.
Public procedures:
CaseFold :: (text: string) -> string
Lower :: (text: string) -> string
LowerInto :: (text: string, output: *Vec(u8)) -> bool
NFCInto :: (text: string, output: *Vec(u8)) -> bool
IsLetter :: (codepoint: u32) -> bool
IsNumber :: (codepoint: u32) -> bool
IsWhiteSpace :: (codepoint: u32) -> bool
Capitalize :: (text: string) -> string
IsWord :: (codepoint: u32) -> bool
IsDecimal :: (codepoint: u32) -> bool
IsPrintable :: (codepoint: u32) -> bool
IsSpace :: (codepoint: u32) -> bool
IsWhitespace :: (text: string) -> bool
SplitWhitespace :: (text: string) -> Vec(string)
The case procedures return immutable strings with independent storage.
CaseFold applies full default, non-Turkic folding: Straße and STRASSE
both become strasse, final and ordinary sigma both become σ, İ becomes
i followed by U+0307, and ffi becomes ffi. It does not normalize text.
Lower applies full default lowercase mappings. Greek U+03A3 becomes final
sigma only when preceded by a cased character and not followed by one,
ignoring Case_Ignorable characters on both sides. Context uses the original
input, including for Capitalize. Locale-specific Turkish, Azeri and
Lithuanian tailoring is excluded, matching Python's locale-independent string
methods.
LowerInto applies the same mappings and contextual final sigma rule, with
strict UTF-8 input validation. NFCInto applies canonical recursive
decomposition, stable canonical combining-class ordering, and blocked
composition with the full composition exclusions. Hangul decomposition and
composition are algorithmic. Compatibility mappings remain untouched: ffi
and ① are unchanged. NFC does not case-fold, lowercase, trim or tailor text.
Both Into procedures replace the previous bytes of a caller-owned Vec(u8).
They succeed on empty input and preserve embedded NUL. Malformed UTF-8 or
allocation/size failure returns false with output count zero; backing capacity
may remain for reuse. They check all vector growth and impose no fixed text
limit beyond representable storage and available allocation. Input must not
borrow the output buffer. Storage belongs to the caller and remains valid
until it mutates or releases the vector. Existing immutable-string procedures
retain their replacement-on-malformed-input behavior.
IsLetter matches exactly the Unicode General_Category aggregate L (Lu, Ll,
Lt, Lm, Lo). IsNumber matches exactly N (Nd, Nl, No), including fractions,
superscripts and Roman numerals. Neither includes combining marks or ASCII
underscore. IsWhiteSpace uses Unicode's White_Space property (25 scalars);
it excludes the U+001C..U+001F controls accepted by Python-style IsSpace.
All three return false for surrogates and values above U+10FFFF.
Capitalize titlecases the first scalar, including full expansions, and
lowercases subsequent scalars. Thus džABC becomes Džabc, ßABC becomes
Ssabc and ǰABC becomes J + U+030C + abc. An initial combining mark,
punctuation or digit still occupies the first position; capitalization never
searches ahead for a letter. Empty input produces empty output. Invalid UTF-8
follows std/utf8.Decode: consume one malformed lead byte and emit U+FFFD.
Embedded NUL is ordinary data. Scanning uses 64-bit byte positions and bounded
UTF-8 windows.
IsWord matches Python Unicode regex \w: letters (categories Lu, Ll, Lt,
Lm and Lo), decimal/digit/numeric characters and ASCII underscore. It includes
superscripts, fractions, Roman numerals and non-ASCII scripts. Combining marks,
other connector punctuation, join controls and emoji do not qualify.
IsDecimal matches Python Unicode regex \d: only General_Category Nd.
ASCII, Arabic-Indic, fullwidth and mathematical decimal digits qualify;
superscript digits, fractions and Roman numerals do not.
IsPrintable uses Python's printability rule with the pinned Unicode 17.0.0
database: categories L (letter), M (mark), N (number), P (punctuation) and
S (symbol), plus ASCII U+0020 space. It preserves combining marks and emoji.
Other Z (separator) and all C (other) categories are excluded, including NBSP,
soft hyphen, zero-width joiner, bidi controls, line/paragraph separators,
private-use, unassigned values, noncharacters and surrogates. Values above
U+10FFFF return false. This is a text property, independent of font support.
Python versions with older Unicode assignments can differ; equivalence to
their historical assigned-character repertoire is not claimed.
A consumer that filters text and applies a length limit should test
IsPrintable first and count retained Unicode scalars. For example, é🙂x
with limit 2 produces é🙂; removing U+200D and U+00A0 from a\u200db\u00a0c produces
abc; a + U+0301 with limit 1 produces a. Byte lengths and grapheme
counts give different results. The focused suite tests this composition.
IsSpace matches Python str.isspace and Unicode regex \s: general category
Zs or bidirectional class WS, B or S. This includes U+001C through U+001F in
addition to Unicode's White_Space property. U+180E, U+200B and U+FEFF are not
whitespace. IsWhitespace requires nonempty text consisting entirely of such
scalars; invalid UTF-8 returns false. Property predicates return false for
surrogates and values above U+10FFFF.
SplitWhitespace matches unlimited Python str.split() with no separator.
It collapses runs, omits leading/trailing empty fields, and returns no fields
for empty or all-whitespace input. The returned vector owns its descriptors;
each string borrows the original input bytes. Keep that input alive. These
operations leave the existing ASCII helpers in std/text and std/text_tools
with their documented byte semantics.
Unicode version and regeneration
Behavior uses Unicode 17.0.0, the same version as Ziran's grapheme tables. Python releases using a different Unicode database can differ for newly assigned or updated characters. UTF-8 strings contain scalars, so Python's isolated-surrogate string behavior is outside this API's input model.
The canonical tables live in std/unicode_data.zi. Regenerate them and the
unpacked expected-value corpus with the native Ziran generator:
build-queue -- build/bin/ziran run --target=c scripts/unicode_data.zi -- \
build/unicode-ucd std/unicode_data.zi tests/spec/unicode_conformance.zi
Supply these official files from https://www.unicode.org/Public/17.0.0/ucd/ in the input directory. Builds and tests use the checked-in outputs and require no downloads. Unicode data is distributed under Unicode License V3.
UnicodeData.txt 2e1efc1dcb59c575eedf5ccae60f95229f706ee6d031835247d843c11d96470c
SpecialCasing.txt efc25faf19de21b92c1194c111c932e03d2a5eaf18194e33f1156e96de4c9588
CaseFolding.txt ff8d8fefbf123574205085d6714c36149eb946d717a0c585c27f0f4ef58c4183
DerivedCoreProperties.txt 24c7fed1195c482faaefd5c1e7eb821c5ee1fb6de07ecdbaa64b56a99da22c08
The generator reads simple UnicodeData mappings, unconditional SpecialCasing overrides, C/F CaseFolding entries, and Cased/Case_Ignorable properties. Final_Sigma is implemented in the portable algorithm. Compact fixed-width base-128 records keep generated literals valid UTF-8 and allow binary search; each literal contains at most 448 bytes. Generated tests store expected UTF-8 strings independently of that packed representation.
Local tests
Canonical normalization and exact L/N/White_Space use the separately generated
std/unicode_normalization_data.zi; the existing case table/generator is
unchanged. Regenerate with:
build-queue -- build/bin/ziran run --target=c scripts/unicode_normalization_data.zi -- \
build/unicode-ucd std/unicode_normalization_data.zi tests/spec/unicode_normalization_cases.zi
The additional official Unicode 17.0.0 inputs and SHA-256 digests are:
CompositionExclusions.txt 2f239196ef3b5b61db5cc476e9bd80f534d15aa1b74e1be1dea5d042a344c85f
PropList.txt 130dcddcaadaf071008bdfce1e7743e04fdfbc910886f017d9f9ac931d8c64dd
NormalizationTest.txt 5019ffd530751a741900c849c0e010332f142a3612234639bd200b82138a87db
The generated test corpus preserves all five UTF-8 columns of the primary NormalizationTest. The normalization suite tests its five NFC invariants for every row and identity normalization for every assigned scalar absent from Part 1, as required by UAX #15. Source and saved IR execute on C, Go and both portable bundle runners. Focused cases cover strict malformed UTF-8, empty/reused outputs, NUL, recursive/blocked composition, Hangul, lowercase context and the distinction between Python whitespace and Unicode White_Space.
build-queue -- sh tests/std_unicode_normalization.sh build/bin/ziran
Set ZIRAN_UNICODE_UCD=build/unicode-ucd to also regenerate and compare both
normalization outputs byte for byte. Normal tests need no network access.
build-queue -- sh tests/std_unicode.sh build/bin/ziran
build-queue -- sh tests/unicode_data.sh build/bin/ziran
The Unicode suite executes source and saved IR on native C, Go, the portable
VM (zi2zx run --vm) and the native bundle runner, comparing source/saved bundles.
It covers every changed fold/lower/title mapping and every word, space, decimal
and printability range's endpoints (8,323 generated assertions), plus contextual
sigma, combining and titlecase expansions, supplementary scalars, splitting, malformed UTF-8,
embedded NUL and result lifetimes. The generator suite uses small official-format
fixtures without downloads. With ZIRAN_UNICODE_UCD=build/unicode-ucd, it also
regenerates both canonical outputs and compares them byte for byte.
std/utf8.DecodeReplace(source: []u8) -> string is a separate whole-input
UTF-8 replacement decoder. It substitutes U+FFFD once per maximal valid prefix
of an ill-formed sequence, as Python UTF-8 decoding with errors='replace'
does. A truncated E2 82 or F0 9F 92 yields one replacement; E1 80 41
yields replacement + A. Invalid starts or forbidden second bytes are
replaced separately: C0 AF yields two replacements, ED A0 80 and
E0 80 80 yield three, and F4 90 80 80 yields four. The immutable result
has independent storage, is always valid UTF-8, and preserves valid scalars
and embedded NUL. This does not alter the existing Decode or bounded Copy
one-malformed-lead-byte contracts. Run build-queue -- sh tests/std_utf8.sh
build/bin/ziran and build-queue -- sh tests/utf8_copy_valid.sh
build/bin/ziran for the new replacement matrix and the original repair-copy
regression suite.
Mapping semantics follow Unicode SpecialCasing and CaseFolding. The Python equivalents are documented in string methods and Unicode regular expression escapes.