kex docs Standard Library

String

type String

Implements Monoid, Blankable, Enumerable, Foldable, Truthyable.

Text. A String is a sequence of Unicode characters, immutable like every other Kex value: every method here answers with a new string rather than changing the receiver.

A String is its own type, not a list of characters. It is Enumerable, so each, reduce, find and friends walk it one Char at a time, and the sequence operations that could reasonably answer in either currency pick the useful one: take, drop and sort hand back a String, while first and last hand back a Char. Use chars to cross over to a real list.

let line = "  Hello, World  "
line.trim.lowerCase.split(", ")   # => ["hello", "world"]
line.trim.take(5)                 # => "Hello"
line.trim.chars.count(~upper?)    # => 2

Strings interpolate with ${...}:

let name = "Ada"
"hello, ${name}"                  # => "hello, Ada"

reduce Enumerable Foldable

reduce(acc: A, f: (A -> Char -> A)) -> A

Folds the string from the left, one Char at a time.

This is String's Enumerable primitive: map, filter, find, any? and the rest are defined in terms of it. Reach for it directly when you are accumulating something that is neither a string nor a list.

Parameters
acc
the initial accumulator
f
combines the accumulator with each character

Returns the final accumulator

Examples

Summing digit values

"12345".reduce(0) { |sum, c| sum + c.codepoint - 48 }   # => 15

Building a character histogram

"banana".reduce({}) do |counts, c|
  counts.put(c.string, counts.get(c.string).or(0) + 1)
end
# => {"a": 3, "b": 1, "n": 2}

mapChars

mapChars(f: (Char -> Char)) -> String

Applies f to every character and joins the results back into a String.

This is the string-preserving counterpart of map. map comes from Enumerable and collects into a list, because its type says (Char -> B) -> [B] and B need not be a Char at all. When you want a string out, name the operation that produces one.

Parameters
f
applied to each character

Returns the mapped characters, as a string

Examples
"hi".mapChars(&.upperCase)   # => "HI"
"hi".map(&.upperCase)        # => ['H', 'I']  (a list, per Enumerable)

Shifting every character up one codepoint

"abc".mapChars do |c|
  String.fromCodepoint(c.codepoint + 1).or("").first.or(c)
end
# => "bcd"

filter Enumerable

filter(pred: (Char -> Bool)) -> String

Returns the characters satisfying pred, as a String.

Parameters
pred
kept when it answers true

Returns the matching characters, in order

Examples

Keeping only the digits

"a1b2c3".filter(~digit?)   # => "123"

Stripping punctuation before comparing

"Hello, World!".filter(~alpha?).lowerCase   # => "helloworld"

uniq

uniq : String

Returns the string with repeated characters removed, keeping the first occurrence of each.

Returns the distinct characters, in first-seen order

Examples
"banana".uniq   # => "ban"
"hello".uniq    # => "helo"

first

first : Char?

A String is its OWN type, not a [Char]: chars converts between them. So the sequence operations below are declared here rather than inherited from List, and they answer in String's terms: take/drop/sort hand back a String, while first/last hand back a Char. The List intrinsics they delegate to already understand the String representation, so there is one implementation for both backends.

Returns the first character, or None when the string is empty.

Returns the first character, or None

Examples
"hi".first   # => Just('h')
"".first     # => None

Testing an initial without risking an empty string

"hello".first.map(~upper?).or(false)   # => false

last

last : Char?

Returns the last character, or None when the string is empty.

Returns the last character, or None

Examples
"hi".last   # => Just('i')
"".last     # => None

get

get(i: Integer) -> Char?
get(i: Integer, default: Char) -> Char

Returns the character at index i, counting from 0.

Answers None for an index past either end rather than failing, so it is safe to index with a computed position.

Parameters
i
the 0-based index

Returns the character, or None when out of range

Examples
"hi".get(1)    # => Just('i')
"hi".get(9)    # => None

take

take(n: Integer) -> String

Returns the first n characters. A short string is returned whole, so take never fails on an n that is too large.

Parameters
n
how many characters to keep

Returns the leading n characters

Examples
"hello".take(2)    # => "he"
"hello".take(99)   # => "hello"
"hello".take(0)    # => ""

Truncating for display

let preview = title.take(30) + (title.count > 30 then "…" else "")

drop

drop(n: Integer) -> String

Returns everything after the first n characters. The complement of take: +s.take(n) + s.drop(n)+ is s.

Parameters
n
how many characters to skip

Returns the remaining characters

Examples
"hello".drop(2)    # => "llo"
"hello".drop(99)   # => ""

Removing a known prefix

let flag = "--verbose"
flag.startsWith?("--") then flag.drop(2) else flag   # => "verbose"

reject

reject(pred: (Char -> Bool)) -> String

Returns the characters that do NOT satisfy pred: the complement of filter.

Parameters
pred
dropped when it answers true

Returns the characters that failed the predicate

Examples
"hello".reject { |c| c == 'l' }   # => "heo"

Removing whitespace

"1 234 567".reject(~space?)   # => "1234567"

sort

sort : String

Returns the characters in ascending codepoint order.

Returns the sorted characters

Examples
"hello".sort   # => "ehllo"

An anagram check

"listen".sort == "silent".sort   # => true

indexOf

indexOf(c: Char) -> Integer?

Returns the index of the first occurrence of c, or None when the character does not appear.

Parameters
c
the character to look for

Returns the 0-based index, or None

Examples
"hello".indexOf('l')   # => Just(2)
"hello".indexOf('z')   # => None

Splitting a +key=value+ pair at the first +=+

let pair = "host=localhost"
pair.indexOf('=').map { |i| (pair.take(i), pair.drop(i + 1)) }
# => Just(("host", "localhost"))

findIndex

findIndex(pred: (Char -> Bool)) -> Integer?

Returns the index of the first character satisfying pred, or None.

The predicate counterpart of indexOf, which searches for one known character.

Parameters
pred
the test applied to each character

Returns the 0-based index, or None

Examples
"hello".findIndex { |c| c == 'l' }   # => Just(2)
"hello".findIndex(~digit?)           # => None

Finding where the leading indentation ends

"    text".findIndex { |c| !c.space? }   # => Just(4)

zip

zip(other: [Y]) -> [(Char, Y)]

Pairs each character with the element at the same index in other, stopping at the shorter of the two.

Parameters
other
the list to pair with

Returns the pairs, in order

Examples
"ab".zip([1, 2])      # => [('a', 1), ('b', 2)]
"abc".zip([1, 2])     # => [('a', 1), ('b', 2)]

Numbering the characters

"abc".zip((0..2).items)   # => [('a', 0), ('b', 1), ('c', 2)]

partition

partition(pred: (Char -> Bool)) -> (String, String)

Splits the string in two: the characters satisfying pred, then those that do not. One pass, both answers.

Parameters
pred
the test applied to each character

Returns the matching and non-matching characters

Examples
"hello".partition { |c| c == 'l' }   # => ("ll", "heo")

Separating digits from the rest

let (digits, other) = "a1b2".partition(~digit?)
digits   # => "12"
other    # => "ab"

count Foldable

count : Integer

Returns the number of characters: characters, not bytes. See bytes for the storage-level length.

Returns the character count

Examples
"hello".count   # => 5
"".count        # => 0
"é".count       # => 1   (but "é".bytes.count is 2)

length

length : Integer

Returns the number of characters. The same as count, under the name familiar from other languages.

Returns the character count

Examples
"hello".length   # => 5

graphemeCount

graphemeCount : Integer

Returns the number of user-perceived characters: extended grapheme clusters (Unicode UAX #29), which is not always count.

count answers the codepoint count, and most text is one codepoint per grapheme cluster, so the two usually agree. They part ways for a base character combined with a following mark, an emoji built from more than one codepoint, and "\r\n", which is one grapheme cluster over two codepoints: reach for graphemeCount over count wherever "how many characters does a person see" is the question, such as sizing text for display or truncating it at a boundary a reader would recognize.

Returns the grapheme cluster count

Examples
"hello".graphemeCount   # => 5, same as count
"\r\n".graphemeCount    # => 1 ("\r\n".count is 2: a CR codepoint and an LF codepoint)

A base character plus a combining mark

let acute = String.fromCodepoint(0x301).or("")     # combining acute accent
let e = "e" + acute
e.count           # => 2 (two codepoints: 'e' and the combining mark)
e.graphemeCount   # => 1 (one character on screen)

empty?

empty? : Bool

Returns true when the string has no characters.

Note that a string of spaces is not empty: use blank? from the Blankable trait when whitespace should not count.

Returns true for the empty string

Examples
"".empty?      # => true
"hi".empty?    # => false
"  ".empty?    # => false

enclose

enclose(wrapper: String) -> String
enclose(left: String, right: String) -> String

Returns the string with wrapper added at both ends.

Parameters
wrapper
placed before and after the string

Returns the wrapped string

Examples
"hello".enclose("*")    # => "*hello*"
"hello".enclose("__")   # => "__hello__"

Quoting a value for output

value.enclose("\"")   # => "\"localhost\""

at

at(i: Integer) -> Char?

Returns the character at index i, counting from 0, or None when the index is out of range. The same as the one-argument get.

Parameters
i
the 0-based index

Returns the character, or None

Examples
"hello".at(1)   # => Just('e')
"hello".at(9)   # => None

second

second : Char?

Returns the second character, or None when the string is shorter than two characters.

Returns the second character, or None

Examples
"hi".second   # => Just('i')
"h".second    # => None

third

third : Char?

Returns the third character, or None when the string is shorter than three characters.

Returns the third character, or None

Examples
"hip".third   # => Just('p')
"hi".third    # => None

rest

rest : String

Returns everything after the first character. The empty string has no rest, and answers with itself.

Returns the string without its first character

Examples
"hello".rest   # => "ello"
"h".rest       # => ""

chars

chars : [Char]

Returns the string's characters as a list.

The bridge from String to the List methods that a string does not have of its own. Join back with .join("").

Returns the characters, in order

Examples
"hi".chars   # => ['h', 'i']

Going out to List and back

"hello".chars.reverse.join("")   # => "olleh"

bytes

bytes : [Byte]

Returns the string's UTF-8 encoding, one Byte per byte.

chars is the TEXT view of a string and bytes is the STORAGE view, so they differ for anything outside ASCII: "é" is one Char and two +Byte+s. Use bytes when the length that matters is the encoded one: a network frame, a file offset, a size limit.

String.fromBytes is the inverse.

Returns the UTF-8 bytes

Examples
"hi".bytes    # => [104, 105]
"é".bytes     # => [195, 169]
"é".chars     # => ['é']

Measuring the encoded size

"héllo".count         # => 5
"héllo".bytes.count   # => 6

split

split : [String]

Splits the string into its individual characters, as one-character strings.

Use chars instead when you want Char values rather than strings.

Separator-less: one part per character, as the example above shows. Only the intrinsic had this form, so the walker answered "hi".split with "'this' used outside of a method context" while BEAM returned the parts. Declared BEFORE the separator form, as sort is in list.kex: the walker resolves a no-argument call against the first clause of that name.

Returns one string per character

Examples
"hi".split   # => ["h", "i"]
split(sep: String | Regex) -> [String]
split : [String]

Splits the string on every occurrence of sep, which may be a literal string or a Regex.

Separators at the ends produce empty parts, so splitting ",a," on "," gives three parts. Filter or trim afterwards when that is not wanted.

Parameters
sep
the separator to split on

Returns the parts, in order

Examples

Splitting a delimited line

"a,b,c".split(",")     # => ["a", "b", "c"]
"a, b, c".split(", ")  # => ["a", "b", "c"]

Splitting on a pattern (needs using Regex)

"a1b22c".split(re`[0-9]+`)   # => ["a", "b", "c"]

Empty parts at the edges are kept

",a,".split(",")   # => ["", "a", ""]

lines

lines : [String]

Splits the string into lines on \n.

A single trailing newline is dropped, so a file that ends in one does not produce a final empty line. Windows line endings are not stripped: split on \r\n explicitly, or trim each line, when that matters.

Returns the lines, without their newlines

Examples
"a\nb".lines      # => ["a", "b"]
"a\nb\n".lines    # => ["a", "b"]
"".lines          # => []

Numbering the lines of a file

text.lines.zip((1..text.lines.count).items).each do |pair|
  let (line, n) = pair
  IO.printLine("${n}: ${line}")
end

indentRest

indentRest(prefix: String) -> String

Indents every line but the first by prefix.

This is what splicing a multi-line value into an indented ${...} hole needs: the hole's own indentation already covers line one, and the rest have to catch up. Blank lines stay blank, and (through lines) a single trailing newline is dropped, so a block does not push a stray empty line into its slot.

Parameters
prefix
the indentation to add

Returns the re-indented string

Examples
"a\nb\n".indentRest("  ")   # => "a\n  b"
"a".indentRest("  ")        # => "a"

Splicing a block into a template

let body = "one\ntwo"
"items:\n  ${body.indentRest("  ")}"
# => "items:\n  one\n  two"

replace

replace(pattern: String, replacement: String) -> String

Replaces every literal occurrence of pattern with replacement.

The pattern is matched literally, not as a regular expression. An empty pattern matches at every character boundary, including both ends.

Parameters
pattern
the text to look for
replacement
what to put in its place

Returns the rewritten string

Examples
"a-b-c".replace("-", "+")   # => "a+b+c"
"abc".replace("", "-")      # => "-a-b-c-"

Normalising a path separator

"a\\b\\c".replace("\\", "/")   # => "a/b/c"

substitute

substitute(replacements: {String: String}) -> String

Replaces every key of replacements with its value, in one pass over the map.

The keys are the placeholders exactly as written: no syntax is imposed and nothing is reserved, so $NAME$, __NAME__, {{name}} and %name% are all equally valid, and a template needs no escaping to be a template. $NAME$ is the convention to reach for when there is no reason to prefer another.

Note that a placeholder is NOT ${name}: that is interpolation, which the compiler resolves before this ever sees the string.

This is what a template wants instead of a chain of replace calls: the chain reads as a pipeline when it is really a substitution table, and it quietly depends on its own order, since an earlier replacement's OUTPUT is still visible to every later one.

Substitutions are applied in the map's canonical key order and each is applied to the result of the last, so a value that itself contains a key can still be rewritten by a later one. Keep values placeholder-free when that matters.

Parameters
replacements
placeholder to replacement text

Returns the filled-in template

Examples
"Hello, $WHO$!".substitute({"$WHO$": "world"})           # => "Hello, world!"
"$A$ and $B$".substitute({"$A$": "x", "$B$": "y"})       # => "x and y"
"__A__".substitute({"__A__": "any key works"})           # => "any key works"

A reusable template

let greeting = "Dear $NAME$,\n\nYour order $ID$ has shipped."
greeting.substitute({"$NAME$": "Ada", "$ID$": "A-1701"})

trim

trim : String

Removes leading and trailing whitespace. Whitespace inside the string is left alone.

Returns the trimmed string

Examples
"  hello  ".trim     # => "hello"
"\n hi \t".trim      # => "hi"
"a  b".trim          # => "a  b"

Cleaning up user input

let name = IO.getLine.or("").trim

upperCase

upperCase : String

Returns the string with every character converted to upper case.

Returns the upper-cased string

Examples
"hello".upperCase       # => "HELLO"
"Hello, ada".upperCase  # => "HELLO, ADA"

Case-insensitive comparison

input.lowerCase == "yes"

lowerCase

lowerCase : String

Returns the string with every character converted to lower case.

Returns the lower-cased string

Examples
"HELLO".lowerCase   # => "hello"

Normalising a lookup key

settings.get(key.trim.lowerCase)

capitalize

capitalize : String

Returns the string with its first character upper-cased and the rest LOWER-cased.

This matches Ruby's String#capitalize, so "hELLO" becomes "Hello", not "HELLO". Use upperCase when the whole string should shout.

It is also what building a type name from data needs: a generated type %name = ... inside a compiled do block requires an upper-case identifier, so a list of lower-case words has to be capitalized first.

Returns the capitalized string

Examples
"hello".capitalize           # => "Hello"
"hELLO".capitalize           # => "Hello"   (tail is lower-cased)
"already Capital".capitalize # => "Already capital"
"".capitalize                # => ""

Title-casing a sentence

"the quick brown fox".split(" ").map(~capitalize).join(" ")
# => "The Quick Brown Fox"

reverse

reverse : String

Returns the string with its characters in reverse order.

Returns the reversed string

Examples
"hello".reverse   # => "olleh"

A palindrome check

let cleaned = "A man, a plan, a canal: Panama".filter(~alpha?).lowerCase
cleaned == cleaned.reverse   # => true

contains?

contains?(sub: String) -> Bool

Returns true when sub appears anywhere in the string.

The search is literal and case-sensitive. Lower-case both sides to ignore case; use Regex when the needle is a pattern.

Parameters
sub
the text to look for

Returns true when it is present

Examples
"hello world".contains?("world")   # => true
"hello world".contains?("xyz")     # => false
"hello world".contains?("World")   # => false

Ignoring case

"Hello".lowerCase.contains?("hello")   # => true

startsWith?

startsWith?(prefix: String) -> Bool

Returns true when the string begins with prefix.

Parameters
prefix
the expected opening text

Returns true when the string starts with it

Examples
"hello".startsWith?("hel")   # => true
"hello".startsWith?("llo")   # => false

Recognising a command-line flag

args.filter { |a| a.startsWith?("--") }

endsWith?

endsWith?(suffix: String) -> Bool

Returns true when the string ends with suffix.

Parameters
suffix
the expected closing text

Returns true when the string ends with it

Examples
"hello".endsWith?("llo")   # => true
"hello".endsWith?("hel")   # => false

Selecting files by extension

paths.filter { |p| p.endsWith?(".kex") }

From Monoid

repeat
Combines this value with itself n times.

From Enumerable

map
Applies f to each item and collects the results into a list.
mapIndexed
Applies f to each item and its 0-based position, and collects the results into a list.
flatMap
Applies f to each item, expecting a list back, and concatenates the results into one flat list.
collect
Applies f to each item, expecting an Optional back, and returns the values that were present: unwrapped.

From Foldable

each
Calls f with each item, for its side effects.
eachIndexed
Calls f with each item and its 0-based position, for its side effects.
all?
Returns true when every item satisfies pred.
any?
Returns true when at least one item satisfies pred.
find
Returns the first item satisfying pred, or None when nothing does.

Defined in other modules

type Char

A single Unicode character.

Character literals are written with single quotes ('a') and are a different type from the one-character string "a". The classification methods (digit?, alpha?, space? and the rest) are what most character code needs; codepoint and String.fromCodepoint are the escape hatch to raw Unicode values.

"hello world".chars.count(~alpha?)   # => 10
'a'.upperCase                        # => 'A'
'a'.string                           # => "a"

string

string : String

Returns this character as a one-character String.

A Char and a String are distinct types, so converting between them is explicit. to(String) is the fallible conversion protocol and answers String? like every other conversion; string is the total one, because a character is always one character of text and has no failure case to report. It is the single-character counterpart of String.chars and [Char].join("").

Returns the character as text

Examples
'a'.string                 # => "a"
'a'.to(String)             # => Just("a")
"hi".chars.map(&.string)   # => ["h", "i"]

upperCase

upperCase : Char

Returns the upper-case form of this character, still as a Char.

Characters with no upper-case form (digits, punctuation) are returned unchanged.

Returns the upper-case character

Examples
'a'.upperCase   # => 'A'
'1'.upperCase   # => '1'

lowerCase

lowerCase : Char

Returns the lower-case form of this character, still as a Char.

Characters with no lower-case form are returned unchanged.

Returns the lower-case character

Examples
'A'.lowerCase   # => 'a'
'!'.lowerCase   # => '!'

digit?

digit? : Bool

Returns true when the character is a decimal digit, '0' through '9'.

Returns true for a decimal digit

Examples
'3'.digit?   # => true
'a'.digit?   # => false

Validating that a field is all digits

"12345".chars.all?(~digit?)   # => true

alpha?

alpha? : Bool

Returns true when the character is alphabetic.

Returns true for a letter

Examples
'a'.alpha?   # => true
'1'.alpha?   # => false
' '.alpha?   # => false

Keeping only letters

"Hello, World!".filter(~alpha?)   # => "HelloWorld"

letter?

letter? : Bool

Returns true when the character is a letter. The Unicode-letter spelling of alpha?.

Returns true for a letter

Examples
'z'.letter?   # => true
'-'.letter?   # => false

upper?

upper? : Bool

Returns true when the character is upper case.

Characters without case (digits, punctuation) answer false.

Returns true for an upper-case letter

Examples
'A'.upper?   # => true
'a'.upper?   # => false
'1'.upper?   # => false

Splitting a camelCase name

"camelCaseName".findIndex(~upper?)   # => Just(5)

lower?

lower? : Bool

Returns true when the character is lower case.

Returns true for a lower-case letter

Examples
'a'.lower?   # => true
'A'.lower?   # => false

space?

space? : Bool

Returns true when the character is whitespace: a space, tab, newline or carriage return.

Returns true for whitespace

Examples
' '.space?    # => true
'\t'.space?   # => true
'a'.space?    # => false

Counting the words on a line

"one two  three".split(" ").reject(~empty?).count   # => 3

codepoint

codepoint : Integer

Returns the character's Unicode codepoint.

String.fromCodepoint is the inverse.

Returns the codepoint

Examples
'A'.codepoint   # => 65
'é'.codepoint   # => 233

Converting a digit character to its value

'7'.codepoint - '0'.codepoint   # => 7

in?

in?(range: Range<Char>) -> Bool

Returns true when the character falls inside range, endpoints included.

Parameters
range
the inclusive character range

Returns true when the character is in range

Examples
'b'.in?('a'..'z')   # => true
'B'.in?('a'..'z')   # => false

A hexadecimal-digit test

let hex?(c: Char) -> Bool = c.digit? || c.lowerCase.in?('a'..'f')

module String

Constructors for String values that are built from something other than text: a Unicode codepoint, or raw UTF-8 bytes.

fromCodepoint

fromCodepoint(value: Integer) -> String?

Builds a one-character string from a Unicode codepoint.

Answers None for a surrogate or a value outside the Unicode scalar range, so the result is always valid text. Char.codepoint is the inverse.

Parameters
value
the Unicode codepoint

Returns the one-character string, or None

Examples
String.fromCodepoint(65)       # => Just("A")
String.fromCodepoint(233)      # => Just("é")
String.fromCodepoint(-1)       # => None

Building the alphabet

(0..25).items.map { |i| String.fromCodepoint(97 + i).or("") }.join("")
# => "abcdefghijklmnopqrstuvwxyz"

fromBytes

fromBytes(values: [Byte]) -> String?

Rebuilds a string from its UTF-8 bytes: the inverse of bytes.

A String is TEXT, so bytes outside 0..255 or a malformed encoding answer None rather than a string that would decode to replacement characters. That makes it a decoding step you can check, not a lossy cast.

Parameters
values
the UTF-8 bytes

Returns the decoded string, or None

Examples
String.fromBytes([104, 105])   # => Just("hi")
String.fromBytes([195, 169])   # => Just("é")
String.fromBytes([255])        # => None

A round trip

String.fromBytes("héllo".bytes)   # => Just("héllo")

type Tuple

A fixed-size group of values, written (a, b). Unlike a list, a tuple's size and the type of each position are part of its type, so the List methods do not apply to it: destructure it, match on it, or convert it with items.

let (name, age) = ("Ada", 36)
[1, 2, 3].partition { |n| n.even? }   # => ([2], [1, 3])

items

items : [Any]

Returns the tuple's elements as a list.

A tuple is not a list (its arity is part of its type) so the List methods do not apply to it. This is the explicit conversion, and it loses the per-position typing in exchange.

Destructuring is usually clearer when you know the shape: let (a, b) = pair.

Returns the elements, in order

Examples
(1, "a").items   # => [1, "a"]

Iterating over a pair

("host", "port").items.each { |s| IO.printLine(s) }