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