How do I work with strings in EK9?

← Getting Started · Ref: Q37

EK9 strings use double quotes for plain text and backticks for interpolation. Strings are streamable as character sequences, so substring extraction and text analysis use the same stream pipeline syntax as collections.

LITERALS AND INTERPOLATION

  greeting <- "Hello World"
  stdout.println(`Welcome to ${lang} version ${ver}`)

Escape sequences: \t
\\ \" \uXXXX.

KEY METHODS

  .upperCase(), .lowerCase(), .trim(), .length(), .count(char)
  .rightPadded(n), .leftPadded(n)

Concatenation with +. Use 'ek9 -h String' for the full API.

STREAMABLE STRINGS

Strings stream as characters using skip/head/map/filter/collect:

  brownFox <- cat sentence | skip 10 | head 9 | collect as String

This replaces substring() with unambiguous skip-and-take semantics. Works identically on strings and collections.

Comparison: ==, <>, <, >, <=, >=, <=>. Hashcode: #?.

See Q33 for regex matching. See Q38 for the Character type. See Q43 for backtick escaping. See Q44 for locale formatting. See Q113 for text construct. See Q170 for string length. See Q171 for string contains. See Q242 for conversion operators.

Example

defines module qa.strings

  defines function

    charToLowerCaseString()
      -> ch as Character
      <- result as String: ch? <- $ch.lowerCase() else String()

    stringComparator()
      ->
        s1 as String
        s2 as String
      <-
        comparison as Integer: s1 <=> s2

    justStringValue()
      -> s1 as String
      <- rtn as String: s1

    isO()
      -> ch as Character
      <- found as Integer: ch == 'o' or ch == 'O' <- 1 else Integer()

  defines program
    StringDemo()
      stdout <- Stdout()

      // === STRING LITERALS ===

      // Double-quoted strings
      greeting <- "Hello World"
      stdout.println(greeting)

      // String interpolation with backticks
      languageName <- "EK9"
      versionNumber <- 1
      stdout.println(`Welcome to ${languageName} version ${versionNumber}`)

      // Escape sequences: \t tab, \n newline, \\ backslash, \" quote
      withTab <- "Column1\tColumn2"
      stdout.println(withTab)

      withNewline <- "Line1\nLine2"
      stdout.println(withNewline)

      // Unicode escapes for non-ASCII characters
      danish <- "\u00C6\u00D8\u00C5"
      stdout.println(`Danish letters: ${danish}`)

      // Unset string
      unsetString <- String()
      require ~unsetString?

      // === STRING METHODS ===

      sentence <- "The Quick Brown Fox"

      // Case conversion
      stdout.println(`Upper: ${sentence.upperCase()}`)
      stdout.println(`Lower: ${sentence.lowerCase()}`)

      // Padding
      stdout.println(`Right padded: [${sentence.rightPadded(30)}]`)
      stdout.println(`Left padded: [${sentence.leftPadded(30)}]`)

      // Trimming
      paddedSentence <- sentence.rightPadded(30)
      stdout.println(`Trimmed: [${paddedSentence.trim()}]`)

      // Length — two equivalent syntaxes
      stdout.println(`Length (method): ${sentence.length()}`)
      stdout.println(`Length (operator): ${length sentence}`)

      // Count character occurrences
      stdout.println(`Count of 'o': ${sentence.count('o')}`)

      // === CONCATENATION ===

      part1 <- "The Quick Brown Fox"
      part2 <- "Jumps Over The Lazy Dog"
      fullSentence <- `${part1} ${part2}`
      stdout.println(fullSentence)

      // === HASHCODE ===

      hashValue <- #? sentence
      stdout.println(`Hashcode: ${hashValue}`)

      // === STRINGS ARE STREAMABLE ===

      // Strings stream as character sequences
      // Extract a substring using skip and head
      brownFox <- cat fullSentence | skip 10 | head 9 | collect as String
      stdout.println(`Extracted: [${brownFox}]`)

      // Going beyond string length is safe — returns what is available
      endPart <- cat fullSentence | skip 35 | head 20 | collect as String
      stdout.println(`End part: [${endPart}]`)

      // Join a list of strings via stream collect
      spacer <- " "
      wordParts <- [part1, spacer, part2]
      rejoined <- cat wordParts | collect as String
      stdout.println(`Rejoined: ${rejoined}`)

      // Count occurrences of o/O using stream map
      oCount <- cat fullSentence | map with isO | collect as Integer
      stdout.println(`Count of o/O: ${oCount}`)

      // Find unique characters used (sorted, lowercase)
      charsUsed <- cat fullSentence | map with charToLowerCaseString | sort with stringComparator | uniq by justStringValue | collect as String
      stdout.println(`Unique chars: [${charsUsed}]`)

      // === COMPARISON ===

      alpha <- "abc"
      beta <- "def"
      require alpha < beta
      require beta > alpha
      require alpha <> beta
      require alpha == "abc"
      require alpha <= "abc"
      require alpha >= "abc"

      // Min operator
      fruit1 <- "apple"
      fruit2 <- "banana"
      lesser <- fruit1 <? fruit2
      require lesser == "apple"
      stdout.println(`Min: ${lesser}`)

Common mistakes

E07620 — The + operator on String requires another String operand. Adding an Integer to a String triggers E07620 — type incompatibility. Use $ to convert to string first: "Hello" + $number. See ek9 -h E07620 for details.

Incorrect:

greeting <- "Hello World" + 42

Correct:

greeting <- "Hello World"
Other ways to ask this
  • How does string interpolation work in EK9?
  • Can I use strings as streams of characters in EK9?
  • What string methods does EK9 provide?
  • How do I extract substrings in EK9?

Coming from another language?

Java: no native interpolation, substring() methods, no streamable characters. Python: f-strings, slicing [start:end]. Kotlin: similar $var templates. EK9: backtick interpolation, strings natively streamable as characters, skip/head replaces substring().

Keywords: stream, pad, backtick, migrate, start, escape, uppercase, unicode, length, trim, interpolation, string, substring, first, intro, count, beginner, concatenation, lowercase