Skip to content

String

String is a heap-owned, NUL-terminated UTF-8 string. Every method that indexes says which unit it works in:

  • Byte in the name → byte semantics. Indexes, lengths and slices count raw bytes. These are O(1), and bytes are the right unit for parsing, protocols and file formats.
  • Char in the name → character semantics. Indexes and counts are Unicode code points decoded from the UTF-8. charAt(1) of "héllo" is U+00E9, not the second byte of its encoding.
  • No unit in the name → whole-string. append, trimmed, equals, hasPrefix … treat the string as a value and take no index.
#import "String.xc" // or the Foundation umbrella

A String owns a growable byte buffer and keeps it NUL-terminated so cString() is always valid. It inherits from Object and needs a real heap (-falloc=heap, the default on the xt 6502 layout and every native backend).

UTF-8 preserves code-point order under byte comparison, so the value methods (equals, compare, hash) need no character-aware variants: byte order is character order. Ordering is lexicographic by unsigned byte then by length, so a prefix sorts before its extension ("go" before "gone").

Searches return a byte index and a miss is notFound(), never a negative number. Out-of-range slicing clamps to empty rather than faulting.

  • Comparable: compare gives total ordering, so String can be sorted and used as a key.
  • Hashable: hash (FNV-1a over the bytes), so String can be a Map/Set key.
  • Copying: copy returns an independent duplicate.

Every String* is also an Object* and fits anywhere one is expected.

Creating strings · withCString · withString · withBytes · withChar · withFormat · withEncodedBytes · init

Numbers → String · withI32 / withU32 · withI16 / withU16 · withI64 / withU64 · withFloat

Reading bytes · byteLength · isEmpty · byteAt · capacity · cString · copyCString · getBytes

Characters (Unicode) · charCount · charAt · charAtByte · charByteLength · byteIndexOfChar · nextCharByte · prevCharByte · isCharBoundary · appendChar · substringChars · isValidUtf8 · sanitizedUtf8

Searching · byteIndexOf · indexOfByte · lastIndexOfByte · contains · hasPrefix · hasSuffix · notFound

Slicing · substringBytes · substringFromByte · substringToByte

Mutating · append · appendCString · appendByte · appendBytes · appendFormat · insertAtByte · insertByte · insertCStringAtByte · deleteByteRange · replaceByteRange · replaceOccurrences · clear · setTo · setCString · reserve

Deriving new strings · appending · replacing · trimmed · uppercased · lowercased · copy · description

Case-insensitive · caseInsensitiveCompare · equalsIgnoringCase

Splitting & joining · splitOnByte · splitOnSet · join

Character sets · byteIndexOfSet · lastByteIndexOfSet · containsByteFromSet · asCharacterSet

Paths · pathSeparator · isAbsolutePath · lastPathComponent · deletingLastPathComponent · pathExtension · deletingPathExtension · appendingPathComponent · appendingPathExtension

Other encodings · withEncodedBytes · (encode via Data.withStringEncoded)

Protocol methods · equals · compare · hash · dealloc


static String* withCString(u8* src)

Builds a String by copying a NUL-terminated C string. The bytes are copied unchanged and assumed to be UTF-8, with no validation. This is the most common constructor.

static String* withString(String* other)

Returns an independent copy of other (same as copy).

static String* withBytes(u8* src, u32 n)

Copies n bytes, including any NUL bytes. The buffer may contain embedded NULs, though cString() stops at the first one.

static String* withChar(u32 cp)

A one-character String: encodes the Unicode code point cp as UTF-8.

static String* withFormat(string fmt, ...)

Builds a String from a printf-style format (see appendFormat for the conversions). String.withFormat("%d items", n).

static String* withEncodedBytes(u8* src, u32 n, StrEncoding enc)

Decodes n bytes in encoding enc into a (UTF-8) String. Malformed input is repaired to U+FFFD. See Other encodings.

void init(void)

The default initializer (an empty String). Prefer new String() or the with… constructors; you rarely call init directly.

↑ Topics

static String* withI32(i32 v)
static String* withU32(u32 v)

Decimal rendering of a 32-bit signed / unsigned integer.

static String* withI16(i16 v)
static String* withU16(u16 v)

Convenience wrappers that widen to 32-bit and render as above.

static String* withI64(i64 v)
static String* withU64(u64 v)

Decimal rendering of a 64-bit signed / unsigned integer.

static String* withFloat(float f, u8 precision)
static String* withFloat(float f) // precision 6

Renders f with precision digits after the point (default 6).

↑ Topics

u32 byteLength(void)

Number of bytes in the string (not counting the terminator). O(1). This is the UTF-8 encoded length, not the character count; see charCount.

bool isEmpty(void)

true when the byte length is zero.

u8 byteAt(u32 idx)

The raw byte at idx. For the character at a position use charAt or charAtByte.

u32 capacity(void)

Bytes currently allocated in the backing buffer (≥ byteLength). See reserve.

u8* cString(void)

A borrowed pointer into the String’s own NUL-terminated buffer. Valid until the next growth (append, appendChar, appendFormat, insertAtByte …), which may reallocate and free the old buffer, leaving the pointer dangling. If the bytes must outlive the next mutation, use copyCString.

u8* copyCString(void)

A new heap copy of the bytes (NUL-terminated) that the caller owns and frees with delete. Use it when you need a pointer that survives mutation of the String.

u32 getBytes(u8* dst, u32 max)

Copies up to max bytes into dst, returning the number copied. Does not NUL-terminate. Safe against overrun.

↑ Topics

Available on every target but xt6502.

u32 charCount(void)

Number of Unicode code points, decoded from the UTF-8. O(n).

u32 charAt(u32 n)

The n-th code point. O(n): it restarts from the front on each call, so a charAt loop is O(n²). Walk by byte index instead (see the example).

u32 charAtByte(u32 at)

The code point whose encoding starts at byte at. Returns U+FFFD if at is not a valid sequence start. O(1).

u32 charByteLength(u32 at)

How many bytes the character starting at byte at occupies (1–4; 1 for an invalid byte).

u32 byteIndexOfChar(u32 n)

The byte offset where the n-th character’s encoding begins.

u32 nextCharByte(u32 at)

The byte offset of the next character after the one at at. Use it to iterate characters in O(n).

u32 prevCharByte(u32 at)

The byte offset of the character before the one at at.

bool isCharBoundary(u32 at)

true if byte at begins a UTF-8 sequence (or is the end).

void appendChar(u32 cp)

Encodes code point cp as UTF-8 and appends it.

String* substringChars(u32 fromChar, u32 count)

A new String of count characters starting at character index fromChar. Clamps to empty if out of range.

bool isValidUtf8(void)

true if the whole buffer is well-formed UTF-8 (rejects overlong encodings, unpaired surrogates, values above U+10FFFF, truncated sequences).

String* sanitizedUtf8(void)

A repaired copy: each maximal invalid subpart becomes one U+FFFD (the Unicode-recommended repair). Valid input comes back byte-identical.

↑ Topics

Byte offsets; a miss is notFound().

u32 byteIndexOf(String* needle)
u32 byteIndexOf(String* needle, u32 from)

First byte offset of needle, optionally starting the search at byte from.

u32 indexOfByte(u8 ch)

First offset of byte ch.

u32 lastIndexOfByte(u8 ch)

Last offset of byte ch.

bool contains(String* needle)

true if needle occurs anywhere (byteIndexOf(needle) != notFound()).

bool hasPrefix(String* p)

true if the string starts with p.

bool hasSuffix(String* s)

true if the string ends with s.

static u32 notFound(void) // 0xFFFFFFFF

The sentinel returned by the search methods on a miss.

↑ Topics

Out-of-range clamps to empty.

String* substringBytes(u32 from, u32 len)

A new String of len bytes starting at byte from.

String* substringFromByte(u32 from)

Everything from byte from to the end.

String* substringToByte(u32 to)

Everything before byte to.

↑ Topics

These modify the String in place.

void append(String* other)

Appends other’s bytes.

void appendCString(u8* src)

Appends a NUL-terminated C string.

void appendByte(u8 ch)

Appends one raw byte. This may leave the buffer holding partial UTF-8 (see sanitizedUtf8).

void appendBytes(u8* src, u32 n)

Appends n bytes.

void appendFormat(string fmt, ...)

Appends printf-style formatted text. Conversions: %d/%u (16-bit), %ld/%lu (32-bit), %x/%lx, %s, %c, %f, %%. See Stdio for the shared format contract.

void insertAtByte(u32 at, String* other)

Inserts other at byte offset at.

void insertByte(u32 at, u8 ch)

Inserts one byte at at.

void insertCStringAtByte(u32 at, u8* src)

Inserts a C string at byte at.

void deleteByteRange(u32 at, u32 len)

Removes len bytes starting at at.

void replaceByteRange(u32 at, u32 len, String* other)

Replaces len bytes at at with other (which may be a different length).

u32 replaceOccurrences(String* find, String* sub)

Replaces every occurrence of find with sub in place; returns the count replaced. For a non-mutating version see replacing.

void clear(void)

Empties the String (keeps the allocated buffer).

void setTo(String* other)

Replaces the contents with a copy of other.

void setCString(u8* src)

Replaces the contents with a C string.

void reserve(u32 need)

Grows the buffer so at least need bytes fit without reallocating, which amortises a series of appends.

↑ Topics

Non-mutating: each returns a new String*, leaving the receiver unchanged.

String* appending(String* other)

A new String of the receiver followed by other.

String* replacing(String* find, String* sub)

A new String with every find replaced by sub (non-mutating replaceOccurrences).

String* trimmed(void) // ASCII whitespace
String* trimmed(CharacterSet* set) // any set

A copy with leading and trailing whitespace (or the bytes of any CharacterSet) removed.

String* uppercased(void)

An ASCII-uppercased copy (non-ASCII bytes pass through).

String* lowercased(void)

An ASCII-lowercased copy.

String* copy(void)

An independent duplicate (the Copying method).

String* description(void)

A String describing the object; for String itself, a copy. This is the Object hook used by printing helpers.

↑ Topics

These use ASCII case folding only; a full Unicode fold is not provided because of its cost.

i8 caseInsensitiveCompare(String* other)

Like compare but ASCII-case-insensitive.

bool equalsIgnoringCase(String* other)

ASCII-case-insensitive equality.

↑ Topics

Array* splitOnByte(u8 sep)

Splits on byte sep, returning an Array of Strings. Empty fields are kept for consecutive, leading or trailing separators, so "a,,b" is three fields. Filter out the empty fields if you want tokens.

Array* splitOnSet(CharacterSet* set)

Splits on any byte in set.

static String* join(Array* parts, String* sep)

Joins an Array of Strings with sep between them. The inverse of splitOnByte.

↑ Topics

Work with CharacterSet.

u32 byteIndexOfSet(CharacterSet* set)

First byte offset of any character in set.

u32 lastByteIndexOfSet(CharacterSet* set)

Last byte offset of any character in set.

bool containsByteFromSet(CharacterSet* set)

true if any byte belongs to set.

CharacterSet* asCharacterSet(void)

A CharacterSet of the distinct bytes in this String.

↑ Topics

Treat the String as a /-separated path. Non-mutating.

static u8 pathSeparator(void) // '/'

The separator byte these methods use.

bool isAbsolutePath(void)

true if the path starts at the root (/).

String* lastPathComponent(void)

The final component (the file name).

String* deletingLastPathComponent(void)

The parent directory (drops the last component).

String* pathExtension(void)

The extension of the last component, without the dot (empty if none).

String* deletingPathExtension(void)

The path with the last component’s extension removed.

String* appendingPathComponent(String* component)

The path with component appended, inserting a separator as needed.

String* appendingPathExtension(String* ext)

The path with .ext appended to the last component.

↑ Topics

Internally a String is always UTF-8; there is no per-String encoding mode. Other encodings are transcoded at the edge:

enum StrEncoding = {ENC_UTF8, ENC_ASCII, ENC_LATIN1, ENC_UTF16LE, ENC_UTF16BE};
String* s = String.withEncodedBytes(buf, n, ENC_LATIN1); // decode: bytes -> String
Data* d = Data.withStringEncoded(s, ENC_ASCII); // encode: String -> bytes
  • Decoding (withEncodedBytes) repairs malformed input to U+FFFD: an unpaired UTF-16 surrogate, an odd trailing byte, an ASCII byte above 127. Latin-1 cannot be malformed, because each byte is its code point.
  • Encoding (Data.withStringEncoded) substitutes ? for a code point the target cannot express (Latin-1 above U+00FF, ASCII above U+007F). UTF-16 emits surrogate pairs for the astral planes.
  • ENC_UTF8 output is a plain byte copy, including any invalid bytes. Call sanitizedUtf8 first if you want repair.

The encoder lives on Data, not String, because Data already imports String and the bridge is kept on one side.

↑ Topics

Inherited/overridden hooks from Object, Comparable and Hashable.

bool equals(String* other)
bool equals(Object* other)

Byte-exact equality. The Object* overload lets a String compare inside a heterogeneous container.

i8 compare(String* other)
i8 compare(Object* other)

Total ordering: lexicographic by unsigned byte, then by length. Returns negative / zero / positive. This method makes String Comparable.

u32 hash(void)

FNV-1a over the bytes. This is the Hashable method, so a String can key a Map or Set.

void dealloc(void)

Frees the backing buffer. ARC calls it when the last reference goes away; you do not call it directly.

↑ Topics

Compiles and runs on every target but xt6502 (examples/compiler/strings.xc):

// strings.xc — the String: bytes and characters, named apart.
#import "Stdio.xc"
#import "Foundation.xc"
i32 main(void)
{
// "héllo⚡" — 6 characters, 9 bytes: é is 2 bytes, ⚡ is 3.
String* s = String.withCString("h");
s.appendChar((u32)$E9); // é U+00E9, encoded as 2 bytes
s.appendCString("llo");
s.appendChar((u32)$26A1); // ⚡ U+26A1, encoded as 3 bytes
Stdio.printf("bytes %d, chars %d\n",
(i16)s.byteLength(), (i16)s.charCount());
// Byte in the name = byte semantics; Char = code points.
Stdio.printf("byteAt(1) %lx, charAt(1) U+%lx\n",
(u32)s.byteAt((u32)1), s.charAt((u32)1));
// Walk characters by byte index — no O(n^2) charAt loop.
u32 i = (u32)0;
while (i < s.byteLength()) {
Stdio.printf("U+%lx ", s.charAtByte(i));
i = s.nextCharByte(i);
}
Stdio.printf("\n");
return 0;
}
bytes 9, chars 6
byteAt(1) 000000C3, charAt(1) U+000000E9
U+00000068 U+000000E9 U+0000006C U+0000006C U+0000006F U+000026A1

String literals also take Unicode escapes directly: "héllo⚡" is the same nine bytes. See lexical structure.