Modules

String

moduleSource
macOSLinuxWindowswasm

#import "String";

Searching, comparing, splitting, trimming and scanning strings, and working with file paths.

Strings are bytes. Case-insensitive procedures fold only the ASCII letters, and nothing here decodes UTF-8; use Unicode for that. Procedures that return a part of their input (trim, the splits, path_filename) return a view into it, not a copy. Procedures that build a new string (join, replace, to_lower_copy, path_join) allocate it with the context allocator, and the caller frees it. A search that finds nothing returns -1, or false with an empty or whole string.

ExampleOpen in playground ↗
assert(trim("  hello  ") == "hello");
found, key, value := split_from_left("name=jai", #char "=");
assert(found && key == "name" && value == "jai");

Procedures#

Tokens#

(line: *string, delimiters := DEFAULT_DELIMITERS, spaces := DEFAULT_SPACES) -> (value: string, success: bool)

Reads the next token from line and removes it, with any spaces before it, from line.

line := "  name =value";
token, ok := parse_token(*line);
assert(ok && token == "name" && line == " =value");

token, ok = parse_token(*line);
assert(ok && token == "=" && line == "value");
↗
(line: *string, match: string, delimiters := DEFAULT_DELIMITERS, spaces := DEFAULT_SPACES) -> (value: string, success: bool)

Reads the next token as parse_token does and returns it with whether it is equal to match. The token is removed from line whether or not it matches.

line := "if (x)";
_, matched := match_token(*line, "if");
assert(matched && line == " (x)");
↗
(line: *string, match: string, delimiters := DEFAULT_DELIMITERS, spaces := DEFAULT_SPACES) -> (value: string, success: bool)

Like match_token, with ASCII letters matched in either case.

line := "IF x";
_, matched := match_token_nocase(*line, "if");
assert(matched);
↗
(line: *string, delimiters := DEFAULT_DELIMITERS, spaces := DEFAULT_SPACES) -> (value: bool, success: bool)

Reads the next token and reads it as true or false, in either case. The second result is false when the token is anything else or there is none; the token is removed from line anyway.

line := "True rest";
value, ok := parse_bool(*line);
assert(ok && value && line == " rest");
↗
(line: *string, $T: Type, delimiters := DEFAULT_DELIMITERS, spaces := DEFAULT_SPACES) -> (value: T, success: bool)

Reads the next token and looks it up by name among the values of the enum type T. The second result is false when no value has that name, and then the value is zero. The name is case sensitive.

Color :: enum { RED; GREEN; }

line := "GREEN;";
value, ok := parse_enum(*line, Color);
assert(ok && value == .GREEN && line == ";");
↗
(line: *string, $T: Type = int, spaces := " \t", delimiters := DEFAULT_DELIMITERS) -> (value: T, success: bool)

Reads the next token as a whole number of type T (int by default). Only space and tab are skipped in front of it unless you pass spaces.

line := " 42;7";
value, ok := parse_int(*line);
assert(ok && value == 42);

big := "257";
small, small_ok := parse_int(*big, u8);
assert(!small_ok && small == 0);
↗
(line: *string, delimiters := DEFAULT_DELIMITERS, spaces := DEFAULT_SPACES) -> (value: float, success: bool)

Reads the next token as a decimal number. The second result is false when the token is not entirely a number, or there is none.

line := "2.5 rest";
value, ok := parse_float(*line);
assert(ok && value == 2.5);
↗

Paths#

(path: string) -> string

The last component of path: everything after the last / or \. Both separators count on every platform. The result points into path.

assert(path_filename("dir/name.ext") == "name.ext");
assert(path_filename("dir/") == "");
↗
(path: string) -> (extension: string, found_extension: bool)

The extension of the last component of path, without the dot, and whether it has one.

ext, found := path_extension("dir/archive.tar.gz");
assert(found && ext == "gz");

_, hidden := path_extension("dir/.hidden");
assert(!hidden);
↗
(path: string) -> string

path without the extension of its last component and the dot before it. A path with no extension is returned unchanged. The result points into path.

assert(path_strip_extension("dir/name.ext") == "dir/name");
assert(path_strip_extension("dir.d/name") == "dir.d/name");
↗
(name: string) -> (path: string, basename: string, extension: string, basename_with_extension: string)

Splits name into its directory (with the trailing separator), its name without the extension, the extension without the dot, and the name with the extension. All four point into name; a missing part is empty.

dir, base, ext, file := path_decomp("src/main.jai");
assert(dir == "src/" && base == "main" && ext == "jai" && file == "main.jai");
↗
(s: string) -> string

The last component of s without its extension.

assert(basename("src/main.jai") == "main");
↗
(path: string, separator: u8 = PATH_SEPARATOR)

Replaces every / and \ in path with separator (the platform's by default), in place. path must be writable, not a string literal.

path := copy_string("a/b\\c");
defer free(path);
path_overwrite_separators(path, #char "/");
assert(path == "a/b/c");
↗
(path: string, extension: string) -> string

path when its extension is already extension (without the dot); otherwise a new string with .extension added. Another extension is kept, so a.txt becomes a.txt.png.

same := path_ensure_extension("a.png", "png");
assert(same == "a.png");

longer := path_ensure_extension("a", "png");
defer free(longer);
assert(longer == "a.png");
↗
(components: ..string, separator: u8 = PATH_SEPARATOR) -> string

The components one after another with separator between them. A trailing separator on a component is dropped first, so no separator is doubled. Empty components still add a separator.

path := path_join("a/", "b", "c.txt", separator = #char "/");
defer free(path);
assert(path == "a/b/c.txt");
↗
(name: string) -> (path: string, basename: string, extension: string, basename_with_extension: string)

Splits name into its directory (with the trailing separator), its name without the extension, the extension without the dot, and the name with the extension. All four point into name; a missing part is empty.

dir, base, ext, file := path_decomp("src/main.jai");
assert(dir == "src/" && base == "main" && ext == "jai" && file == "main.jai");
↗
(s: string) -> string

The last component of s without its extension.

assert(basename("src/main.jai") == "main");
↗

Bytes#

(s: string, index: s64, count: s64) -> string

The part of s that starts at index and holds at most count bytes; empty when index is past the end. Both must not be negative: a negative one fails an assert.

(a: string, b: string) -> int

Orders two strings byte by byte: -1 when a sorts first, 1 when b does, 0 when they are equal.

assert(compare("apple", "banana") == -1);
assert(compare("b", "a") == 1);
assert(compare("ab", "ab") == 0);
assert(compare("a", "ab") == -1);
↗
(a: string, b: string) -> int

Like compare, but ASCII letters match in either case. Bytes outside A-Z and a-z compare as they are, so this does not fold other scripts.

assert(compare_nocase("Hello", "hELLO") == 0);
assert(compare_nocase("a", "B") == -1);
↗
(a: string, b: string) -> bool

Whether the two strings hold the same bytes. The same as a == b.

(a: string, b: string) -> bool

Whether the two strings are equal when ASCII letters are matched in either case.

assert(equal_nocase("MiXeD", "mixed"));
assert(!equal_nocase("abc", "abcd"));
↗
(s: string, prefix: string) -> bool

Whether s starts with prefix. An empty prefix always matches.

assert(begins_with("filename.txt", "file"));
assert(!begins_with("file", "filename"));
↗
(s: string, suffix: string) -> bool

Whether s ends with suffix. An empty suffix always matches.

assert(ends_with("filename.txt", ".txt"));
assert(!ends_with("txt", "name.txt"));
↗
(c: u8, chars: string) -> bool

Whether the byte c appears in chars.

assert(is_any(#char "b", "abc"));
assert(!is_any(#char "z", "abc"));
↗
(s: string, substring: string) -> (int, found: bool)

The index where substring first appears in s when ASCII letters are matched in either case, and whether it was found. When it is not found the index is -1. An empty substring, or one longer than s, is never found.

index, found := index_of_string_nocase("Hello World", "WORLD");
assert(found && index == 6);
↗
contains2 overloads
(s: string, substring: string) -> (found: bool, remainder: string)

Whether substring appears in s, and the part of s after its first match. When it does not appear the remainder is empty. An empty substring is never found. The remainder points into s.

found, rest := contains("copper/blue", "per/");
assert(found && rest == "blue");
↗
(s: string, substring: string) -> (found: bool, remainder: string)

Like contains, with ASCII letters matched in either case.

found, rest := contains_nocase("abcDEFghi", "def");
assert(found && rest == "ghi");
↗
(s: string, chars: string) -> (found: bool, remainder: string)

Whether any byte of chars appears in s, and the part of s after the first such byte.

found, rest := contains_any_character("a,b;c", ";,");
assert(found && rest == "b;c");
↗
(s: string, substrings: ..string) -> (found: bool, remainder: string)

Whether any of the substrings appears in s, and the part of s after the earliest match. At one position the first substring listed wins. Empty substrings are ignored.

found, rest := contains_any_string("say hello world", "world", "hello");
assert(found && rest == " world");
↗
(s: string, chars: string, replacement: u8)

Overwrites every byte of s that is in chars with replacement. s is changed in place, so it must not be a string literal.

text := copy_string("a-b_c");
defer free(text);
replace_chars(text, "-_", #char " ");
assert(text == "a b c");
↗
(s: string)

Turns the ASCII capitals of s into lower case, in place. Other bytes are left as they are, so UTF-8 text is not folded. s must be writable, not a string literal.

text := copy_string("MiXeD");
defer free(text);
to_lower_in_place(text);
assert(text == "mixed");
↗
(s: string)

Turns the ASCII lower case letters of s into capitals, in place. See to_lower_in_place.

text := copy_string("MiXeD");
defer free(text);
to_upper_in_place(text);
assert(text == "MIXED");
↗
(s: string) -> string

A copy of s with ASCII capitals turned into lower case. The copy is allocated with the context allocator and the caller frees it.

lower := to_lower_copy("Hello");
defer free(lower);
assert(lower == "hello");
↗
(s: string) -> string

A copy of s with ASCII lower case letters turned into capitals. The copy is allocated with the context allocator and the caller frees it.

upper := to_upper_copy("Hello");
defer free(upper);
assert(upper == "HELLO");
↗
(s: string, chars := DEFAULT_SPACES) -> string

s without the chars at its start; the result points into s.

assert(trim_left("  hi ") == "hi ");
↗
(s: string, chars := DEFAULT_SPACES) -> string

s without the chars at its end; the result points into s.

assert(trim_right("  hi ") == "  hi");
↗
(s: string, chars := DEFAULT_SPACES) -> string

s without the chars at either end; the result points into s.

assert(trim("  hi \t") == "hi");
assert(trim("--x--", "-") == "x");
↗
(_s: string) -> string

_s without the spaces, tabs and line breaks at its end. The same as trim_right.

(s: string, bytes: string) -> string

The part of s before the first byte that is in bytes, or all of s when there is none.

assert(stop_at_any("name=value", "=:") == "name");
↗
(s: *string)

Removes the carriage return from every \r\n pair in s, in place, and shortens s. A lone \r is kept. s must be writable.

text := copy_string("a\r\nb\rc");
defer free(text);
normalize_line_endings(*text);
assert(text == "a\nb\rc");
↗
split_from_left2 overloads
(s: string, byte: u8) -> (found: bool, left: string, right: string)

Splits s at the first byte. Returns whether it was found, the part before it and the part after it.

found, left, right := split_from_left("key=a=b", #char "=");
assert(found && left == "key" && right == "a=b");

found, left, right = split_from_left("none", #char "=");
assert(!found && left == "none" && right == "");
↗
split_from_right2 overloads
(s: string, byte: u8) -> (found: bool, left: string, right: string)

Splits s at the last byte. Returns whether it was found, the part before it and the part after it.

found, left, right := split_from_right("a.b.c", #char ".");
assert(found && left == "a.b" && right == "c");

found, left, right = split_from_right("abc", #char ".");
assert(!found && left == "abc" && right == "");
↗
(s: string, bytes: string) -> (found: bool, left: string, right: string)

Splits s at the first byte that is in bytes. Returns whether one was found, the part before it and the part after it. When none is found the result is false, all of s and an empty string.

found, left, right := split_from_left_by_any("a,b;c", ",;");
assert(found && left == "a" && right == "b;c");
↗
(s: string, bytes: string) -> (found: bool, left: string, right: string)

Splits s at the last byte that is in bytes. Returns whether one was found, the part before it and the part after it. When none is found the result is false, all of s and an empty string.

found, left, right := split_from_right_by_any("a,b;c", ",;");
assert(found && left == "a,b" && right == "c");
↗
(s: string, separator: $T/ .[string, u8]) -> [] string

The pieces of s between each separator (a string or a byte).

parts := split("a,b,c", ",");
assert(parts.count == 3 && parts[1] == "b");
↗
(inputs: ..string, separator := "", before_first := false, after_last := false) -> string

The strings one after another, with separator between them.

assert(join("a", "b", "c", separator = "-") == "a-b-c");
↗
(s: string, old: string, new: string) -> (string, ocurrences: int)

A copy of s with every non-overlapping old replaced by new, and how many were replaced.

result, count := replace("aaaaa", "aa", "x");
defer free(result);
assert(result == "xxa" && count == 2);
↗
(s: string, prefix: string) -> bool

Whether s starts with prefix. An empty prefix always matches.

assert(begins_with("filename.txt", "file"));
assert(!begins_with("file", "filename"));
↗

Patterns#

Scan#

(format: string, text: string) -> (bool, [] Any)

Reads values from text as format describes. Returns whether the whole text matched, and the values read, as Anys.

ok, values := scan("%d %s", "-8 \"yes\"");
assert(ok && values.count == 2);
assert((cast(*s64) values[0].value_pointer).* == -8);
assert((cast(*string) values[1].value_pointer).* == "yes");

percent_ok, percent_values := scan("%d%%", "50%");
assert(percent_ok && percent_values.count == 1);
↗
(text: string, format: string, args: ..Any) -> bool

Reads values from text as format describes, storing them through the pointers in args. Returns whether the whole text matched.

count: int;
flag: bool;
name: string;
ok := scan2("7 true \"bob\"", "%d %b %s", *count, *flag, *name);
assert(ok && count == 7 && flag && name == "bob");
↗

Self test#

()

Runs a few checks of the module with asserts. A smoke test; the real tests are in the module's test folder.

Constants#

:: " \t\r\n"

The bytes trim, trim_left and trim_right remove by default: space, tab, carriage return and line feed.

:: ";=()"

The bytes parse_token treats as tokens of their own by default: ;, =, ( and ).

:: #char "\\"

The separator a path joined on this platform uses: \ on Windows and / elsewhere.