chars

Characters and code points: making a slug, and the two renderings of a Char.

examples/text/chars.dawn

# Characters and code points: making a slug, and the two renderings of a Char.
#
# A `Char` is one Unicode scalar value and `'a'` is a literal of that type.
# A string's characters come out of `code_points`; none of this is bytes and
# none of it is UTF-16 units, which is why the balloon below counts as one
# character and why slicing never lands in the middle of it.
#
# Run: dawn run examples/text/chars.dawn

use std/char
use std/str

## A slug: folded to lower case, letters and digits kept, every run of anything
## else one hyphen, and no hyphen at either end.
##
## `char.is_alnum` answers Unicode's question, not ASCII's -- CJK is letters,
## so 世界 survives and the emoji next to it does not.
fn slug(title: String) -> String = {
  var out: List[Char] = []
  var gap = false
  for c in code_points(str.to_lower(title)) {
    if char.is_alnum(c) {
      if gap && len(out) > 0 { out = out ++ ['-'] }
      out = out ++ [c]
      gap = false
    } else {
      gap = true
    }
  }
  from_code_points(out)
}

## The first character of each word, upper-cased.
##
## There is no `char.to_upper`. Case mapping lives on `String` because it is
## not in general one character for one character, so the trip out of `Char`
## is `str.from_char` and the answer comes back as text.
fn initials(name: String) -> String = {
  var out = ""
  for word in str.split(name, " ") {
    if not str.is_empty(word) {
      out = out ++ str.to_upper(str.from_char(str.at(word, 0)))
    }
  }
  out
}

pub fn main() -> Unit !io = {
  for title in ["Hello, World!", "Dawn v0.57 released", "你好 世界 🎈"] {
    # No padding here on purpose: a code point count is not a display width,
    # and lining these up by str.len would put the CJK row in the wrong place.
    println("$title -> ${slug(title)}")
  }

  println(initials("ada lovelace"))

  # `"${c}"` is the character and `show(c)` is the source literal: std/char
  # writes an impl at both layers, Display for the top level and Show for the
  # nested one, so the same character inside a list keeps its quotes.
  println("'a' interpolates as ${'a'}, and inside a list as ${to_string(['a'])}")

  # One balloon: one character, four UTF-8 bytes, two UTF-16 units, and none
  # of those last two numbers is one this program can observe.
  let third = str.at("a🎈b", 2)
  println("\"a🎈b\" is ${str.len("a🎈b")} characters, the third being ${str.from_char(third)}")
}

test "a slug keeps letters and digits and joins the rest with one hyphen" {
  assert slug("Hello, World!") == "hello-world"
  assert slug("  Dawn v0.57 released  ") == "dawn-v0-57-released"
  assert slug("!!!") == ""
}

test "letters means Unicode's letters" {
  assert slug("你好 世界 🎈") == "你好-世界"
}

test "one emoji is one character" {
  assert str.len("a🎈b") == 3
  assert code_points("a🎈b") == ['a', '🎈', 'b']
  assert str.at("a🎈b", 2) == 'b'
  assert str.slice("a🎈b", 0, 2) == "a🎈"
}

test "Char is opaque over Int: comparable, not arithmetic" {
  assert 'a' < 'b'
  assert char.code('a') == 97
  assert char.of(97) == Some('a')
  # Half a surrogate pair is not a character, so there is no Char for it.
  assert char.of(0xD800) == None
  # `'a' + 1` does not compile; going through the code point is the way.
  assert char.of(char.code('a') + 1) == Some('b')
}

test "a Char renders as itself on top and as a literal inside" {
  # `Display` decides the top-level rendering, `Show` the nested one, and
  # std/char writes both (spec §3.5, §4.3)
  assert to_string('a') == "a"
  assert "${'🎈'}" == "🎈"
  assert to_string(['a', '🎈']) == "['a', '🎈']"
  # which is a different answer from the Int's, and from a List[String]
  assert to_string(['a', '🎈']) != to_string([97, 127880])
  assert to_string(['a']) != to_string(["a"])
  # `str.from_char` asks for the top-level string by name, without the impl
  assert str.from_char('🎈') == to_string('🎈')
}

test "initials take the first character, not the first byte" {
  assert initials("ada lovelace") == "AL"
  assert initials("世 界") == "世界"
}