//////////////////////////////// //~ Conversion helpers //- Char conversion String StringFromChar(Arena *arena, char c) { u8 *dst = PushStructNoZero(arena, u8); *dst = c; return (String) { .len = 1, .text = dst }; } //- Unsigned int conversion String StringFromU64(Arena *arena, u64 n, u64 base, u64 zfill) { /* Base too large */ Assert(base <= (countof(IntChars) - 1)); TempArena scratch = BeginScratch(arena); /* Build backwards text starting from least significant digit */ u64 len = 0; u8 *backwards_text = PushDry(scratch.arena, u8); do { StringFromChar(scratch.arena, IntChars[n % base]); ++len; n /= base; } while (n > 0); while (len < zfill) { StringFromChar(scratch.arena, '0'); ++len; } /* Reverse text into final string */ u8 *final_text = PushStructsNoZero(arena, u8, len); for (u64 i = 0; i < len; ++i) { final_text[i] = backwards_text[len - i - 1]; } EndScratch(scratch); return (String) { .len = len, .text = final_text }; } //- Signed int conversion String StringFromI64(Arena *arena, i64 n, u64 base, u64 zfill) { u8 *final_text = PushDry(arena, u8); u8 len = 0; if (n < 0) { /* Push sign */ StringFromChar(arena, '-'); len = 1; n = -n; } /* Push unsigned number */ String uint_str = StringFromU64(arena, n, base, zfill); return (String) { .len = len + uint_str.len, .text = final_text }; } //- Pointer conversion String StringFromPtr(Arena *arena, void *ptr) { String prepend = PushString(arena, Lit("0x")); String uint_str = StringFromU64(arena, (u64)ptr, 16, sizeof(ptr)); return (String) { .len = prepend.len + uint_str.len, .text = prepend.text }; } //- Floating point conversion String StringFromF64(Arena *arena, f64 f, u32 precision) { TempArena scratch = BeginScratch(arena); String result = ZI; result.text = PushDry(arena, u8); if (IsF32Nan(f)) { result.len += PushString(arena, Lit("NaN")).len; } else if (f == F64Infinity) { result.len += PushString(arena, Lit("inf")).len; } else if (f == -F64Infinity) { result.len += PushString(arena, Lit("-inf")).len; } else { if (f < 0) { StringFromChar(arena, '-'); f = -f; ++result.len; } /* Add one half of next precision level to round up */ f += 0.5 / (f64)PowU64(10, (u8)precision); f64 part_whole = TruncF64(f); f64 part_decimal = f - part_whole; /* Print whole part */ { /* Build backwards text starting from least significant digit */ u8 *backwards_text = PushDry(scratch.arena, u8); u64 backwards_text_len = 0; do { u64 digit = (u64)RoundF64ToI64(ModF64(part_whole, 10.0)); StringFromChar(scratch.arena, IntChars[digit % 10]); ++backwards_text_len; part_whole = TruncF64(part_whole / 10.0); } while (part_whole > 0); /* Reverse text into final string */ PushStructsNoZero(arena, u8, backwards_text_len); for (u64 i = backwards_text_len; i-- > 0;) { result.text[result.len++] = backwards_text[i]; } } /* Print decimal part */ if (precision > 0) { StringFromChar(arena, '.'); for (u64 i = 0; i < precision; ++i) { part_decimal *= 10.0; u64 digit = (u64)part_decimal; part_decimal -= digit; StringFromChar(arena, IntChars[digit % 10]); } result.len += (u64)precision + 1; } } EndScratch(scratch); return result; } //- Handle conversion String StringFromhandle(Arena *arena, u64 v0, u64 v1) { String result = ZI; result.text = PushDry(arena, u8); result.len += PushString(arena, Lit("h")).len; result.len += StringFromU64(arena, v0, 16, 0).len; result.len += PushString(arena, Lit("x")).len; result.len += StringFromU64(arena, v1, 16, 0).len; return result; } //- Uid conversion String StringFromUid(Arena *arena, Uid uid) { String result = ZI; result.text = PushDry(arena, u8); result.len += StringFromU64(arena, (uid.hi >> 32), 16, 8).len; return result; } //////////////////////////////// //~ String helpers //- Copy String PushString(Arena *arena, String src) { String str = { .len = src.len, .text = PushStructsNoZero(arena, u8, src.len) }; CopyBytes(str.text, src.text, src.len); return str; } String PushStringToBuff(String dst, String src) { String result = ZI; result.len = MinU64(dst.len, src.len); result.text = dst.text; CopyBytes(result.text, src.text, result.len); return result; } //- Repeat String RepeatString(Arena *arena, String src, u64 count) { u64 final_len = src.len * count; u8 *final_text = PushStructsNoZero(arena, u8, final_len); for (u64 i = 0; i < count; ++i) { CopyBytes(final_text + (src.len * i), src.text, src.len); } return (String) { .text = final_text, .len = final_len }; } //- Concatenate String CatString(Arena *arena, String str1, String str2) { String new_str = ZI; new_str.len = str1.len + str2.len; new_str.text = PushStructsNoZero(arena, u8, new_str.len); CopyBytes(new_str.text, str1.text, str1.len); CopyBytes(new_str.text + str1.len, str2.text, str2.len); return new_str; } //- Split /* `arena` is where pieces will be allocated. These strings point * into the existing string and do not allocate any new text. */ StringArray SplitString(Arena *arena, String str, String delim) { StringArray pieces = ZI; pieces.strings = PushDry(arena, String); i64 piece_start = 0; for (i64 i = 0; i < (i64)str.len - (i64)delim.len; ++i) { String cmp = ZI; cmp.text = &str.text[i]; cmp.len = MinI64(str.len - i, delim.len); b32 is_delimiter = EqString(cmp, delim); if (is_delimiter) { String piece = ZI; piece.text = &str.text[piece_start]; piece.len = i - piece_start; i += delim.len; piece_start = i; if (piece.len > 0) { *PushStructNoZero(arena, String) = piece; ++pieces.count; } } } if (piece_start < (i64)str.len) { String piece = ZI; piece.text = &str.text[piece_start]; piece.len = str.len - piece_start; *PushStructNoZero(arena, String) = piece; ++pieces.count; } return pieces; } //- Indent /* NOTE: Really slow */ String IndentString(Arena *arena, String str, u32 indent) { TempArena scratch = BeginScratch(arena); u64 final_len = 0; u8 *final_text = PushDry(arena, u8); StringArray split = SplitString(scratch.arena, str, Lit("\n")); for (u64 i = 0; i < split.count; ++i) { String piece = split.strings[i]; for (u32 j = 0; j < indent; ++j) { StringFromChar(arena, ' '); ++final_len; } PushString(arena, piece); final_len += piece.len; if (i < split.count - 1) { StringFromChar(arena, '\n'); ++final_len; } } EndScratch(scratch); return (String) { .len = final_len, .text = final_text }; } //- Lower String LowerString(Arena *arena, String str) { String result = ZI; result.text = PushStructsNoZero(arena, u8, str.len); result.len = str.len; for (u64 i = 0; i < str.len; ++i) { u8 c = str.text[i]; if (65 <= c && c <= 90) { c += 32; } result.text[i] = c; } return result; } //- Compare b32 EqString(String str1, String str2) { b32 eq = 1; if (str1.len == str2.len) { for (u64 i = 0; i < str1.len; ++i) { if (str1.text[i] != str2.text[i]) { eq = 0; break; } } } else { eq = 0; } return eq; } //- Match b32 StringContains(String str, String substring) { if (substring.len > str.len) { return 0; } for (u64 i = 0; i <= str.len - substring.len; ++i) { b32 match = 1; for (u64 j = 0; j < substring.len; ++j) { if (str.text[i + j] != substring.text[j]) { match = 0; break; } } if (match) { return 1; } } return 0; } b32 StringStartsWith(String str, String substring) { if (str.len >= substring.len) { for (u64 i = 0; i < substring.len; ++i) { if (str.text[i] != substring.text[i]) { return 0; } } return 1; } return 0; } b32 StringEndsWith(String str, String substring) { if (str.len >= substring.len) { u64 start = str.len - substring.len; for (u64 i = 0; i < substring.len; ++i) { if (str.text[start + i] != substring.text[i]) { return 0; } } return 1; } return 0; } //////////////////////////////// //~ String list helpers StringListNode *PushStringToList(Arena *arena, StringList *l, String s) { StringListNode *n = PushStruct(arena, StringListNode); n->s = s; n->prev = l->last; if (l->last) { l->last->next = n; } else { l->first = n; } l->last = n; ++l->count; return n; } String StringFromList(Arena *arena, StringList l, String separator) { String result = ZI; result.text = PushDry(arena, u8); for (StringListNode *n = l.first; n; n = n->next) { String s = n->s; PushString(arena, s); result.len += s.len; if (n->next) { PushString(arena, separator); result.len += separator.len; } } return result; } //////////////////////////////// //~ Trimming helpers String TrimLeft(String s, String pattern) { String result = s; while (StringStartsWith(result, pattern)) { result.text += pattern.len; result.len -= pattern.len; } return result; } String TrimRight(String s, String pattern) { String result = s; while (StringEndsWith(result, pattern)) { result.len -= pattern.len; } return result; } String Trim(String s, String pattern) { return TrimLeft(TrimRight(s, pattern), pattern); } String TrimWhitespace(String s) { b32 stop = 0; while (!stop) { if (StringStartsWith(s, Lit("\n")) || StringStartsWith(s, Lit("\r")) || StringStartsWith(s, Lit(" "))) { s.text += 1; s.len -= 1; } else if (StringEndsWith(s, Lit("\n")) || StringEndsWith(s, Lit("\r")) || StringEndsWith(s, Lit(" "))) { s.len -= 1; } else { stop = 1; } } return s; } //////////////////////////////// //~ Formatting /* String formatting only has one format specifier: "%F". All specifier info is * included in the arguments (instead of w/ the specifier like in printf). * * Example: * FormatString(arena, Lit("Hello there %F"), FmtString(Lit("George"))) * * NOTE: FmtEnd must be passed as the last arg in the va_list (this is * done automatically by the `FormatString` macro). * * Format arguments: * FmtChar: Format a single u8 character * FmtString: Format a `string` struct * FmtUint: Format a u64 * FmtSint: Format an i64 * FmtFloat: Format an f64 with DefaultFmtPrecision * FmtFloatP: Format an f64 with specified precision * FmtHex: Format a u64 in hexadecimal notation * FmtPtr: Format a pointer in hexadecimal notation prefixed by "0x" * * FmtEnd (internal): Denote the end of the va_list * * TODO: * %n equivalent? (nothing) * %e/%E equivalent? (scientific notation of floats) * %o equivalent? (octal representation) */ String FormatStringV(Arena *arena, String fmt, va_list args) { __prof; u64 final_len = 0; u8 *final_text = PushDry(arena, u8); u8 *end = fmt.text + fmt.len; b32 no_more_args = 0; for (u8 *c = fmt.text; c < end; ++c) { u8 *next = ((c + 1) < end) ? (c + 1) : (u8 *)"\0"; /* Escape '%%' */ b32 escape = !no_more_args && *c == '%' && *next == '%'; if (escape) { /* Skip the escape '%' char from parsing */ ++c; } if (!no_more_args && !escape && *c == '%' && *next == 'F') { String parsed_str = ZI; /* Detect arg type and parse to string */ FmtArg arg = va_arg(args, FmtArg); switch (arg.kind) { default: { /* Unknown format type */ Assert(0); parsed_str = PushString(arena, Lit("")); no_more_args = 1; } break; case FmtKind_Char: { parsed_str = StringFromChar(arena, arg.value.c); } break; case FmtKind_String: { parsed_str = PushString(arena, arg.value.string); } break; case FmtKind_Uint: { parsed_str = StringFromU64(arena, arg.value.uint, 10, arg.zfill); } break; case FmtKind_Sint: { parsed_str = StringFromI64(arena, arg.value.sint, 10, arg.zfill); } break; case FmtKind_Hex: { parsed_str = StringFromU64(arena, arg.value.sint, 16, arg.zfill); } break; case FmtKind_Ptr: { parsed_str = StringFromPtr(arena, arg.value.ptr); } break; case FmtKind_Float: { parsed_str = StringFromF64(arena, arg.value.f, arg.precision); } break; case FmtKind_Handle: { parsed_str = StringFromhandle(arena, arg.value.handle.h64[0], arg.value.handle.h64[1]); } break; case FmtKind_Uid: { parsed_str = StringFromUid(arena, arg.value.uid); } break; case FmtKind_End: { /* Unexpected end. Not enough FMT args passed to function. */ Assert(0); parsed_str = PushString(arena, Lit("")); no_more_args = 1; } break; } /* Update final string len / start */ final_len += parsed_str.len; /* Skip 'F' from parsing */ ++c; } else { /* Parse character normally */ StringFromChar(arena, *c); ++final_len; } } #if RtcIsEnabled if (!no_more_args) { FmtArg last_arg = va_arg(args, FmtArg); /* End arg not reached. Too many FMT values passed to function. */ Assert(last_arg.kind == FmtKind_End); } #endif return (String) { .len = final_len, .text = final_text }; } String FormatString_(Arena *arena, String fmt, ...) { va_list args; va_start(args, fmt); String new_str = FormatStringV(arena, fmt, args); va_end(args); return new_str; } //////////////////////////////// //~ Unicode //- Codepoint iteration CodepointIter BeginCodepointIter(String str) { return (CodepointIter) { .src = str }; } /* Returns 0 if done iterating */ b32 AdvanceCodepointIter(CodepointIter *iter) { if (iter->pos < iter->src.len) { String str_remaining = { .len = (iter->src.len - iter->pos), .text = iter->src.text + iter->pos }; Utf8DecodeResult decoded = DecodeUtf8(str_remaining); iter->pos += decoded.advance8; iter->codepoint = decoded.codepoint; return 1; } else { return 0; } } void EndCodepointIter(CodepointIter *iter) { /* Do nothing */ LAX iter; } //- String decode /* utf8 <- utf16 */ String StringFromString16(Arena *arena, String16 str16) { String result = { .len = 0, .text = PushDry(arena, u8) }; u64 pos16 = 0; while (pos16 < str16.len) { String16 str16_remaining = { .len = (str16.len - pos16), .text = str16.text + pos16 }; Utf16DecodeResult decoded = DecodeUtf16(str16_remaining); Utf8EncodeResult encoded = EncodeUtf8(decoded.codepoint); u8 *dst = PushStructsNoZero(arena, u8, encoded.count8); CopyBytes(dst, encoded.chars8, encoded.count8); pos16 += decoded.advance16; result.len += encoded.count8; } return result; } /* utf8 <- utf32 */ String StringFromString32(Arena *arena, String32 str32) { String result = { .len = 0, .text = PushDry(arena, u8) }; u64 pos32 = 0; while (pos32 < str32.len) { String32 str32_remaining = { .len = (str32.len - pos32), .text = str32.text + pos32 }; Utf32DecodeResult decoded = DecodeUtf32(str32_remaining); Utf8EncodeResult encoded = EncodeUtf8(decoded.codepoint); u8 *dst = PushStructsNoZero(arena, u8, encoded.count8); CopyBytes(dst, &encoded.chars8, encoded.count8); pos32 += 1; result.len += encoded.count8; } return result; } //- String encode /* utf16 <- utf8 */ String16 String16FromString(Arena *arena, String str8) { String16 result = { .len = 0, .text = PushDry(arena, u16) }; u64 pos8 = 0; while (pos8 < str8.len) { String str8_remaining = { .len = (str8.len - pos8), .text = str8.text + pos8 }; Utf8DecodeResult decoded = DecodeUtf8(str8_remaining); Utf16EncodeResult encoded = EncodeUtf16(decoded.codepoint); u16 *dst = PushStructsNoZero(arena, u16, encoded.count16); CopyBytes(dst, encoded.chars16, (encoded.count16 << 1)); pos8 += decoded.advance8; result.len += encoded.count16; } return result; } /* utf32 <- utf8 */ String32 String32FromString(Arena *arena, String str8) { String32 result = { .len = 0, .text = PushDry(arena, u32) }; u64 pos8 = 0; while (pos8 < str8.len) { String str8_remaining = { .len = (str8.len - pos8), .text = str8.text + pos8 }; Utf8DecodeResult decoded = DecodeUtf8(str8_remaining); Utf32EncodeResult encoded = EncodeUtf32(decoded.codepoint); u32 *dst = PushStructNoZero(arena, u32); *dst = encoded.chars32; pos8 += decoded.advance8; result.len += 1; } return result; } //////////////////////////////// //~ Legacy null-terminated C string operations //- Narrow C strings u64 CstrLenNoLimit(char *cstr) { char *end = cstr; if (cstr) { while (*end) { ++end; } } return end - cstr; } u64 CstrLen(char *cstr, u64 limit) { char *end = cstr; if (cstr) { for (u64 i = 0; i < limit; ++i) { if (*end) { ++end; } else { break; } } } return end - cstr; } char *CstrFromString(Arena *arena, String src) { u8 *text = PushStructsNoZero(arena, u8, src.len + 1); CopyBytes(text, src.text, src.len); text[src.len] = 0; return (char *)text; } char *CstrFromStringToBuff(String dst_buff, String src) { if (dst_buff.len > 0) { u64 len = MinU64(src.len, dst_buff.len - 1); CopyBytes(dst_buff.text, src.text, len); dst_buff.text[len] = 0; } return (char *)dst_buff.text; } String StringFromCstrNoLimit(char *cstr) { u64 len = CstrLenNoLimit(cstr); return (String) { .len = len, .text = (u8 *)cstr }; } String StringFromCstr(char *cstr, u64 limit) { u64 len = CstrLen(cstr, limit); return (String) { .text = (u8 *)cstr, .len = len }; } //- Wide C strings u64 WstrLenNoLimit(wchar_t *wstr) { wchar_t *end = wstr; if (end) { while (*end) { ++end; } } return end - wstr; } u64 WstrLen(wchar_t *wstr, u64 limit) { wchar_t *end = wstr; if (wstr) { for (u64 i = 0; i < limit; ++i) { if (*end) { ++end; } else { break; } } } return end - wstr; } wchar_t *WstrFromString(Arena *arena, String src) { String16 str16 = String16FromString(arena, src); *PushStructNoZero(arena, u16) = 0; return (wchar_t *)str16.text; } wchar_t *WstrFromString16(Arena *arena, String16 src) { u16 *text = PushStructsNoZero(arena, u16, src.len + 1); text[src.len] = 0; return (wchar_t *)text; } String StringFromWstrNoLimit(Arena *arena, wchar_t *wstr) { String16 str16 = String16FromWstrNoLimit(wstr); return StringFromString16(arena, str16); } String StringFromWstr(Arena *arena, wchar_t *wstr, u64 limit) { String16 str16 = String16FromWstr(wstr, limit); return StringFromString16(arena, str16); } String16 String16FromWstrNoLimit(wchar_t *wstr) { u64 len = WstrLenNoLimit(wstr); return (String16) { .len = len, .text = (u16 *)wstr }; } String16 String16FromWstr(wchar_t *wstr, u64 limit) { u64 len = WstrLen(wstr, limit); return (String16) { .len = len, .text = (u16 *)wstr }; }