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