How do I work with characters in EK9?

← Getting Started · Ref: Q38

EK9 has a dedicated Character type for single Unicode characters, separate from String. Single quotes for characters, double quotes for strings. No implicit conversion — use #^ promote operator.

CHARACTER LITERALS

  letter <- 'a'
  accent as Character := '\u00E9'

Character() creates unset, Character(stringValue) creates from single-char string.

CASE CONVERSION

Instance methods:

  upper <- letter.upperCase()
  lower <- upper.lowerCase()
  roundTrip <- letter.upperCase().lowerCase()

PROMOTE TO STRING: THE #^ OPERATOR

  asString <- #^ letter               Prefix syntax
  alsoString <- letter.#^()            Method call syntax
  typed as String := #^letter          With explicit type

All conversions explicit and visible. $ also converts to string for display, but #^ is the semantic type promotion.

LENGTH

Set character has length 1, unset has length 0:

  length letter          returns 1
  letter.length()        method call syntax

HASHCODE

  hashValue <- #? letter               Prefix syntax
  hashValue <- letter.#?()             Method call syntax

COPY OPERATOR

  copied <- Character()
  copied :=: 'z'

INCREMENT AND DECREMENT

  working <- 'a'
  working++              now 'b'
  working--              back to 'a'

COMPARISON

By Unicode code point: ==, <>, <, >, <=, >=, <=>.

TRI-STATE

Comparing with unset returns unset Boolean:

  result <- Character() == 'a'     result is unset

CHARACTERS AND STRINGS TOGETHER

Strings are streamable as character sequences (see Q37). #^ converts Characters back to Strings for stream operations.

WHY SEPARATE?

Python/JavaScript have no character type. EK9 keeps them distinct: Character has case conversion, code-point comparison, ++/--. String has trim(), padded(), count(). The #^ operator makes the boundary explicit.

Use 'ek9 -h Character' for the full API.

See Q37 for strings. See Q43 for escape and interpolation.

Example

defines module qa.character

  defines program
    CharacterDemo()
      stdout <- Stdout()

      // === CHARACTER LITERALS ===

      // Single-quoted character literals
      letter <- 'a'
      digit <- '9'
      stdout.println(`Letter: ${letter}, Digit: ${digit}`)

      // Unicode escape for non-ASCII
      accent as Character := '\u00E9'
      stdout.println(`Accented: ${accent}`)

      // Unset character
      unsetChar <- Character()
      require ~unsetChar?
      stdout.println(`Unset isSet: ${unsetChar?}`)

      // === CASE CONVERSION ===

      // Instance methods — not static utility calls
      upper <- letter.upperCase()
      stdout.println(`Uppercase of ${letter}: ${upper}`)

      lower <- upper.lowerCase()
      stdout.println(`Lowercase of ${upper}: ${lower}`)

      // Method chaining
      roundTrip <- letter.upperCase().lowerCase()
      require roundTrip == letter
      stdout.println(`Round trip: ${roundTrip}`)

      // === PROMOTE TO STRING ===

      // The #^ operator explicitly converts Character to String
      asString <- #^ letter
      stdout.println(`Promoted: ${asString}`)

      // Also available as method call syntax
      alsoString <- letter.#^()
      stdout.println(`Method promote: ${alsoString}`)

      // Explicit typing shows the conversion
      typed as String := #^letter
      require typed?
      stdout.println(`Typed promote: ${typed}`)

      // === LENGTH ===

      // A set character always has length 1
      stdout.println(`Length of '${letter}': ${length letter}`)
      stdout.println(`Length method: ${letter.length()}`)

      // An unset character has length 0
      stdout.println(`Unset length: ${length unsetChar}`)

      // === HASHCODE ===

      // Two syntax forms for hashcode
      hashPrefix <- #? letter
      hashMethod <- letter.#?()
      stdout.println(`Hashcode prefix: ${hashPrefix}`)
      stdout.println(`Hashcode method: ${hashMethod}`)

      // String interpolation with hashcode
      stdout.println(`Hashcode of ${accent} is ${#?accent}`)

      // === COPY OPERATOR ===

      // :=: deep copies one character to another
      copied <- Character()
      copied :=: 'z'
      require copied == 'z'
      stdout.println(`Copied: ${copied}`)

      // === INCREMENT AND DECREMENT ===

      // ++ moves to next character, -- to previous
      working <- 'a'
      working++
      require working == 'b'
      stdout.println(`After ++: ${working}`)

      working--
      require working == 'a'
      stdout.println(`After --: ${working}`)

      // === COMPARISON ===

      // Characters compare by Unicode value
      require 'a' < 'z'
      require 'z' > 'a'
      require 'a' <> 'z'
      require 'a' == 'a'
      require 'a' <= 'a'
      require 'a' >= 'a'

      // Comparisons with unset return unset (not true, not false)
      unsetCompare <- unsetChar == letter
      require ~unsetCompare?
      stdout.println(`Unset compare isSet: ${unsetCompare?}`)

      // Spaceship operator for sorting
      ordering <- letter <=> 'z'
      stdout.println(`letter <=> 'z': ${ordering}`)

Common mistakes

E50060 — EK9 Character uses upperCase() and lowerCase(), not toUpper() or toUpperCase(). Triggers E50060 — method not resolved. See ek9 -h Character for the full API.

Incorrect:

upper <- letter.toUpper()

Correct:

upper <- letter.upperCase()
Other ways to ask this
  • Does EK9 have a character type separate from strings?
  • How do I convert a character to a string in EK9?
  • How does the promote operator work with characters?
  • How do I iterate through characters in EK9?

Coming from another language?

Java: primitive char (16-bit UTF-16), static Character.toUpperCase(). Python/JS: no character type. Rust: char is full Unicode scalar. EK9: single-quoted literals, full Unicode, instance methods .upperCase()/.lowerCase(), #^ promote to String, ++/-- operators.

Keywords: character, lowercase, quote, migrate, char, increment, uppercase, unicode, promote, intro, string, decrement, literal, first, beginner, single, convert, start