# Strings

> Brine strings are immutable UTF-8; this page covers byte indexing and slicing, every string method, f-strings and format specs, escapes, and conversions to and from numbers.

Source: https://docs.brinelang.org/language/strings/

A `str` is immutable UTF-8 text. `len(s)`, `s[i]` and slices count **bytes**, not characters; `for c in s` and `s.chars()` walk characters. Methods never change a string; they return a new one.

```brine
fn main():
    let s = "hello, brine"
    let name = "brine"
    print(s.upper(), len(s), s[7:], s.find("brine"))
    print(f"{name} has {len(name)} letters, pi is {3.14159:.2f}")
```

Output

```text
HELLO, BRINE 12 brine Some(7)
brine has 5 letters, pi is 3.14
```

## String literals

[Section titled “String literals”](#string-literals)

Strings use double quotes (single quotes work too). These escapes are the only ones:

| Escape                 | Meaning                                          |
| ---------------------- | ------------------------------------------------ |
| `\n`, `\t`, `\r`, `\0` | newline, tab, carriage return, NUL               |
| `\\`, `\"`, `\'`       | backslash, quotes                                |
| `\{`, `\}`             | literal braces                                   |
| `\u{XXXX}`             | a Unicode code point in hex: `"\u{e9}"` is `"é"` |

Anything else, such as `\x41`, is error E0101.

## Bytes and characters

[Section titled “Bytes and characters”](#bytes-and-characters)

```brine
fn main():
    let s = "héllo"
    print(len(s), len(s.chars()))
    print(s[0], s[1:3], s[3:])
    print(s.chars())
    print(s.bytes())
    for c in s:
        print(c, end=" ")
    print()
```

Output

```text
6 5
h é llo
["h", "é", "l", "l", "o"]
[104, 195, 169, 108, 108, 111]
h é l l o
```

* `len(s)` is the number of bytes. `é` takes two bytes in UTF-8, so `"héllo"` has length 6.
* `s[i]` is a one-byte `str`. For ASCII text that’s the character at `i`. Inside a multi-byte character it’s a single byte, which isn’t valid text on its own, so for non-ASCII text use `s.chars()` or `for c in s`.
* `s[a:b]` is the bytes from `a` up to `b`. Indexes outside the string and `a > b` panic. There are no negative indexes: write `s[len(s) - 1]`.
* `s.chars()` is a `[str]` of characters; `s.bytes()` is an `[int]` of byte values.

Why bytes? Byte indexing is constant time and is what parsers and file formats need. Python’s character indexing costs either memory (fixed-width storage) or time.

Strings can’t be changed in place:

```brine
fn main():
    var s = "abc"
    s[0] = "x"
```

```text
error[E0404]: cannot assign to a character of a string
 --> main.br:3:5
  |
3 |     s[0] = "x"
  |     ^^^^ strings can't be changed in place
  |
  = help: build a new string, e.g. `s = s[:i] + "x" + s[i + 1:]`
  = run `brine explain E0404` for more about this error
error: could not compile due to 1 error
```

## Joining strings

[Section titled “Joining strings”](#joining-strings)

`+` joins two strings. `+=` on a `var` appends, growing the string in place when nothing else holds it, so building a string in a loop is fast:

```brine
fn main():
    var out = ""
    for i in 0..5:
        out += str(i)
        out += " "
    let line = "[" + out.rstrip() + "]"
    print(line, "-".join(["a", "b", "c"]), "ab".repeat(3))
```

Output

```text
[0 1 2 3 4] a-b-c ababab
```

`+` needs two strings. `"n = " + 5` is error E0305: convert with `str(5)` or use an f-string. `"ab" * 3` is an error too; write `"ab".repeat(3)`.

Strings compare with `==`, `!=`, `<`, `<=`, `>` and `>=` (byte order), and `x in s` tests for a substring.

## Methods

[Section titled “Methods”](#methods)

```brine
fn main():
    let s = "hello, brine"
    print(s.find("brine"), s.find("zzz"), s.count("l"))
    print(s.starts_with("he"), s.ends_with("!"), "ri" in s, s.contains("ri"))
    print(s.split(", "), "a b  c".split(), "a,,b".split(","))
    print(s.replace("l", "L"), "-".join(["x", "y"]), "ab".repeat(2))
    print("[" + "  x ".strip() + "]", "[" + "  x ".lstrip() + "]", "[" + "  x ".rstrip() + "]")
    print("straße".upper(), "ÉCOLE".lower())
    print("one\ntwo\r\nthree\n".lines())
```

Output

```text
Some(7) None 2
true false true true
["hello", "brine"] ["a", "b", "c"] ["a", "", "b"]
heLLo, brine x-y abab
[x] [x ] [  x]
STRASSE école
["one", "two", "three"]
```

| Method                                  | Result        | What it does                                         |
| --------------------------------------- | ------------- | ---------------------------------------------------- |
| `s.find(sub)`                           | `Option[int]` | byte index of the first `sub`, or `None`             |
| `s.contains(sub)`, `sub in s`           | `bool`        | substring test                                       |
| `s.count(sub)`                          | `int`         | non-overlapping occurrences                          |
| `s.starts_with(p)`, `s.ends_with(p)`    | `bool`        |                                                      |
| `s.split(sep)`                          | `[str]`       | split on every `sep`; keeps empty parts              |
| `s.split()`                             | `[str]`       | split on runs of whitespace; no empty parts          |
| `sep.join(xs)`                          | `str`         | join a `[str]` with `sep` between                    |
| `s.replace(old, new)`                   | `str`         | replace every `old`                                  |
| `s.repeat(n)`                           | `str`         | `n` copies                                           |
| `s.strip()`, `s.lstrip()`, `s.rstrip()` | `str`         | remove whitespace from both ends, the start, the end |
| `s.upper()`, `s.lower()`                | `str`         | Unicode case conversion                              |
| `s.lines()`                             | `[str]`       | split on `\n` or `\r\n`; no trailing empty line      |
| `s.chars()`                             | `[str]`       | each character                                       |
| `s.bytes()`                             | `[int]`       | each byte (0 to 255)                                 |
| `s.is_digit()`                          | `bool`        | non-empty and only `0`-`9`                           |
| `s.is_alpha()`, `s.is_alnum()`          | `bool`        | Unicode letters (and digits)                         |
| `s.is_space()`                          | `bool`        | Unicode whitespace                                   |
| `len(s)`                                | `int`         | length in bytes                                      |

* `find`, `split(sep)`, `replace` and `count` panic when the pattern is empty (`split() separator is empty`). `find("")` is `Some(0)`.
* `strip()` and its two variants take no arguments; they remove whitespace only.
* Case conversion and `is_alpha`/`is_alnum`/`is_space` follow Unicode, as in Python: `"straße".upper()` is `"STRASSE"`. `is_digit` accepts only `0`-`9`, the digits `int()` reads, so `"٣".is_digit()` is `false`.

### Characters and code points

[Section titled “Characters and code points”](#characters-and-code-points)

```brine
fn main():
    print(ord("a"), ord("é"), chr(97), chr(0x1F600))
    print("42".is_digit(), "-4".is_digit(), "".is_digit(), "é".is_alpha(), " \t".is_space())
```

Output

```text
97 233 a 😀
true false false true true
```

`ord(c)` needs exactly one character and panics otherwise (`ord() needs exactly one character, got a string of length 2`). `chr(n)` panics for negative numbers and other values that aren’t Unicode scalar values.

## f-strings

[Section titled “f-strings”](#f-strings)

An `f` before the quote makes an f-string: each `{expr}` is replaced by the value of the expression, printed the way `print` shows it. `{{` and `}}` are literal braces.

```brine
fn main():
    let name = "brine"
    let xs = [1, 2]
    let n = 42
    print(f"{name.upper()} {n + 1} {xs} {n > 40} {{braces}}")
```

Output

```text
BRINE 43 [1, 2] true {braces}
```

An expression inside an f-string can’t contain a string literal (`f"{"a"}"` is an error); put the string in a variable first.

### Format specs

[Section titled “Format specs”](#format-specs)

Add `:spec` after the expression to format a number or pad a value. The spec is `[<|>][0][width][.precision][kind]`:

```brine
fn main():
    let pi = 3.14159265
    let n = 42
    let name = "brine"
    print(f"[{pi:.2f}] [{pi:.0f}] [{pi:e}] [{pi:.3e}] [{pi:.2}]")
    print(f"[{n:5}] [{n:<5}] [{n:>5}] [{n:05}] [{-n:05}]")
    print(f"[{n:x}] [{n:b}] [{255:08b}] [{n:.1f}]")
    print(f"[{name:8}] [{name:<8}] [{pi:10.3f}] [{pi:<10.3f}] [{pi:08.2f}]")
```

Output

```text
[3.14] [3] [3.141593e+00] [3.142e+00] [3.14]
[   42] [42   ] [   42] [00042] [-0042]
[2a] [101010] [11111111] [42.0]
[   brine] [brine   ] [     3.142] [3.142     ] [00003.14]
```

| Spec       | Meaning                            | Example                  |
| ---------- | ---------------------------------- | ------------------------ |
| `.Nf`      | fixed point, `N` decimals          | `{pi:.2f}` → `3.14`      |
| `.N`       | the same as `.Nf`                  | `{pi:.2}` → `3.14`       |
| `e`, `.Ne` | scientific (6 decimals by default) | `{pi:.3e}` → `3.142e+00` |
| `x`        | hexadecimal, integers only         | `{255:x}` → `ff`         |
| `b`        | binary, integers only              | `{5:b}` → `101`          |
| `N`        | minimum width `N`, right-aligned   | `{n:5}` → `42`           |
| `<N`       | left-align in `N` columns          | `{n:<5}` → `42`          |
| `>N`       | right-align (the default)          | `{n:>5}` → `42`          |
| `0N`       | pad with zeros after the sign      | `{n:05}` → `00042`       |

* Every value is right-aligned by default, strings too (Python left-aligns strings). Use `<` for left-aligned columns of text.
* `f`, `e` and `.N` need a number; an integer is printed as a float (`{42:.1f}` is `42.0`). `x` and `b` need an integer.
* There is no centering (`^`), no fill character other than `0`, and no `,` thousands separator. An unsupported spec is error E0318:

```brine
fn main():
    let n = 7
    print(f"{n:^5}")
```

```text
error[E0318]: invalid format spec `^5`
 --> main.br:3:14
  |
3 |     print(f"{n:^5}")
  |              ^ in this f-string field
  |
  = help: supported: `.2f` (fixed), `.3e` (scientific), `8` (width), `<8` (left-align), `08` (zero-pad), `x` (hex), `b` (binary)
  = run `brine explain E0318` for more about this error
error: could not compile due to 1 error
```

## Converting to and from strings

[Section titled “Converting to and from strings”](#converting-to-and-from-strings)

`str(x)` converts any printable value. `int(s)` and `float(s)` parse text, and so do the sized types (`i32("7")`, `u8("200")`, `f32("1.5")`):

```brine
fn main():
    print(str(42) + "!", str(2.5), str(true), str([1, 2]), str(Some(3)))
    print(int("42") + 1, int("-7"), int(" 42"), float("2.5"), float("1e3"))
    print(str(1.0), str(0.1 + 0.2), str(1e20), str(1e-7))
```

Output

```text
42! 2.5 true [1, 2] Some(3)
43 -7 42 2.5 1000.0
1.0 0.30000000000000004 1e+20 1e-07
```

Bad input panics. Text from outside the program (files, arguments) should be checked first, for example with `is_digit()`:

```brine
fn main():
    let text = "4x2"
    print(int(text))
```

Output

```text
panic: invalid integer: "4x2"
  --> main.br:3:11
```

| Call                 | Panics when                                                                                        |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| `int(s)`             | `s` isn’t a decimal integer (`"3.5"`, `"0x1f"` and `"1_000"` all panic) or doesn’t fit in an `int` |
| `u8(s)`, `i32(s)`, … | the same, or the number is out of range for the type                                               |
| `float(s)`           | `s` isn’t a number; `"inf"` and `"nan"` are accepted                                               |
| `ord(c)`             | `c` isn’t exactly one character                                                                    |
| `chr(n)`             | `n` isn’t a valid code point                                                                       |

Surrounding whitespace is ignored when parsing (`int(" 42\n")` is 42).

Floats print in the shortest form that reads back as the same value, always with a decimal point or exponent: `1.0`, `0.30000000000000004`, `1e+20`.

## Rules

[Section titled “Rules”](#rules)

* Strings are immutable UTF-8. Methods return new strings.
* `len`, `s[i]` and slices count bytes; `for c in s` and `s.chars()` count characters.
* No negative indexes. Out-of-range indexes and slices panic.
* `+` joins strings only; convert other values with `str()` or an f-string.
* `int()` and `float()` panic on bad input.

Related: [arrays](/language/arrays/), [built-in functions](/language/builtins/), [values and types](/language/values-and-types/), [error handling](/language/error-handling/).
