Packages
ferricstore
0.3.1
0.11.14
0.11.12
0.11.11
0.11.10
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.2.0
0.1.0
FerricFlow durable workflows and queues with native-protocol storage, Raft durability, and Bitcask persistence.
Current section
Files
Jump to
Current section
Files
lib/ferricstore/commands/sorted_set.ex
defmodule Ferricstore.Commands.SortedSet do
@moduledoc """
Handles Redis sorted set commands: ZADD, ZSCORE, ZRANK, ZRANGE, ZCARD,
ZREM, ZINCRBY, ZCOUNT, ZPOPMIN, ZPOPMAX, ZRANGEBYSCORE, ZREVRANGE,
ZSCAN, ZRANDMEMBER, ZMSCORE.
Each sorted set member is stored as a compound key:
Z:redis_key\\0member -> score_string
The score is stored as a string representation of a float64. This allows
O(1) score lookups by member. For range queries, all members are loaded
and sorted in memory -- acceptable for typical sorted set sizes in cache
workloads.
## Type Enforcement
All sorted set commands check type metadata. Using sorted set commands on
a key that holds a different type returns WRONGTYPE.
"""
alias Ferricstore.Store.CompoundKey
alias Ferricstore.Store.Ops
alias Ferricstore.Store.TypeRegistry
@doc """
Handles a sorted set command.
## Parameters
- `cmd` - Uppercased command name (e.g. `"ZADD"`, `"ZRANGE"`)
- `args` - List of string arguments
- `store` - Injected store map with compound key callbacks
## Returns
Plain Elixir term: integer, float, string, list, nil, or `{:error, message}`.
"""
@spec handle(binary(), [binary()], map()) :: term()
def handle(cmd, args, store)
# ---------------------------------------------------------------------------
# ZADD key [NX|XX] [GT|LT] [CH] score member [score member ...]
# ---------------------------------------------------------------------------
def handle("ZADD", [key | rest], store) when rest != [] do
with {:ok, opts, score_member_pairs} <- parse_zadd_opts(rest),
:ok <- TypeRegistry.check_or_set(key, :zset, store) do
{added, changed} =
Enum.reduce(score_member_pairs, {0, 0}, fn {score, member}, {add_acc, ch_acc} ->
compound_key = CompoundKey.zset_member(key, member)
existing = Ops.compound_get(store, key, compound_key)
cond do
# NX: only add new elements, don't update existing
opts.nx and existing != nil ->
{add_acc, ch_acc}
# XX: only update existing elements, don't add new
opts.xx and existing == nil ->
{add_acc, ch_acc}
existing == nil ->
Ops.compound_put(store, key, compound_key, Float.to_string(score), 0)
{add_acc + 1, ch_acc}
true ->
existing_score =
case Float.parse(existing) do
{score, ""} -> score
_ -> 0.0
end
should_update =
cond do
opts.gt -> score > existing_score
opts.lt -> score < existing_score
true -> true
end
if should_update and score != existing_score do
Ops.compound_put(store, key, compound_key, Float.to_string(score), 0)
{add_acc, ch_acc + 1}
else
{add_acc, ch_acc}
end
end
end)
if opts.ch, do: added + changed, else: added
end
end
def handle("ZADD", _args, _store) do
{:error, "ERR wrong number of arguments for 'zadd' command"}
end
# ---------------------------------------------------------------------------
# ZSCORE key member
# ---------------------------------------------------------------------------
def handle("ZSCORE", [key, member], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
compound_key = CompoundKey.zset_member(key, member)
case Ops.compound_get(store, key, compound_key) do
nil -> nil
score_str -> format_score_str(score_str)
end
end
end
def handle("ZSCORE", _args, _store) do
{:error, "ERR wrong number of arguments for 'zscore' command"}
end
# ---------------------------------------------------------------------------
# ZRANK key member
# ---------------------------------------------------------------------------
def handle("ZRANK", [key, member], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
sorted = load_sorted_members(key, store)
case Enum.find_index(sorted, fn {m, _s} -> m == member end) do
nil -> nil
idx -> idx
end
end
end
def handle("ZRANK", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrank' command"}
end
# ---------------------------------------------------------------------------
# ZRANGE key start stop [WITHSCORES]
# ---------------------------------------------------------------------------
def handle("ZRANGE", [key, start_str, stop_str | opts], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case {Integer.parse(start_str), Integer.parse(stop_str)} do
{{start, ""}, {stop, ""}} ->
sorted = load_sorted_members(key, store)
len = length(sorted)
start_idx = normalize_index(start, len)
stop_idx = normalize_index(stop, len)
with_scores = "WITHSCORES" in opts
if start_idx > stop_idx or start_idx >= len do
[]
else
sliced = Enum.slice(sorted, start_idx..stop_idx)
if with_scores do
Enum.flat_map(sliced, fn {member, score} ->
[member, format_score(score)]
end)
else
Enum.map(sliced, fn {member, _score} -> member end)
end
end
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
end
def handle("ZRANGE", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrange' command"}
end
# ---------------------------------------------------------------------------
# ZREVRANGE key start stop [WITHSCORES]
# ---------------------------------------------------------------------------
def handle("ZREVRANGE", [key, start_str, stop_str | opts], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case {Integer.parse(start_str), Integer.parse(stop_str)} do
{{start, ""}, {stop, ""}} ->
sorted = load_sorted_members(key, store) |> Enum.reverse()
len = length(sorted)
start_idx = normalize_index(start, len)
stop_idx = normalize_index(stop, len)
with_scores = "WITHSCORES" in opts
if start_idx > stop_idx or start_idx >= len do
[]
else
sliced = Enum.slice(sorted, start_idx..stop_idx)
if with_scores do
Enum.flat_map(sliced, fn {member, score} ->
[member, format_score(score)]
end)
else
Enum.map(sliced, fn {member, _score} -> member end)
end
end
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
end
def handle("ZREVRANGE", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrevrange' command"}
end
# ---------------------------------------------------------------------------
# ZCARD key
# ---------------------------------------------------------------------------
def handle("ZCARD", [key], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
prefix = CompoundKey.zset_prefix(key)
Ops.compound_count(store, key, prefix)
end
end
def handle("ZCARD", _args, _store) do
{:error, "ERR wrong number of arguments for 'zcard' command"}
end
# ---------------------------------------------------------------------------
# ZREM key member [member ...]
# ---------------------------------------------------------------------------
def handle("ZREM", [key | members], store) when members != [] do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
removed =
Enum.reduce(members, 0, fn member, acc ->
compound_key = CompoundKey.zset_member(key, member)
if Ops.compound_get(store, key, compound_key) != nil do
Ops.compound_delete(store, key, compound_key)
acc + 1
else
acc
end
end)
if removed > 0 do
prefix = CompoundKey.zset_prefix(key)
if Ops.compound_count(store, key, prefix) == 0 do
TypeRegistry.delete_type(key, store)
end
end
removed
end
end
def handle("ZREM", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrem' command"}
end
# ---------------------------------------------------------------------------
# ZINCRBY key increment member
# ---------------------------------------------------------------------------
def handle("ZINCRBY", [key, increment_str, member], store) do
with :ok <- TypeRegistry.check_or_set(key, :zset, store) do
case Float.parse(increment_str) do
{increment, ""} ->
compound_key = CompoundKey.zset_member(key, member)
existing = Ops.compound_get(store, key, compound_key)
current_score =
case existing do
nil ->
0.0
score_str ->
case Float.parse(score_str) do
{score, ""} -> score
_ -> 0.0
end
end
new_score = current_score + increment
Ops.compound_put(store, key, compound_key, Float.to_string(new_score), 0)
format_score(new_score)
:error ->
# Try integer parse
case Integer.parse(increment_str) do
{increment, ""} ->
compound_key = CompoundKey.zset_member(key, member)
existing = Ops.compound_get(store, key, compound_key)
current_score =
case existing do
nil ->
0.0
score_str ->
case Float.parse(score_str) do
{score, ""} -> score
_ -> 0.0
end
end
new_score = current_score + increment * 1.0
Ops.compound_put(store, key, compound_key, Float.to_string(new_score), 0)
format_score(new_score)
_ ->
{:error, "ERR value is not a valid float"}
end
end
end
end
def handle("ZINCRBY", _args, _store) do
{:error, "ERR wrong number of arguments for 'zincrby' command"}
end
# ---------------------------------------------------------------------------
# ZCOUNT key min max
# ---------------------------------------------------------------------------
def handle("ZCOUNT", [key, min_str, max_str], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case {parse_score_bound(min_str), parse_score_bound(max_str)} do
{{:ok, min_val, min_excl}, {:ok, max_val, max_excl}} ->
sorted = load_sorted_members(key, store)
Enum.count(sorted, fn {_member, score} ->
above_min = score_gte?(score, min_val, min_excl)
below_max = score_lte?(score, max_val, max_excl)
above_min and below_max
end)
_ ->
{:error, "ERR min or max is not a float"}
end
end
end
def handle("ZCOUNT", _args, _store) do
{:error, "ERR wrong number of arguments for 'zcount' command"}
end
# ---------------------------------------------------------------------------
# ZPOPMIN key [count]
# ---------------------------------------------------------------------------
def handle("ZPOPMIN", [key], store) do
handle("ZPOPMIN", [key, "1"], store)
end
def handle("ZPOPMIN", [key, count_str], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case Integer.parse(count_str) do
{count, ""} when count >= 0 ->
sorted = load_sorted_members(key, store)
to_pop = Enum.take(sorted, count)
result =
Enum.flat_map(to_pop, fn {member, score} ->
compound_key = CompoundKey.zset_member(key, member)
Ops.compound_delete(store, key, compound_key)
[member, format_score(score)]
end)
if to_pop != [] do
prefix = CompoundKey.zset_prefix(key)
if Ops.compound_count(store, key, prefix) == 0 do
TypeRegistry.delete_type(key, store)
end
end
result
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
end
def handle("ZPOPMIN", _args, _store) do
{:error, "ERR wrong number of arguments for 'zpopmin' command"}
end
# ---------------------------------------------------------------------------
# ZPOPMAX key [count]
# ---------------------------------------------------------------------------
def handle("ZPOPMAX", [key], store) do
handle("ZPOPMAX", [key, "1"], store)
end
def handle("ZPOPMAX", [key, count_str], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case Integer.parse(count_str) do
{count, ""} when count >= 0 ->
sorted = load_sorted_members(key, store) |> Enum.reverse()
to_pop = Enum.take(sorted, count)
result =
Enum.flat_map(to_pop, fn {member, score} ->
compound_key = CompoundKey.zset_member(key, member)
Ops.compound_delete(store, key, compound_key)
[member, format_score(score)]
end)
if to_pop != [] do
prefix = CompoundKey.zset_prefix(key)
if Ops.compound_count(store, key, prefix) == 0 do
TypeRegistry.delete_type(key, store)
end
end
result
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
end
def handle("ZPOPMAX", _args, _store) do
{:error, "ERR wrong number of arguments for 'zpopmax' command"}
end
# ---------------------------------------------------------------------------
# ZSCAN key cursor [MATCH pattern] [COUNT count]
# ---------------------------------------------------------------------------
def handle("ZSCAN", [key, cursor_str | opts], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store),
{:ok, cursor} <- parse_cursor(cursor_str),
{:ok, match_pattern, count} <- parse_zscan_opts(opts) do
prefix = CompoundKey.zset_prefix(key)
pairs = Ops.compound_scan(store, key, prefix)
filtered =
case match_pattern do
nil ->
pairs
pattern ->
Enum.filter(pairs, fn {member, _score} -> Ferricstore.GlobMatcher.match?(member, pattern) end)
end
{next_cursor, batch} = paginate(filtered, cursor, count)
elements = Enum.flat_map(batch, fn {member, score} -> [member, format_score_str(score)] end)
[next_cursor, elements]
end
end
def handle("ZSCAN", [_key], _store) do
{:error, "ERR wrong number of arguments for 'zscan' command"}
end
def handle("ZSCAN", [], _store) do
{:error, "ERR wrong number of arguments for 'zscan' command"}
end
# ---------------------------------------------------------------------------
# ZRANDMEMBER key [count [WITHSCORES]]
# ---------------------------------------------------------------------------
def handle("ZRANDMEMBER", [key], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
prefix = CompoundKey.zset_prefix(key)
pairs = Ops.compound_scan(store, key, prefix)
case pairs do
[] -> nil
_ ->
{member, _score} = Enum.random(pairs)
member
end
end
end
def handle("ZRANDMEMBER", [key, count_str], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case Integer.parse(count_str) do
{count, ""} ->
prefix = CompoundKey.zset_prefix(key)
pairs = Ops.compound_scan(store, key, prefix)
select_random_zset_members(pairs, count, false)
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
end
def handle("ZRANDMEMBER", [key, count_str, withscores_str], store) do
if String.upcase(withscores_str) != "WITHSCORES" do
{:error, "ERR syntax error"}
else
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case Integer.parse(count_str) do
{count, ""} ->
prefix = CompoundKey.zset_prefix(key)
pairs = Ops.compound_scan(store, key, prefix)
select_random_zset_members(pairs, count, true)
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
end
end
def handle("ZRANDMEMBER", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrandmember' command"}
end
# ---------------------------------------------------------------------------
# ZMSCORE key member [member ...]
# ---------------------------------------------------------------------------
def handle("ZMSCORE", [key | members], store) when members != [] do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
Enum.map(members, fn member ->
compound_key = CompoundKey.zset_member(key, member)
case Ops.compound_get(store, key, compound_key) do
nil -> nil
score_str -> format_score_str(score_str)
end
end)
end
end
def handle("ZMSCORE", _args, _store) do
{:error, "ERR wrong number of arguments for 'zmscore' command"}
end
# ---------------------------------------------------------------------------
# ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
# ---------------------------------------------------------------------------
def handle("ZRANGEBYSCORE", [key, min_str, max_str | opts], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case {parse_score_bound(min_str), parse_score_bound(max_str)} do
{{:ok, min_val, min_excl}, {:ok, max_val, max_excl}} ->
case parse_range_by_score_opts(opts) do
{:error, _} = err ->
err
{with_scores, offset, count} ->
sorted = load_sorted_members(key, store)
filtered =
Enum.filter(sorted, fn {_member, score} ->
score_gte?(score, min_val, min_excl) and score_lte?(score, max_val, max_excl)
end)
paginated = apply_limit(filtered, offset, count)
if with_scores do
Enum.flat_map(paginated, fn {member, score} -> [member, format_score(score)] end)
else
Enum.map(paginated, fn {member, _score} -> member end)
end
end
_ ->
{:error, "ERR min or max is not a float"}
end
end
end
def handle("ZRANGEBYSCORE", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrangebyscore' command"}
end
# ---------------------------------------------------------------------------
# ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
# ---------------------------------------------------------------------------
def handle("ZREVRANGEBYSCORE", [key, max_str, min_str | opts], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
case {parse_score_bound(min_str), parse_score_bound(max_str)} do
{{:ok, min_val, min_excl}, {:ok, max_val, max_excl}} ->
case parse_range_by_score_opts(opts) do
{:error, _} = err ->
err
{with_scores, offset, count} ->
sorted = load_sorted_members(key, store) |> Enum.reverse()
filtered =
Enum.filter(sorted, fn {_member, score} ->
score_gte?(score, min_val, min_excl) and score_lte?(score, max_val, max_excl)
end)
paginated = apply_limit(filtered, offset, count)
if with_scores do
Enum.flat_map(paginated, fn {member, score} -> [member, format_score(score)] end)
else
Enum.map(paginated, fn {member, _score} -> member end)
end
end
_ ->
{:error, "ERR min or max is not a float"}
end
end
end
def handle("ZREVRANGEBYSCORE", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrevrangebyscore' command"}
end
# ---------------------------------------------------------------------------
# ZREVRANK key member
# ---------------------------------------------------------------------------
def handle("ZREVRANK", [key, member], store) do
with :ok <- TypeRegistry.check_type(key, :zset, store) do
sorted = load_sorted_members(key, store)
reversed = Enum.reverse(sorted)
case Enum.find_index(reversed, fn {m, _s} -> m == member end) do
nil -> nil
idx -> idx
end
end
end
def handle("ZREVRANK", _args, _store) do
{:error, "ERR wrong number of arguments for 'zrevrank' command"}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp load_sorted_members(key, store) do
prefix = CompoundKey.zset_prefix(key)
pairs = Ops.compound_scan(store, key, prefix)
pairs
|> Enum.map(fn {member, score_str} ->
score =
case Float.parse(score_str) do
{score, ""} -> score
_ -> 0.0
end
{member, score}
end)
|> Enum.sort_by(fn {member, score} -> {score, member} end)
end
defp normalize_index(index, len) when index < 0, do: max(0, len + index)
defp normalize_index(index, _len), do: index
defp format_score(score) when is_float(score) do
# Redis returns scores as strings
:erlang.float_to_binary(score, [:compact, decimals: 17])
end
# Parses a stored score string to float, then formats it consistently.
# Ensures ZSCORE, ZMSCORE, and ZSCAN use the same format as ZRANGE WITHSCORES.
defp format_score_str(score_str) do
case Float.parse(score_str) do
{score, ""} -> format_score(score)
_ -> score_str
end
end
# Parse ZADD options and score/member pairs
defp parse_zadd_opts(args) do
parse_zadd_opts(args, %{nx: false, xx: false, gt: false, lt: false, ch: false})
end
defp parse_zadd_opts(["NX" | rest], opts), do: parse_zadd_opts(rest, %{opts | nx: true})
defp parse_zadd_opts(["XX" | rest], opts), do: parse_zadd_opts(rest, %{opts | xx: true})
defp parse_zadd_opts(["GT" | rest], opts), do: parse_zadd_opts(rest, %{opts | gt: true})
defp parse_zadd_opts(["LT" | rest], opts), do: parse_zadd_opts(rest, %{opts | lt: true})
defp parse_zadd_opts(["CH" | rest], opts), do: parse_zadd_opts(rest, %{opts | ch: true})
defp parse_zadd_opts(score_member_args, opts) do
# Validate mutually exclusive flag combinations (Redis compat)
cond do
opts.nx and opts.xx ->
{:error, "ERR XX and NX options at the same time are not compatible"}
opts.gt and opts.lt ->
{:error, "ERR GT, LT, and NX options at the same time are not compatible"}
opts.gt and opts.nx ->
{:error, "ERR GT, LT, and NX options at the same time are not compatible"}
opts.lt and opts.nx ->
{:error, "ERR GT, LT, and NX options at the same time are not compatible"}
rem(length(score_member_args), 2) != 0 or score_member_args == [] ->
{:error, "ERR wrong number of arguments for 'zadd' command"}
true ->
pairs =
score_member_args
|> Enum.chunk_every(2)
|> Enum.reduce_while([], fn [score_str, member], acc ->
case parse_score(score_str) do
{:ok, score} -> {:cont, [{score, member} | acc]}
:error -> {:halt, :error}
end
end)
case pairs do
:error -> {:error, "ERR value is not a valid float"}
pairs -> {:ok, opts, Enum.reverse(pairs)}
end
end
end
defp parse_score(str) do
case Float.parse(str) do
{score, ""} ->
{:ok, score}
_ ->
case Integer.parse(str) do
{int, ""} -> {:ok, int * 1.0}
_ -> :error
end
end
end
defp parse_score_bound("-inf"), do: {:ok, :neg_infinity, false}
defp parse_score_bound("+inf"), do: {:ok, :infinity, false}
defp parse_score_bound("inf"), do: {:ok, :infinity, false}
defp parse_score_bound("(" <> rest) do
case parse_score(rest) do
{:ok, score} -> {:ok, score, true}
:error -> :error
end
end
defp parse_score_bound(str) do
case parse_score(str) do
{:ok, score} -> {:ok, score, false}
:error -> :error
end
end
# Score comparison helpers that handle :infinity and :neg_infinity atoms.
defp score_gte?(_score, :neg_infinity, _exclusive), do: true
defp score_gte?(_score, :infinity, _exclusive), do: false
defp score_gte?(score, bound, true), do: score > bound
defp score_gte?(score, bound, false), do: score >= bound
defp score_lte?(_score, :infinity, _exclusive), do: true
defp score_lte?(_score, :neg_infinity, _exclusive), do: false
defp score_lte?(score, bound, true), do: score < bound
defp score_lte?(score, bound, false), do: score <= bound
# ---------------------------------------------------------------------------
# ZRANGEBYSCORE / ZREVRANGEBYSCORE option parsing
# ---------------------------------------------------------------------------
# Parses optional [WITHSCORES] [LIMIT offset count] from the trailing args.
# Returns {with_scores, offset, count} where count == :all means no limit.
defp parse_range_by_score_opts(opts) do
do_parse_range_by_score_opts(opts, false, 0, :all)
end
defp do_parse_range_by_score_opts([], ws, offset, count), do: {ws, offset, count}
defp do_parse_range_by_score_opts([opt | rest], ws, offset, count) do
case String.upcase(opt) do
"WITHSCORES" ->
do_parse_range_by_score_opts(rest, true, offset, count)
"LIMIT" ->
case rest do
[offset_str, count_str | remaining] ->
with {off, ""} <- Integer.parse(offset_str),
{cnt, ""} <- Integer.parse(count_str) do
if off < 0 do
{:error, "ERR syntax error"}
else
# Redis: negative count means all remaining from offset
real_count = if cnt < 0, do: :all, else: cnt
do_parse_range_by_score_opts(remaining, ws, off, real_count)
end
else
_ -> {:error, "ERR value is not an integer or out of range"}
end
_ ->
{ws, offset, count}
end
_ ->
{ws, offset, count}
end
end
# Applies LIMIT offset count to a filtered list.
defp apply_limit(list, 0, :all), do: list
defp apply_limit(list, offset, :all) do
Enum.drop(list, offset)
end
defp apply_limit(list, offset, count) do
list |> Enum.drop(offset) |> Enum.take(count)
end
# ---------------------------------------------------------------------------
# ZSCAN helpers
# ---------------------------------------------------------------------------
defp parse_cursor(cursor_str) do
case Integer.parse(cursor_str) do
{cursor, ""} when cursor >= 0 -> {:ok, cursor}
_ -> {:error, "ERR invalid cursor"}
end
end
defp parse_zscan_opts(opts), do: do_parse_zscan_opts(opts, nil, 10)
defp do_parse_zscan_opts([], match, count), do: {:ok, match, count}
defp do_parse_zscan_opts([opt, value | rest], match, count) do
case String.upcase(opt) do
"MATCH" ->
do_parse_zscan_opts(rest, value, count)
"COUNT" ->
case Integer.parse(value) do
{n, ""} when n > 0 -> do_parse_zscan_opts(rest, match, n)
_ -> {:error, "ERR value is not an integer or out of range"}
end
_ ->
{:error, "ERR syntax error"}
end
end
defp do_parse_zscan_opts([_ | _], _match, _count) do
{:error, "ERR syntax error"}
end
defp paginate(items, cursor, count) do
total = length(items)
if cursor >= total do
{"0", []}
else
batch = Enum.slice(items, cursor, count)
batch_len = min(count, total - cursor)
next_pos = cursor + batch_len
if next_pos >= total do
{"0", batch}
else
{Integer.to_string(next_pos), batch}
end
end
end
defp select_random_zset_members(pairs, count, with_scores) do
cond do
count == 0 ->
[]
count > 0 ->
selected = Enum.take_random(pairs, count)
if with_scores do
Enum.flat_map(selected, fn {member, score} -> [member, format_score_str(score)] end)
else
Enum.map(selected, fn {member, _score} -> member end)
end
count < 0 ->
abs_count = abs(count)
if pairs == [] do
[]
else
selected = for _ <- 1..abs_count, do: Enum.random(pairs)
if with_scores do
Enum.flat_map(selected, fn {member, score} -> [member, format_score_str(score)] end)
else
Enum.map(selected, fn {member, _score} -> member end)
end
end
end
end
end