import 'package:lanebow/models/member_model.dart'; import 'package:lanebow/models/score_model.dart'; import 'package:lanebow/models/tie_breaker_option.dart'; /// 동점자 처리를 위한 유틸리티 클래스 class TieBreakerUtil { /// 동점자 처리 옵션에 따라 점수 목록을 정렬 /// /// [scores] 정렬할 점수 목록 /// [options] 적용할 동점자 처리 옵션 우선순위 목록 /// [members] 회원 정보 목록 (연령 기준 정렬 시 필요) /// [includeHandicap] 핸디캡 포함 여부 /// /// 반환값: 정렬된 점수 목록 static List sortScoresByOptions( List scores, List? options, List? members, bool includeHandicap ) { // 옵션이 없거나 비어있으면 기본 정렬만 적용 if (options == null || options.isEmpty) { return _sortScoresByDefault(scores, includeHandicap); } // olderAge 옵션이 단독으로 있는 경우 특별 처리 if (options.length == 1 && options.contains(TieBreakerOption.olderAge)) { if (members == null || members.isEmpty) { return _sortScoresByDefault(scores, includeHandicap); } // 연장자 순으로 정렬 return List.from(scores)..sort((a, b) { DateTime? birthDateA = _findMemberBirthDate(a.memberId, members); DateTime? birthDateB = _findMemberBirthDate(b.memberId, members); if (birthDateA == null || birthDateB == null) { // 생년월일이 없으면 점수로 정렬 int scoreA = includeHandicap ? (a.totalScore + (a.handicap ?? 0)) : a.totalScore; int scoreB = includeHandicap ? (b.totalScore + (b.handicap ?? 0)) : b.totalScore; return scoreB.compareTo(scoreA); // 내림차순 } return birthDateA.compareTo(birthDateB); // 오름차순 정렬로 연장자 우선 }); } // 기본 점수 정렬 (점수 내림차순) List sortedScores = _sortScoresByDefault(scores, includeHandicap); // 점수별 그룹화 Map> scoreGroups = {}; for (int i = 0; i < sortedScores.length; i++) { int currentScore = includeHandicap ? (sortedScores[i].totalScore + (sortedScores[i].handicap ?? 0)) : sortedScores[i].totalScore; if (!scoreGroups.containsKey(currentScore)) { scoreGroups[currentScore] = []; } scoreGroups[currentScore]?.add(sortedScores[i]); } // 결과 목록 준비 List result = []; // 점수 내림차순 정렬 List sortedScoreValues = scoreGroups.keys.toList()..sort((a, b) => b.compareTo(a)); // 각 점수 그룹에 대해 처리 for (int scoreValue in sortedScoreValues) { List group = scoreGroups[scoreValue]!; // 그룹 크기가 1이면 그대로 추가 if (group.length == 1) { result.add(group[0]); continue; } // 동점자 그룹 정렬 List resolvedGroup = List.from(group); // 옵션에 따른 정렬 적용 if (options.contains(TieBreakerOption.lowerHandicap)) { // 핸디캡이 낮은 순으로 정렬 resolvedGroup.sort((a, b) { int handicapA = a.handicap ?? 0; int handicapB = b.handicap ?? 0; return handicapA.compareTo(handicapB); }); } // 핸디캡이 같은 경우 연장자 우선 정렬 if (options.contains(TieBreakerOption.olderAge) && members != null && members.isNotEmpty) { // 핸디캡이 같은 그룹들로 나누기 Map> handicapGroups = {}; for (var score in resolvedGroup) { int handicap = score.handicap ?? 0; if (!handicapGroups.containsKey(handicap)) { handicapGroups[handicap] = []; } handicapGroups[handicap]!.add(score); } // 각 핸디캡 그룹 내에서 연장자 순으로 정렬 List newResolvedGroup = []; List handicapValues = handicapGroups.keys.toList()..sort((a, b) => a.compareTo(b)); for (int handicap in handicapValues) { List handicapGroup = handicapGroups[handicap]!; if (handicapGroup.length > 1) { handicapGroup.sort((a, b) { DateTime? birthDateA = _findMemberBirthDate(a.memberId, members); DateTime? birthDateB = _findMemberBirthDate(b.memberId, members); if (birthDateA == null || birthDateB == null) return 0; return birthDateA.compareTo(birthDateB); // 오름차순 정렬로 연장자 우선 }); } newResolvedGroup.addAll(handicapGroup); } resolvedGroup = newResolvedGroup; } // 점수 격차가 적은 순으로 정렬 if (options.contains(TieBreakerOption.lowerScoreGap)) { // 이미 정렬된 그룹에서 추가 정렬 적용 List scoreGapSorted = []; // 핸디캡과 연령으로 이미 정렬된 그룹들을 찾아서 각 그룹 내에서 점수 격차 정렬 적용 Map> tiedGroups = {}; String currentKey = ""; for (int i = 0; i < resolvedGroup.length; i++) { Score current = resolvedGroup[i]; int handicap = current.handicap ?? 0; String memberId = current.memberId; String key = "$handicap-$memberId"; if (i == 0 || key != currentKey) { currentKey = key; tiedGroups[currentKey] = []; } tiedGroups[currentKey]!.add(current); } // 각 그룹에 대해 점수 격차 정렬 적용 for (var group in tiedGroups.values) { if (group.length > 1) { group.sort((a, b) { int gapA = _calculateScoreGap(a.frames); int gapB = _calculateScoreGap(b.frames); return gapA.compareTo(gapB); // 오름차순 (격차가 적은 것이 우선) }); } scoreGapSorted.addAll(group); } resolvedGroup = scoreGapSorted; } // 정렬된 그룹 추가 result.addAll(resolvedGroup); } return result; } /// 기본 점수 정렬 (점수 내림차순) static List _sortScoresByDefault(List scores, bool includeHandicap) { return List.from(scores)..sort((a, b) { int scoreA = includeHandicap ? (a.totalScore + (a.handicap ?? 0)) : a.totalScore; int scoreB = includeHandicap ? (b.totalScore + (b.handicap ?? 0)) : b.totalScore; return scoreB.compareTo(scoreA); // 내림차순 }); } /// 동점자 그룹 찾기 static Map> _findTiedGroups(List sortedScores, bool includeHandicap) { Map> tiedGroups = {}; for (int i = 0; i < sortedScores.length; i++) { int currentScore = includeHandicap ? (sortedScores[i].totalScore + (sortedScores[i].handicap ?? 0)) : sortedScores[i].totalScore; List sameScores = [sortedScores[i]]; // 같은 점수를 가진 항목 찾기 for (int j = i + 1; j < sortedScores.length; j++) { int nextScore = includeHandicap ? (sortedScores[j].totalScore + (sortedScores[j].handicap ?? 0)) : sortedScores[j].totalScore; if (currentScore == nextScore) { sameScores.add(sortedScores[j]); i = j; // 이미 처리한 항목 건너뛰기 } else { break; // 다른 점수 발견 시 중단 } } // 동점자가 2명 이상인 경우만 그룹에 추가 if (sameScores.length > 1) { tiedGroups[currentScore] = sameScores; } } return tiedGroups; } /// 동점자 처리 옵션 적용 static List _applyTieBreakerOption( List tiedScores, TieBreakerOption option, List? members, bool includeHandicap ) { switch (option) { case TieBreakerOption.lowerHandicap: return _sortByLowerHandicap(tiedScores); case TieBreakerOption.lowerScoreGap: return _sortByLowerScoreGap(tiedScores); case TieBreakerOption.olderAge: return _sortByOlderAge(tiedScores, members); case TieBreakerOption.none: return tiedScores; // 변경 없음 } } /// 핸디캡이 적은 순으로 정렬 static List _sortByLowerHandicap(List tiedScores) { return List.from(tiedScores)..sort((a, b) { int handicapA = a.handicap ?? 0; int handicapB = b.handicap ?? 0; return handicapA.compareTo(handicapB); // 오름차순 (적은 것이 우선) }); } /// 점수 격차가 적은 순으로 정렬 static List _sortByLowerScoreGap(List tiedScores) { return List.from(tiedScores)..sort((a, b) { // 프레임 점수 중 최고점과 최저점의 차이 계산 int gapA = _calculateScoreGap(a.frames); int gapB = _calculateScoreGap(b.frames); return gapA.compareTo(gapB); // 오름차순 (격차가 적은 것이 우선) }); } /// 연장자 우선으로 정렬 static List _sortByOlderAge(List tiedScores, List? members) { if (members == null || members.isEmpty) return tiedScores; return List.from(tiedScores)..sort((a, b) { // 회원 정보에서 생년월일 찾기 DateTime? birthDateA = _findMemberBirthDate(a.memberId, members); DateTime? birthDateB = _findMemberBirthDate(b.memberId, members); // 생년월일이 없으면 정렬 불가 if (birthDateA == null || birthDateB == null) return 0; // 생년월일 비교 (오래된 날짜 = 연장자가 우선) // 연장자가 우선이므로 더 오래된 날짜(더 작은 값)가 앞에 와야 함 // 연장자 우선이므로 더 오래된 날짜(A)가 더 최근 날짜(B)보다 앞에 와야 함 return birthDateA.compareTo(birthDateB); // 오름차순 정렬로 연장자 우선 }); } /// 회원 ID로 생년월일 찾기 static DateTime? _findMemberBirthDate(String? memberId, List members) { if (memberId == null) return null; for (var member in members) { if (member.id == memberId) { return member.birthDate; } } return null; } /// 프레임 점수의 최고점과 최저점 차이 계산 static int _calculateScoreGap(List frames) { if (frames.isEmpty) return 0; int max = frames.reduce((curr, next) => curr > next ? curr : next); int min = frames.reduce((curr, next) => curr < next ? curr : next); return max - min; } /// 모든 점수가 서로 다른지 확인 static bool _allScoresHaveDifferentValues(List scores, bool includeHandicap) { Set uniqueScores = {}; for (var score in scores) { int totalScore = includeHandicap ? (score.totalScore + (score.handicap ?? 0)) : score.totalScore; if (uniqueScores.contains(totalScore)) { return false; } uniqueScores.add(totalScore); } return true; } /// 정렬된 동점자 그룹으로 원래 목록 업데이트 static Map calculateRanks( List scores, bool includeHandicap, { List? options, List? members }) { Map ranks = {}; // 점수가 없으면 빈 맵 반환 if (scores.isEmpty) { return ranks; } // 핸디캡 포함 여부에 따른 총점 계산 List sortedScores = List.from(scores); // 순위 계산을 위한 총점 그룹 구분 Map> totalScoreGroups = {}; for (var score in sortedScores) { int totalScore = includeHandicap ? (score.totalScore + (score.handicap ?? 0)) : score.totalScore; if (!totalScoreGroups.containsKey(totalScore)) { totalScoreGroups[totalScore] = []; } totalScoreGroups[totalScore]!.add(score); } // 총점 내림차순 정렬 List sortedTotalScores = totalScoreGroups.keys.toList(); sortedTotalScores.sort((a, b) => b.compareTo(a)); int currentRank = 1; // 총점별 그룹 순회 for (int totalScore in sortedTotalScores) { List totalScoreGroup = totalScoreGroups[totalScore]!; // 그룹 내 점수가 1개라면 현재 순위 부여 if (totalScoreGroup.length == 1) { ranks[totalScoreGroup[0].id] = currentRank; currentRank++; continue; } // 옵션이 없으면 모두 동일 순위 부여 if (options == null || options.isEmpty) { for (var score in totalScoreGroup) { ranks[score.id] = currentRank; } // 다음 순위는 현재 그룹의 크기만큼 증가 currentRank += totalScoreGroup.length; } else { // 순위 부여 규칙: // - olderAge가 포함된 경우: 동점 그룹 내에서 현재 리스트 순서대로 개별 순위를 부여 // - 그 외(옵션 없음/olderAge 제외 옵션): 동점 그룹 전체에 동일 순위를 부여 if (options.contains(TieBreakerOption.olderAge)) { // olderAge가 포함된 경우의 기본: 순서대로 개별 순위 부여 // 단, 옵션 순서가 lowerScoreGap -> lowerHandicap -> olderAge인 경우에는 // 동일 핸디캡을 가진 연속 항목들은 동일 순위를 부여하고, 핸디캡이 바뀔 때만 순위를 증가시킨다. final idxGap = options.indexOf(TieBreakerOption.lowerScoreGap); final idxHcp = options.indexOf(TieBreakerOption.lowerHandicap); final idxAge = options.indexOf(TieBreakerOption.olderAge); final isGapThenHcpThenAge = idxGap != -1 && idxHcp != -1 && idxAge != -1 && idxGap < idxHcp && idxHcp < idxAge; if (isGapThenHcpThenAge) { int? lastHandicap; int currentRankLocal = currentRank; int blockSize = 0; for (var i = 0; i < totalScoreGroup.length; i++) { final score = totalScoreGroup[i]; final hcp = score.handicap ?? 0; if (lastHandicap == null) { // 첫 항목은 현재 랭크 부여 lastHandicap = hcp; blockSize = 1; ranks[score.id] = currentRankLocal; } else if (hcp == lastHandicap) { // 같은 핸디캡: 동일 랭크 부여 blockSize += 1; ranks[score.id] = currentRankLocal; } else { // 다른 핸디캡: 블록 크기만큼 랭크를 증가시키고 새 블록 시작 currentRankLocal += blockSize; lastHandicap = hcp; blockSize = 1; ranks[score.id] = currentRankLocal; } } currentRank += totalScoreGroup.length; } else { for (var i = 0; i < totalScoreGroup.length; i++) { ranks[totalScoreGroup[i].id] = currentRank + i; } currentRank += totalScoreGroup.length; } } else { for (var score in totalScoreGroup) { ranks[score.id] = currentRank; } currentRank += totalScoreGroup.length; } } } return ranks; } }