Skip to content

Commit 9134ef5

Browse files
feat(api): add StringValue.as_time for parsing strings into times (#10278)
1 parent 7402c1e commit 9134ef5

18 files changed

+101
-6
lines changed

ibis/backends/polars/compiler.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -955,13 +955,17 @@ def string_to_date(op, **kw):
955955
)
956956

957957

958+
@translate.register(ops.StringToTime)
959+
def string_to_time(op, **kw):
960+
arg = translate(op.arg, **kw)
961+
return arg.str.to_time(format=_literal_value(op.format_str))
962+
963+
958964
@translate.register(ops.StringToTimestamp)
959965
def string_to_timestamp(op, **kw):
960966
arg = translate(op.arg, **kw)
961-
return arg.str.strptime(
962-
dtype=pl.Datetime,
963-
format=_literal_value(op.format_str),
964-
)
967+
format = _literal_value(op.format_str)
968+
return arg.str.strptime(dtype=pl.Datetime, format=format)
965969

966970

967971
@translate.register(ops.TimestampDiff)

ibis/backends/sql/compilers/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,9 @@ def visit_NotNull(self, op, *, arg):
10421042
def visit_InValues(self, op, *, value, options):
10431043
return value.isin(*options)
10441044

1045+
def visit_StringToTime(self, op, *, arg, format_str):
1046+
return self.f.time(self.f.str_to_time(arg, format_str))
1047+
10451048
### Counting
10461049

10471050
def visit_CountDistinct(self, op, *, arg, where):

ibis/backends/sql/compilers/clickhouse.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class ClickHouseCompiler(SQLGlotCompiler):
5353
ops.TimeDelta,
5454
ops.StringToTimestamp,
5555
ops.StringToDate,
56+
ops.StringToTime,
5657
ops.Levenshtein,
5758
)
5859

ibis/backends/sql/compilers/datafusion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class DataFusionCompiler(SQLGlotCompiler):
4949
ops.TypeOf,
5050
ops.StringToDate,
5151
ops.StringToTimestamp,
52+
ops.StringToTime,
5253
)
5354

5455
SIMPLE_OPS = {

ibis/backends/sql/compilers/druid.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class DruidCompiler(SQLGlotCompiler):
5555
ops.StringAscii,
5656
ops.StringSplit,
5757
ops.StringToDate,
58+
ops.StringToTime,
5859
ops.StringToTimestamp,
5960
ops.TimeDelta,
6061
ops.TimestampBucket,

ibis/backends/sql/compilers/duckdb.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,5 +709,8 @@ def visit_TableUnnest(
709709
.join(unnest, join_type="CROSS" if not keep_empty else "LEFT")
710710
)
711711

712+
def visit_StringToTime(self, op, *, arg, format_str):
713+
return self.cast(self.f.str_to_time(arg, format_str), to=dt.time)
714+
712715

713716
compiler = DuckDBCompiler()

ibis/backends/sql/compilers/exasol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class ExasolCompiler(SQLGlotCompiler):
6565
ops.StringSplit,
6666
ops.StringToDate,
6767
ops.StringToTimestamp,
68+
ops.StringToTime,
6869
ops.TimeDelta,
6970
ops.TimestampAdd,
7071
ops.TimestampBucket,

ibis/backends/sql/compilers/flink.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class FlinkCompiler(SQLGlotCompiler):
8787
ops.RowID,
8888
ops.StringSplit,
8989
ops.Translate,
90+
ops.StringToTime,
9091
)
9192

9293
SIMPLE_OPS = {

ibis/backends/sql/compilers/impala.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class ImpalaCompiler(SQLGlotCompiler):
4141
ops.RegexSplit,
4242
ops.RowID,
4343
ops.StringSplit,
44+
ops.StringToTime,
4445
ops.StructColumn,
4546
ops.Time,
4647
ops.TimeDelta,

ibis/backends/sql/compilers/mssql.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class MSSQLCompiler(SQLGlotCompiler):
115115
ops.StringSplit,
116116
ops.StringToDate,
117117
ops.StringToTimestamp,
118+
ops.StringToTime,
118119
ops.StructColumn,
119120
ops.TimestampDiff,
120121
ops.Unnest,

ibis/backends/sql/compilers/oracle.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class OracleCompiler(SQLGlotCompiler):
7373
ops.ExtractDayOfYear,
7474
ops.RowID,
7575
ops.RandomUUID,
76+
ops.StringToTime,
7677
)
7778

7879
SIMPLE_OPS = {

ibis/backends/sql/compilers/postgres.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,5 +827,8 @@ def visit_ArrayAny(self, op, *, arg):
827827
def visit_ArrayAll(self, op, *, arg):
828828
return self._array_reduction(arg=arg, reduction="bool_and")
829829

830+
def visit_StringToTime(self, op, *, arg, format_str):
831+
return self.cast(self.f.str_to_time(arg, format_str), to=dt.time)
832+
830833

831834
compiler = PostgresCompiler()

ibis/backends/sql/compilers/pyspark.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class PySparkCompiler(SQLGlotCompiler):
6363
ops.RowID,
6464
ops.TimestampBucket,
6565
ops.RandomUUID,
66+
ops.StringToTime,
6667
)
6768

6869
LOWERED_OPS = {

ibis/backends/sql/compilers/sqlite.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class SQLiteCompiler(SQLGlotCompiler):
5858
ops.TimestampDiff,
5959
ops.StringToDate,
6060
ops.StringToTimestamp,
61+
ops.StringToTime,
6162
ops.TimeDelta,
6263
ops.TimestampDelta,
6364
ops.TryCast,

ibis/backends/sql/compilers/trino.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class TrinoCompiler(SQLGlotCompiler):
5252
ops.Median,
5353
ops.RowID,
5454
ops.TimestampBucket,
55+
ops.StringToTime,
5556
)
5657

5758
LOWERED_OPS = {

ibis/backends/tests/test_temporal.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,7 +1179,7 @@ def test_integer_to_timestamp(backend, con, unit):
11791179
raises=com.OperationNotDefinedError,
11801180
)
11811181
@pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
1182-
def test_string_to_timestamp(alltypes, fmt):
1182+
def test_string_as_timestamp(alltypes, fmt):
11831183
table = alltypes
11841184
result = table.mutate(date=table.date_string_col.as_timestamp(fmt)).execute()
11851185

@@ -1250,7 +1250,7 @@ def test_string_to_timestamp(alltypes, fmt):
12501250
raises=com.OperationNotDefinedError,
12511251
)
12521252
@pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
1253-
def test_string_to_date(alltypes, fmt):
1253+
def test_string_as_date(alltypes, fmt):
12541254
table = alltypes
12551255
result = table.mutate(date=table.date_string_col.as_date(fmt)).execute()
12561256

@@ -1260,6 +1260,37 @@ def test_string_to_date(alltypes, fmt):
12601260
assert val.strftime("%m/%d/%y") == result["date_string_col"][i]
12611261

12621262

1263+
@pytest.mark.notyet(
1264+
[
1265+
"pyspark",
1266+
"exasol",
1267+
"clickhouse",
1268+
"impala",
1269+
"mssql",
1270+
"oracle",
1271+
"trino",
1272+
"druid",
1273+
"datafusion",
1274+
"flink",
1275+
],
1276+
raises=com.OperationNotDefinedError,
1277+
)
1278+
@pytest.mark.notimpl(["sqlite"], raises=com.UnsupportedOperationError)
1279+
def test_string_as_time(backend, alltypes):
1280+
fmt = "%H:%M:%S"
1281+
table = alltypes.mutate(
1282+
time_string_col=alltypes.timestamp_col.truncate("s").time().cast(str)
1283+
)
1284+
expr = table.mutate(time=table.time_string_col.as_time(fmt))
1285+
result = expr.execute()
1286+
1287+
# TEST: do we get the same date out, that we put in?
1288+
# format string assumes that we are using pandas' strftime
1289+
backend.assert_series_equal(
1290+
result["time"], result["timestamp_col"].dt.floor("s").dt.time.rename("time")
1291+
)
1292+
1293+
12631294
@pytest.mark.parametrize(
12641295
("date", "expected_index", "expected_day"),
12651296
[

ibis/expr/operations/temporal.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ class StringToDate(Value):
9494
dtype = dt.date
9595

9696

97+
@public
98+
class StringToTime(Value):
99+
"""Convert a string to a time."""
100+
101+
arg: Value[dt.String]
102+
format_str: Value[dt.String]
103+
104+
shape = rlz.shape_like("args")
105+
dtype = dt.time
106+
107+
97108
@public
98109
class ExtractTemporalField(Unary):
99110
"""Extract a field from a temporal value."""

ibis/expr/types/strings.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,35 @@ def as_date(self, format_str: str) -> ir.DateValue:
13361336
def to_date(self, format_str: str) -> ir.DateValue:
13371337
return self.as_date(format_str=format_str)
13381338

1339+
def as_time(self, format_str: str) -> ir.TimeValue:
1340+
"""Parse a string and return a time.
1341+
1342+
Parameters
1343+
----------
1344+
format_str
1345+
Format string in `strptime` format
1346+
1347+
Returns
1348+
-------
1349+
TimeValue
1350+
Parsed time value
1351+
1352+
Examples
1353+
--------
1354+
>>> import ibis
1355+
>>> ibis.options.interactive = True
1356+
>>> t = ibis.memtable({"ts": ["20:01:02"]})
1357+
>>> t.ts.as_time("%H:%M:%S")
1358+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
1359+
┃ StringToTime(ts, '%H:%M:%S') ┃
1360+
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
1361+
│ time │
1362+
├──────────────────────────────┤
1363+
│ 20:01:02 │
1364+
└──────────────────────────────┘
1365+
"""
1366+
return ops.StringToTime(self, format_str).to_expr()
1367+
13391368
def protocol(self):
13401369
"""Parse a URL and extract protocol.
13411370

0 commit comments

Comments
 (0)