# 5. Strings and text

> Chapter 5: immutable strings, interpolation, escapes, heredocs, regular expressions and formatted output.

Canonical: https://alice-lang.org/documentation/learn/strings/

[Documentation index](https://alice-lang.org/documentation/llms.txt) · [Complete book](https://alice-lang.org/llms-full.txt)

Almost every program reads text or writes it: a name typed by a user, a line from a file, a receipt, a log message. This chapter shows how Alice represents text, how to build it, how to search it with patterns, and how to lay it out neatly.

## 5.1 Strings do not change

A `String` is a sequence of characters. Methods that seem to change a string actually return a new one:

```alice
name = "alice"
loud = name.upcase
puts name
puts loud
```

```text title="Output"
alice
ALICE
```

`name` is still `"alice"`. Strings in Alice are **immutable**, like .NET strings. Assigning to a character does not compile:

```alice fail="AL0314"
name = "alice"
name[0] = 'A'
```

```text title="Error"
error[AL0314]
is read-only
```

Immutability sounds like a limit, but it makes strings safe to share. You can pass a string to any method, store it in a hash or hand it to another thread, and nobody can change it under you. To "change" a string, build a new one and assign it:

```alice
name = "alice"
name = name.capitalize
puts name
```

```text title="Output"
Alice
```

## 5.2 Building strings

You already know interpolation: `#{...}` inside double quotes. Any expression works there, not only variables:

```alice
cups = 3
price = 250
puts "#{cups} cups at #{price} cents: #{cups * price} cents"
puts "Tea" + " " + "time"
puts "-" * 10
```

```text title="Output"
3 cups at 250 cents: 750 cents
Tea time
----------
```

`+` joins two strings. `*` repeats one.

### Escapes and quotes

Inside double quotes, a backslash starts an **escape**: a way to write a character you cannot type directly.

```alice
puts "one\ttab"
puts "a \"quoted\" word"
puts "back\\slash"
puts "tea: \u{1F375}"
puts 'single quotes: #{not interpolated}\n'
```

```text title="Output"
one	tab
a "quoted" word
back\slash
tea: 🍵
single quotes: #{not interpolated}\n
```

`\n` is a newline and `\t` a tab. `\u{...}` writes any Unicode character by its number. Single quotes make a raw string, as in Ruby: no interpolation, and only `\\` and `\'` are escapes.

### Text over several lines: heredocs

A long piece of text, such as a letter or an SQL query, is awkward with `\n`. A **heredoc** lets you write it as it is. `<<~EOS` starts it, and a line with only `EOS` ends it. Any word works in place of `EOS`.

```alice
guest = "Alice"
letter = <<~EOS
  Dear #{guest},
    you are invited to tea.
  The Hatter
EOS
puts letter
```

```text title="Output"
Dear Alice,
  you are invited to tea.
The Hatter
```

The `~` removes the common indentation, so the heredoc can be indented with your code while the text keeps its own shape. Interpolation works inside.

### Many small pieces: `String.build`

Joining strings in a loop with `+` makes a new string every time. When you build a big string from many pieces, use `String.build`, which appends into one buffer:

```alice
text = String.build do |io|
  (1..3).each { |i| io << "cup " << i.to_s << "; " }
end
puts text
```

```text title="Output"
cup 1; cup 2; cup 3; 
```

## 5.3 Looking inside a string

```alice
s = "Earl Grey"
puts s.size
puts s.includes?("Grey")
puts s.starts_with?("Earl")
puts s.index("G")
puts s.downcase
puts s.reverse
puts s.split(" ")
puts s.chars.first
puts "  padded  ".strip + "|"
```

```text title="Output"
9
true
true
5
earl grey
yerG lraE
["Earl", "Grey"]
E
padded|
```

`size` counts characters. Text sent over a network or written to a file is stored as bytes in UTF-8, where one character may take several bytes. `bytesize` tells you how many:

```alice
puts "tea".size
puts "thé".size
puts "thé".bytesize
```

```text title="Output"
3
3
4
```

## 5.4 Patterns: regular expressions

How do you find "a number followed by a word" in `"2 Assam, 1 Sencha"`? Searching for fixed text is not enough. A **regular expression** describes a pattern of text. Alice writes it between slashes, as Ruby does:

| Pattern         | Matches                              |
| --------------- | ------------------------------------ |
| `\d`            | one digit                            |
| `\d+`           | one or more digits                   |
| `[a-z]`         | one lowercase letter                 |
| `\w+`           | a word: letters, digits, underscores |
| `\s`            | a space or tab                       |
| `( ... )`       | a group you can read back            |
| `(?<name> ...)` | a group with a name                  |

```alice
order = "2 Assam, 1 Sencha"
puts order.match?(/\d+/)
puts order.scan(/\d+/)
puts order.gsub(/\d+/, "#")
puts order =~ /Sencha/
```

```text title="Output"
true
["2", "1"]
# Assam, # Sencha
11
```

- `match?` asks whether the pattern occurs at all.
- `scan` returns every match.
- `gsub` replaces every match. `sub` replaces only the first.
- `=~` gives the position of the first match, or `nil`.

To read the parts of a match, use `match`. It returns a `MatchData?`, which is `nil` when nothing matched, so the compiler makes you check it:

```alice
line = "3 Earl Grey"
m = line.match(/(?<cups>\d+) (?<tea>.+)/)
if m
  puts m["cups"]
  puts m["tea"]
else
  puts "not an order"
end
```

```text title="Output"
3
Earl Grey
```

`m[0]` is the whole match, `m[1]` the first group, and `m["cups"]` the group named `cups`. A regex literal is compiled once and reused, so it is cheap to use in a loop.

`gsub` can also take a block that computes each replacement:

```alice
puts "2 Assam, 1 Sencha".gsub(/\d+/) { |n| (n.to_i * 2).to_s }
```

```text title="Output"
4 Assam, 2 Sencha
```

## 5.5 Formatting output

A receipt needs columns that line up and prices with exactly two decimals. Alice has two tools for that.

**Padding** methods fill a string up to a width:

```alice
puts "Assam".ljust(10, ".") + "2.50"
puts "Sencha".ljust(10, ".") + "3.50"
puts "42".rjust(6) + "|"
puts "tea".center(9, "*")
```

```text title="Output"
Assam.....2.50
Sencha....3.50
    42|
***tea***
```

**Format strings** describe the whole line with placeholders, as `printf` does in C. Use `format(...)`, or the `%` operator with an array of values:

```alice
puts format("%-10s %5.2f", "Assam", 2.5)
puts "%05d" % [42]
puts "%x %o %b" % [255, 8, 5]
puts "%d%%" % [15]
```

```text title="Output"
Assam       2.50
00042
ff 10 101
15%
```

| Placeholder            | Meaning                                        |
| ---------------------- | ---------------------------------------------- |
| `%d`                   | an integer                                     |
| `%s`                   | anything, as text                              |
| `%f`, `%.2f`           | a float, with 2 decimals                       |
| `%x`, `%o`, `%b`       | hexadecimal, octal, binary                     |
| `%5d`, `%-10s`, `%05d` | width 5, left-aligned in 10, padded with zeros |
| `%%`                   | a literal `%`                                  |

## Project: reading orders as text

Customers write orders as text: `"2 Assam"`. Let's turn such lines into a receipt, and reject lines that are not orders.

```alice
MENU : Hash(String, Int32) = {"Assam" => 250, "Earl Grey" => 300, "Sencha" => 350}

def money(cents : Int64) : String
  format("%d.%02d", cents / 100, cents % 100)
end

lines = ["2 Assam", "1 Earl Grey", "three Sencha", "1 Sencha"]
total = 0_i64
puts "Item".ljust(14) + "Price".rjust(8)
puts "-" * 22
lines.each do |line|
  m = line.match(/^(?<cups>\d+) (?<tea>.+)$/)
  if m
    cups = m["cups"].not_nil!.to_i
    tea = m["tea"].not_nil!
    cents = MENU[tea].to_i64 * cups
    total += cents
    puts "#{cups} x #{tea}".ljust(14) + money(cents).rjust(8)
  else
    puts "skipped: #{line}"
  end
end
puts "-" * 22
puts "Total".ljust(14) + money(total).rjust(8)
```

```text title="Output"
Item             Price
----------------------
2 x Assam         5.00
1 x Earl Grey     3.00
skipped: three Sencha
1 x Sencha        3.50
----------------------
Total            11.50
```

A group read with `m["cups"]` is a `String?`, because in some patterns a group can stay empty. In this pattern both groups always take part when the line matches, so `not_nil!` states what we know.

A constant that holds a collection needs its type written out, as in `MENU : Hash(String, Int32) = ...`. Constants are shared by every method, so Alice wants their type stated rather than guessed.

`^` and `$` anchor the pattern to the start and end of the line, so `"2 Assam extra"` would match only as a tea named `"Assam extra"`. What happens if the tea is not on the menu? `MENU[tea]` raises. [Chapter 8](https://alice-lang.org/documentation/learn/errors.md) handles that properly.

:::caution\[Common mistakes]

- **Trying to change a string in place.** `s[0] = 'A'` is `error[AL0314]`. Build a new string and assign it.
- **Interpolating in single quotes.** `'#{x}'` prints the braces. Use double quotes.
- **Using a `match` result without a check.** It is `MatchData?`: `error[AL0310]` until you check it with `if m`.
- **Building a big string with `+=` in a loop.** Each step copies the whole string. Use `String.build` or `join`.
  :::

## Exercises

1. Print `"Hello, World"` with every vowel replaced by `*`.
2. Write `initials(name : String) : String` so that `initials("Alice Pleasance Liddell")` is `"APL"`.
3. Extract every number from `"tables 4, chairs 12, cups 30"` and print their sum.
4. Print a three-line table of the squares of 1 to 3, numbers right-aligned in a width of 4, like `   1   1`.

<details>
<summary>Answers</summary>

1. A character class `[aeiou]` matches any one vowel. The `i` flag ignores case:

   ```alice
   puts "Hello, World".gsub(/[aeiou]/i, "*")
   ```

   ```text title="Output"
   H*ll*, W*rld
   ```

2. Answer:

   ```alice
   def initials(name : String) : String
     name.split(" ").map { |w| w.chars.first.to_s }.join
   end

   puts initials("Alice Pleasance Liddell")
   ```

   ```text title="Output"
   APL
   ```

3. Answer:

   ```alice
   puts "tables 4, chairs 12, cups 30".scan(/\d+/).map { |n| n.to_i }.sum
   ```

   ```text title="Output"
   46
   ```

4. Answer:

   ```alice
   (1..3).each { |i| puts "%4d%4d" % [i, i * i] }
   ```

   ```text title="Output"
      1   1
      2   4
      3   9
   ```

</details>

## Summary

- Strings are immutable. Methods return new strings; assign the result to keep it.
- `"#{expr}"` interpolates; single quotes do not. Escapes start with `\`.
- `<<~EOS` heredocs hold multi-line text; `String.build` assembles many pieces efficiently.
- Regular expressions are written `/.../`. `match?`, `scan`, `gsub`, `=~` and `match` search with them; `match` returns `MatchData?`.
- `ljust`, `rjust`, `center`, `format` and `%` lay out text.

In [chapter 6](https://alice-lang.org/documentation/learn/types.md) you will define your own types.

## Continue reading

- Previous: [4. Collections](https://alice-lang.org/documentation/learn/collections.md)
- Next: [6. Structs, records, classes and modules](https://alice-lang.org/documentation/learn/types.md)
