9. String Functions

The String library provides strings/ functions for Unicode text. Character counts and indices count Unicode code points, starting at zero. Substring end indices are excluded. A combining mark counts separately from the character before it. Operations do not normalize text unless their documentation says so. Searches match literal text rather than patterns or regular expressions.

Case conversion and word boundaries recognize Unicode letters and digits. Case conversion is locale independent. Character classification and full case mappings use Unicode 17.0.0, including expanding mappings and the contextual Greek final sigma rule.

Table of Contents

9.1. String functions ↑

9.1.1. strings/case ↑

Function

(strings/case case value) → string
(strings/case cases value) → string

Converts value with one case selector or a list of selectors. A list applies the conversions from left to right. An empty list returns the input unchanged. The selector and input must be strings. Unknown selectors produce an error.

Selector Conversion
upper Unicode uppercase
lower Unicode lowercase
camel Words joined in camel case
pascal Words joined in Pascal case
snake Lowercase words joined with underscores
kebab Lowercase words joined with hyphens
title Capitalized words joined with spaces
screaming-snake Uppercase words joined with underscores
case-fold Unicode default full case folding

Case conversion can change the number of code points: straße becomes STRASSE with upper. Case folding is for caseless matching. It does not normalize text or use Turkic-specific mappings.

Naming conversions split words at separators and case transitions. An acronym followed by a capitalized word is split: HTTPServer contains HTTP and Server. (strings/case [snake upper] HTTPServer) returns HTTP_SERVER.

DUST
(strings/case upper straße)
(strings/case lower HELLO)
(strings/case camel HTTP_server)
(strings/case title hELLO_world)
(strings/case case-fold Straße)
=
DUST Result
STRASSE
hello
httpServer
"Hello World"
strasse
DUST
(strings/case [snake upper] HTTPServer)
(strings/case [upper lower] Straße)
(strings/case [] Hello)
(strings/case [camel snake] hello_world)
=
DUST Result
HTTP_SERVER
strasse
Hello
hello_world
DUST
(strings/case unknown abc)
=
DUST Result
#err <invalid value>
DUST
(strings/case 1 abc)
=
DUST Result
#err <mismatch type>

9.1.2. strings/trim ↑

Function

(strings/trim opts s) → string
(strings/trim opts text s) → string

Returns s with whitespace or text removed from the ends selected by opts.

opts is a map tagged with exactly one of left, right, or both, optionally with repeat, as in {<both repeat>}. Whitespace trimming removes full Unicode whitespace runs. Text trimming removes one exact match per end, or repeats with repeat. Both ends are processed left first. Empty text leaves s unchanged.

DUST
(strings/trim
  <both>
  "\t \nalpha beta\r ")
(strings/trim <both>  hi )
(strings/trim <both>  hi )
(strings/trim <both> "")
(strings/trim
  <both>
  "\t\n")
(strings/trim <both> ​hi​)
(strings/trim <both> "hi  ")
=
DUST Result
"alpha beta"
hi
hi
""
""
​hi​
hi
DUST
(strings/trim <left> ha hahaha)
(strings/trim <both> ha hahaha)
(strings/trim <both repeat> ha hahaha)
(strings/trim <both> aba ababa)
(strings/trim <both repeat> aba ababa)
(strings/trim <left repeat> ha hahaxhaha)
(strings/trim <repeat right> ha hahaxhaha)
(strings/trim <both repeat> ha hahaxhahaxhaha)
(strings/trim <left repeat> ab aabb)
(strings/trim <both> abc abc)
(strings/trim <both repeat> abc ab)
(strings/trim <both repeat> "" abc)
(strings/trim <both repeat> "" "")
(strings/trim <both repeat> a "")
(strings/trim <both repeat> 🙂 🙂🙂x🙂🙂)
(strings/trim <both repeat> é Éxé)
(strings/trim <both repeat> é éxé)
(strings/trim <both> " " "  x  ")
(strings/trim <both repeat> " " "  x  ")
=
DUST Result
haha
ha
""
ba
ba
xhaha
hahax
xhahax
aabb
""
ab
abc
""
""
x
Éx
éxé
" x "
x
DUST
{both-left (strings/trim <both left> abc)
 both-right (strings/trim <both right> abc)
 conflicting-sides (strings/trim <left right> abc)
 extra (strings/trim <both> a abc extra)
 missing (strings/trim)
 missing-side (strings/trim {} abc)
 old-arity (strings/trim abc)
 opts-list (strings/trim [both] abc)
 opts-null (strings/trim null abc)
 opts-string (strings/trim both abc)
 repeat-only (strings/trim <repeat> abc)
 source-null (strings/trim <both> null)
 source-number (strings/trim <left> 1)
 text-null (strings/trim <left> null abc)
 text-number (strings/trim <right> 1 abc)
 text-options (strings/trim <all both> a abc)
 text-opts-null (strings/trim null a abc)
 text-source-null (strings/trim <right> a null)
 text-source-number (strings/trim <both repeat> a 1)
 unknown-option (strings/trim <all both> abc)}
→
Evaluation cases
#cases [{output #err <invalid value>
         select missing-side}
        {output #err <invalid value>
         select repeat-only}
        {output #err <invalid value>
         select conflicting-sides}
        {output #err <invalid value>
         select both-left}
        {output #err <invalid value>
         select both-right}
        {output #err <invalid value>
         select unknown-option}
        {output #err <invalid value>
         select text-options}
        {output #err <mismatch type>
         select opts-string}
        {output #err <mismatch type>
         select opts-list}
        {output #err <mismatch type>
         select opts-null}
        {output #err <mismatch type>
         select text-opts-null}
        {output #err <mismatch type>
         select source-null}
        {output #err <mismatch type>
         select source-number}
        {output #err <mismatch type>
         select text-source-null}
        {output #err <mismatch type>
         select text-source-number}
        {output #err <mismatch type>
         select text-null}
        {output #err <mismatch type>
         select text-number}
        {output #err <arity mismatch>
         select missing}
        {output #err <arity mismatch>
         select old-arity}
        {output #err <arity mismatch>
         select extra}]

9.1.3. strings/split ↑

Function

(strings/split string separator) → list
(strings/split string separator limit) → list

Splits at each literal occurrence of separator, preserving empty fields. An empty separator splits into Unicode code points; splitting an empty string with an empty separator returns a list containing the empty string.

An optional positive limit permits at most that many parts and keeps the unsplit remainder in the final part. A zero limit removes trailing empty parts. A negative limit preserves all parts, including trailing empty strings. Omitting limit preserves trailing empty parts.

DUST
(strings/split "a,b,c" ",")
(strings/split é猫🙂 "")
(strings/split a.b. ".")
(strings/split "" ",")
(strings/split "" "")
=
DUST Result
[[a b c] [é 猫 🙂] [a b ""] [""] []]

9.1.4. strings/join ↑

Function

(strings/join strings separator) → string

Joins a list of strings with separator between consecutive elements. An empty list produces the empty string. Elements and the separator must be strings.

DUST
(strings/join [a b] ",")
(strings/join [é 猫 🙂] "")
(strings/join [] 猫)
=
DUST Result
"a,b"
é猫🙂
""

9.1.5. strings/replace ↑

Function

(strings/replace string search replacement) → string

Replaces non-overlapping literal occurrences of search, from left to right. An empty search inserts replacement before, between, and after the input's Unicode code points: (strings/replace ab "" x) returns xaxbx.

DUST
(strings/replace banana na x)
(strings/replace ab "" x)
(strings/replace é猫 "" 🙂)
(strings/replace a.b. "." 猫)
=
DUST Result
baxx
xaxbx
🙂é🙂猫🙂
a猫b猫

9.1.6. strings/substring ↑

Function

(strings/substring string start) → string
(strings/substring string start end) → string

Returns the code points from start up to, but excluding, end. Without end, returns the rest of the string. Both indices must be integers between zero and the string length, and start must not exceed end. Other indices produce #err <bounds index>.

DUST
(strings/substring Aé猫Z 1 3)
(strings/substring é猫🙂 1 3)
(strings/substring é猫 2 2)
(strings/substring é猫🙂 1)
=
DUST Result
é猫
猫🙂
""
猫🙂

9.1.7. strings/starts-with? ↑

Function

(strings/starts-with? string prefix) → boolean

Returns whether the string begins with the literal prefix. Every string starts with the empty string.

DUST
(strings/starts-with? abc a)
(strings/starts-with? é猫 é)
(strings/starts-with? é猫 猫)
(strings/starts-with? é猫 "")
=
DUST Result
true
true
false
true

9.1.8. strings/ends-with? ↑

Function

(strings/ends-with? string suffix) → boolean

Returns whether the string ends with the literal suffix. Every string ends with the empty string.

DUST
(strings/ends-with? abc c)
(strings/ends-with? é猫 猫)
(strings/ends-with? é猫 é)
(strings/ends-with? é猫 "")
=
DUST Result
true
true
false
true

9.1.9. strings/includes? ↑

Function

(strings/includes? string search) → boolean

Returns whether the string contains the literal search. Every string includes the empty string.

DUST
(strings/includes? a.b ".")
(strings/includes? é猫🙂 猫)
(strings/includes? é猫 z)
(strings/includes? é猫 "")
(strings/includes? é é)
=
DUST Result
true
true
false
true
false

9.1.10. strings/split-lines ↑

Function

(strings/split-lines s) → list

Returns the lines of s separated by LF or CRLF, without their line endings.

Trailing empty lines are omitted. A lone CR is ordinary content. An empty string produces a list containing one empty string. A nonempty string containing only line endings produces an empty list.

DUST
[(strings/split-lines "a\nb\r\nc\n")
 (strings/split-lines "")
 (strings/split-lines "\n\r\n")
 (strings/split-lines "a\rb")
 (strings/split-lines "a\n\nb")
 (strings/split-lines "a
b")]
→
Canonical DUST
(strings/split-lines
  """
    a
    b\r
    c

  """
)
(strings/split-lines "")
(strings/split-lines
  """

    \r

  """
)
(strings/split-lines
  "a\rb")
(strings/split-lines
  """
    a

    b
  """
)
(strings/split-lines a
b)
=
DUST Result
[[a b c]
 [""]
 []
 [
   "a\rb"]
 [a "" b]
 [a
b]]

9.1.11. strings/index-of ↑

Function

(strings/index-of s value) → integer or null
(strings/index-of s value start) → integer or null

Returns the first index of the string value in s at or after start, or null if not found.

The integer start defaults to zero. Negative starts are treated as zero. A start beyond the string length finds no nonempty value. An empty value matches at the start clamped to the range from zero through the string length. Indices are not narrowed to a host's smaller integer type.

DUST
[(strings/index-of banana ana)
 (strings/index-of banana ana 2)
 (strings/index-of banana ana -3)
 (strings/index-of banana ana 9)
 (strings/index-of "🙂a🙂b" b)
 (strings/index-of "é" "é")
 (strings/index-of ABC a)
 (strings/index-of abc "" 99)
 (strings/index-of abc "" -9)
 (strings/index-of "" "")
 (strings/index-of abc b 9223372036854775807)
 (strings/index-of abc "" 9223372036854775807)]
→
Canonical DUST
(strings/index-of banana ana)
(strings/index-of banana ana 2)
(strings/index-of banana ana -3)
(strings/index-of banana ana 9)
(strings/index-of 🙂a🙂b b)
(strings/index-of é é)
(strings/index-of ABC a)
(strings/index-of abc "" 99)
(strings/index-of abc "" -9)
(strings/index-of "" "")
(strings/index-of abc b 9223372036854775807)
(strings/index-of abc "" 9223372036854775807)
=
DUST Result
1
3
1
null
3
null
null
3
0
0
null
3

9.1.12. strings/last-index-of ↑

Function

(strings/last-index-of s value) → integer or null
(strings/last-index-of s value start) → integer or null

Returns the last index of the string value in s at or before start, or null if not found.

The integer start defaults to the string length. Starts beyond the string length are treated as the string length. A negative start returns null, including when value is empty. An empty value matches at any index from zero through the string length. The start limits the beginning of a match, not its end. Indices are not narrowed to a host's smaller integer type.

DUST
[(strings/last-index-of banana ana)
 (strings/last-index-of banana ana 2)
 (strings/last-index-of banana ana -1)
 (strings/last-index-of banana ana 9223372036854775807)
 (strings/last-index-of abc abc 1)
 (strings/last-index-of abc "")
 (strings/last-index-of abc "" -1)
 (strings/last-index-of abc "" 99)
 (strings/last-index-of "🙂a🙂b" "🙂")
 (strings/last-index-of "é" "é")
 (strings/last-index-of "" "")]
→
Canonical DUST
(strings/last-index-of banana ana)
(strings/last-index-of banana ana 2)
(strings/last-index-of banana ana -1)
(strings/last-index-of banana ana 9223372036854775807)
(strings/last-index-of abc abc 1)
(strings/last-index-of abc "")
(strings/last-index-of abc "" -1)
(strings/last-index-of abc "" 99)
(strings/last-index-of 🙂a🙂b 🙂)
(strings/last-index-of é é)
(strings/last-index-of "" "")
=
DUST Result
3
1
null
3
0
3
null
3
2
null
0

9.1.13. strings/replace-first ↑

Function

(strings/replace-first s match replacement) → string

Returns s with the first occurrence of match replaced by replacement.

Both match and replacement are literal strings. An empty match inserts replacement at the start. Returns s unchanged when there is no match.

DUST
[(strings/replace-first abab ab X)
 (strings/replace-first aaa aa X)
 (strings/replace-first "a.b.a" "." "$1")
 (strings/replace-first ab "" "-")
 (strings/replace-first "🙂x" "" "-")
 (strings/replace-first "" "" "+")
 (strings/replace-first abc x q)
 (strings/replace-first a a aa)
 (strings/replace-first "$1" "$1" "\\")]
→
Canonical DUST
(strings/replace-first abab ab X)
(strings/replace-first aaa aa X)
(strings/replace-first a.b.a "." $1)
(strings/replace-first ab "" -)
(strings/replace-first 🙂x "" -)
(strings/replace-first "" "" +)
(strings/replace-first abc x q)
(strings/replace-first a a aa)
(strings/replace-first $1 $1 "\\")
=
DUST Result
Xab
Xa
a$1b.a
-ab
-🙂x
+
abc
aa
"\\"

9.1.14. strings/blank? ↑

Function

(strings/blank? s) → boolean

Returns true if s is null, empty, or contains only whitespace, otherwise false.

Other non-string values produce #err <mismatch type>.

DUST
[(strings/blank? null)
 (strings/blank? "")
 (strings/blank? " \t\r\n")
 (strings/blank? " ")
 (strings/blank? " ")
 (strings/blank? " ")
 (strings/blank? " ")
 (strings/blank? "​")
 (strings/blank? " a ")]
→
Canonical DUST
(strings/blank? null)
(strings/blank? "")
(strings/blank?
  " \t\r\n")
(strings/blank?  )
(strings/blank?  )
(strings/blank?  )
(strings/blank?  )
(strings/blank? ​)
(strings/blank? " a ")
=
DUST Result
true
true
true
true
true
true
true
false
false

9.1.15. String templates ↑

A template retains literal strings and evaluated values. It is accepted as a string by string operations; template? distinguishes it from an ordinary string. Rendering joins the literal pieces with the string forms of the values.

(let [name Ada]
  (templates/parts ~"hello-,.name"))
; => {strings [hello- ""] values [Ada]}

(let [name Ada]
  (template? ~"hello-,.name"))
; => true

(let [name Ada]
  ~"hello-,.name")
; => hello-Ada

(let [name Ada]
  (str hello- .name))
; => hello-Ada

Use the template to embed forms in a string. Use str to concatenate values.

DUST
(let
  [name Ada]
  [(= ~"hello-,.name" (str hello- .name)) (template? ~"hello-,.name")
   (template? (str hello- .name))])
=
DUST Result
true
true
false

templates/from-parts constructs a template from a map with strings and values lists. The strings list must have one more item than values.

(templates/from-parts {strings [hello- ""] values [Ada]})
; => hello-Ada

templates/render renders a template. Its optional escape function receives each evaluated embedded value before it is rendered; literal pieces pass through unchanged.

For example, escape a query value without escaping its literal parameter prefix:

(let [term "cats & dogs"]
  (templates/render
    ~"search=,.term"
    {escape (strings/escape url-query-value)}))
; => "search=cats%20%26%20dogs"

Use the corresponding unescape function to decode an escaped string.

Passing a template to str or strings/join renders its evaluated values. The result is an ordinary string.

Map literal keys are strings and are not evaluated. To create a key from a template, pass it as a value to a map operation:

(let [name Ada]
  (put {} ~"hello-,.name" 1))
; => {hello-Ada 1}

9.1.16. strings/escape ↑

Function

(strings/escape mode text) → string
(strings/escape mode) → function

Escapes text for the specified mode. With only a mode, returns a unary function that escapes its argument, for use as a template's escape option. The function prints and hashes as its call, such as (strings/escape html-text), and renders a template argument before escaping it.

(strings/escape json-pointer "a/b~") ; => a~1b~0
(strings/escape url-query-value "a&b c") ; => a%26b%20c
(strings/escape url-form-field "a&b c") ; => a%26b+c
(strings/escape csv "a,b") ; => "\"a,b\""
(let [escape-html (strings/escape html-text)]
  (.escape-html "<Alice>")) ; => "&lt;Alice&gt;"

Modes are json, dust, ron, csv, json-pointer, html-text, html-attribute, url-path-segment, url-query-value, and url-form-field. JSON Pointer mode escapes one segment; URL modes escape one component; CSV mode escapes one field. The json, dust, and ron modes escape the contents of a double-quoted string without adding quotes. HTML attribute mode also escapes quotation marks. URL form fields encode spaces as +; the other URL modes use %20. An unknown mode produces #err <invalid value>.

DUST
[(strings/escape html-attribute "&<>\"'")
 (strings/escape html-attribute "<b>Tom & 'Sue'</b>")
 (strings/escape html-attribute "&amp;")
 (strings/escape html-attribute "\\$1🙂")
 (strings/escape html-attribute "")]
→
Canonical DUST
(strings/escape html-attribute &<>"')
(strings/escape html-attribute "<b>Tom & 'Sue'</b>")
(strings/escape html-attribute &amp;)
(strings/escape html-attribute "\\$1🙂")
(strings/escape html-attribute "")
=
DUST Result
&amp;&lt;&gt;&quot;&#39;
"&lt;b&gt;Tom &amp; &#39;Sue&#39;&lt;/b&gt;"
&amp;amp;
"\\$1🙂"
""
DUST
(strings/escape json-pointer "a/b~")
(strings/unescape json-pointer "a~1b~0")
(strings/escape url-query-value "a&b c")
(strings/escape url-form-field "a&b c")
(strings/unescape url-form-field "a%26b+c")
(strings/escape csv "a,b")
(strings/unescape csv "\"a,b\"")
(strings/escape url-form-field "*~ ")
→
Canonical DUST
(strings/escape json-pointer a/b~)
(strings/unescape json-pointer a~1b~0)
(strings/escape url-query-value "a&b c")
(strings/escape url-form-field "a&b c")
(strings/unescape url-form-field a%26b+c)
(strings/escape csv "a,b")
(strings/unescape
  csv
  """
    "a,b"
  """
)
(strings/escape url-form-field "*~ ")
=
DUST Result
a~1b~0
a/b~
a%26b%20c
a%26b+c
"a&b c"
"""
  "a,b"
"""
"a,b"
*%7E+
DUST
(strings/escape json "a\"b\n")
(strings/unescape json "\\uD83D\\uDE00")
(strings/unescape ron (strings/escape ron "a\"b\n"))
(strings/unescape dust (strings/escape dust "a\\b"))
(strings/unescape json "\\q")
(strings/unescape url-query-value "%2G")
(strings/unescape json-pointer "~2")
→
Canonical DUST
(strings/escape
  json
  """
    a"b

  """
)
(strings/unescape json "\\uD83D\\uDE00")
(strings/unescape
  ron
  (strings/escape
    ron
    """
      a"b

    """
  ))
(strings/unescape dust (strings/escape dust a\b))
(strings/unescape json "\\q")
(strings/unescape url-query-value %2G)
(strings/unescape json-pointer "~2")
=
DUST Result
""a\u005c"b\\n""
😀
"""
  a"b

"""
a\b
#err {<invalid value>
      message "invalid string escape"}
#err {<invalid value>
      message "invalid URL percent escape"}
#err {<invalid value>
      message "invalid JSON Pointer escape"}
DUST
(strings/escape html-text "<a & b>")
(strings/escape html-attribute "\"<a>\"")
(strings/unescape html-text "&lt;a &amp; b&gt;")
(strings/unescape html-attribute "&#x1F600; &quot;x&quot;")
(strings/unescape html-text "&copy; &NotEqualTilde;")
→
Canonical DUST
(strings/escape html-text "<a & b>")
(strings/escape
  html-attribute
  """
    "<a>"
  """
)
(strings/unescape html-text "&lt;a &amp; b&gt;")
(strings/unescape html-attribute "&#x1F600; &quot;x&quot;")
(strings/unescape html-text "&copy; &NotEqualTilde;")
=
DUST Result
"&lt;a &amp; b&gt;"
&quot;&lt;a&gt;&quot;
"<a & b>"
""😀 "x"""
"© ≂̸"
DUST
(strings/escape url-path-segment "a/b")
(strings/unescape url-path-segment a%2Fb)
(let [escape-html (strings/escape html-text)] (.escape-html "<Alice>"))
(let [decode (strings/unescape html-text)] (.decode "&lt;Alice&gt;"))
(let [name "<Alice>"] (templates/render ~"<p>,.name,</p>" {escape (strings/escape html-text)}))
(fn? (strings/escape html-text))
(strings/escape unknown x)
(strings/unescape unknown x)
(strings/escape 1 x)
(strings/escape html-text 1)
((strings/escape html-text) x y)
→
Canonical DUST
(strings/escape url-path-segment a/b)
(strings/unescape url-path-segment a%2Fb)
(let [escape-html (strings/escape html-text)] (.escape-html "<Alice>"))
(let [decode (strings/unescape html-text)] (.decode &lt;Alice&gt;))
(let [name "<Alice>"] (templates/render ~"<p>,.name,</p>" {escape (strings/escape html-text)}))
(fn? (strings/escape html-text))
(strings/escape unknown x)
(strings/unescape unknown x)
(strings/escape 1 x)
(strings/escape html-text 1)
((strings/escape html-text) x y)
=
DUST Result
a%2Fb
a/b
&lt;Alice&gt;
"<Alice>"
"<p>&lt;Alice&gt;</p>"
true
#err <invalid value>
#err <invalid value>
#err <mismatch type>
#err <mismatch type>
#err <arity mismatch>
DUST
(let [escape-html (strings/escape html-text)]
  [(.escape-html "<Alice>")
   (fn? .escape-html)
   (.escape-html ~"<,(str b),>")
   (hash/digest sha256 .escape-html)])
→
Canonical DUST
(let
  [escape-html (strings/escape html-text)]
  [(.escape-html "<Alice>") (fn? .escape-html)
   (.escape-html ~"<,(str b),>") (hash/digest sha256 .escape-html)])
=
DUST Result
&lt;Alice&gt;
true
&lt;b&gt;
#hash #sha256 2d140a5655b694b51aecece118441a80fdd245852dc87ae6babb33de709aba34
DUST
[(strings/escape)
 (strings/unescape html-text a b)
 ((strings/escape html-text))
 ((strings/unescape html-text) 1)]
→
Canonical DUST
(strings/escape)
(strings/unescape html-text a b)
((strings/escape html-text))
((strings/unescape html-text) 1)
=
DUST Result
#err <arity mismatch>
#err <arity mismatch>
#err <arity mismatch>
#err <mismatch type>

9.1.17. strings/unescape ↑

Function

(strings/unescape mode text) → string
(strings/unescape mode) → function

Decodes text for the same modes as strings/escape. With only a mode, returns a unary decoding function that prints and hashes as its call, such as (strings/unescape url-query-value). Invalid JSON Pointer, URL percent, CSV, and quoted string escapes produce #err <invalid value>, as do URL percent escapes whose bytes are not UTF-8, since strings are always Unicode text. HTML decoding recognizes semicolon-terminated named and numeric character references.

(strings/unescape json-pointer a~1b~0) ; => a/b~
(strings/unescape html-text "&lt;Alice&gt;") ; => "<Alice>"
(let [decode (strings/unescape url-query-value)]
  (.decode cats%20%26%20dogs)) ; => "cats & dogs"

9.1.18. strings/normalize ↑

Function

(strings/normalize s) → string
(strings/normalize s form) → string

Returns s normalized to the Unicode normalization form, defaulting to NFC.

The form is one of the exact strings NFC, NFD, NFKC, or NFKD. Other strings produce an error. NFC and NFD apply canonical normalization. NFKC and NFKD also apply compatibility normalization, which can remove meaningful distinctions. Normalization does not perform case folding or guarantee visually distinct identifiers.

DUST
[(strings/normalize "é")
 (strings/normalize "é" NFD)
 (strings/normalize "A①ffi" NFKC)
 (strings/normalize "Å①" NFKD)
 (strings/normalize "①" NFC)
 (strings/normalize "가" NFC)
 (strings/normalize "" NFD)
 (strings/normalize "É" NFC)
 (strings/normalize "ࢡ" NFC)]
→
Canonical DUST
(strings/normalize é)
(strings/normalize é NFD)
(strings/normalize A①ffi NFKC)
(strings/normalize Å① NFKD)
(strings/normalize ① NFC)
(strings/normalize 가 NFC)
(strings/normalize "" NFD)
(strings/normalize É NFC)
(strings/normalize ࢡ NFC)
=
DUST Result
é
é
A1ffi
Å1
①
가
""
É
ࢡ
DUST
(strings/normalize "é" nfc)
→
Canonical DUST
(strings/normalize é nfc)
=
DUST Result
#err <form invalid normalization>

9.1.19. strings/from-code-points ↑

Function

(strings/from-code-points points) → string

Returns the string formed by the Unicode scalar values in the list points, in order.

Each element must be an integer from zero through 1114111, excluding the surrogate range 55296 through 57343. Invalid scalar values and non-integer elements produce errors. An empty list produces the empty string. Unassigned code points and noncharacters are accepted when they are valid scalar values. The result is not normalized.

DUST
[(strings/from-code-points [65 769])
 (strings/from-code-points [128578])
 (strings/from-code-points [])
 (strings/from-code-points [0 65])
 (strings/from-code-points [1114111])
 (strings/from-code-points [888])]
→
Canonical DUST
(strings/from-code-points [65 769])
(strings/from-code-points [128578])
(strings/from-code-points [])
(strings/from-code-points [0 65])
(strings/from-code-points [1114111])
(strings/from-code-points [888])
=
DUST Result
Á
🙂
""
"\u0000A"
􏿿
͸
DUST
(strings/from-code-points [55296])
=
DUST Result
#err <code invalid point>

9.2. strings/humanize ↑

Function

(strings/humanize value options?) → string

Formats an integer, float, duration, or UTC value as readable text. Options are a map with string keys. Unknown options and options irrelevant to the value's kind are errors.

Value kind Options Defaults
Integer or float scale, unit, precision SI scale, empty unit, precision 1
Duration precision Precision 1
UTC mode, now Absolute mode

For numbers, scale is si or binary. SI uses powers of 1000 with prefixes such as k and M; binary uses powers of 1024 with prefixes such as Ki and Mi. A string unit is appended directly, allowing B, /s, and Unicode units. Signed integers retain full precision before rounding.

precision is an integer from zero through nine and selects the maximum fractional decimal places. Rounding is halfway away from zero; trailing fractional zeros are omitted. A rounded value can carry into the next unit: 999950 at the default precision becomes 1M.

DUST
(strings/humanize 1500)
(strings/humanize 1536 {scale binary unit B})
(strings/humanize 1234 {precision 2 unit "/s"})
(strings/humanize -1250)
(strings/humanize 999950)
(strings/humanize 9223372036854775807 {precision 9})
=
DUST Result
1.5k
1.5KiB
1.23k/s
-1.3k
1M
9.223372037E

Durations use hours, minutes, seconds, or a suitable subsecond unit. Examples include 1h 30m, 2.5s, 150µs, and 100ns. The precision applies to the displayed fractional unit; the stored tick value is unchanged.

DUST
(strings/humanize #dur PT1H30M)
(strings/humanize #dur PT2.5S)
(strings/humanize #dur PT0.00015S)
(strings/humanize #dur PT59.99S)
(strings/humanize #dur PT1.2345678S {precision 9})
(strings/humanize #dur PT0.0000001S)
=
DUST Result
"1h 30m"
2.5s
150µs
1m
1.2345678s
100ns

UTC mode is absolute or relative. Absolute formatting produces the canonical UTC timestamp, retaining its 100 ns precision. Relative formatting requires an explicit UTC now option and produces text such as 3 minutes ago or in 2 days. It selects the largest whole unit among days, hours, minutes, and seconds; differences shorter than a second produce now. Supplying now in absolute mode is an error. The function never reads the clock implicitly.

DUST
(strings/humanize #utc 2026-10-29T12:00:00.1234567Z)
(strings/humanize #utc 2026-10-29T12:00Z {mode relative now #utc 2026-10-29T12:03Z})
(strings/humanize #utc 2026-10-31T12:00Z {mode relative now #utc 2026-10-29T12:00Z})
(strings/humanize #utc 2026-10-29T12:00Z {mode relative now #utc 2026-10-29T12:00Z})
→
Canonical DUST
(strings/humanize #utc 2026-10-29T12:00:00.1234567Z)
(strings/humanize #utc 2026-10-29T12:00:00Z {mode relative now #utc 2026-10-29T12:03:00Z})
(strings/humanize #utc 2026-10-31T12:00:00Z {mode relative now #utc 2026-10-29T12:00:00Z})
(strings/humanize #utc 2026-10-29T12:00:00Z {mode relative now #utc 2026-10-29T12:00:00Z})
=
DUST Result
2026-10-29T12:00:00.1234567Z
"3 minutes ago"
"in 2 days"
now
DUST
(strings/humanize #utc 2026-10-29T12:00Z {mode relative})
→
Canonical DUST
(strings/humanize #utc 2026-10-29T12:00:00Z {mode relative})
=
DUST Result
#err <invalid value>
DUST
(strings/humanize #dur PT1S {scale si})
=
DUST Result
#err <invalid value>
DUST
(strings/humanize 1 {precision 10})
=
DUST Result
#err <invalid value>
DUST
(strings/humanize 1 {unknown 1})
=
DUST Result
#err <invalid value>
DUST
(strings/humanize #link 1)
=
DUST Result
#err <mismatch type>

See the Time library for temporal constructors, arithmetic, calendar operations, and time/now.