lib/explorer/backend/series.ex

defmodule Explorer.Backend.Series do
  @moduledoc """
  The behaviour for series backends.
  """

  @type t :: struct()

  @type s :: Explorer.Series.t()
  @type df :: Explorer.DataFrame.t()
  @type dtype :: :integer | :float | :boolean | :string | :date | :datetime

  # Conversion

  @callback from_list(list(), dtype()) :: s
  @callback to_list(s) :: list()
  @callback to_enum(s) :: Enumerable.t()
  @callback cast(s, dtype) :: s

  # Introspection

  @callback dtype(s) :: dtype()
  @callback size(s) :: integer()
  @callback inspect(s, opts :: Inspect.Opts.t()) :: Inspect.Algebra.t()

  # Slice and dice

  @callback head(s, n :: integer()) :: s
  @callback tail(s, n :: integer()) :: s
  @callback sample(s, n :: integer(), replacement :: boolean(), seed :: integer()) :: s
  @callback take_every(s, integer()) :: s
  @callback filter(s, mask :: s) :: s
  @callback filter(s, function()) :: s
  @callback slice(s, offset :: integer(), length :: integer()) :: s
  @callback take(s, indices :: list()) :: s
  @callback get(s, idx :: integer()) :: s
  @callback concat(s, s) :: s
  @callback coalesce(s, s) :: s

  # Aggregation

  @callback sum(s) :: number()
  @callback min(s) :: number() | Date.t() | NaiveDateTime.t()
  @callback max(s) :: number() | Date.t() | NaiveDateTime.t()
  @callback mean(s) :: float()
  @callback median(s) :: float()
  @callback var(s) :: float()
  @callback std(s) :: float()
  @callback quantile(s, float()) :: number | Date.t() | NaiveDateTime.t()

  # Cumulative

  @callback cumulative_max(s, reverse? :: boolean()) :: s
  @callback cumulative_min(s, reverse? :: boolean()) :: s
  @callback cumulative_sum(s, reverse? :: boolean()) :: s

  # Local minima/maxima

  @callback peaks(s, :max | :min) :: s

  # Arithmetic

  @callback add(s, s | number()) :: s
  @callback subtract(s, s | number()) :: s
  @callback multiply(s, s | number()) :: s
  @callback divide(s, s | number()) :: s
  @callback pow(s, number()) :: s

  # Comparisons

  @callback eq(s, s | number()) :: s
  @callback neq(s, s | number()) :: s
  @callback gt(s, s | number()) :: s
  @callback gt_eq(s, s | number()) :: s
  @callback lt(s, s | number()) :: s
  @callback lt_eq(s, s | number()) :: s
  @callback all_equal?(s, s) :: boolean()

  @callback binary_and(s, s) :: s
  @callback binary_or(s, s) :: s

  # Coercion

  # Sort

  @callback sort(s, reverse? :: boolean()) :: s
  @callback argsort(s, reverse? :: boolean()) :: s
  @callback reverse(s) :: s

  # Distinct

  @callback distinct(s) :: s
  @callback unordered_distinct(s) :: s
  @callback n_distinct(s) :: integer()
  @callback count(s) :: df

  # Rolling

  @type window_option ::
          {:weights, [float()] | nil}
          | {:min_periods, integer() | nil}
          | {:center, boolean()}

  @callback window_sum(s, window_size :: integer(), [window_option()]) :: s
  @callback window_min(s, window_size :: integer(), [window_option()]) :: s
  @callback window_max(s, window_size :: integer(), [window_option()]) :: s
  @callback window_mean(s, window_size :: integer(), [window_option()]) :: s

  # Nulls

  @callback fill_missing(s, strategy :: :backward | :forward | :min | :max | :mean) :: s
  @callback nil?(s) :: s
  @callback not_nil?(s) :: s

  # Escape hatch

  @callback transform(s, fun) :: s | list()

  # Functions

  import Inspect.Algebra
  alias Explorer.Series

  @doc """
  Default inspect implementation for backends.
  """
  def inspect(series, backend, n_rows, inspect_opts, opts \\ [])
      when is_binary(backend) and (is_integer(n_rows) or is_nil(n_rows)) and is_list(opts) do
    open = color("[", :list, inspect_opts)
    close = color("]", :list, inspect_opts)
    dtype = color("#{Series.dtype(series)}", :atom, inspect_opts)

    data =
      container_doc(
        open,
        series |> Series.slice(0, inspect_opts.limit + 1) |> Series.to_list(),
        close,
        inspect_opts,
        &Explorer.Shared.to_string/2
      )

    concat([dtype, open, "#{n_rows || "???"}", close, line(), data])
  end
end