33from typing import TYPE_CHECKING
44
55from narwhals ._duration import parse_interval_string
6- from narwhals ._spark_like .utils import UNITS_DICT
6+ from narwhals ._spark_like .utils import UNITS_DICT , strptime_to_pyspark_format
77
88if TYPE_CHECKING :
99 from sqlframe .base .column import Column
@@ -15,6 +15,40 @@ class SparkLikeExprDateTimeNamespace:
1515 def __init__ (self , expr : SparkLikeExpr ) -> None :
1616 self ._compliant_expr = expr
1717
18+ def to_string (self , format : str ) -> SparkLikeExpr :
19+ F = self ._compliant_expr ._F # noqa: N806
20+
21+ def _to_string (_input : Column ) -> Column :
22+ # Handle special formats
23+ if format == "%G-W%V" :
24+ return self ._format_iso_week (_input )
25+ if format == "%G-W%V-%u" :
26+ return self ._format_iso_week_with_day (_input )
27+
28+ format_ , suffix = self ._format_microseconds (_input , format )
29+
30+ # Convert Python format to PySpark format
31+ pyspark_fmt = strptime_to_pyspark_format (format_ )
32+
33+ result = F .date_format (_input , pyspark_fmt )
34+ if "T" in format_ :
35+ # `strptime_to_pyspark_format` replaces "T" with " " since pyspark
36+ # does not support the literal "T" in `date_format`.
37+ # If no other spaces are in the given format, then we can revert this
38+ # operation, otherwise we raise an exception.
39+ if " " not in format_ :
40+ result = F .replace (result , F .lit (" " ), F .lit ("T" ))
41+ else : # pragma: no cover
42+ msg = (
43+ "`dt.to_string` with a format that contains both spaces and "
44+ " the literal 'T' is not supported for spark-like backends."
45+ )
46+ raise NotImplementedError (msg )
47+
48+ return F .concat (result , * suffix )
49+
50+ return self ._compliant_expr ._with_callable (_to_string )
51+
1852 def date (self ) -> SparkLikeExpr :
1953 return self ._compliant_expr ._with_callable (self ._compliant_expr ._F .to_date )
2054
@@ -89,3 +123,40 @@ def replace_time_zone(self, time_zone: str | None) -> SparkLikeExpr:
89123 else : # pragma: no cover
90124 msg = "`replace_time_zone` with non-null `time_zone` not yet implemented for spark-like"
91125 raise NotImplementedError (msg )
126+
127+ def _format_iso_week_with_day (self , _input : Column ) -> Column :
128+ """Format datetime as ISO week string with day."""
129+ F = self ._compliant_expr ._F # noqa: N806
130+
131+ year = F .date_format (_input , "yyyy" )
132+ week = F .lpad (F .weekofyear (_input ).cast ("string" ), 2 , "0" )
133+ day = F .dayofweek (_input )
134+ # Adjust Sunday from 1 to 7
135+ day = F .when (day == 1 , 7 ).otherwise (day - 1 )
136+ return F .concat (year , F .lit ("-W" ), week , F .lit ("-" ), day .cast ("string" ))
137+
138+ def _format_iso_week (self , _input : Column ) -> Column :
139+ """Format datetime as ISO week string."""
140+ F = self ._compliant_expr ._F # noqa: N806
141+
142+ year = F .date_format (_input , "yyyy" )
143+ week = F .lpad (F .weekofyear (_input ).cast ("string" ), 2 , "0" )
144+ return F .concat (year , F .lit ("-W" ), week )
145+
146+ def _format_microseconds (
147+ self , _input : Column , format : str
148+ ) -> tuple [str , tuple [Column , ...]]:
149+ """Format microseconds if present in format, else it's a no-op."""
150+ F = self ._compliant_expr ._F # noqa: N806
151+
152+ suffix : tuple [Column , ...]
153+ if format .endswith ((".%f" , "%.f" )):
154+ import re
155+
156+ micros = F .unix_micros (_input ) % 1_000_000
157+ micros_str = F .lpad (micros .cast ("string" ), 6 , "0" )
158+ suffix = (F .lit ("." ), micros_str )
159+ format_ = re .sub (r"(.%|%.)f$" , "" , format )
160+ return format_ , suffix
161+
162+ return format , ()
0 commit comments