defmodule LimitedMapSet do @moduledoc """ A bounded, processless version of `MapSet` that keeps insertion order (FIFO) and evicts the oldest elements when reaching the given limit. Combines `MapSet` (for fast membership) with `:queue` (for insertion order). """ defstruct set: MapSet.new(), queue: :queue.new(), limit: 200, size: 0 @opaque t() :: %__MODULE__{ set: MapSet.t(), queue: :queue.queue(), limit: pos_integer(), size: non_neg_integer() } @doc """ Creates an empty `LimitedMapSet` with a specified limit. ## Examples iex> LimitedMapSet.new(100) %LimitedMapSet{limit: 100, size: 0} """ @spec new(pos_integer()) :: t() def new(limit) when is_integer(limit) and limit > 0 do %__MODULE__{limit: limit} end @doc """ Creates a `LimitedMapSet` from a list of values with a specified limit. If the list exceeds the limit, the oldest items are trimmed. ## Examples iex> LimitedMapSet.new([1, 2, 3, 4], 3) |> LimitedMapSet.to_list() [2, 3, 4] """ @spec new(list(), pos_integer()) :: t() def new(list, limit) when is_list(list) and is_integer(limit) and limit > 0 do trimmed = if length(list) > limit do Enum.take(Enum.reverse(list), limit) |> Enum.reverse() else list end queue = Enum.reduce(trimmed, :queue.new(), fn v, q -> :queue.in(v, q) end) set = MapSet.new(trimmed) %__MODULE__{set: set, queue: queue, limit: limit, size: length(trimmed)} end @doc "Checks if the given value is in the set." @spec member?(t(), any()) :: boolean() def member?(%__MODULE__{set: set}, value), do: MapSet.member?(set, value) @doc "Returns the number of elements in the set." @spec size(t()) :: non_neg_integer() def size(%__MODULE__{size: s}), do: s @doc "Returns all elements as a list in insertion order (oldest → newest)." @spec to_list(t()) :: [any()] def to_list(%__MODULE__{queue: queue}), do: :queue.to_list(queue) @doc """ Adds a new element to the set. - If it already exists, returns the set unchanged. - If full, evicts the oldest element (FIFO). """ @spec put(t(), any()) :: t() def put(%__MODULE__{set: set, queue: queue, size: size, limit: limit} = s, value) do cond do MapSet.member?(set, value) -> s size < limit -> %{ s | set: MapSet.put(set, value), queue: :queue.in(value, queue), size: size + 1 } true -> {{:value, oldest}, q2} = :queue.out(queue) q2 = :queue.in(value, q2) set = set |> MapSet.delete(oldest) |> MapSet.put(value) %{s | set: set, queue: q2} end end @doc "Removes a value if it exists." @spec delete(t(), any()) :: t() def delete(%__MODULE__{set: set, queue: queue, size: size} = s, value) do if MapSet.member?(set, value) do new_queue = :queue.filter(&(&1 != value), queue) %{s | set: MapSet.delete(set, value), queue: new_queue, size: size - 1} else s end end @doc "Clears all elements." @spec clear(t()) :: t() def clear(%__MODULE__{limit: limit}), do: new(limit) defimpl Enumerable do def count(lset), do: {:ok, LimitedMapSet.size(lset)} def member?(lset, value), do: {:ok, LimitedMapSet.member?(lset, value)} def slice(_), do: {:error, __MODULE__} def reduce(lset, acc, fun) do Enumerable.List.reduce(LimitedMapSet.to_list(lset), acc, fun) end end defimpl Inspect do import Inspect.Algebra def inspect(%LimitedMapSet{limit: limit, size: size}, opts) do concat(["#LimitedMapSet"]) end end end