Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Iterating Runes

A Rune is a Unicode code point carried as an Int (Basic Types). To walk the characters of a String — not its raw bytes — put the String on DS and use foreach … /foreach. The loop variable i is each successive Rune.

You already met foreach for lists and maps in Iterators: foreach. String participates in the same protocol: foreach obtains a fresh rune iterator and drives the loop until the text is exhausted.

"aé🙂日" foreach
  i r>s $ sp
/foreach
nl
// prints: a é 🙂 日

Print a single Rune with r>s $ (r>s is ( rune:i -- s ); $ prints the String). Ordinary $ on an Int prints the decimal code point, which is rarely what you want for text. Inside a fmt string, use %c with the Int code point instead of converting first.

TextReader is also iterable by rune (Stream Types). Prefer foreach on a String when the text is already in memory; prefer a reader when the source is a file or you want line APIs as well.


Byte length vs rune count

: runeCount ( s:String -- n:i )
  0 swap
  foreach
    drop
    1 +
  /foreach
;

"é" dup str::len $ sp    // 2
runeCount $ nl           // 1

That gap is exactly the Utf-8 story from Unicode and Utf-8: str::len is storage; foreach is characters.


Example: source file rune census

This small program reads a Utf-8 source file into a String, walks every Rune, and prints a compact summary: how many code points, how the Utf-8 widths break down, and which non-ASCII characters appear. It is a handy sanity check when a file “looks fine” in an editor but something downstream complains about encoding — and it shows foreach, str::len, and StringBuilder working together.

: utf8Width ( r:i -- n:i )
  dup 0x80 < if
    drop 1
  else
    dup 0x800 < if
      drop 2
    else
      0x10000 < if
        3
      else
        4
      then
    then
  then
;

: readFileString ( path:s -- s:String true | err:s false )
  TextReader.newFromFile?^ not if
    false
  else
    >ts
    StringBuilder.new^ >xs
    t:peek foreach
      i x:.writeRune^
    /foreach
    t:.close^
    t:drop
    x:.toString^
    x:drop
    true
  then
;

: printRuneCensus ( path:s -- )
  dup >ys
  readFileString not if
    "cannot read: " y:peek str::+ $ nl
    "  " swap str::+ $ nl
    y:drop
  else
    >xs
    newdo >ts
    0 t:.runes!
    0 t:.w1!
    0 t:.w2!
    0 t:.w3!
    0 t:.w4!
    StringBuilder.new^ >zs
    x:peek foreach
      t:.runes@ 1 + t:.runes!
      i utf8Width
      switch
        a 1 == case
          t:.w1@ 1 + t:.w1!
        /case
        a 2 == case
          t:.w2@ 1 + t:.w2!
        /case
        a 3 == case
          t:.w3@ 1 + t:.w3!
        /case
        default
          t:.w4@ 1 + t:.w4!
        /default
      /switch
      i 0x80 >= if
        i z:.writeRune^
        32 z:.writeRune^
      then
    /foreach
    "File: " y:peek str::+ $ nl
    "  bytes (str::len): " x:peek str::len "%d" fmt str::+ $ nl
    "  runes:            " t:.runes@ "%d" fmt str::+ $ nl
    "  utf-8 widths:     " $
      t:.w1@ "%d×1 " fmt $
      t:.w2@ "%d×2 " fmt $
      t:.w3@ "%d×3 " fmt $
      t:.w4@ "%d×4" fmt $ nl
    "  non-ASCII runes:  " $
    z:.toString^ dup str::isEmpty if
      drop "(none)" $ nl
    else
      $ nl
    then
    z:drop
    t:drop
    x:drop
    y:drop
  then
;

"examples/stringRune-iter.fnl" printRuneCensus

Sample shape of the report (counts depend on the file):

File: examples/stringRune-iter.fnl
  bytes (str::len): 218
  runes:            210
  utf-8 widths:     204×1 2×2 0×3 4×4
  non-ASCII runes:  é 🙂 日

The interesting lines are the ones where bytes ≠ runes and the width histogram is not “all ×1”: that is Utf-8 doing its job, and foreach making the character stream easy to measure without hand-rolled decoding.

Next: StringBuilder for the mutable side of building text.