diff --git a/lib/explorer/backend/lazy_series.ex b/lib/explorer/backend/lazy_series.ex index 688474c09..26ee1080f 100644 --- a/lib/explorer/backend/lazy_series.ex +++ b/lib/explorer/backend/lazy_series.ex @@ -151,14 +151,20 @@ defmodule Explorer.Backend.LazySeries do floor: 1, ceil: 1, # Date functions - day_of_week: 1, + year: 1, + month: 1, + day_of_month: 1, + is_leap_year: 1, + quarter_of_year: 1, + days_of_month: 1, day_of_year: 1, + iso_year: 1, week_of_year: 1, - month: 1, - year: 1, + day_of_week: 1, hour: 1, minute: 1, second: 1, + nanosecond: 1, # List functions join: 2, lengths: 1, @@ -685,8 +691,36 @@ defmodule Explorer.Backend.LazySeries do end @impl true - def day_of_week(%Series{} = s) do - data = new(:day_of_week, [lazy_series!(s)], {:s, 8}) + def year(%Series{} = s) do + data = new(:year, [lazy_series!(s)], {:s, 32}) + + Backend.Series.new(data, {:s, 32}) + end + + @impl true + def month(%Series{} = s) do + data = new(:month, [lazy_series!(s)], {:s, 8}) + + Backend.Series.new(data, {:s, 8}) + end + + @impl true + def day_of_month(%Series{} = s) do + data = new(:day_of_month, [lazy_series!(s)], {:s, 8}) + + Backend.Series.new(data, {:s, 8}) + end + + @impl true + def is_leap_year(%Series{} = s) do + data = new(:is_leap_year, [lazy_series!(s)], :boolean) + + Backend.Series.new(data, :boolean) + end + + @impl true + def quarter_of_year(%Series{} = s) do + data = new(:quarter_of_year, [lazy_series!(s)], {:s, 8}) Backend.Series.new(data, {:s, 8}) end @@ -699,24 +733,24 @@ defmodule Explorer.Backend.LazySeries do end @impl true - def week_of_year(%Series{} = s) do - data = new(:week_of_year, [lazy_series!(s)], {:s, 8}) + def iso_year(%Series{} = s) do + data = new(:iso_year, [lazy_series!(s)], {:s, 32}) - Backend.Series.new(data, {:s, 8}) + Backend.Series.new(data, {:s, 32}) end @impl true - def month(%Series{} = s) do - data = new(:month, [lazy_series!(s)], {:s, 8}) + def week_of_year(%Series{} = s) do + data = new(:week_of_year, [lazy_series!(s)], {:s, 8}) Backend.Series.new(data, {:s, 8}) end @impl true - def year(%Series{} = s) do - data = new(:year, [lazy_series!(s)], {:s, 32}) + def day_of_week(%Series{} = s) do + data = new(:day_of_week, [lazy_series!(s)], {:s, 8}) - Backend.Series.new(data, {:s, 32}) + Backend.Series.new(data, {:s, 8}) end @impl true @@ -740,6 +774,13 @@ defmodule Explorer.Backend.LazySeries do Backend.Series.new(data, {:s, 8}) end + @impl true + def nanosecond(%Series{} = s) do + data = new(:nanosecond, [lazy_series!(s)], {:s, 32}) + + Backend.Series.new(data, {:s, 32}) + end + @impl true def ewm_mean(%Series{} = series, alpha, adjust, min_periods, ignore_nils) do args = [lazy_series!(series), alpha, adjust, min_periods, ignore_nils] diff --git a/lib/explorer/backend/series.ex b/lib/explorer/backend/series.ex index 8fa08363b..4871c43bd 100644 --- a/lib/explorer/backend/series.ex +++ b/lib/explorer/backend/series.ex @@ -321,14 +321,19 @@ defmodule Explorer.Backend.Series do # Date / DateTime - @callback day_of_week(s) :: s + @callback year(s) :: s + @callback month(s) :: s + @callback day_of_month(s) :: s + @callback is_leap_year(s) :: s + @callback quarter_of_year(s) :: s @callback day_of_year(s) :: s + @callback iso_year(s) :: s @callback week_of_year(s) :: s - @callback month(s) :: s - @callback year(s) :: s + @callback day_of_week(s) :: s @callback hour(s) :: s @callback minute(s) :: s @callback second(s) :: s + @callback nanosecond(s) :: s # List @callback join(s, String.t()) :: s diff --git a/lib/explorer/polars_backend/expression.ex b/lib/explorer/polars_backend/expression.ex index 2442c793a..93092fe63 100644 --- a/lib/explorer/polars_backend/expression.ex +++ b/lib/explorer/polars_backend/expression.ex @@ -25,14 +25,20 @@ defmodule Explorer.PolarsBackend.Expression do coalesce: 2, count: 1, size: 1, - day_of_week: 1, + year: 1, + month: 1, + day_of_month: 1, + is_leap_year: 1, + quarter_of_year: 1, + days_of_month: 1, day_of_year: 1, + iso_year: 1, week_of_year: 1, - month: 1, - year: 1, + day_of_week: 1, hour: 1, minute: 1, second: 1, + nanosecond: 1, distinct: 1, equal: 2, exp: 1, diff --git a/lib/explorer/polars_backend/native.ex b/lib/explorer/polars_backend/native.ex index 749825bda..26de3e44d 100644 --- a/lib/explorer/polars_backend/native.ex +++ b/lib/explorer/polars_backend/native.ex @@ -495,14 +495,19 @@ defmodule Explorer.PolarsBackend.Native do def s_ewm_standard_deviation(_s, _alpha, _adjust, _bias, _min_periods, _ignore_nils), do: err() def s_ewm_variance(_s, _alpha, _adjust, _bias, _min_periods, _ignore_nils), do: err() def s_in(_s, _other), do: err() - def s_day_of_week(_s), do: err() + def s_year(_s), do: err() + def s_month(_s), do: err() + def s_day_of_month(_s), do: err() + def s_is_leap_year(_s), do: err() + def s_quarter_of_year(_s), do: err() def s_day_of_year(_s), do: err() + def s_iso_year(_s), do: err() def s_week_of_year(_s), do: err() - def s_month(_s), do: err() - def s_year(_s), do: err() + def s_day_of_week(_s), do: err() def s_hour(_s), do: err() def s_minute(_s), do: err() def s_second(_s), do: err() + def s_nanosecond(_s), do: err() def s_sin(_s), do: err() def s_cos(_s), do: err() def s_tan(_s), do: err() diff --git a/lib/explorer/polars_backend/series.ex b/lib/explorer/polars_backend/series.ex index 274718056..72fd769c8 100644 --- a/lib/explorer/polars_backend/series.ex +++ b/lib/explorer/polars_backend/series.ex @@ -755,24 +755,40 @@ defmodule Explorer.PolarsBackend.Series do # Date / DateTime @impl true - def day_of_week(series), - do: Shared.apply_series(series, :s_day_of_week) + def year(series), + do: Shared.apply_series(series, :s_year) + + @impl true + def month(series), + do: Shared.apply_series(series, :s_month) + + @impl true + def day_of_month(series), + do: Shared.apply_series(series, :s_day_of_month) + + @impl true + def is_leap_year(series), + do: Shared.apply_series(series, :s_is_leap_year) + + @impl true + def quarter_of_year(series), + do: Shared.apply_series(series, :s_quarter_of_year) @impl true def day_of_year(series), do: Shared.apply_series(series, :s_day_of_year) @impl true - def week_of_year(series), - do: Shared.apply_series(series, :s_week_of_year) + def iso_year(series), + do: Shared.apply_series(series, :s_iso_year) @impl true - def month(series), - do: Shared.apply_series(series, :s_month) + def week_of_year(series), + do: Shared.apply_series(series, :s_week_of_year) @impl true - def year(series), - do: Shared.apply_series(series, :s_year) + def day_of_week(series), + do: Shared.apply_series(series, :s_day_of_week) @impl true def hour(series), @@ -786,6 +802,10 @@ defmodule Explorer.PolarsBackend.Series do def second(series), do: Shared.apply_series(series, :s_second) + @impl true + def nanosecond(series), + do: Shared.apply_series(series, :s_nanosecond) + # Lists @impl true diff --git a/lib/explorer/series.ex b/lib/explorer/series.ex index 15ff5c404..70aec204b 100644 --- a/lib/explorer/series.ex +++ b/lib/explorer/series.ex @@ -6403,35 +6403,6 @@ defmodule Explorer.Series do # Date / DateTime - @doc """ - Returns the month number starting from 1. The return value ranges from 1 to 12. - - ## Examples - - iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2023-02-16], ~D[2023-03-20], nil]) - iex> Explorer.Series.month(s) - #Explorer.Series< - Polars[4] - s8 [1, 2, 3, nil] - > - - It can also be called on a datetime series. - - iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2023-02-16 23:59:59.999999], ~N[2023-03-20 12:00:00], nil]) - iex> Explorer.Series.month(s) - #Explorer.Series< - Polars[4] - s8 [1, 2, 3, nil] - > - """ - @doc type: :datetime_wise - @spec month(Series.t()) :: Series.t() - def month(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), - do: apply_series(series, :month) - - def month(%Series{dtype: dtype}), - do: super_dtype_error("month/1", dtype, [:date, :datetime, :naive_datetime]) - @doc """ Returns the year number in the calendar date. @@ -6462,94 +6433,122 @@ defmodule Explorer.Series do do: super_dtype_error("year/1", dtype, [:date, :datetime, :naive_datetime]) @doc """ - Returns the hour number from 0 to 23. + Returns the month number starting from 1. The return value ranges from 1 to 12. ## Examples - iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) - iex> Explorer.Series.hour(s) + iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2023-02-16], ~D[2023-03-20], nil]) + iex> Explorer.Series.month(s) #Explorer.Series< Polars[4] - s8 [0, 23, 12, nil] + s8 [1, 2, 3, nil] + > + + It can also be called on a datetime series. + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2023-02-16 23:59:59.999999], ~N[2023-03-20 12:00:00], nil]) + iex> Explorer.Series.month(s) + #Explorer.Series< + Polars[4] + s8 [1, 2, 3, nil] > """ @doc type: :datetime_wise - @spec hour(Series.t()) :: Series.t() - def hour(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), - do: apply_series(series, :hour) + @spec month(Series.t()) :: Series.t() + def month(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), + do: apply_series(series, :month) - def hour(%Series{dtype: dtype}), - do: super_dtype_error("hour/1", dtype, [:time, :datetime, :naive_datetime]) + def month(%Series{dtype: dtype}), + do: super_dtype_error("month/1", dtype, [:date, :datetime, :naive_datetime]) @doc """ - Returns the minute number from 0 to 59. + Returns the day of the month as a number starting from 1. The return value ranges from 1 to 31. ## Examples - iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) - iex> Explorer.Series.minute(s) + iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2023-02-16], ~D[2023-03-20], nil]) + iex> Explorer.Series.day_of_month(s) #Explorer.Series< Polars[4] - s8 [0, 59, 0, nil] + s8 [15, 16, 20, nil] + > + + It can also be called on a datetime series. + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2023-02-16 23:59:59.999999], ~N[2023-03-20 12:00:00], nil]) + iex> Explorer.Series.day_of_month(s) + #Explorer.Series< + Polars[4] + s8 [15, 16, 20, nil] > """ @doc type: :datetime_wise - @spec minute(Series.t()) :: Series.t() - def minute(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), - do: apply_series(series, :minute) + @spec day_of_month(Series.t()) :: Series.t() + def day_of_month(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), + do: apply_series(series, :day_of_month) - def minute(%Series{dtype: dtype}), - do: super_dtype_error("minute/1", dtype, [:time, :datetime, :naive_datetime]) + def day_of_month(%Series{dtype: dtype}), + do: super_dtype_error("day_of_month/1", dtype, [:date, :datetime, :naive_datetime]) @doc """ - Returns the second number from 0 to 59. + Returns a boolean for whether the current calendar year is a leap year. ## Examples - iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) - iex> Explorer.Series.second(s) + iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2020-03-20], nil]) + iex> Explorer.Series.is_leap_year(s) #Explorer.Series< - Polars[4] - s8 [0, 59, 0, nil] + Polars[3] + boolean [false, true, nil] + > + + It can also be called on a datetime series. + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2020-03-20 12:00:00], nil]) + iex> Explorer.Series.is_leap_year(s) + #Explorer.Series< + Polars[3] + boolean [false, true, nil] > """ @doc type: :datetime_wise - @spec second(Series.t()) :: Series.t() - def second(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), - do: apply_series(series, :second) + @spec is_leap_year(Series.t()) :: Series.t() + def is_leap_year(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), + do: apply_series(series, :is_leap_year) - def second(%Series{dtype: dtype}), - do: super_dtype_error("minute/1", dtype, [:time, :datetime, :naive_datetime]) + def is_leap_year(%Series{dtype: dtype}), + do: super_dtype_error("is_leap_year/1", dtype, [:date, :datetime, :naive_datetime]) @doc """ - Returns a day-of-week number starting from Monday = 1. (ISO 8601 weekday number) + Returns the quarter of the calendar year, as a number between 1 and 4. + + Quarter 1 is the months Jan, Feb, Mar. Quarter 2 is Apr, May, Jun. Quarter 3 is Jul, Aug, Sep. Quarter 4 is Oct, Nov, Dec. ## Examples - iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2023-01-16], ~D[2023-01-20], nil]) - iex> Explorer.Series.day_of_week(s) + iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2021-04-20], nil]) + iex> Explorer.Series.quarter_of_year(s) #Explorer.Series< - Polars[4] - s8 [7, 1, 5, nil] + Polars[3] + s8 [1, 2, nil] > It can also be called on a datetime series. - iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2023-01-16 23:59:59.999999], ~N[2023-01-20 12:00:00], nil]) - iex> Explorer.Series.day_of_week(s) + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2021-04-20 12:00:00], nil]) + iex> Explorer.Series.quarter_of_year(s) #Explorer.Series< - Polars[4] - s8 [7, 1, 5, nil] + Polars[3] + s8 [1, 2, nil] > """ - @doc type: :datetime_wise - @spec day_of_week(Series.t()) :: Series.t() - def day_of_week(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), - do: apply_series(series, :day_of_week) + @spec quarter_of_year(Series.t()) :: Series.t() + def quarter_of_year(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), + do: apply_series(series, :quarter_of_year) - def day_of_week(%Series{dtype: dtype}), - do: super_dtype_error("day_of_week/1", dtype, [:date, :datetime, :naive_datetime]) + def quarter_of_year(%Series{dtype: dtype}), + do: super_dtype_error("quarter_of_year/1", dtype, [:date, :datetime, :naive_datetime]) @doc """ Returns the day-of-year number starting from 1. @@ -6584,6 +6583,35 @@ defmodule Explorer.Series do def day_of_year(%Series{dtype: dtype}), do: super_dtype_error("day_of_year/1", dtype, [:date, :datetime, :naive_datetime]) + @doc """ + Returns the year number according to the ISO week calendar, which may not match the calendar year. + + ## Examples + + iex> s = Explorer.Series.from_list([~D[2023-01-01], ~D[2021-03-20], nil]) + iex> Explorer.Series.iso_year(s) + #Explorer.Series< + Polars[3] + s32 [2022, 2021, nil] + > + + It can also be called on a datetime series. + + iex> s = Explorer.Series.from_list([~N[2023-01-01 00:00:00], ~N[2021-03-20 12:00:00], nil]) + iex> Explorer.Series.iso_year(s) + #Explorer.Series< + Polars[3] + s32 [2022, 2021, nil] + > + """ + @doc type: :datetime_wise + @spec iso_year(Series.t()) :: Series.t() + def iso_year(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), + do: apply_series(series, :iso_year) + + def iso_year(%Series{dtype: dtype}), + do: super_dtype_error("iso_year/1", dtype, [:date, :datetime, :naive_datetime]) + @doc """ Returns the week-of-year number. @@ -6620,6 +6648,116 @@ defmodule Explorer.Series do def week_of_year(%Series{dtype: dtype}), do: super_dtype_error("week_of_year/1", dtype, [:date, :datetime, :naive_datetime]) + @doc """ + Returns a day-of-week number starting from Monday = 1. (ISO 8601 weekday number) + + ## Examples + + iex> s = Explorer.Series.from_list([~D[2023-01-15], ~D[2023-01-16], ~D[2023-01-20], nil]) + iex> Explorer.Series.day_of_week(s) + #Explorer.Series< + Polars[4] + s8 [7, 1, 5, nil] + > + + It can also be called on a datetime series. + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2023-01-16 23:59:59.999999], ~N[2023-01-20 12:00:00], nil]) + iex> Explorer.Series.day_of_week(s) + #Explorer.Series< + Polars[4] + s8 [7, 1, 5, nil] + > + """ + + @doc type: :datetime_wise + @spec day_of_week(Series.t()) :: Series.t() + def day_of_week(%Series{dtype: dtype} = series) when is_date_like_dtype(dtype), + do: apply_series(series, :day_of_week) + + def day_of_week(%Series{dtype: dtype}), + do: super_dtype_error("day_of_week/1", dtype, [:date, :datetime, :naive_datetime]) + + @doc """ + Returns the hour number from 0 to 23. + + ## Examples + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) + iex> Explorer.Series.hour(s) + #Explorer.Series< + Polars[4] + s8 [0, 23, 12, nil] + > + """ + @doc type: :datetime_wise + @spec hour(Series.t()) :: Series.t() + def hour(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), + do: apply_series(series, :hour) + + def hour(%Series{dtype: dtype}), + do: super_dtype_error("hour/1", dtype, [:time, :datetime, :naive_datetime]) + + @doc """ + Returns the minute number from 0 to 59. + + ## Examples + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) + iex> Explorer.Series.minute(s) + #Explorer.Series< + Polars[4] + s8 [0, 59, 0, nil] + > + """ + @doc type: :datetime_wise + @spec minute(Series.t()) :: Series.t() + def minute(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), + do: apply_series(series, :minute) + + def minute(%Series{dtype: dtype}), + do: super_dtype_error("minute/1", dtype, [:time, :datetime, :naive_datetime]) + + @doc """ + Returns the second number from 0 to 59. + + ## Examples + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) + iex> Explorer.Series.second(s) + #Explorer.Series< + Polars[4] + s8 [0, 59, 0, nil] + > + """ + @doc type: :datetime_wise + @spec second(Series.t()) :: Series.t() + def second(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), + do: apply_series(series, :second) + + def second(%Series{dtype: dtype}), + do: super_dtype_error("second/1", dtype, [:time, :datetime, :naive_datetime]) + + @doc """ + Returns the nanosecond number from 0 to 999999999. + + ## Examples + + iex> s = Explorer.Series.from_list([~N[2023-01-15 00:00:00], ~N[2022-02-16 23:59:59.999999], ~N[2021-03-20 12:00:00], nil]) + iex> Explorer.Series.nanosecond(s) + #Explorer.Series< + Polars[4] + s32 [0, 999999000, 0, nil] + > + """ + @doc type: :datetime_wise + @spec nanosecond(Series.t()) :: Series.t() + def nanosecond(%Series{dtype: dtype} = series) when is_time_like_dtype(dtype), + do: apply_series(series, :nanosecond) + + def nanosecond(%Series{dtype: dtype}), + do: super_dtype_error("nanosecond/1", dtype, [:time, :datetime, :naive_datetime]) + @deprecated "Use cast(:date) instead" @doc false def to_date(%Series{dtype: dtype} = series) when is_datetime_like_dtype(dtype), diff --git a/native/explorer/src/expressions.rs b/native/explorer/src/expressions.rs index 9d185ca57..d230c7b14 100644 --- a/native/explorer/src/expressions.rs +++ b/native/explorer/src/expressions.rs @@ -1044,10 +1044,38 @@ pub fn expr_clip_float(expr: ExExpr, min: f64, max: f64) -> ExExpr { } #[rustler::nif] -pub fn expr_day_of_week(expr: ExExpr) -> ExExpr { +pub fn expr_year(expr: ExExpr) -> ExExpr { let expr = expr.clone_inner(); - ExExpr::new(expr.dt().weekday()) + ExExpr::new(expr.dt().year()) +} + +#[rustler::nif] +pub fn expr_month(expr: ExExpr) -> ExExpr { + let expr = expr.clone_inner(); + + ExExpr::new(expr.dt().month()) +} + +#[rustler::nif] +pub fn expr_day_of_month(expr: ExExpr) -> ExExpr { + let expr = expr.clone_inner(); + + ExExpr::new(expr.dt().day()) +} + +#[rustler::nif] +pub fn expr_is_leap_year(expr: ExExpr) -> ExExpr { + let expr = expr.clone_inner(); + + ExExpr::new(expr.dt().is_leap_year()) +} + +#[rustler::nif] +pub fn expr_quarter_of_year(expr: ExExpr) -> ExExpr { + let expr = expr.clone_inner(); + + ExExpr::new(expr.dt().quarter()) } #[rustler::nif] @@ -1058,24 +1086,24 @@ pub fn expr_day_of_year(expr: ExExpr) -> ExExpr { } #[rustler::nif] -pub fn expr_week_of_year(expr: ExExpr) -> ExExpr { +pub fn expr_iso_year(expr: ExExpr) -> ExExpr { let expr = expr.clone_inner(); - ExExpr::new(expr.dt().week()) + ExExpr::new(expr.dt().iso_year()) } #[rustler::nif] -pub fn expr_month(expr: ExExpr) -> ExExpr { +pub fn expr_week_of_year(expr: ExExpr) -> ExExpr { let expr = expr.clone_inner(); - ExExpr::new(expr.dt().month()) + ExExpr::new(expr.dt().week()) } #[rustler::nif] -pub fn expr_year(expr: ExExpr) -> ExExpr { +pub fn expr_day_of_week(expr: ExExpr) -> ExExpr { let expr = expr.clone_inner(); - ExExpr::new(expr.dt().year()) + ExExpr::new(expr.dt().weekday()) } #[rustler::nif] @@ -1099,6 +1127,13 @@ pub fn expr_second(expr: ExExpr) -> ExExpr { ExExpr::new(expr.dt().second()) } +#[rustler::nif] +pub fn expr_nanosecond(expr: ExExpr) -> ExExpr { + let expr = expr.clone_inner(); + + ExExpr::new(expr.dt().nanosecond()) +} + #[rustler::nif] pub fn expr_join(expr: ExExpr, sep: String) -> ExExpr { let expr = expr.clone_inner(); diff --git a/native/explorer/src/series.rs b/native/explorer/src/series.rs index 5a37d69b9..cbb69123a 100644 --- a/native/explorer/src/series.rs +++ b/native/explorer/src/series.rs @@ -1542,8 +1542,36 @@ pub fn s_abs(s: ExSeries) -> Result { } #[rustler::nif(schedule = "DirtyCpu")] -pub fn s_day_of_week(s: ExSeries) -> Result { - let s1 = s.weekday()?.into_series(); +pub fn s_year(s: ExSeries) -> Result { + let s1 = s.year()?.into_series(); + + Ok(ExSeries::new(s1)) +} + +#[rustler::nif(schedule = "DirtyCpu")] +pub fn s_month(s: ExSeries) -> Result { + let s1 = s.month()?.into_series(); + + Ok(ExSeries::new(s1)) +} + +#[rustler::nif(schedule = "DirtyCpu")] +pub fn s_day_of_month(s: ExSeries) -> Result { + let s1 = s.day()?.into_series(); + + Ok(ExSeries::new(s1)) +} + +#[rustler::nif(schedule = "DirtyCpu")] +pub fn s_is_leap_year(s: ExSeries) -> Result { + let s1 = s.is_leap_year()?.into_series(); + + Ok(ExSeries::new(s1)) +} + +#[rustler::nif(schedule = "DirtyCpu")] +pub fn s_quarter_of_year(s: ExSeries) -> Result { + let s1 = s.quarter()?.into_series(); Ok(ExSeries::new(s1)) } @@ -1556,22 +1584,22 @@ pub fn s_day_of_year(s: ExSeries) -> Result { } #[rustler::nif(schedule = "DirtyCpu")] -pub fn s_week_of_year(s: ExSeries) -> Result { - let s1 = s.week()?.into_series(); +pub fn s_iso_year(s: ExSeries) -> Result { + let s1 = s.iso_year()?.into_series(); Ok(ExSeries::new(s1)) } #[rustler::nif(schedule = "DirtyCpu")] -pub fn s_month(s: ExSeries) -> Result { - let s1 = s.month()?.into_series(); +pub fn s_week_of_year(s: ExSeries) -> Result { + let s1 = s.week()?.into_series(); Ok(ExSeries::new(s1)) } #[rustler::nif(schedule = "DirtyCpu")] -pub fn s_year(s: ExSeries) -> Result { - let s1 = s.year()?.into_series(); +pub fn s_day_of_week(s: ExSeries) -> Result { + let s1 = s.weekday()?.into_series(); Ok(ExSeries::new(s1)) } @@ -1597,6 +1625,13 @@ pub fn s_second(s: ExSeries) -> Result { Ok(ExSeries::new(s1)) } +#[rustler::nif(schedule = "DirtyCpu")] +pub fn s_nanosecond(s: ExSeries) -> Result { + let s1 = s.nanosecond()?.into_series(); + + Ok(ExSeries::new(s1)) +} + #[rustler::nif(schedule = "DirtyCpu")] pub fn s_strptime( s: ExSeries, diff --git a/test/explorer/data_frame_test.exs b/test/explorer/data_frame_test.exs index c15d90a65..a0c4393bf 100644 --- a/test/explorer/data_frame_test.exs +++ b/test/explorer/data_frame_test.exs @@ -2014,19 +2014,24 @@ defmodule Explorer.DataFrameTest do df1 = DF.mutate(df, - c: day_of_week(a), - d: day_of_week(b), + c: year(a), + d: year(b), e: month(a), f: month(b), - g: year(a), - h: year(b), - i: hour(b), - j: minute(b), - k: second(b), - l: day_of_year(a), - m: day_of_year(b), - n: week_of_year(a), - o: week_of_year(b) + g: day_of_month(a), + h: day_of_month(b), + i: is_leap_year(a), + j: is_leap_year(b), + k: day_of_year(a), + l: day_of_year(b), + m: week_of_year(a), + n: week_of_year(b), + o: day_of_week(a), + p: day_of_week(b), + q: hour(b), + r: minute(b), + s: second(b), + t: nanosecond(b) ) assert DF.to_columns(df1, atom_keys: true) == %{ @@ -2037,37 +2042,47 @@ defmodule Explorer.DataFrameTest do ~N[2021-03-20 03:03:03.003030], nil ], - c: [7, 3, 6, nil], - d: [7, 3, 6, nil], + c: [2023, 2022, 2021, nil], + d: [2023, 2022, 2021, nil], e: [1, 2, 3, nil], f: [1, 2, 3, nil], - g: [2023, 2022, 2021, nil], - h: [2023, 2022, 2021, nil], - i: [1, 2, 3, nil], - j: [1, 2, 3, nil], - k: [1, 2, 3, nil], + g: [15, 16, 20, nil], + h: [15, 16, 20, nil], + i: [false, false, false, nil], + j: [false, false, false, nil], + k: [15, 47, 79, nil], l: [15, 47, 79, nil], - m: [15, 47, 79, nil], + m: [2, 7, 11, nil], n: [2, 7, 11, nil], - o: [2, 7, 11, nil] + o: [7, 3, 6, nil], + p: [7, 3, 6, nil], + q: [1, 2, 3, nil], + r: [1, 2, 3, nil], + s: [1, 2, 3, nil], + t: [0, 0, 3_030_000, nil] } assert df1.dtypes == %{ "a" => :date, "b" => {:naive_datetime, :microsecond}, - "c" => {:s, 8}, - "d" => {:s, 8}, + "c" => {:s, 32}, + "d" => {:s, 32}, "e" => {:s, 8}, "f" => {:s, 8}, - "g" => {:s, 32}, - "h" => {:s, 32}, - "i" => {:s, 8}, - "j" => {:s, 8}, - "k" => {:s, 8}, + "g" => {:s, 8}, + "h" => {:s, 8}, + "i" => :boolean, + "j" => :boolean, + "k" => {:s, 16}, "l" => {:s, 16}, - "m" => {:s, 16}, + "m" => {:s, 8}, "n" => {:s, 8}, - "o" => {:s, 8} + "o" => {:s, 8}, + "p" => {:s, 8}, + "q" => {:s, 8}, + "r" => {:s, 8}, + "s" => {:s, 8}, + "t" => {:s, 32} } end diff --git a/test/explorer/series/datetime_test.exs b/test/explorer/series/datetime_test.exs index 1af31da31..166fa40f0 100644 --- a/test/explorer/series/datetime_test.exs +++ b/test/explorer/series/datetime_test.exs @@ -6,7 +6,13 @@ defmodule Explorer.Series.DateTimeTest do describe "ns" do setup do - # This is a dataframe with a single column called datetime with 3 values [~N[2023-04-19 16:14:35.474487],~N[2023-04-20 16:14:35.474487], ~N[2023-04-21 16:14:35.474487]] with datetime[ns] precession + # This is a dataframe with a single column called datetime with 4 values with datetime[ns] precision + # [ + # ~N[2023-04-20 16:14:35.474487], + # ~N[2023-04-19 16:14:35.474487], + # ~N[2023-04-21 16:14:35.474487], + # nil + # ] df = Explorer.DataFrame.from_parquet!("test/support/datetime_with_ns_res.parquet") series = Explorer.DataFrame.to_series(df) [series: series["datetime"]] @@ -24,28 +30,61 @@ defmodule Explorer.Series.DateTimeTest do assert Series.quantile(series, 0.5) == ~N[2023-04-20 16:14:35.474487] end - test "day_of_week", %{series: series} do - assert Series.day_of_week(series) |> Series.to_list() == [4, 3, 5] + test "year", %{series: series} do + assert Series.year(series) |> Series.to_list() == [2023, 2023, 2023, nil] end test "month", %{series: series} do - assert Series.month(series) |> Series.to_list() == [4, 4, 4] + assert Series.month(series) |> Series.to_list() == [4, 4, 4, nil] end - test "year", %{series: series} do - assert Series.year(series) |> Series.to_list() == [2023, 2023, 2023] + test "day_of_month", %{series: series} do + assert Series.day_of_month(series) |> Series.to_list() == [20, 19, 21, nil] + end + + test "is_leap_year", %{series: series} do + assert Series.is_leap_year(series) |> Series.to_list() == [false, false, false, nil] + end + + test "quarter_of_year", %{series: series} do + assert Series.quarter_of_year(series) |> Series.to_list() == [2, 2, 2, nil] + end + + test "day_of_year", %{series: series} do + assert Series.day_of_year(series) |> Series.to_list() == [110, 109, 111, nil] + end + + test "iso_year", %{series: series} do + assert Series.iso_year(series) |> Series.to_list() == [2023, 2023, 2023, nil] + end + + test "week_of_year", %{series: series} do + assert Series.week_of_year(series) |> Series.to_list() == [16, 16, 16, nil] + end + + test "day_of_week", %{series: series} do + assert Series.day_of_week(series) |> Series.to_list() == [4, 3, 5, nil] end test "hour", %{series: series} do - assert Series.hour(series) |> Series.to_list() == [16, 16, 16] + assert Series.hour(series) |> Series.to_list() == [16, 16, 16, nil] end test "minute", %{series: series} do - assert Series.minute(series) |> Series.to_list() == [14, 14, 14] + assert Series.minute(series) |> Series.to_list() == [14, 14, 14, nil] end test "second", %{series: series} do - assert Series.second(series) |> Series.to_list() == [35, 35, 35] + assert Series.second(series) |> Series.to_list() == [35, 35, 35, nil] + end + + test "nanosecond", %{series: series} do + assert Series.nanosecond(series) |> Series.to_list() == [ + 474_487_000, + 474_487_000, + 474_487_000, + nil + ] end end diff --git a/test/support/datetime_with_ns_res.parquet b/test/support/datetime_with_ns_res.parquet index 55a1a58c7..f636ff2da 100644 Binary files a/test/support/datetime_with_ns_res.parquet and b/test/support/datetime_with_ns_res.parquet differ