kex docs Standard Library 0.4.0-alpha kex.run ↗

Stringprelude

make String implements Enumerable, Foldable

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

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.

reduce(acc, f) : A -> (A -> Char -> A) -> A
Parameters
acc A
the initial accumulator
f A -> Char -> A
combines the accumulator with each character

Returns: A — 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

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.

mapChars(f) : (Char -> Char) -> String
Parameters
f Char -> Char
applied to each character

Returns: String — 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

Returns the characters satisfying pred, as a String.

filter(pred) : (Char -> Bool) -> String
Parameters
pred Char -> Bool
kept when it answers true

Returns: String — the matching characters, in order

Examples

Keeping only the digits

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

Stripping punctuation before comparing

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

get

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.

get(i) : Integer -> Char?
get(i) : Integer -> Char -> Char
Parameters
i Integer
the 0-based index

Returns: Char? — the character, or None when out of range

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

take

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

take(n) : Integer -> String
Parameters
n Integer
how many characters to keep

Returns: String — 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

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

drop(n) : Integer -> String
Parameters
n Integer
how many characters to skip

Returns: String — 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

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

reject(pred) : (Char -> Bool) -> String
Parameters
pred Char -> Bool
dropped when it answers true

Returns: String — the characters that failed the predicate

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

Removing whitespace

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

indexOf

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

indexOf(c) : Char -> Integer?
Parameters
c Char
the character to look for

Returns: Integer? — 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

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

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

findIndex(pred) : (Char -> Bool) -> Integer?
Parameters
pred (Char) -> Bool
the test applied to each character

Returns: Integer? — 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

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

zip(other) : [Y] -> [(Char, Y)]
Parameters
other [Y]
the list to pair with

Returns: [(Char, Y)] — 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

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

partition(pred) : (Char -> Bool) -> (String, String)
Parameters
pred Char -> Bool
the test applied to each character

Returns: (String, String) — 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"

enclose

Returns the string with wrapper added at both ends.

enclose(wrapper) : String -> String
enclose(wrapper) : String -> String -> String
Parameters
wrapper String
placed before and after the string

Returns: String — the wrapped string

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

Quoting a value for output

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

at

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.

at(i) : Integer -> Char?
Parameters
i Integer
the 0-based index

Returns: Char? — the character, or None

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

split

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.

split(sep) : String | Regex -> [String]
split : [String]
Parameters
sep String | Regex
the separator to split on

Returns: [String] — 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", ""]

indentRest

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.

indentRest(prefix) : String -> String
Parameters
prefix String
the indentation to add

Returns: String — 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

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.

replace(pattern, replacement) : String -> String -> String
Parameters
pattern String
the text to look for
replacement String
what to put in its place

Returns: String — 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

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.

substitute(replacements) : {String: String} -> String
Parameters
replacements {String: String}
placeholder to replacement text

Returns: String — 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"})

contains?

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.

contains?(sub) : String -> Bool
Parameters
sub String
the text to look for

Returns: Booltrue 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?

Returns true when the string begins with prefix.

startsWith?(prefix) : String -> Bool
Parameters
prefix String
the expected opening text

Returns: Booltrue 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?

Returns true when the string ends with suffix.

endsWith?(suffix) : String -> Bool
Parameters
suffix String
the expected closing text

Returns: Booltrue 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") }

make 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"

in?

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

in?(range) : Range<Char> -> Bool
Parameters
range Range<Char>
the inclusive character range

Returns: Booltrue 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.

function fromCodepoint

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.

fromCodepoint(value) : Integer -> String?
Parameters
value Integer
the Unicode codepoint

Returns: String? — 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"

function fromBytes

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.

fromBytes(values) : [Byte] -> String?
Parameters
values [Byte]
the UTF-8 bytes

Returns: String? — 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")

make 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])