Packages

LimitedMapSet is a processless, size-limited alternative to MapSet that maintains insertion order (FIFO) and evicts the oldest entries when reaching the configured limit.

Current section

Files

Jump to
limited_map_set lib limited_map_set.ex
Raw

lib/limited_map_set.ex

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
@type t() :: %__MODULE__{
set: term(),
queue: term(),
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<limit: ", to_doc(limit, opts), ", size: ", to_doc(size, opts), ">"])
end
end
end