LCOV - code coverage report

Current view: | top level - lib/utils - tie_breaker_util.dart |  |  | Coverage | Total | Hit
Test: | lcov.info |  | Lines: | 66.7 % | 168 | 112
Test Date: | 2025-08-12 03:21:51 |  | Functions: | - | 0 | 0

                Line data    Source code

           1              : import 'package:lanebow/models/member_model.dart';
           2              : import 'package:lanebow/models/score_model.dart';
           3              : import 'package:lanebow/models/tie_breaker_option.dart';
           4              :
           5              : /// 동점자 처리를 위한 유틸리티 클래스
           6              : class TieBreakerUtil {
           7              :   /// 동점자 처리 옵션에 따라 점수 목록을 정렬
           8              :   ///
           9              :   /// [scores] 정렬할 점수 목록
          10              :   /// [options] 적용할 동점자 처리 옵션 우선순위 목록
          11              :   /// [members] 회원 정보 목록 (연령 기준 정렬 시 필요)
          12              :   /// [includeHandicap] 핸디캡 포함 여부
          13              :   ///
          14              :   /// 반환값: 정렬된 점수 목록
          15            2 :   static List<Score> sortScoresByOptions(
          16              :     List<Score> scores,
          17              :     List<TieBreakerOption>? options,
          18              :     List<Member>? members,
          19              :     bool includeHandicap
          20              :   ) {
          21              :     // 옵션이 없거나 비어있으면 기본 정렬만 적용
          22            2 :     if (options == null || options.isEmpty) {
          23            1 :       return _sortScoresByDefault(scores, includeHandicap);
          24              :     }
          25              :
          26              :     // olderAge 옵션이 단독으로 있는 경우 특별 처리
          27            5 :     if (options.length == 1 && options.contains(TieBreakerOption.olderAge)) {
          28            1 :       if (members == null || members.isEmpty) {
          29            0 :         return _sortScoresByDefault(scores, includeHandicap);
          30              :       }
          31              :
          32              :       // 연장자 순으로 정렬
          33            3 :       return List.from(scores)..sort((a, b) {
          34            2 :         DateTime? birthDateA = _findMemberBirthDate(a.memberId, members);
          35            2 :         DateTime? birthDateB = _findMemberBirthDate(b.memberId, members);
          36              :
          37              :         if (birthDateA == null || birthDateB == null) {
          38              :           // 생년월일이 없으면 점수로 정렬
          39            0 :           int scoreA = includeHandicap ? (a.totalScore + (a.handicap ?? 0)) : a.totalScore;
          40            0 :           int scoreB = includeHandicap ? (b.totalScore + (b.handicap ?? 0)) : b.totalScore;
          41            0 :           return scoreB.compareTo(scoreA); // 내림차순
          42              :         }
          43              :
          44            1 :         return birthDateA.compareTo(birthDateB); // 오름차순 정렬로 연장자 우선
          45              :       });
          46              :     }
          47              :
          48              :     // 기본 점수 정렬 (점수 내림차순)
          49            2 :     List<Score> sortedScores = _sortScoresByDefault(scores, includeHandicap);
          50              :
          51              :     // 점수별 그룹화
          52            2 :     Map<int, List<Score>> scoreGroups = {};
          53              :
          54            6 :     for (int i = 0; i < sortedScores.length; i++) {
          55              :       int currentScore = includeHandicap
          56           10 :           ? (sortedScores[i].totalScore + (sortedScores[i].handicap ?? 0))
          57            4 :           : sortedScores[i].totalScore;
          58              :
          59            2 :       if (!scoreGroups.containsKey(currentScore)) {
          60            4 :         scoreGroups[currentScore] = [];
          61              :       }
          62            6 :       scoreGroups[currentScore]?.add(sortedScores[i]);
          63              :     }
          64              :
          65              :     // 결과 목록 준비
          66            2 :     List<Score> result = [];
          67              :
          68              :     // 점수 내림차순 정렬
          69           10 :     List<int> sortedScoreValues = scoreGroups.keys.toList()..sort((a, b) => b.compareTo(a));
          70              :
          71              :     // 각 점수 그룹에 대해 처리
          72            4 :     for (int scoreValue in sortedScoreValues) {
          73            2 :       List<Score> group = scoreGroups[scoreValue]!;
          74              :
          75              :       // 그룹 크기가 1이면 그대로 추가
          76            4 :       if (group.length == 1) {
          77            4 :         result.add(group[0]);
          78              :         continue;
          79              :       }
          80              :
          81              :       // 동점자 그룹 정렬
          82            2 :       List<Score> resolvedGroup = List.from(group);
          83              :
          84              :       // 옵션에 따른 정렬 적용
          85            2 :       if (options.contains(TieBreakerOption.lowerHandicap)) {
          86              :         // 핸디캡이 낮은 순으로 정렬
          87            4 :         resolvedGroup.sort((a, b) {
          88            2 :           int handicapA = a.handicap ?? 0;
          89            2 :           int handicapB = b.handicap ?? 0;
          90            2 :           return handicapA.compareTo(handicapB);
          91              :         });
          92              :       }
          93              :
          94              :       // 핸디캡이 같은 경우 연장자 우선 정렬
          95            4 :       if (options.contains(TieBreakerOption.olderAge) && members != null && members.isNotEmpty) {
          96              :         // 핸디캡이 같은 그룹들로 나누기
          97            2 :         Map<int, List<Score>> handicapGroups = {};
          98            4 :         for (var score in resolvedGroup) {
          99            2 :           int handicap = score.handicap ?? 0;
         100            2 :           if (!handicapGroups.containsKey(handicap)) {
         101            4 :             handicapGroups[handicap] = [];
         102              :           }
         103            4 :           handicapGroups[handicap]!.add(score);
         104              :         }
         105              :
         106              :         // 각 핸디캡 그룹 내에서 연장자 순으로 정렬
         107            2 :         List<Score> newResolvedGroup = [];
         108           10 :         List<int> handicapValues = handicapGroups.keys.toList()..sort((a, b) => a.compareTo(b));
         109              :
         110            4 :         for (int handicap in handicapValues) {
         111            2 :           List<Score> handicapGroup = handicapGroups[handicap]!;
         112            4 :           if (handicapGroup.length > 1) {
         113            2 :             handicapGroup.sort((a, b) {
         114            2 :               DateTime? birthDateA = _findMemberBirthDate(a.memberId, members);
         115            2 :               DateTime? birthDateB = _findMemberBirthDate(b.memberId, members);
         116              :
         117              :               if (birthDateA == null || birthDateB == null) return 0;
         118            1 :               return birthDateA.compareTo(birthDateB); // 오름차순 정렬로 연장자 우선
         119              :             });
         120              :           }
         121            2 :           newResolvedGroup.addAll(handicapGroup);
         122              :         }
         123              :
         124              :         resolvedGroup = newResolvedGroup;
         125              :       }
         126              :
         127              :       // 점수 격차가 적은 순으로 정렬
         128            2 :       if (options.contains(TieBreakerOption.lowerScoreGap)) {
         129              :         // 이미 정렬된 그룹에서 추가 정렬 적용
         130            2 :         List<Score> scoreGapSorted = [];
         131              :
         132              :         // 핸디캡과 연령으로 이미 정렬된 그룹들을 찾아서 각 그룹 내에서 점수 격차 정렬 적용
         133            2 :         Map<String, List<Score>> tiedGroups = {};
         134              :         String currentKey = "";
         135              :
         136            6 :         for (int i = 0; i < resolvedGroup.length; i++) {
         137            2 :           Score current = resolvedGroup[i];
         138            2 :           int handicap = current.handicap ?? 0;
         139            2 :           String memberId = current.memberId;
         140              :
         141            2 :           String key = "$handicap-$memberId";
         142            4 :           if (i == 0 || key != currentKey) {
         143              :             currentKey = key;
         144            4 :             tiedGroups[currentKey] = [];
         145              :           }
         146            4 :           tiedGroups[currentKey]!.add(current);
         147              :         }
         148              :
         149              :         // 각 그룹에 대해 점수 격차 정렬 적용
         150            4 :         for (var group in tiedGroups.values) {
         151            4 :           if (group.length > 1) {
         152            0 :             group.sort((a, b) {
         153            0 :               int gapA = _calculateScoreGap(a.frames);
         154            0 :               int gapB = _calculateScoreGap(b.frames);
         155            0 :               return gapA.compareTo(gapB); // 오름차순 (격차가 적은 것이 우선)
         156              :             });
         157              :           }
         158            2 :           scoreGapSorted.addAll(group);
         159              :         }
         160              :
         161              :         resolvedGroup = scoreGapSorted;
         162              :       }
         163              :
         164              :       // 정렬된 그룹 추가
         165            2 :       result.addAll(resolvedGroup);
         166              :     }
         167              :
         168              :     return result;
         169              :   }
         170              :
         171              :   /// 기본 점수 정렬 (점수 내림차순)
         172            2 :   static List<Score> _sortScoresByDefault(List<Score> scores, bool includeHandicap) {
         173            6 :     return List.from(scores)..sort((a, b) {
         174            8 :       int scoreA = includeHandicap ? (a.totalScore + (a.handicap ?? 0)) : a.totalScore;
         175            8 :       int scoreB = includeHandicap ? (b.totalScore + (b.handicap ?? 0)) : b.totalScore;
         176            2 :       return scoreB.compareTo(scoreA); // 내림차순
         177              :     });
         178              :   }
         179              :
         180              :   /// 동점자 그룹 찾기
         181            0 :   static Map<int, List<Score>> _findTiedGroups(List<Score> sortedScores, bool includeHandicap) {
         182            0 :     Map<int, List<Score>> tiedGroups = {};
         183              :
         184            0 :     for (int i = 0; i < sortedScores.length; i++) {
         185              :       int currentScore = includeHandicap
         186            0 :           ? (sortedScores[i].totalScore + (sortedScores[i].handicap ?? 0))
         187            0 :           : sortedScores[i].totalScore;
         188              :
         189            0 :       List<Score> sameScores = [sortedScores[i]];
         190              :
         191              :       // 같은 점수를 가진 항목 찾기
         192            0 :       for (int j = i + 1; j < sortedScores.length; j++) {
         193              :         int nextScore = includeHandicap
         194            0 :             ? (sortedScores[j].totalScore + (sortedScores[j].handicap ?? 0))
         195            0 :             : sortedScores[j].totalScore;
         196              :
         197            0 :         if (currentScore == nextScore) {
         198            0 :           sameScores.add(sortedScores[j]);
         199              :           i = j; // 이미 처리한 항목 건너뛰기
         200              :         } else {
         201              :           break; // 다른 점수 발견 시 중단
         202              :         }
         203              :       }
         204              :
         205              :       // 동점자가 2명 이상인 경우만 그룹에 추가
         206            0 :       if (sameScores.length > 1) {
         207            0 :         tiedGroups[currentScore] = sameScores;
         208              :       }
         209              :     }
         210              :
         211              :     return tiedGroups;
         212              :   }
         213              :
         214              :   /// 동점자 처리 옵션 적용
         215            0 :   static List<Score> _applyTieBreakerOption(
         216              :     List<Score> tiedScores,
         217              :     TieBreakerOption option,
         218              :     List<Member>? members,
         219              :     bool includeHandicap
         220              :   ) {
         221              :     switch (option) {
         222            0 :       case TieBreakerOption.lowerHandicap:
         223            0 :         return _sortByLowerHandicap(tiedScores);
         224              :
         225            0 :       case TieBreakerOption.lowerScoreGap:
         226            0 :         return _sortByLowerScoreGap(tiedScores);
         227              :
         228            0 :       case TieBreakerOption.olderAge:
         229            0 :         return _sortByOlderAge(tiedScores, members);
         230              :
         231            0 :       case TieBreakerOption.none:
         232              :         return tiedScores; // 변경 없음
         233              :     }
         234              :   }
         235              :
         236              :   /// 핸디캡이 적은 순으로 정렬
         237            0 :   static List<Score> _sortByLowerHandicap(List<Score> tiedScores) {
         238            0 :     return List.from(tiedScores)..sort((a, b) {
         239            0 :       int handicapA = a.handicap ?? 0;
         240            0 :       int handicapB = b.handicap ?? 0;
         241            0 :       return handicapA.compareTo(handicapB); // 오름차순 (적은 것이 우선)
         242              :     });
         243              :   }
         244              :
         245              :   /// 점수 격차가 적은 순으로 정렬
         246            0 :   static List<Score> _sortByLowerScoreGap(List<Score> tiedScores) {
         247            0 :     return List.from(tiedScores)..sort((a, b) {
         248              :       // 프레임 점수 중 최고점과 최저점의 차이 계산
         249            0 :       int gapA = _calculateScoreGap(a.frames);
         250            0 :       int gapB = _calculateScoreGap(b.frames);
         251            0 :       return gapA.compareTo(gapB); // 오름차순 (격차가 적은 것이 우선)
         252              :     });
         253              :   }
         254              :
         255              :   /// 연장자 우선으로 정렬
         256            0 :   static List<Score> _sortByOlderAge(List<Score> tiedScores, List<Member>? members) {
         257            0 :     if (members == null || members.isEmpty) return tiedScores;
         258              :
         259            0 :     return List.from(tiedScores)..sort((a, b) {
         260              :       // 회원 정보에서 생년월일 찾기
         261            0 :       DateTime? birthDateA = _findMemberBirthDate(a.memberId, members);
         262            0 :       DateTime? birthDateB = _findMemberBirthDate(b.memberId, members);
         263              :
         264              :       // 생년월일이 없으면 정렬 불가
         265              :       if (birthDateA == null || birthDateB == null) return 0;
         266              :
         267              :       // 생년월일 비교 (오래된 날짜 = 연장자가 우선)
         268              :       // 연장자가 우선이므로 더 오래된 날짜(더 작은 값)가 앞에 와야 함
         269              :       // 연장자 우선이므로 더 오래된 날짜(A)가 더 최근 날짜(B)보다 앞에 와야 함
         270            0 :       return birthDateA.compareTo(birthDateB); // 오름차순 정렬로 연장자 우선
         271              :     });
         272              :   }
         273              :
         274              :   /// 회원 ID로 생년월일 찾기
         275            1 :   static DateTime? _findMemberBirthDate(String? memberId, List<Member> members) {
         276              :     if (memberId == null) return null;
         277              :
         278            2 :     for (var member in members) {
         279            2 :       if (member.id == memberId) {
         280            1 :         return member.birthDate;
         281              :       }
         282              :     }
         283              :     return null;
         284              :   }
         285              :
         286              :   /// 프레임 점수의 최고점과 최저점 차이 계산
         287            0 :   static int _calculateScoreGap(List<int> frames) {
         288            0 :     if (frames.isEmpty) return 0;
         289              :
         290            0 :     int max = frames.reduce((curr, next) => curr > next ? curr : next);
         291            0 :     int min = frames.reduce((curr, next) => curr < next ? curr : next);
         292              :
         293            0 :     return max - min;
         294              :   }
         295              :
         296              :   /// 모든 점수가 서로 다른지 확인
         297            0 :   static bool _allScoresHaveDifferentValues(List<Score> scores, bool includeHandicap) {
         298              :     Set<int> uniqueScores = {};
         299              :
         300            0 :     for (var score in scores) {
         301              :       int totalScore = includeHandicap
         302            0 :           ? (score.totalScore + (score.handicap ?? 0))
         303            0 :           : score.totalScore;
         304              :
         305            0 :       if (uniqueScores.contains(totalScore)) {
         306              :         return false;
         307              :       }
         308              :
         309            0 :       uniqueScores.add(totalScore);
         310              :     }
         311              :
         312              :     return true;
         313              :   }
         314              :
         315              :   /// 정렬된 동점자 그룹으로 원래 목록 업데이트
         316            2 :   static Map<String, int> calculateRanks(
         317              :     List<Score> scores,
         318              :     bool includeHandicap, {
         319              :     List<TieBreakerOption>? options,
         320              :     List<Member>? members
         321              :   }) {
         322            2 :     Map<String, int> ranks = {};
         323              :
         324              :     // 점수가 없으면 빈 맵 반환
         325            2 :     if (scores.isEmpty) {
         326              :       return ranks;
         327              :     }
         328              :
         329              :     // 핸디캡 포함 여부에 따른 총점 계산
         330            2 :     List<Score> sortedScores = List.from(scores);
         331              :
         332              :     // 순위 계산을 위한 총점 그룹 구분
         333            2 :     Map<int, List<Score>> totalScoreGroups = {};
         334              :
         335            4 :     for (var score in sortedScores) {
         336            8 :       int totalScore = includeHandicap ? (score.totalScore + (score.handicap ?? 0)) : score.totalScore;
         337              :
         338            2 :       if (!totalScoreGroups.containsKey(totalScore)) {
         339            4 :         totalScoreGroups[totalScore] = [];
         340              :       }
         341              :
         342            4 :       totalScoreGroups[totalScore]!.add(score);
         343              :     }
         344              :
         345              :     // 총점 내림차순 정렬
         346            4 :     List<int> sortedTotalScores = totalScoreGroups.keys.toList();
         347            6 :     sortedTotalScores.sort((a, b) => b.compareTo(a));
         348              :
         349              :     int currentRank = 1;
         350              :
         351              :     // 총점별 그룹 순회
         352            4 :     for (int totalScore in sortedTotalScores) {
         353            2 :       List<Score> totalScoreGroup = totalScoreGroups[totalScore]!;
         354              :
         355              :       // 그룹 내 점수가 1개라면 현재 순위 부여
         356            4 :       if (totalScoreGroup.length == 1) {
         357            6 :         ranks[totalScoreGroup[0].id] = currentRank;
         358            2 :         currentRank++;
         359              :         continue;
         360              :       }
         361              :
         362              :       // 옵션이 없으면 모두 동일 순위 부여
         363            1 :       if (options == null || options.isEmpty) {
         364            4 :         for (var score in totalScoreGroup) {
         365            4 :           ranks[score.id] = currentRank;
         366              :         }
         367              :
         368              :         // 다음 순위는 현재 그룹의 크기만큼 증가
         369            4 :         currentRank += totalScoreGroup.length;
         370              :       } else {
         371              :         // 순위 부여 규칙:
         372              :         // - olderAge가 포함된 경우: 동점 그룹 내에서 현재 리스트 순서대로 개별 순위를 부여
         373              :         // - 그 외(옵션 없음/olderAge 제외 옵션): 동점 그룹 전체에 동일 순위를 부여
         374            1 :         if (options.contains(TieBreakerOption.olderAge)) {
         375              :           // olderAge가 포함된 경우의 기본: 순서대로 개별 순위 부여
         376              :           // 단, 옵션 순서가 lowerScoreGap -> lowerHandicap -> olderAge인 경우에는
         377              :           // 동일 핸디캡을 가진 연속 항목들은 동일 순위를 부여하고, 핸디캡이 바뀔 때만 순위를 증가시킨다.
         378            1 :           final idxGap = options.indexOf(TieBreakerOption.lowerScoreGap);
         379            1 :           final idxHcp = options.indexOf(TieBreakerOption.lowerHandicap);
         380            1 :           final idxAge = options.indexOf(TieBreakerOption.olderAge);
         381              :           final isGapThenHcpThenAge =
         382            8 :               idxGap != -1 && idxHcp != -1 && idxAge != -1 && idxGap < idxHcp && idxHcp < idxAge;
         383              :
         384              :           if (isGapThenHcpThenAge) {
         385              :             int? lastHandicap;
         386              :             int currentRankLocal = currentRank;
         387              :             int blockSize = 0;
         388            3 :             for (var i = 0; i < totalScoreGroup.length; i++) {
         389            1 :               final score = totalScoreGroup[i];
         390            1 :               final hcp = score.handicap ?? 0;
         391              :               if (lastHandicap == null) {
         392              :                 // 첫 항목은 현재 랭크 부여
         393              :                 lastHandicap = hcp;
         394              :                 blockSize = 1;
         395            2 :                 ranks[score.id] = currentRankLocal;
         396            1 :               } else if (hcp == lastHandicap) {
         397              :                 // 같은 핸디캡: 동일 랭크 부여
         398            1 :                 blockSize += 1;
         399            2 :                 ranks[score.id] = currentRankLocal;
         400              :               } else {
         401              :                 // 다른 핸디캡: 블록 크기만큼 랭크를 증가시키고 새 블록 시작
         402            1 :                 currentRankLocal += blockSize;
         403              :                 lastHandicap = hcp;
         404              :                 blockSize = 1;
         405            2 :                 ranks[score.id] = currentRankLocal;
         406              :               }
         407              :             }
         408            2 :             currentRank += totalScoreGroup.length;
         409              :           } else {
         410            3 :             for (var i = 0; i < totalScoreGroup.length; i++) {
         411            4 :               ranks[totalScoreGroup[i].id] = currentRank + i;
         412              :             }
         413            2 :             currentRank += totalScoreGroup.length;
         414              :           }
         415              :         } else {
         416            2 :           for (var score in totalScoreGroup) {
         417            2 :             ranks[score.id] = currentRank;
         418              :           }
         419            2 :           currentRank += totalScoreGroup.length;
         420              :         }
         421              :       }
         422              :     }
         423              :
         424              :     return ranks;
         425              :   }
         426              :
         427              : }

Generated by: LCOV version 2.3.1-1