`strings`, text
strings is text manipulation. Twenty-nine functions, and the organising convention is worth
learning once: a leading I means case-insensitive. Contains and IContains,
PhraseCount and IPhraseCount, and so on.
On extracted documents, prefer the I forms unless a case difference is itself meaningful. OCR
capitalisation is not evidence of anything.
Case and trimming
Section titled “Case and trimming”strings.ToUpper(s) → string · strings.ToLower(s) → string · strings.ToTitle(s) → string
Section titled “strings.ToUpper(s) → string · strings.ToLower(s) → string · strings.ToTitle(s) → string”| Parameter | Type | Meaning |
|---|---|---|
s | string | The text to convert |
ToTitle capitalises the first letter of each word and lowers the rest, so an all-caps name read
from a form comes out as a name.
rule NormaliseBorrowerName "store the name in title case" { when in.Has("borrower.name") then out.Set("borrowerName", strings.ToTitle(in.Str("borrower.name", "")));}{ "borrower": { "name": "JOHN SMITH" } }{ "borrowerName": "John Smith" }strings.TrimSpace(s) → string
Section titled “strings.TrimSpace(s) → string”Removes leading and trailing whitespace.
| Parameter | Type | Meaning |
|---|---|---|
s | string | The text to trim |
strings.Len(s) → int64
Section titled “strings.Len(s) → int64”The length of the text in bytes: an accented letter counts as two.
| Parameter | Type | Meaning |
|---|---|---|
s | string | The text to measure |
Substring tests
Section titled “Substring tests”strings.Contains(s, substr) → bool · strings.IContains(s, substr) → bool
Section titled “strings.Contains(s, substr) → bool · strings.IContains(s, substr) → bool”Reports whether s contains substr. IContains ignores case.
| Parameter | Type | Meaning |
|---|---|---|
s | string | The text to search |
substr | string | What to look for |
rule TrustDeedPresent "the document mentions a deed of trust" { when strings.IContains(bbox.Doc().AllText().Str(), "deed of trust") then out.Set("instrumentType", "deed_of_trust");}strings.HasPrefix(s, prefix) → bool · strings.HasSuffix(s, suffix) → bool
Section titled “strings.HasPrefix(s, prefix) → bool · strings.HasSuffix(s, suffix) → bool”| Parameter | Type | Meaning |
|---|---|---|
s | string | The text to test |
prefix / suffix | string | What to test for |
rule ServiceAccount "identifiers starting with svc- are service accounts" { when strings.HasPrefix(in.Str("actor.id", ""), "svc-") then out.Set("actorKind", "service");}{ "actor": { "id": "svc-billing" } }{ "actorKind": "service" }Splitting, joining, replacing
Section titled “Splitting, joining, replacing”strings.Split(s, sep) → list of string
Section titled “strings.Split(s, sep) → list of string”| Parameter | Type | Meaning |
|---|---|---|
s | string | The text to split |
sep | string | The separator |
strings.Join(elems, sep) → string
Section titled “strings.Join(elems, sep) → string”| Parameter | Type | Meaning |
|---|---|---|
elems | list of string | The pieces |
sep | string | The separator to place between them |
rule ListTags "the caller's semicolon list, comma-separated" { when in.Has("ticket.tags") then out.Set("tags", strings.Join(strings.Split(in.Str("ticket.tags", ""), ";"), ", ")); out.Set("tagCount", strings.Split(in.Str("ticket.tags", ""), ";").Len());}{ "ticket": { "tags": "billing;urgent;vip" } }{ "tags": "billing, urgent, vip", "tagCount": 3 }Count a split with .Len(); array.Len needs the list wrapped in to.List first.
strings.Replace(s, old, new) → string
Section titled “strings.Replace(s, old, new) → string”Replaces every occurrence of old with new.
| Parameter | Type | Meaning |
|---|---|---|
s | string | The text |
old | string | What to replace |
new | string | What to replace it with |
strings.ReplaceChars(s, replacement, charset) → string
Section titled “strings.ReplaceChars(s, replacement, charset) → string”Replaces characters rather than a substring: every character present in charset becomes
replacement, and everything else is kept.
| Parameter | Type | Meaning |
|---|---|---|
s | string | The text |
replacement | string | What each matched character becomes |
charset | string | The set of characters to replace |
The cleaning function for identifiers read off a document, where the separators vary run to run.
rule NormaliseSSN "strip separators from the tax identifier" { when in.Has("applicant.ssn") then out.Set("ssn", strings.ReplaceChars(in.Str("applicant.ssn", ""), "", "- ."));}{ "applicant": { "ssn": "123-45 6789" } }{ "ssn": "123456789" }Regular expressions
Section titled “Regular expressions”Four forms, and choosing the right one saves a lot of post-processing. Patterns use Go’s regular
expression syntax; inside a GRL string a backslash is doubled, "\\d+". An invalid pattern never
raises: it matches nothing, finds "", or leaves the text unchanged.
strings.MatchRegex(pattern, s) → bool
Section titled “strings.MatchRegex(pattern, s) → bool”Reports whether pattern matches anywhere in s. Anchor with ^ and $ to test the whole text.
| Parameter | Type | Meaning |
|---|---|---|
pattern | string | The regular expression |
s | string | The text to test |
rule LoanNumberWellFormed "loan numbers are three letters then eight digits" { when in.Has("loan.number") && !strings.MatchRegex("^[A-Z]{3}[0-9]{8}$", in.Str("loan.number", "")) then findings.Error("LoanNumberWellFormed", "format", "Loan number does not match the expected shape"); out.Set("loanNumberValid", false);}{ "loan": { "number": "AB-1234" } }{ "loanNumberValid": false }strings.FindRegex(pattern, s) → string
Section titled “strings.FindRegex(pattern, s) → string”Returns the first match, or the empty string when there is none.
| Parameter | Type | Meaning |
|---|---|---|
pattern | string | The regular expression |
s | string | The text to search |
strings.FindRegex("\\d+", "order 12345 confirmed") returns "12345".
strings.FindRegexGroups(pattern, s) → map
Section titled “strings.FindRegexGroups(pattern, s) → map”Returns the named capture groups of the first match, as a map of text. Unnamed groups are ignored, and no match returns an empty map.
| Parameter | Type | Meaning |
|---|---|---|
pattern | string | The regular expression, with (?P<name>...) groups |
s | string | The text to search |
The map’s values are text, which the map functions do not read, and out.Set writes the whole
map as its text form ("map[area:512 line:0199]") rather than as an object. To read one group,
use ReplaceRegex with ${name}, below.
strings.ExtractRegex(pattern, s, sep) → string
Section titled “strings.ExtractRegex(pattern, s, sep) → string”Finds all captures, named or unnamed, across all matches, and joins the non-empty ones with
sep.
| Parameter | Type | Meaning |
|---|---|---|
pattern | string | The regular expression |
s | string | The text to search |
sep | string | The separator to join captures with |
strings.ExtractRegex("(\\d+)", "order 123 item 456", " ") returns "123 456".
strings.ReplaceRegex(pattern, s, replacement) → string
Section titled “strings.ReplaceRegex(pattern, s, replacement) → string”Replaces all matches. $1, $2 refer to positional groups and ${name} to named ones.
| Parameter | Type | Meaning |
|---|---|---|
pattern | string | The regular expression |
s | string | The text |
replacement | string | The replacement, which may reference groups |
rule ReformatDate "turn MM/DD/YYYY into ISO" { when strings.MatchRegex("^[0-9]{2}/[0-9]{2}/[0-9]{4}$", in.Str("raw.date", "")) then out.Set("date", strings.ReplaceRegex("^(?P<m>[0-9]{2})/(?P<d>[0-9]{2})/(?P<y>[0-9]{4})$", in.Str("raw.date", ""), "${y}-${m}-${d}"));}{ "raw": { "date": "03/15/2026" } }{ "date": "2026-03-15" }Matching the whole text and replacing it with one group is how a rule reads a single capture:
rule AreaCode "the area code of the contact number" { when strings.MatchRegex("[0-9]{3}[-. ][0-9]{3}[-. ][0-9]{4}", in.Str("contact.phone", "")) then out.Set("areaCode", strings.ReplaceRegex("^.*?(?P<area>[0-9]{3})[-. ][0-9]{3}[-. ][0-9]{4}.*$", in.Str("contact.phone", ""), "${area}"));}{ "contact": { "phone": "Call 512-555-0199 after nine" } }{ "areaCode": "512" }Counting phrases
Section titled “Counting phrases”A phrase here may contain several words, which is what separates these from the substring tests: they are built for looking for real language in a body of extracted text. Matching is exact text, not fuzzy.
strings.PhraseCount(text, phrase) → int64 · strings.IPhraseCount(text, phrase) → int64
Section titled “strings.PhraseCount(text, phrase) → int64 · strings.IPhraseCount(text, phrase) → int64”How many times one phrase occurs.
| Parameter | Type | Meaning |
|---|---|---|
text | string | The text to search |
phrase | string | The phrase to count |
strings.CountPhrases(text, phrases...) → int64 · strings.ICountPhrases(text, phrases...) → int64
Section titled “strings.CountPhrases(text, phrases...) → int64 · strings.ICountPhrases(text, phrases...) → int64”The total occurrences of several phrases, a sum across all of them, not a count of how many were present.
| Parameter | Type | Meaning |
|---|---|---|
text | string | The text to search |
phrases | string, variadic | The phrases to count |
rule HeavilyRedacted "many redaction markers suggest a redacted copy" { when strings.ICountPhrases(bbox.Doc().AllText().Str(), "REDACTED", "[REMOVED]", "XXXXX") > 10 then findings.Warn("HeavilyRedacted", "provenance", "Document appears heavily redacted");}strings.HasAllPhrases(text, phrases...) → bool · strings.IHasAllPhrases(text, phrases...) → bool
Section titled “strings.HasAllPhrases(text, phrases...) → bool · strings.IHasAllPhrases(text, phrases...) → bool”Reports whether every phrase is present.
| Parameter | Type | Meaning |
|---|---|---|
text | string | The text to search |
phrases | string, variadic | The phrases that must all appear |
rule NoticeComplete "the notice carries all its required sections" { when strings.IHasAllPhrases(in.Str("notice.body", ""), "Loan Terms", "Projected Payments", "Costs at Closing") then out.Set("formComplete", true);}{ "notice": { "body": "LOAN TERMS ... projected payments ... Costs at Closing" } }{ "formComplete": true }For a presence test on a document itself, bbox.Doc().HasAll(...)
is usually better. It matches fuzzily, absorbing the OCR variation these exact-match functions
will not.
Locating a phrase
Section titled “Locating a phrase”strings.PhraseLineNum(text, phrase) → int64 · strings.IPhraseLineNum(text, phrase) → int64
Section titled “strings.PhraseLineNum(text, phrase) → int64 · strings.IPhraseLineNum(text, phrase) → int64”The 1-based line number where the phrase first appears, or 0 when it is not found. Lines are
split on newlines.
| Parameter | Type | Meaning |
|---|---|---|
text | string | The text to search |
phrase | string | The phrase to locate |
Zero means “not found”. It is not a line number, since lines start at one.
strings.PhraseLineNums(text, phrase) → list of int64 · strings.IPhraseLineNums(text, phrase) → list of int64
Section titled “strings.PhraseLineNums(text, phrase) → list of int64 · strings.IPhraseLineNums(text, phrase) → list of int64”Every 1-based line number where the phrase appears.
| Parameter | Type | Meaning |
|---|---|---|
text | string | The text to search |
phrase | string | The phrase to locate |
rule SignatureBlockPosition "the signature block should be near the end" { when strings.IPhraseLineNum(bbox.Doc().AllText().Str(), "IN WITNESS WHEREOF") > 0 && strings.IPhraseLineNum(bbox.Doc().AllText().Str(), "IN WITNESS WHEREOF") < math.Round(to.Num(bbox.Doc().LineCount()) * 0.5, 0) then findings.Warn("SignatureBlockPosition", "structure", "Signature block appears in the first half of the document");}Formatting
Section titled “Formatting”strings.Sprintf(format, args...) → string
Section titled “strings.Sprintf(format, args...) → string”Formats a string.
| Parameter | Type | Meaning |
|---|---|---|
format | string | The format, with %s, %d, %f and the usual verbs |
args | any, variadic | The values |
Mostly used to build a readable finding message.
rule AmountOverLimit "message names the actual figure" { when in.Double("loan.amount", 0.0) > 500000.0 then findings.Warn("AmountOverLimit", "underwriting", strings.Sprintf("Loan amount %s exceeds the 500,000 limit", to.Grouped(in.Double("loan.amount", 0.0), 2))); out.Set("limitMessage", strings.Sprintf("Loan amount %s exceeds the 500,000 limit", to.Grouped(in.Double("loan.amount", 0.0), 2)));}{ "loan": { "amount": 612500.0 } }{ "limitMessage": "Loan amount 612,500.00 exceeds the 500,000 limit" }Prefer to.Grouped / to.Fixed for the numbers inside a message rather than %f, which prints
float noise.
See also
Section titled “See also”to: conversion: turning text into numbers and datesvocab: named lists: keeping phrase lists out of rules- The Document API: fuzzy matching, which these functions do not do