Strings¶
Text methods sit behind Str(). A method returns an expression, so a second one
needs Str() again: Col("name").Str().StripChars(" ").Str().ToLower(). The
examples read a small frame of people:
func people() *ursus.LazyFrame {
return ursus.Frame(
ursus.Values("name", []string{
" ada LOVELACE",
"Zoë Smith ",
"ben o'brien",
}),
ursus.Values("contact", []string{
"ada@example.com, +44 20 7946 0958",
"zoe.smith@mail.example.org",
"call 555-0100 or 555-0199",
}),
ursus.Values("joined", []string{"2024-03-01", "2025-11-17", "bad date"}),
)
}
Cleaning¶
name := ursus.Col("name").Str()
trimmed := name.StripChars(" ").Str() // a method gives an Expr: Str() again
show(people().Select(
trimmed.ToTitlecase().Alias("name"),
trimmed.ToLower().Alias("lower"),
name.LenChars().Alias("chars"),
name.LenBytes().Alias("bytes"),
))
shape: (3, 4)
┌────────────────┬────────────────┬────────┬────────┐
│ name │ lower │ chars │ bytes │
│ String │ String │ Uint32 │ Uint32 │
├────────────────┼────────────────┼────────┼────────┤
│ "Ada Lovelace" │ "ada lovelace" │ 14 │ 14 │
│ "Zoë Smith" │ "zoë smith" │ 10 │ 11 │
│ "Ben O'Brien" │ "ben o'brien" │ 11 │ 11 │
└────────────────┴────────────────┴────────┴────────┘
StripChars(chars)trims those characters from both ends, andStripCharsStartandStripCharsEndfrom one;StripPrefixandStripSuffixremove a whole affix;ToUpper,ToLowerandToTitlecase;LenCharscounts characters andLenBytesbytes of UTF-8: "Zoë Smith " is ten characters and eleven bytes.
Matching¶
Contains(pattern, literal) tests a regular expression, or a literal string when
literal is true. StartsWith and EndsWith take a literal, and CountMatches
counts a pattern's matches:
contact := ursus.Col("contact").Str()
show(people().Select(
ursus.Col("contact"),
contact.Contains("@", true).Alias("email"),
contact.Contains(`\d{3}-\d{4}`, false).Alias("us_phone"),
contact.EndsWith(".org").Alias("org"),
contact.CountMatches(`\d`, false).Alias("digits"),
))
shape: (3, 5)
┌─────────────────────────────────────┬───────┬──────────┬───────┬────────┐
│ contact │ email │ us_phone │ org │ digits │
│ String │ Bool │ Bool │ Bool │ Uint32 │
├─────────────────────────────────────┼───────┼──────────┼───────┼────────┤
│ "ada@example.com, +44 20 7946 0958" │ true │ false │ false │ 12 │
│ "zoe.smith@mail.example.org" │ true │ false │ true │ 0 │
│ "call 555-0100 or 555-0199" │ false │ true │ false │ 14 │
└─────────────────────────────────────┴───────┴──────────┴───────┴────────┘
Patterns are Go's regular expressions, RE2, as Polars' are Rust's regex: the same
syntax for nearly everything, and no backreferences or look-around in either.
ContainsAny(patterns, ...) and ReplaceMany match many literal strings at once, in
one pass.
Extracting¶
Extract(pattern, group) takes a capture group of the first match, null where nothing
matches; ExtractAll(pattern) takes every match, a list a row; ExtractGroups every
named group, as a struct.
contact := ursus.Col("contact").Str()
show(people().Select(
contact.Extract(`([\w.]+)@([\w.]+)`, 2).Alias("domain"),
contact.ExtractAll(`\d{3}-\d{4}`).Alias("phones"),
))
shape: (3, 2)
┌────────────────────┬──────────────────────────┐
│ domain │ phones │
│ String │ List(String) │
├────────────────────┼──────────────────────────┤
│ "example.com" │ [] │
│ "mail.example.org" │ [] │
│ null │ ["555-0100", "555-0199"] │
└────────────────────┴──────────────────────────┘
Replacing¶
Replace replaces the first match and ReplaceAll every one; ReplaceN the first n.
With a regular expression, the replacement may refer to groups: $1, or ${name}.
contact := ursus.Col("contact").Str()
show(people().Select(
contact.Replace(`\d`, "#", false).Alias("first_digit"),
contact.ReplaceAll(`\d`, "#", false).Alias("every_digit"),
))
shape: (3, 2)
┌─────────────────────────────────────┬─────────────────────────────────────┐
│ first_digit │ every_digit │
│ String │ String │
├─────────────────────────────────────┼─────────────────────────────────────┤
│ "ada@example.com, +#4 20 7946 0958" │ "ada@example.com, +## ## #### ####" │
│ "zoe.smith@mail.example.org" │ "zoe.smith@mail.example.org" │
│ "call #55-0100 or 555-0199" │ "call ###-#### or ###-####" │
└─────────────────────────────────────┴─────────────────────────────────────┘
Splitting¶
Split(by) gives a list a row. SplitExact(by, n, inclusive) gives a struct of n + 1
fields, null where there are fewer parts, which Unnest turns into columns:
show(people().Select(
ursus.Col("name").Str().StripChars(" ").Str().Split(" ").Alias("words"),
))
show(people().
Select(ursus.Col("joined").Str().SplitExact("-", 2, false).Alias("parts")).
Unnest("parts"))
shape: (3, 1)
┌─────────────────────┐
│ words │
│ List(String) │
├─────────────────────┤
│ ["ada", "LOVELACE"] │
│ ["Zoë", "Smith"] │
│ ["ben", "o'brien"] │
└─────────────────────┘
shape: (3, 3)
┌────────────┬─────────┬─────────┐
│ field_0 │ field_1 │ field_2 │
│ String │ String │ String │
├────────────┼─────────┼─────────┤
│ "2024" │ "03" │ "01" │
│ "2025" │ "11" │ "17" │
│ "bad date" │ null │ null │
└────────────┴─────────┴─────────┘
SplitN(by, n) splits at most n − 1 times, the rest in the last part.
Padding and slicing¶
code := ursus.Frame(ursus.Values("code", []string{"7", "42", "1234"}))
c := ursus.Col("code").Str()
show(code.Select(
ursus.Col("code"),
c.ZFill(5).Alias("zfill"),
c.PadEnd(5, ".").Alias("pad_end"),
c.Head(2).Alias("head"),
c.Slice(1, 2).Alias("slice"),
c.Reverse().Alias("reverse"),
))
shape: (3, 6)
┌────────┬─────────┬─────────┬────────┬────────┬─────────┐
│ code │ zfill │ pad_end │ head │ slice │ reverse │
│ String │ String │ String │ String │ String │ String │
├────────┼─────────┼─────────┼────────┼────────┼─────────┤
│ "7" │ "00007" │ "7...." │ "7" │ "" │ "7" │
│ "42" │ "00042" │ "42..." │ "42" │ "2" │ "24" │
│ "1234" │ "01234" │ "1234." │ "12" │ "23" │ "4321" │
└────────┴─────────┴─────────┴────────┴────────┴─────────┘
ZFill, PadStart and PadEnd pad to a width; Slice(offset, length), Head and
Tail count characters, not bytes, and Reverse reverses them.
Parsing¶
Strptime(type, format, strict) parses dates and times with a strftime format;
ToInteger parses integers:
joined := ursus.Col("joined").Str()
show(people().Select(
ursus.Col("joined"),
joined.Strptime(ursus.Date, "%Y-%m-%d", false).Alias("date"),
joined.Head(4).Str().ToInteger().Alias("year"),
))
shape: (3, 3)
┌──────────────┬────────────┬───────┐
│ joined │ date │ year │
│ String │ Date │ Int64 │
├──────────────┼────────────┼───────┤
│ "2024-03-01" │ 2024-03-01 │ 2024 │
│ "2025-11-17" │ 2025-11-17 │ 2025 │
│ "bad date" │ null │ null │
└──────────────┴────────────┴───────┘
ToInteger, ToDate and ToDatetime turn what does not parse into a null. Polars'
to_integer and to_date fail the query by default instead. For that, ask for
strictness: Strptime(..., true), or ToIntegerWith(base, type, true).
New in 0.6.7: ToTime(format, strict) parses a time of day, by a format or, with
none, each value as it comes, and ToDecimal(scale) a number into a Decimal(38,
scale), rounded half away from zero, as Polars' to_time and to_decimal.
New in 0.6.7: ToDate, ToDatetime and ToTime without a format, and Strptime
with an empty one, read the patterns Polars' to_date and to_datetime infer:
2024-01-02, 2024/1/2, 02.01.2024 and 02/01/2024, the year first or the day,
never the month; 2024-01-02 03:04:05.123, a T or a space between; and an offset,
Z, +09:00 or +0900, which makes the value its instant. ToDatetime(unit, tz)
reads a value with no offset on tz's wall clock, as Polars' does, where it took it
as UTC's. Each value is read by its own pattern; Polars infers one from the first.
JsonDecode(type) parses JSON into a value of a type, a struct or a list, and
Encode and Decode convert to and from hex or base64.
Joining¶
ConcatStr(sep, exprs...) joins columns row by row, and Format fills a template;
Str().Join(sep) in an aggregation joins a group's values into one string:
show(sales().Head(3).Select(
ursus.ConcatStr("/", ursus.Col("region"), ursus.Col("product")).Alias("path"),
ursus.Format("{} x {}", ursus.Col("qty"), ursus.Col("product")).Alias("line"),
))
show(sales().
GroupBy(ursus.Col("region")).
Agg(ursus.Col("product").Str().Join(", ").Alias("products")).
Sort(ursus.Asc(ursus.Col("region"))))
shape: (3, 2)
┌───────────────┬──────────────┐
│ path │ line │
│ String │ String │
├───────────────┼──────────────┤
│ "north/apple" │ "10 x apple" │
│ "south/apple" │ "4 x apple" │
│ "north/pear" │ "7 x pear" │
└───────────────┴──────────────┘
shape: (3, 2)
┌─────────┬────────────────────┐
│ region │ products │
│ String │ String │
├─────────┼────────────────────┤
│ "east" │ "pear, apple" │
│ "north" │ "apple, pear, fig" │
│ "south" │ "apple, fig, pear" │
└─────────┴────────────────────┘
A null in ConcatStr's inputs makes the row null, as in Polars; ConcatStrIgnoreNulls
skips nulls instead.