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 : }
|