Skip to content

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"),
))
Output
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, and StripCharsStart and StripCharsEnd from one; StripPrefix and StripSuffix remove a whole affix;
  • ToUpper, ToLower and ToTitlecase;
  • LenChars counts characters and LenBytes bytes 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"),
))
Output
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"),
))
Output
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"),
))
Output
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"))
Output
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"),
))
Output
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"),
))
Output
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"))))
Output
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.