Strings & Text

Nim strings are mutable, length-prefixed byte sequences with value semantics on assignment. That single sentence explains most of the differences you will notice coming from other languages: no pointer arithmetic, no terminator to check, and a byte index that is not always the same thing as a character.

String Basics

A string stores bytes and knows its own length. Indices run from 0 to len - 1, and the ^ prefix counts from the end so you rarely need to compute that arithmetic yourself.

Indexing and Iteration

let word = "nim"

echo word.len            # 3 — a stored length, not a scan for a terminator
echo word[0]             # n — indexing yields a char
echo word[^1]            # m — '^1' is the last byte, no off-by-one arithmetic

for ch in word:          # iterate bytes directly
  echo ch, "=", ord(ch)  # n=110  i=105  m=109

# Slices copy, so the original is untouched and immutable when declared 'let'.
echo word[0 .. 1]        # ni
var mutable = "abc"
mutable[0] = 'A'         # strings are mutable when the binding allows it
echo mutable             # Abc

Bytes Versus Characters

Non-ASCII text occupies several bytes per character. When a length must count characters rather than bytes, use the Unicode helpers instead of len.

import std/unicode

let greek = "αβγ"
echo greek.len                 # 6 — bytes, because each Greek letter is 2 bytes
echo greek.runeLen             # 3 — characters

for rune in greek.runes:       # iterate characters, not bytes
  echo rune                    # α β γ, one per line

echo greek.toUpperAscii()      # ASCII-only case rules: non-ASCII stays as is

Manipulation with strutils

std/strutils holds the everyday text operations. Most of them return a new string; a few mutate in place, and the names make the difference explicit.

Split, Join and Trim

import std/strutils

let csv = " ada , grace , linus "
let fields = csv.split(',')            # seq[string]; whitespace is NOT removed
echo fields.len, " fields"             # 3

var cleaned: seq[string]
for field in fields:
  cleaned.add(field.strip())           # strip removes surrounding whitespace
echo cleaned.join("|")                 # ada|grace|linus

echo "Hello".toUpperAscii(), " ", "Hello".toLowerAscii()   # HELLO hello
echo "report.txt".startsWith("report"), " ", "report.txt".endsWith(".txt")
echo "a,b,,c".count(',')               # 3 — count occurrences of a substring

Building and Formatting Text

Text is assembled with concatenation when there are two or three parts, and with interpolation or joins when there are more. Casting any value to text goes through the $ operator.

Concatenation, Repetition, Padding

import std/strutils

echo "log" & "-" & "file"       # log-file — '&' joins two strings
echo "ab".repeat(3)             # ababab

var line = "item"
line.add(": 3")                 # 'add' appends in place, avoiding a new string
line.add('!')                   # chars append too
echo line                       # item: 3!

# Padding and alignment matter when printing columns.
echo "id".alignLeft(6, '.') & "name"          # id....name
echo align("42", 5, '0')                      # 00042 — right-aligned, zero filled

echo 3.14159.formatFloat(ffDecimal, 2)        # 3.14 — fixed decimals
echo $12 & " as text"                         # '$' converts any value to text

Interpolation with strformat

std/strformat turns a string literal into a template with typed placeholders. It is checked at compile time, so a misspelled name is an error, not an empty field at runtime.

import std/strformat

let user = "ada"
let attempts = 3
let ratio = 0.875

echo &"user={user} attempts={attempts}"        # user=ada attempts=3
echo &"success {ratio * 100:.1f}%"            # 87.5% — float with one decimal
echo &"{attempts:>4}|"                        # '   3|' — width 4, right aligned
echo &"{user:^9}|"                            # '   ada   |' — centered in width 9

# A format string can be stored and reused; the fields are resolved when used.
const template = "attempt {attempts} for {user}"
echo &template