Skip to content

Commit faad9d3

Browse files
kaijennissencpcloud
authored andcommitted
feat(api): add day_of_week.iso_index method for date and timestamp types
1 parent f50cbfc commit faad9d3

19 files changed

+125
-17
lines changed

ibis/backends/dask/kernels.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def inner(df):
4444
),
4545
ops.TimestampFromUNIX: lambda arg, unit: dd.to_datetime(arg, unit=unit.short),
4646
ops.DayOfWeekIndex: lambda arg: dd.to_datetime(arg).dt.dayofweek,
47+
ops.IsoDayOfWeekIndex: lambda arg: dd.to_datetime(arg).dt.dayofweek + 1,
4748
ops.DayOfWeekName: lambda arg: dd.to_datetime(arg).dt.day_name(),
4849
}
4950

ibis/backends/pandas/kernels.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ def wrapper(*args, **kwargs):
429429
),
430430
ops.Capitalize: lambda arg: arg.str.capitalize(),
431431
ops.Date: lambda arg: arg.dt.floor("d"),
432+
ops.IsoDayOfWeekIndex: lambda arg: pd.to_datetime(arg).dt.dayofweek + 1,
432433
ops.DayOfWeekIndex: lambda arg: pd.to_datetime(arg).dt.dayofweek,
433434
ops.DayOfWeekName: lambda arg: pd.to_datetime(arg).dt.day_name(),
434435
ops.EndsWith: lambda arg, end: arg.str.endswith(end),

ibis/backends/polars/compiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,7 @@ def extract_epoch_seconds(op, **kw):
10641064
ops.Ceil: lambda arg: arg.ceil().cast(pl.Int64),
10651065
ops.Cos: operator.methodcaller("cos"),
10661066
ops.Cot: lambda arg: 1.0 / arg.tan(),
1067+
ops.IsoDayOfWeekIndex: (lambda arg: arg.dt.weekday().cast(pl.Int16)),
10671068
ops.DayOfWeekIndex: (
10681069
lambda arg: arg.dt.weekday().cast(pl.Int16) - _day_of_week_offset
10691070
),

ibis/backends/sql/compilers/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,9 @@ def visit_TimeTruncate(self, op, *, arg, unit):
861861
def visit_DayOfWeekIndex(self, op, *, arg):
862862
return (self.f.dayofweek(arg) + 6) % 7
863863

864+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
865+
return ((self.f.dayofweek(arg) + 6) % 7) + 1
866+
864867
def visit_DayOfWeekName(self, op, *, arg):
865868
# day of week number is 0-indexed
866869
# Sunday == 0

ibis/backends/sql/compilers/bigquery.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ def visit_StringJoin(self, op, *, arg, sep):
218218
def visit_DayOfWeekIndex(self, op, *, arg):
219219
return self.f.mod(self.f.extract(self.v.dayofweek, arg) + 5, 7)
220220

221+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
222+
return self.f.mod(self.f.extract(self.v.dayofweek, arg) + 5, 7) + 1
223+
221224
def visit_DayOfWeekName(self, op, *, arg):
222225
return self.f.initcap(sge.Cast(this=arg, to="STRING FORMAT 'DAY'"))
223226

ibis/backends/sql/compilers/clickhouse.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,9 @@ def visit_DayOfWeekIndex(self, op, *, arg):
471471
weekdays = len(calendar.day_name)
472472
return (((self.f.toDayOfWeek(arg) - 1) % weekdays) + weekdays) % weekdays
473473

474+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
475+
return self.f.toDayOfWeek(arg)
476+
474477
def visit_DayOfWeekName(self, op, *, arg):
475478
# ClickHouse 20 doesn't support dateName
476479
#

ibis/backends/sql/compilers/datafusion.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ def visit_ExtractDayOfYear(self, op, *, arg):
247247
def visit_DayOfWeekIndex(self, op, *, arg):
248248
return (self.f.date_part("dow", arg) + 6) % 7
249249

250+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
251+
return ((self.f.date_part("dow", arg) + 6) % 7) + 1
252+
250253
def visit_DayOfWeekName(self, op, *, arg):
251254
return sg.exp.Case(
252255
this=sge.paren(self.f.date_part("dow", arg) + 6, copy=False) % 7,

ibis/backends/sql/compilers/exasol.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ class ExasolCompiler(SQLGlotCompiler):
5151
ops.DateAdd,
5252
ops.DateSub,
5353
ops.DateFromYMD,
54-
ops.DayOfWeekIndex,
5554
ops.ElementWiseVectorizedUDF,
5655
ops.IntervalFromInteger,
5756
ops.IsInf,
@@ -200,6 +199,12 @@ def visit_ExtractDayOfYear(self, op, *, arg):
200199
def visit_ExtractWeekOfYear(self, op, *, arg):
201200
return self.cast(self.f.to_char(arg, "IW"), op.dtype)
202201

202+
def visit_DayOfWeekIndex(self, op, *, arg):
203+
return self.cast(self.f.to_char(arg, "ID"), op.dtype) - 1
204+
205+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
206+
return self.cast(self.f.to_char(arg, "ID"), op.dtype)
207+
203208
def visit_ExtractIsoYear(self, op, *, arg):
204209
return self.cast(self.f.to_char(arg, "IYYY"), op.dtype)
205210

ibis/backends/sql/compilers/flink.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,9 @@ def visit_ExtractMicrosecond(self, op, *, arg):
441441
def visit_DayOfWeekIndex(self, op, *, arg):
442442
return (self.f.dayofweek(arg) + 5) % 7
443443

444+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
445+
return ((self.f.dayofweek(arg) + 5) % 7) + 1
446+
444447
def visit_DayOfWeekName(self, op, *, arg):
445448
index = self.cast(self.f.dayofweek(self.cast(arg, dt.date)), op.dtype)
446449
lookup_table = self.f.str_to_map(

ibis/backends/sql/compilers/impala.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ def visit_RandomScalar(self, op, **_):
137137
def visit_DayOfWeekIndex(self, op, *, arg):
138138
return self.f.pmod(self.f.dayofweek(arg) - 2, 7)
139139

140+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
141+
return self.f.pmod(self.f.dayofweek(arg) - 2, 7) + 1
142+
140143
def visit_ExtractMillisecond(self, op, *, arg):
141144
return self.f.extract(self.v.millisecond, arg) % 1_000
142145

ibis/backends/sql/compilers/mssql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ def visit_CountDistinct(self, op, *, arg, where):
203203
def visit_DayOfWeekIndex(self, op, *, arg):
204204
return self.f.datepart(self.v.weekday, arg) - 1
205205

206+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
207+
return self.f.datepart(self.v.weekday, arg)
208+
206209
def visit_DayOfWeekName(self, op, *, arg):
207210
days = calendar.day_name
208211
return sge.Case(

ibis/backends/sql/compilers/mysql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ def visit_GroupConcat(self, op, *, arg, sep, where):
177177
def visit_DayOfWeekIndex(self, op, *, arg):
178178
return (self.f.dayofweek(arg) + 5) % 7
179179

180+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
181+
return ((self.f.dayofweek(arg) + 5) % 7) + 1
182+
180183
def visit_Literal(self, op, *, value, dtype):
181184
# avoid casting NULL: the set of types allowed by MySQL and
182185
# MariaDB when casting is a strict subset of allowed types in other

ibis/backends/sql/compilers/postgres.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,9 @@ def visit_TimestampBucket(self, op, *, arg, interval, offset):
490490
def visit_DayOfWeekIndex(self, op, *, arg):
491491
return self.cast(self.f.extract("dow", arg) + 6, dt.int16) % 7
492492

493+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
494+
return self.cast(self.f.extract("isodow", arg), dt.int16)
495+
493496
def visit_DayOfWeekName(self, op, *, arg):
494497
return self.f.trim(self.f.to_char(arg, "Day"), string.whitespace)
495498

ibis/backends/sql/compilers/pyspark.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ def visit_IntervalFromInteger(self, op, *, arg, unit):
164164
def visit_DayOfWeekIndex(self, op, *, arg):
165165
return (self.f.dayofweek(arg) + 5) % 7
166166

167+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
168+
return ((self.f.dayofweek(arg) + 5) % 7) + 1
169+
167170
def visit_DayOfWeekName(self, op, *, arg):
168171
return sge.Case(
169172
this=(self.f.dayofweek(arg) + 5) % 7,

ibis/backends/sql/compilers/sqlite.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,16 @@ def visit_DayOfWeekIndex(self, op, *, arg):
414414
self.f.mod(self.cast(self.f.strftime("%w", arg) + 6, dt.int64), 7), dt.int64
415415
)
416416

417+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
418+
# return self.cast(self.f.strftime("%u", arg), dt.int64)
419+
return (
420+
self.cast(
421+
self.f.mod(self.cast(self.f.strftime("%w", arg) + 6, dt.int64), 7),
422+
dt.int64,
423+
)
424+
+ 1
425+
)
426+
417427
def visit_DayOfWeekName(self, op, *, arg):
418428
return sge.Case(
419429
this=self.f.strftime("%w", arg),

ibis/backends/sql/compilers/trino.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ def visit_DayOfWeekIndex(self, op, *, arg):
209209
sge.paren(self.f.day_of_week(arg) + 6, copy=False) % 7, op.dtype
210210
)
211211

212+
def visit_IsoDayOfWeekIndex(self, op, *, arg):
213+
return self.cast(sge.paren(self.f.day_of_week(arg), copy=False), op.dtype)
214+
212215
def visit_DayOfWeekName(self, op, *, arg):
213216
return self.f.date_format(arg, "%W")
214217

ibis/backends/tests/test_temporal.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def test_iso_year_does_not_match_date_year(con):
164164
id="day_of_week_index",
165165
marks=[
166166
pytest.mark.notimpl(
167-
["druid", "oracle", "exasol"], raises=com.OperationNotDefinedError
167+
["druid", "oracle"], raises=com.OperationNotDefinedError
168168
),
169169
],
170170
),
@@ -1561,29 +1561,34 @@ def test_string_to_date(alltypes, fmt):
15611561

15621562

15631563
@pytest.mark.parametrize(
1564-
("date", "expected_index", "expected_day"),
1564+
("date", "expected_index", "expected_iso_index", "expected_day"),
15651565
[
1566-
param("2017-01-01", 6, "Sunday", id="sunday"),
1567-
param("2017-01-02", 0, "Monday", id="monday"),
1568-
param("2017-01-03", 1, "Tuesday", id="tuesday"),
1569-
param("2017-01-04", 2, "Wednesday", id="wednesday"),
1570-
param("2017-01-05", 3, "Thursday", id="thursday"),
1571-
param("2017-01-06", 4, "Friday", id="friday"),
1572-
param("2017-01-07", 5, "Saturday", id="saturday"),
1566+
param("2017-01-01", 6, 7, "Sunday", id="sunday"),
1567+
param("2017-01-02", 0, 1, "Monday", id="monday"),
1568+
param("2017-01-03", 1, 2, "Tuesday", id="tuesday"),
1569+
param("2017-01-04", 2, 3, "Wednesday", id="wednesday"),
1570+
param("2017-01-05", 3, 4, "Thursday", id="thursday"),
1571+
param("2017-01-06", 4, 5, "Friday", id="friday"),
1572+
param("2017-01-07", 5, 6, "Saturday", id="saturday"),
15731573
],
15741574
)
15751575
@pytest.mark.notimpl(["druid", "oracle"], raises=com.OperationNotDefinedError)
1576-
@pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
1576+
# @pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
15771577
@pytest.mark.broken(
15781578
["risingwave"],
15791579
raises=AssertionError,
15801580
reason="Refer to https://github.com/risingwavelabs/risingwave/issues/14670",
15811581
)
1582-
def test_day_of_week_scalar(con, date, expected_index, expected_day):
1582+
def test_day_of_week_scalar(
1583+
con, date, expected_index, expected_iso_index, expected_day
1584+
):
15831585
expr = ibis.literal(date).cast(dt.date)
15841586
result_index = con.execute(expr.day_of_week.index().name("tmp"))
15851587
assert result_index == expected_index
15861588

1589+
result_iso_index = con.execute(expr.day_of_week.iso_index().name("tmp"))
1590+
assert result_iso_index == expected_iso_index
1591+
15871592
result_day = con.execute(expr.day_of_week.full_name().name("tmp"))
15881593
assert result_day.lower() == expected_day.lower()
15891594

@@ -1594,7 +1599,7 @@ def test_day_of_week_scalar(con, date, expected_index, expected_day):
15941599
raises=AttributeError,
15951600
reason="StringColumn' object has no attribute 'day_of_week'",
15961601
)
1597-
@pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
1602+
# @pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
15981603
@pytest.mark.broken(
15991604
["risingwave"],
16001605
raises=AssertionError,
@@ -1608,6 +1613,11 @@ def test_day_of_week_column(backend, alltypes, df):
16081613

16091614
backend.assert_series_equal(result_index, expected_index, check_names=False)
16101615

1616+
result_iso_index = expr.iso_index().name("tmp").execute()
1617+
expected_iso_index = df.timestamp_col.dt.isocalendar().day.astype("int16")
1618+
1619+
backend.assert_series_equal(result_iso_index, expected_iso_index, check_names=False)
1620+
16111621
result_day = expr.full_name().name("tmp").execute()
16121622
expected_day = df.timestamp_col.dt.day_name()
16131623

@@ -1621,9 +1631,6 @@ def test_day_of_week_column(backend, alltypes, df):
16211631
lambda t: t.timestamp_col.day_of_week.index().count(),
16221632
lambda s: s.dt.dayofweek.count(),
16231633
id="day_of_week_index",
1624-
marks=[
1625-
pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError)
1626-
],
16271634
),
16281635
param(
16291636
lambda t: t.timestamp_col.day_of_week.full_name().length().sum(),

ibis/expr/operations/temporal.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ class DayOfWeekIndex(Unary):
190190
dtype = dt.int16
191191

192192

193+
@public
194+
class IsoDayOfWeekIndex(Unary):
195+
arg: Value[dt.Date | dt.Timestamp]
196+
197+
dtype = dt.int16
198+
199+
193200
@public
194201
class DayOfWeekName(Unary):
195202
"""Extract the name of the day of the week from a date or timestamp."""
@@ -199,6 +206,13 @@ class DayOfWeekName(Unary):
199206
dtype = dt.string
200207

201208

209+
@public
210+
class IsoDayOfWeekName(Unary):
211+
arg: Value[dt.Date | dt.Timestamp]
212+
213+
dtype = dt.string
214+
215+
202216
@public
203217
class Time(Unary):
204218
"""Extract the time from a timestamp."""

ibis/expr/types/temporal.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,34 @@ def day_of_week(self) -> DayOfWeek:
5252
Returns
5353
-------
5454
DayOfWeek
55-
An namespace expression containing methods to use to extract
55+
A namespace expression with methods for extracting day-of-week
5656
information.
57+
58+
Examples
59+
--------
60+
>>> import ibis
61+
>>> import datetime as dt
62+
>>> from ibis import _
63+
>>> ibis.options.interactive = True
64+
>>> t = ibis.memtable({"date": [dt.datetime(2024, 4, x) for x in range(14, 21)]})
65+
>>> t.mutate(
66+
... day_of_week=_.date.day_of_week.index(),
67+
... day_of_week_name=_.date.day_of_week.full_name(),
68+
... iso_day_of_week=_.date.day_of_week.iso_index(),
69+
... )
70+
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
71+
┃ date ┃ day_of_week ┃ day_of_week_name ┃ iso_day_of_week ┃
72+
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
73+
│ timestamp │ int16 │ string │ int16 │
74+
├─────────────────────┼─────────────┼──────────────────┼─────────────────┤
75+
│ 2024-04-14 00:00:00 │ 6 │ Sunday │ 7 │
76+
│ 2024-04-15 00:00:00 │ 0 │ Monday │ 1 │
77+
│ 2024-04-16 00:00:00 │ 1 │ Tuesday │ 2 │
78+
│ 2024-04-17 00:00:00 │ 2 │ Wednesday │ 3 │
79+
│ 2024-04-18 00:00:00 │ 3 │ Thursday │ 4 │
80+
│ 2024-04-19 00:00:00 │ 4 │ Friday │ 5 │
81+
│ 2024-04-20 00:00:00 │ 5 │ Saturday │ 6 │
82+
└─────────────────────┴─────────────┴──────────────────┴─────────────────┘
5783
"""
5884
return DayOfWeek(self)
5985

@@ -994,3 +1020,13 @@ def full_name(self):
9941020
The name of the day of the week
9951021
"""
9961022
return ops.DayOfWeekName(self._expr).to_expr()
1023+
1024+
def iso_index(self):
1025+
"""Get the index of the day of the week in iso-format (1=Monday, 7=Sunday).
1026+
1027+
Returns
1028+
-------
1029+
IntegerValue
1030+
The index of the day of the week in iso-format (1=Monday, 7=Sunday).
1031+
"""
1032+
return ops.IsoDayOfWeekIndex(self._expr).to_expr()

0 commit comments

Comments
 (0)