Skip to content

Commit

Permalink
Add strftime (#627)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-khong authored Jun 26, 2023
1 parent d0932d2 commit 6671ae3
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 4 deletions.
8 changes: 8 additions & 0 deletions lib/explorer/backend/lazy_series.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ defmodule Explorer.Backend.LazySeries do
select: 3,
abs: 1,
strptime: 2,
strftime: 2,

# Trigonometric functions
acos: 1,
Expand Down Expand Up @@ -689,6 +690,13 @@ defmodule Explorer.Backend.LazySeries do
Backend.Series.new(data, :datetime)
end

@impl true
def strftime(%Series{} = series, format_string) do
data = new(:strftime, [lazy_series!(series), format_string])

Backend.Series.new(data, :datetime)
end

@impl true
def sin(%Series{} = series) do
data = new(:sin, [lazy_series!(series)])
Expand Down
1 change: 1 addition & 0 deletions lib/explorer/backend/series.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule Explorer.Backend.Series do
@callback cast(s, dtype) :: s
@callback categorise(s, s) :: s
@callback strptime(s, String.t()) :: s
@callback strftime(s, String.t()) :: s

# Introspection

Expand Down
1 change: 1 addition & 0 deletions lib/explorer/polars_backend/expression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ defmodule Explorer.PolarsBackend.Expression do

# Conversions
strptime: 2,
strftime: 2,

# Strings
contains: 2,
Expand Down
1 change: 1 addition & 0 deletions lib/explorer/polars_backend/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ defmodule Explorer.PolarsBackend.Native do
def s_exp(_s), do: err()
def s_abs(_s), do: err()
def s_strptime(_s, _format_string), do: err()
def s_strftime(_s, _format_string), do: err()
def s_fill_missing_with_strategy(_s, _strategy), do: err()
def s_fill_missing_with_boolean(_s, _value), do: err()
def s_fill_missing_with_bin(_s, _value), do: err()
Expand Down
5 changes: 5 additions & 0 deletions lib/explorer/polars_backend/series.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ defmodule Explorer.PolarsBackend.Series do
Shared.apply_series(series, :s_strptime, [format_string])
end

@impl true
def strftime(%Series{} = series, format_string) do
Shared.apply_series(series, :s_strftime, [format_string])
end

# Introspection

@impl true
Expand Down
25 changes: 25 additions & 0 deletions lib/explorer/series.ex
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,31 @@ defmodule Explorer.Series do
def strptime(%Series{dtype: dtype}, _format_string),
do: dtype_error("strptime/2", dtype, [:string])

@doc """
Converts a datetime series to a string series.
For the format string specification, refer to the
[chrono crate documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
Use `cast(series, :string)` for the default `"%Y-%m-%d %H:%M:%S%.6f"` format.
## Examples
iex> s = Explorer.Series.from_list([~N[2023-01-05 12:34:56], nil])
iex> Explorer.Series.strftime(s, "%Y/%m/%d %H:%M:%S")
#Explorer.Series<
Polars[2]
string ["2023/01/05 12:34:56", nil]
>
"""
@doc type: :element_wise
@spec strftime(series :: Series.t(), format_string :: String.t()) :: Series.t()
def strftime(%Series{dtype: dtype} = series, format_string) when K.in(dtype, [:datetime]),
do: apply_series(series, :strftime, [format_string])

def strftime(%Series{dtype: dtype}, _format_string),
do: dtype_error("strftime/2", dtype, [:datetime])

# Introspection

@doc """
Expand Down
5 changes: 5 additions & 0 deletions native/explorer/src/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,11 @@ pub fn expr_strptime(expr: ExExpr, format_string: &str) -> ExExpr {
)
}

#[rustler::nif]
pub fn expr_strftime(expr: ExExpr, format_string: &str) -> ExExpr {
ExExpr::new(expr.clone_inner().dt().strftime(format_string))
}

#[rustler::nif]
pub fn expr_day_of_week(expr: ExExpr) -> ExExpr {
let expr = expr.clone_inner();
Expand Down
2 changes: 2 additions & 0 deletions native/explorer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ rustler::init!(
expr_minute,
expr_second,
expr_strptime,
expr_strftime,
expr_fill_missing_with_strategy,
expr_fill_missing_with_value,
expr_float,
Expand Down Expand Up @@ -289,6 +290,7 @@ rustler::init!(
s_minute,
s_second,
s_strptime,
s_strftime,
s_downcase,
s_cumulative_max,
s_cumulative_min,
Expand Down
6 changes: 6 additions & 0 deletions native/explorer/src/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,12 @@ pub fn s_strptime(s: ExSeries, format_string: &str) -> Result<ExSeries, Explorer
Ok(ExSeries::new(s1))
}

#[rustler::nif(schedule = "DirtyCpu")]
pub fn s_strftime(s: ExSeries, format_string: &str) -> Result<ExSeries, ExplorerError> {
let s1 = s.strftime(format_string)?;
Ok(ExSeries::new(s1))
}

#[rustler::nif(schedule = "DirtyCpu")]
pub fn s_sin(s: ExSeries) -> Result<ExSeries, ExplorerError> {
let s1 = s.f64()?.apply(|o| o.sin()).into();
Expand Down
19 changes: 17 additions & 2 deletions test/explorer/data_frame_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1407,11 +1407,13 @@ defmodule Explorer.DataFrameTest do
end
end

test "parse datetime from string" do
test "conversion between string and datetime" do
df =
DF.new(
a: ["2023-01-05 12:34:56", nil],
b: ["2023/30/01 00:11:22", "XYZ"]
b: ["2023/30/01 00:11:22", "XYZ"],
c: [~N[2023-01-05 12:34:56.000000], nil],
d: [~N[2023-01-30 00:11:22.000000], nil]
)

df1 =
Expand All @@ -1426,6 +1428,19 @@ defmodule Explorer.DataFrameTest do
c: [~N[2023-01-05 12:34:56.000000], nil],
d: [~N[2023-01-30 00:11:22.000000], nil]
}

df1 =
DF.mutate(df,
a: strftime(c, "%Y-%m-%d %H:%M:%S"),
b: strftime(d, "%Y/%d/%m %H:%M:%S")
)

assert DF.to_columns(df1, atom_keys: true) == %{
a: ["2023-01-05 12:34:56", nil],
b: ["2023/30/01 00:11:22", nil],
c: [~N[2023-01-05 12:34:56.000000], nil],
d: [~N[2023-01-30 00:11:22.000000], nil]
}
end

test "add columns with date and datetime operations" do
Expand Down
13 changes: 11 additions & 2 deletions test/explorer/series_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3834,23 +3834,32 @@ defmodule Explorer.SeriesTest do
end
end

describe "strptime/2" do
describe "strptime/2 and strftime/2" do
test "parse datetime from string" do
series = Series.from_list(["2023-01-05 12:34:56", "XYZ", nil])

assert Series.strptime(series, "%Y-%m-%d %H:%M:%S") |> Series.to_list() ==
[~N[2023-01-05 12:34:56.000000], nil, nil]
end

test "convert datetime to string" do
series = Series.from_list([~N[2023-01-05 12:34:56], nil])

assert Series.strftime(series, "%Y-%m-%d %H:%M:%S") |> Series.to_list() ==
["2023-01-05 12:34:56", nil]
end

test "ensure compatibility with chrono's format" do
for {dt, dt_str, format_string} <- [
{~N[2001-07-08 00:00:00.000000], "07/08/01", "%D"},
{~N[2000-11-03 00:00:00.000000], "11/03/00 % \t \n", "%D %% %t %n"},
{~N[1987-06-05 00:35:00.026000], "1987-06-05 00:34:60.026", "%F %X%.3f"},
{~N[1987-06-05 00:35:00.026000], "1987-06-05 00:35:00.026", "%F %X%.3f"},
{~N[1999-03-01 00:00:00.000000], "1999/3/1", "%Y/%-m/%-d"}
] do
series = Series.from_list([dt_str])
assert Series.strptime(series, format_string) |> Series.to_list() == [dt]
series = Series.from_list([dt])
assert Series.strftime(series, format_string) |> Series.to_list() == [dt_str]
end
end
end
Expand Down

0 comments on commit 6671ae3

Please sign in to comment.