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; final int maxFrame = frames.reduce((max, score) => score > max ? score : max); final int minFrame = frames.reduce((min, score) => score < min ? score : min); final int scoreGap = maxFrame - minFrame; return scoreGap; } /// 모든 점수가 서로 다른지 확인 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 void _updateScoresWithResolvedTies(List originalScores, List resolvedTiedScores) { // 원래 목록에서 동점자 그룹에 해당하는 항목 찾아 업데이트 for (int i = 0; i < originalScores.length; i++) { for (int j = 0; j < resolvedTiedScores.length; j++) { if (originalScores[i].id == resolvedTiedScores[j].id) { originalScores[i] = resolvedTiedScores[j]; break; } } } } /// 순위 계산 /// /// [scores] 정렬된 점수 목록 /// [includeHandicap] 핸디캅 포함 여부 /// [options] 적용된 동점자 처리 옵션 목록 /// /// 반환값: 점수 ID를 키로 하고 순위를 값으로 하는 맵 static Map calculateRanks( List scores, bool includeHandicap, {List? options} ) { // 점수 목록이 비어있으면 빈 맵 반환 if (scores.isEmpty) return {}; Map ranks = {}; // 옵션 확인 bool hasOlderAgeOption = options?.contains(TieBreakerOption.olderAge) ?? false; bool hasLowerScoreGapOption = options?.contains(TieBreakerOption.lowerScoreGap) ?? false; bool hasLowerHandicapOption = options?.contains(TieBreakerOption.lowerHandicap) ?? false; // 순위 부여 방식 결정 // 1. lowerScoreGap 옵션이 있을 때는 기본적으로 동점자 동일 순위 부여 // - 단, lowerScoreGap -> lowerHandicap -> olderAge 조합에서는 특별 처리 // 2. olderAge 옵션이 있고 lowerScoreGap 옵션이 없을 때는 순차적 순위 부여 // 3. 그 외의 경우는 동점자 동일 순위 부여 // 기본 순위 계산 로직 // 점수별 그룹화 Map> scoreGroups = {}; // 점수와 ID 맵핑 for (var score in scores) { int totalScore = includeHandicap ? (score.totalScore + (score.handicap ?? 0)) : score.totalScore; if (!scoreGroups.containsKey(totalScore)) { scoreGroups[totalScore] = []; } scoreGroups[totalScore]!.add(score); } // 점수 내림차순 정렬 List sortedScores = scoreGroups.keys.toList()..sort((a, b) => b.compareTo(a)); int currentRank = 1; // lowerScoreGap -> lowerHandicap -> olderAge 조합 특별 처리 if (hasLowerScoreGapOption && hasLowerHandicapOption && hasOlderAgeOption) { // 정렬된 점수 목록에서 점수 격차 그룹화 Map> scoreGapGroups = {}; // 점수 격차 계산 및 그룹화 for (var score in scores) { int scoreGap = _calculateScoreGap(score.frames); if (!scoreGapGroups.containsKey(scoreGap)) { scoreGapGroups[scoreGap] = []; } scoreGapGroups[scoreGap]!.add(score); } // 점수 격차 오름차순 정렬 (격차가 적은 것이 우선) List sortedGaps = scoreGapGroups.keys.toList()..sort((a, b) => a.compareTo(b)); // 각 점수 격차 그룹에 대해 처리 for (int gap in sortedGaps) { List gapGroup = scoreGapGroups[gap]!; // 동일 점수 격차 그룹 내에서는 동일 순위 부여 (lowerScoreGap 옵션에 의해) for (var score in gapGroup) { ranks[score.id] = currentRank; } // 다음 그룹은 이전 그룹의 크기만큼 순위 증가 currentRank += gapGroup.length; } } // lowerScoreGap 옵션이 있고 위 특별 케이스가 아닌 경우 동점자에게 동일 순위 부여 else if (hasLowerScoreGapOption) { for (int scoreValue in sortedScores) { List group = scoreGroups[scoreValue]!; // 동일 순위 부여 for (var score in group) { ranks[score.id] = currentRank; } currentRank += group.length; } } // olderAge 옵션이 있고 lowerScoreGap 옵션이 없는 경우 순차적 순위 부여 else if (hasOlderAgeOption && !hasLowerScoreGapOption) { for (int scoreValue in sortedScores) { List group = scoreGroups[scoreValue]!; List orderedGroup = []; // scores 리스트의 순서를 유지하면서 현재 그룹의 점수들을 추출 for (var score in scores) { if (group.any((groupScore) => groupScore.id == score.id)) { orderedGroup.add(score); } } // 정렬된 순서대로 순차적 순위 부여 for (int i = 0; i < orderedGroup.length; i++) { ranks[orderedGroup[i].id] = currentRank + i; } currentRank += orderedGroup.length; } } // 그 외의 경우는 동점자 동일 순위 부여 else { for (int scoreValue in sortedScores) { List group = scoreGroups[scoreValue]!; // 동일 순위 부여 for (var score in group) { ranks[score.id] = currentRank; } currentRank += group.length; } } return ranks; } }