api specs
This commit is contained in:
272
phoenix/lib/shooting_event_phx/domain/derived.ex
Normal file
272
phoenix/lib/shooting_event_phx/domain/derived.ex
Normal file
@@ -0,0 +1,272 @@
|
||||
defmodule ShootingEventPhx.Domain.Derived do
|
||||
@moduledoc false
|
||||
alias ShootingEventPhx.Domain.Stages
|
||||
|
||||
def compute(players, scores) do
|
||||
player_by_id = Map.new(players, &{&1.id, &1})
|
||||
|
||||
pre_rows =
|
||||
players
|
||||
|> Enum.map(fn p ->
|
||||
%{
|
||||
"playerId" => p.id,
|
||||
"nameAr" => p.name_ar,
|
||||
"nameEn" => p.name_en,
|
||||
"groupCode" => p.group_code,
|
||||
"imageData" => p.image_data,
|
||||
"score" => score_total(scores, p.id, Stages.preliminary_round_stages()),
|
||||
"tieBreak" => get_score(scores, "prelim_tiebreak", p.id),
|
||||
"rank" => 0,
|
||||
"seed" => 0,
|
||||
"finalGroup" => 0
|
||||
}
|
||||
end)
|
||||
|> Enum.sort_by(fn row -> {-row["score"], row["playerId"]} end)
|
||||
|> assign_dense_rank_by_score()
|
||||
|
||||
{pre_tie, finalists} = compute_finalists(pre_rows)
|
||||
|
||||
finalists =
|
||||
finalists
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {row, idx} ->
|
||||
row
|
||||
|> Map.put("seed", idx)
|
||||
|> Map.put("finalGroup", if(idx <= 6, do: 1, else: 2))
|
||||
end)
|
||||
|
||||
final_group1 = Enum.filter(finalists, &(&1["finalGroup"] == 1))
|
||||
final_group2 = Enum.filter(finalists, &(&1["finalGroup"] == 2))
|
||||
|
||||
final_rows =
|
||||
finalists
|
||||
|> Enum.map(fn finalist ->
|
||||
p = Map.fetch!(player_by_id, finalist["playerId"])
|
||||
|
||||
%{
|
||||
"playerId" => finalist["playerId"],
|
||||
"nameAr" => p.name_ar,
|
||||
"nameEn" => p.name_en,
|
||||
"groupCode" => p.group_code,
|
||||
"imageData" => p.image_data,
|
||||
"score" => score_total(scores, finalist["playerId"], Stages.final_round_stages()),
|
||||
"tieBreak" => get_score(scores, "final_tiebreak", finalist["playerId"]),
|
||||
"rank" => 0,
|
||||
"seed" => finalist["seed"],
|
||||
"finalGroup" => finalist["finalGroup"]
|
||||
}
|
||||
end)
|
||||
|> compute_final_ranking()
|
||||
|
||||
podium = final_rows |> Enum.take(3)
|
||||
final_tie = final_tie_info(final_rows)
|
||||
|
||||
%{
|
||||
"preliminaryRanking" => %{
|
||||
"rows" => pre_rows,
|
||||
"tieBreak" => pre_tie,
|
||||
"unresolved" => pre_tie["required"] and not pre_tie["resolved"]
|
||||
},
|
||||
"finalists" => finalists,
|
||||
"finalGroups" => %{"group1" => final_group1, "group2" => final_group2},
|
||||
"finalRanking" => %{
|
||||
"rows" => final_rows,
|
||||
"tieBreak" => final_tie,
|
||||
"unresolved" => final_tie["required"] and not final_tie["resolved"]
|
||||
},
|
||||
"podium" => podium
|
||||
}
|
||||
end
|
||||
|
||||
defp compute_finalists(pre_rows) do
|
||||
if length(pre_rows) <= 12 do
|
||||
finalists =
|
||||
pre_rows
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {row, seed} -> Map.put(row, "seed", seed) end)
|
||||
|
||||
{empty_tie(), finalists}
|
||||
else
|
||||
cutoff = Enum.at(pre_rows, 11)["score"]
|
||||
above = Enum.filter(pre_rows, &(&1["score"] > cutoff))
|
||||
at_cutoff = Enum.filter(pre_rows, &(&1["score"] == cutoff))
|
||||
slots = max(12 - length(above), 0)
|
||||
|
||||
if length(at_cutoff) <= slots do
|
||||
{empty_tie(), above ++ at_cutoff}
|
||||
else
|
||||
tie = %{
|
||||
"required" => true,
|
||||
"resolved" => true,
|
||||
"slots" => slots,
|
||||
"playerIds" => at_cutoff |> Enum.map(& &1["playerId"]) |> Enum.sort()
|
||||
}
|
||||
|
||||
sorted_at_cutoff =
|
||||
Enum.sort_by(at_cutoff, fn row -> {-row["tieBreak"], row["playerId"]} end)
|
||||
|
||||
tie =
|
||||
if slots > 0 do
|
||||
boundary = Enum.at(sorted_at_cutoff, slots - 1)["tieBreak"]
|
||||
greater = Enum.count(sorted_at_cutoff, &(&1["tieBreak"] > boundary))
|
||||
equal = Enum.count(sorted_at_cutoff, &(&1["tieBreak"] == boundary))
|
||||
resolved = not (greater < slots and greater + equal > slots)
|
||||
Map.put(tie, "resolved", resolved)
|
||||
else
|
||||
tie
|
||||
end
|
||||
|
||||
finalists = above ++ Enum.take(sorted_at_cutoff, min(slots, length(sorted_at_cutoff)))
|
||||
|
||||
finalists =
|
||||
finalists
|
||||
|> Enum.sort_by(fn row ->
|
||||
if tie["required"] do
|
||||
{-row["score"], -row["tieBreak"], row["playerId"]}
|
||||
else
|
||||
{-row["score"], row["playerId"]}
|
||||
end
|
||||
end)
|
||||
|> assign_dense_rank(fn a, b ->
|
||||
if tie["required"],
|
||||
do: a["score"] == b["score"] and a["tieBreak"] == b["tieBreak"],
|
||||
else: a["score"] == b["score"]
|
||||
end)
|
||||
|
||||
{tie, finalists}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp compute_final_ranking(rows) do
|
||||
sorted = Enum.sort_by(rows, fn row -> {-row["score"], row["seed"]} end)
|
||||
tied_top = tied_top_for_podium(sorted)
|
||||
|
||||
rows =
|
||||
if MapSet.size(tied_top) > 0 do
|
||||
Enum.sort_by(sorted, fn row ->
|
||||
tied = MapSet.member?(tied_top, row["playerId"])
|
||||
tie_break_key = if tied, do: -row["tieBreak"], else: 0
|
||||
{-row["score"], tie_break_key, row["seed"]}
|
||||
end)
|
||||
else
|
||||
sorted
|
||||
end
|
||||
|
||||
if MapSet.size(tied_top) > 0 do
|
||||
assign_dense_rank(rows, fn a, b ->
|
||||
if a["score"] != b["score"] do
|
||||
false
|
||||
else
|
||||
tied_a = MapSet.member?(tied_top, a["playerId"])
|
||||
tied_b = MapSet.member?(tied_top, b["playerId"])
|
||||
if tied_a and tied_b, do: a["tieBreak"] == b["tieBreak"], else: true
|
||||
end
|
||||
end)
|
||||
else
|
||||
assign_dense_rank_by_score(rows)
|
||||
end
|
||||
end
|
||||
|
||||
defp final_tie_info(final_rows) do
|
||||
tied_top = tied_top_for_podium(final_rows)
|
||||
|
||||
if MapSet.size(tied_top) == 0 do
|
||||
empty_tie()
|
||||
else
|
||||
player_ids = tied_top |> MapSet.to_list() |> Enum.sort()
|
||||
|
||||
resolved =
|
||||
if length(final_rows) >= 3 do
|
||||
third = Enum.at(final_rows, 2)
|
||||
|
||||
{greater, equal} =
|
||||
Enum.reduce(final_rows, {0, 0}, fn row, {g, e} ->
|
||||
cond do
|
||||
row["score"] > third["score"] ->
|
||||
{g + 1, e}
|
||||
|
||||
row["score"] < third["score"] ->
|
||||
{g, e}
|
||||
|
||||
true ->
|
||||
row_tied = MapSet.member?(tied_top, row["playerId"])
|
||||
third_tied = MapSet.member?(tied_top, third["playerId"])
|
||||
|
||||
cond do
|
||||
row_tied and third_tied and row["tieBreak"] > third["tieBreak"] -> {g + 1, e}
|
||||
row_tied and third_tied and row["tieBreak"] == third["tieBreak"] -> {g, e + 1}
|
||||
not row_tied or not third_tied -> {g, e + 1}
|
||||
true -> {g, e}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
not (greater < 3 and greater + equal > 3)
|
||||
else
|
||||
true
|
||||
end
|
||||
|
||||
%{"required" => true, "resolved" => resolved, "slots" => 0, "playerIds" => player_ids}
|
||||
end
|
||||
end
|
||||
|
||||
defp tied_top_for_podium(rows) do
|
||||
rows
|
||||
|> Enum.chunk_by(& &1["score"])
|
||||
|> Enum.reduce({MapSet.new(), 1}, fn chunk, {set, start_pos} ->
|
||||
size = length(chunk)
|
||||
end_pos = start_pos + size - 1
|
||||
score = List.first(chunk)["score"]
|
||||
|
||||
include? =
|
||||
size > 1 and score > 0 and
|
||||
(start_pos <= 3 or end_pos <= 3 or (start_pos < 3 and end_pos > 3))
|
||||
|
||||
next_set =
|
||||
if include? do
|
||||
Enum.reduce(chunk, set, fn row, acc -> MapSet.put(acc, row["playerId"]) end)
|
||||
else
|
||||
set
|
||||
end
|
||||
|
||||
{next_set, end_pos + 1}
|
||||
end)
|
||||
|> elem(0)
|
||||
end
|
||||
|
||||
defp empty_tie, do: %{"required" => false, "resolved" => true, "slots" => 0, "playerIds" => []}
|
||||
|
||||
defp score_total(scores, player_id, stages) do
|
||||
Enum.reduce(stages, 0, fn stage, acc -> acc + get_score(scores, stage, player_id) end)
|
||||
end
|
||||
|
||||
defp get_score(scores, stage, player_id) do
|
||||
stage_map = Map.get(scores, stage, %{})
|
||||
Map.get(stage_map, player_id, 0)
|
||||
end
|
||||
|
||||
defp assign_dense_rank_by_score(rows) do
|
||||
assign_dense_rank(rows, fn a, b -> a["score"] == b["score"] end)
|
||||
end
|
||||
|
||||
defp assign_dense_rank([], _equal_fun), do: []
|
||||
|
||||
defp assign_dense_rank(rows, equal_fun) do
|
||||
{ranked_rev, _prev, _rank, _idx} =
|
||||
Enum.reduce(rows, {[], nil, 1, 0}, fn row, {acc, prev, rank, idx} ->
|
||||
idx1 = idx + 1
|
||||
|
||||
next_rank =
|
||||
if is_map(prev) and equal_fun.(row, prev) do
|
||||
rank
|
||||
else
|
||||
idx1
|
||||
end
|
||||
|
||||
{[%{row | "rank" => next_rank} | acc], row, next_rank, idx1}
|
||||
end)
|
||||
|
||||
Enum.reverse(ranked_rev)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user