TDD 작성
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
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<Score> sortScoresByOptions(
|
||||
List<Score> scores,
|
||||
List<TieBreakerOption>? options,
|
||||
List<Member>? 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<Score> sortedScores = _sortScoresByDefault(scores, includeHandicap);
|
||||
|
||||
// 점수별 그룹화
|
||||
Map<int, List<Score>> 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<Score> result = [];
|
||||
|
||||
// 점수 내림차순 정렬
|
||||
List<int> sortedScoreValues = scoreGroups.keys.toList()..sort((a, b) => b.compareTo(a));
|
||||
|
||||
// 각 점수 그룹에 대해 처리
|
||||
for (int scoreValue in sortedScoreValues) {
|
||||
List<Score> group = scoreGroups[scoreValue]!;
|
||||
|
||||
// 그룹 크기가 1이면 그대로 추가
|
||||
if (group.length == 1) {
|
||||
result.add(group[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 동점자 그룹 정렬
|
||||
List<Score> 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<int, List<Score>> handicapGroups = {};
|
||||
for (var score in resolvedGroup) {
|
||||
int handicap = score.handicap ?? 0;
|
||||
if (!handicapGroups.containsKey(handicap)) {
|
||||
handicapGroups[handicap] = [];
|
||||
}
|
||||
handicapGroups[handicap]!.add(score);
|
||||
}
|
||||
|
||||
// 각 핸디캡 그룹 내에서 연장자 순으로 정렬
|
||||
List<Score> newResolvedGroup = [];
|
||||
List<int> handicapValues = handicapGroups.keys.toList()..sort((a, b) => a.compareTo(b));
|
||||
|
||||
for (int handicap in handicapValues) {
|
||||
List<Score> 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<Score> scoreGapSorted = [];
|
||||
|
||||
// 핸디캡과 연령으로 이미 정렬된 그룹들을 찾아서 각 그룹 내에서 점수 격차 정렬 적용
|
||||
Map<String, List<Score>> 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<Score> _sortScoresByDefault(List<Score> 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<int, List<Score>> _findTiedGroups(List<Score> sortedScores, bool includeHandicap) {
|
||||
Map<int, List<Score>> tiedGroups = {};
|
||||
|
||||
for (int i = 0; i < sortedScores.length; i++) {
|
||||
int currentScore = includeHandicap
|
||||
? (sortedScores[i].totalScore + (sortedScores[i].handicap ?? 0))
|
||||
: sortedScores[i].totalScore;
|
||||
|
||||
List<Score> 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<Score> _applyTieBreakerOption(
|
||||
List<Score> tiedScores,
|
||||
TieBreakerOption option,
|
||||
List<Member>? 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<Score> _sortByLowerHandicap(List<Score> tiedScores) {
|
||||
return List.from(tiedScores)..sort((a, b) {
|
||||
int handicapA = a.handicap ?? 0;
|
||||
int handicapB = b.handicap ?? 0;
|
||||
return handicapA.compareTo(handicapB); // 오름차순 (적은 것이 우선)
|
||||
});
|
||||
}
|
||||
|
||||
/// 점수 격차가 적은 순으로 정렬
|
||||
static List<Score> _sortByLowerScoreGap(List<Score> tiedScores) {
|
||||
return List.from(tiedScores)..sort((a, b) {
|
||||
// 프레임 점수 중 최고점과 최저점의 차이 계산
|
||||
int gapA = _calculateScoreGap(a.frames);
|
||||
int gapB = _calculateScoreGap(b.frames);
|
||||
return gapA.compareTo(gapB); // 오름차순 (격차가 적은 것이 우선)
|
||||
});
|
||||
}
|
||||
|
||||
/// 연장자 우선으로 정렬
|
||||
static List<Score> _sortByOlderAge(List<Score> tiedScores, List<Member>? 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<Member> members) {
|
||||
if (memberId == null) return null;
|
||||
|
||||
for (var member in members) {
|
||||
if (member.id == memberId) {
|
||||
return member.birthDate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 프레임 점수의 최고점과 최저점 차이 계산
|
||||
static int _calculateScoreGap(List<int> 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<Score> scores, bool includeHandicap) {
|
||||
Set<int> 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<String, int> calculateRanks(
|
||||
List<Score> scores,
|
||||
bool includeHandicap, {
|
||||
List<TieBreakerOption>? options,
|
||||
List<Member>? members
|
||||
}) {
|
||||
Map<String, int> ranks = {};
|
||||
|
||||
// 점수가 없으면 빈 맵 반환
|
||||
if (scores.isEmpty) {
|
||||
return ranks;
|
||||
}
|
||||
|
||||
// 핸디캡 포함 여부에 따른 총점 계산
|
||||
List<Score> sortedScores = List.from(scores);
|
||||
|
||||
// 순위 계산을 위한 총점 그룹 구분
|
||||
Map<int, List<Score>> 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<int> sortedTotalScores = totalScoreGroups.keys.toList();
|
||||
sortedTotalScores.sort((a, b) => b.compareTo(a));
|
||||
|
||||
int currentRank = 1;
|
||||
|
||||
// 총점별 그룹 순회
|
||||
for (int totalScore in sortedTotalScores) {
|
||||
List<Score> 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user