Skip to content

Commit 51b3302

Browse files
feat: Add support for {Expr,Series}.str.to_titlecase (#3116)
Co-authored-by: Dan Redding <[email protected]> --------- Co-authored-by: dangotbanned <[email protected]>
1 parent 8ac061c commit 51b3302

File tree

18 files changed

+289
-4
lines changed

18 files changed

+289
-4
lines changed

docs/api-reference/expr_str.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- to_date
1919
- to_datetime
2020
- to_lowercase
21+
- to_titlecase
2122
- to_uppercase
2223
- zfill
2324
show_source: false

docs/api-reference/series_str.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- to_date
1919
- to_datetime
2020
- to_lowercase
21+
- to_titlecase
2122
- to_uppercase
2223
- zfill
2324
show_source: false

narwhals/_arrow/series_str.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ def to_uppercase(self) -> ArrowSeries:
7979
def to_lowercase(self) -> ArrowSeries:
8080
return self.with_native(pc.utf8_lower(self.native))
8181

82+
def to_titlecase(self) -> ArrowSeries:
83+
return self.with_native(pc.utf8_title(self.native))
84+
8285
def zfill(self, width: int) -> ArrowSeries:
8386
binary_join: Incomplete = pc.binary_join_element_wise
8487
native = self.native

narwhals/_compliant/any_namespace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def split(self, by: str) -> CompliantT_co: ...
100100
def to_datetime(self, format: str | None) -> CompliantT_co: ...
101101
def to_date(self, format: str | None) -> CompliantT_co: ...
102102
def to_lowercase(self) -> CompliantT_co: ...
103+
def to_titlecase(self) -> CompliantT_co: ...
103104
def to_uppercase(self) -> CompliantT_co: ...
104105
def zfill(self, width: int) -> CompliantT_co: ...
105106

narwhals/_compliant/expr.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,9 @@ def to_uppercase(self) -> EagerExprT:
11561156
def zfill(self, width: int) -> EagerExprT:
11571157
return self.compliant._reuse_series_namespace("str", "zfill", width=width)
11581158

1159+
def to_titlecase(self) -> EagerExprT:
1160+
return self.compliant._reuse_series_namespace("str", "to_titlecase")
1161+
11591162

11601163
class EagerExprStructNamespace(
11611164
EagerExprNamespace[EagerExprT], StructNamespace[EagerExprT], Generic[EagerExprT]

narwhals/_dask/expr_str.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ def to_lowercase(self) -> DaskExpr:
113113
lambda expr: expr.str.lower(), "to_lowercase"
114114
)
115115

116+
def to_titlecase(self) -> DaskExpr:
117+
return self.compliant._with_callable(
118+
lambda expr: expr.str.title(), "to_titlecase"
119+
)
120+
116121
def zfill(self, width: int) -> DaskExpr:
117122
return self.compliant._with_callable(
118123
lambda expr, width: expr.str.zfill(width), "zfill", width=width

narwhals/_duckdb/expr_str.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
from typing import TYPE_CHECKING
44

5-
from narwhals._duckdb.utils import F, lit
5+
from narwhals._duckdb.utils import F, col, concat_str, lit
66
from narwhals._sql.expr_str import SQLExprStringNamespace
7-
from narwhals._utils import not_implemented
7+
from narwhals._utils import not_implemented, requires
88

99
if TYPE_CHECKING:
10+
from duckdb import Expression
11+
1012
from narwhals._duckdb.expr import DuckDBExpr
1113

1214

@@ -27,4 +29,25 @@ def to_date(self, format: str | None) -> DuckDBExpr:
2729
compliant_expr = self.compliant
2830
return compliant_expr.cast(compliant_expr._version.dtypes.Date())
2931

32+
@requires.backend_version((1, 2))
33+
def to_titlecase(self) -> DuckDBExpr:
34+
from narwhals._duckdb.utils import lambda_expr
35+
36+
def _to_titlecase(expr: Expression) -> Expression:
37+
extract_expr = F(
38+
"regexp_extract_all", F("lower", expr), lit(r"[a-z0-9]*[^a-z0-9]*")
39+
)
40+
elem = col("_")
41+
capitalize = lambda_expr(
42+
elem,
43+
concat_str(
44+
F("upper", F("array_extract", elem, lit(1))),
45+
F("substring", elem, lit(2)),
46+
),
47+
)
48+
capitalized_expr = F("list_transform", extract_expr, capitalize)
49+
return F("list_aggregate", capitalized_expr, lit("string_agg"), lit(""))
50+
51+
return self.compliant._with_elementwise(_to_titlecase)
52+
3053
replace = not_implemented()

narwhals/_duckdb/utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import duckdb
77
import duckdb.typing as duckdb_dtypes
8+
from duckdb import Expression
89
from duckdb.typing import DuckDBPyType
910

1011
from narwhals._utils import Version, isinstance_or_issubclass, zip_strict
@@ -13,7 +14,7 @@
1314
if TYPE_CHECKING:
1415
from collections.abc import Mapping, Sequence
1516

16-
from duckdb import DuckDBPyRelation, Expression
17+
from duckdb import DuckDBPyRelation
1718

1819
from narwhals._compliant.typing import CompliantLazyFrameAny
1920
from narwhals._duckdb.dataframe import DuckDBLazyFrame
@@ -50,6 +51,22 @@
5051
"""Alias for `duckdb.FunctionExpression`."""
5152

5253

54+
def lambda_expr(
55+
params: str | Expression | tuple[Expression, ...], expr: Expression, /
56+
) -> Expression:
57+
"""Wraps [`duckdb.LambdaExpression`].
58+
59+
[`duckdb.LambdaExpression`]: https://duckdb.org/docs/stable/sql/functions/lambda
60+
"""
61+
try:
62+
from duckdb import LambdaExpression
63+
except ModuleNotFoundError as exc: # pragma: no cover
64+
msg = f"DuckDB>=1.2.0 is required for this operation. Found: DuckDB {duckdb.__version__}"
65+
raise NotImplementedError(msg) from exc
66+
args = (params,) if isinstance(params, Expression) else params
67+
return LambdaExpression(args, expr)
68+
69+
5370
def concat_str(*exprs: Expression, separator: str = "") -> Expression:
5471
"""Concatenate many strings, NULL inputs are skipped.
5572

narwhals/_ibis/expr_str.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ def fn(expr: ir.StringColumn) -> ir.DateValue:
8181
return self.compliant._with_callable(fn)
8282

8383
replace = not_implemented()
84+
to_titlecase = not_implemented()

narwhals/_pandas_like/series_str.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,8 @@ def to_uppercase(self) -> PandasLikeSeries:
8888
def to_lowercase(self) -> PandasLikeSeries:
8989
return self.with_native(self.native.str.lower())
9090

91+
def to_titlecase(self) -> PandasLikeSeries:
92+
return self.with_native(self.native.str.title())
93+
9194
def zfill(self, width: int) -> PandasLikeSeries:
9295
return self.with_native(self.native.str.zfill(width))

0 commit comments

Comments
 (0)