diff --git a/data/xsl/index.xsl b/data/xsl/index.xsl index 8f077f58b..e817426df 100644 --- a/data/xsl/index.xsl +++ b/data/xsl/index.xsl @@ -234,7 +234,14 @@ - + + + HM + + + + + @@ -342,7 +349,14 @@ - + + + HM + + + + + diff --git a/src/edu/csus/ecs/pc2/core/model/FinalizeData.java b/src/edu/csus/ecs/pc2/core/model/FinalizeData.java index af58b213d..a7f2a108c 100644 --- a/src/edu/csus/ecs/pc2/core/model/FinalizeData.java +++ b/src/edu/csus/ecs/pc2/core/model/FinalizeData.java @@ -1,4 +1,4 @@ -// Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2026 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.core.model; import java.io.Serializable; @@ -199,4 +199,21 @@ public int getHighHonorSolvedCount() { public int getHonorSolvedCount() { return honorSolvedCount; } + + + /** + * Generate some default finalize data + * @return FinalizeData object + */ + public static FinalizeData getDefaultFinalizeData() + { + FinalizeData finalizeData = new FinalizeData(); + finalizeData.setGoldRank(4); + finalizeData.setSilverRank(8); + finalizeData.setBronzeRank(12); + finalizeData.setCertified(false); + finalizeData.setComment("Preliminary Results - Contest not Finalized"); + finalizeData.setUseWFGroupRanking(true); + return(finalizeData); + } } diff --git a/src/edu/csus/ecs/pc2/core/scoring/DefaultScoringAlgorithm.java b/src/edu/csus/ecs/pc2/core/scoring/DefaultScoringAlgorithm.java index 8b8c65286..e3b4c34af 100644 --- a/src/edu/csus/ecs/pc2/core/scoring/DefaultScoringAlgorithm.java +++ b/src/edu/csus/ecs/pc2/core/scoring/DefaultScoringAlgorithm.java @@ -1,4 +1,4 @@ -// Copyright (C) 1989-2025 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2026 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.core.scoring; import java.io.IOException; @@ -354,6 +354,8 @@ public String getStandings(IInternalContest theContest, Run[] runs, Integer divi this.log = inputLog; long freezeSeconds = -1; boolean isThawn = false; + FinalizeData finalizeData = null; + if (obeyFreeze) { String freezeTime = theContest.getContestInformation().getFreezeTime(); try { @@ -366,7 +368,7 @@ public String getStandings(IInternalContest theContest, Run[] runs, Integer divi log.warning("Could not convert '"+freezeTime+"' to seconds"); throw new InvalidParameterException("Invalid freezeTime "+freezeTime); } - FinalizeData finalizeData = theContest.getFinalizeData(); // sometimes, eg junit this is null + finalizeData = theContest.getFinalizeData(); // sometimes, eg junit this is null if (finalizeData != null && finalizeData.isCertified() && theContest.getContestInformation().isUnfrozen()) { isThawn = true; } @@ -410,10 +412,10 @@ public String getStandings(IInternalContest theContest, Run[] runs, Integer divi } summaryMemento.putLong("problemCount", problems.length); - + Site[] sites = theContest.getSites(); summaryMemento.putInteger("siteCount", sites.length); - + Group[] groups = theContest.getGroups(); boolean bGroupsExcluded = false; if (groups != null) { @@ -425,7 +427,7 @@ public String getStandings(IInternalContest theContest, Run[] runs, Integer divi //Note also that generateSummaryTotalsForProblem() already existed and was inserting MOST of that information in the output already; // the "colorList" processing code which used to be here was simply added to that method instead (adding internalId, letter, and // url to the elements), avoiding duplication of problem description data in the output XML. - + if (runs == null) { // Note: we do not deal with divisionNumber here since // 1) it is being deprecated @@ -512,16 +514,46 @@ public String getStandings(IInternalContest theContest, Run[] runs, Integer divi applyScoringAdjustments(standingsRecordHash, accountList); + boolean wfStandings = false; + int medianSolved = 0; + + DefaultStandingsRecordComparator dsrc = new DefaultStandingsRecordComparator(); + dsrc.setCachedAccountList(accountList); + // use TreeMap to sort - DefaultStandingsRecordComparator src = new DefaultStandingsRecordComparator(); - src.setCachedAccountList(accountList); - TreeMap treeMap = new TreeMap(src); + TreeMap treeMap = new TreeMap(dsrc); Collection enumeration = standingsRecordHash.values(); for (StandingsRecord record : enumeration) { treeMap.put(record, record); } - createStandingXML(treeMap, mementoRoot, accountList, problems, problemsIndexHash, groups, theContest, summaryMemento, bGroupsExcluded); + // Determine how we want to sort the teams. We may have to re-sort using WF rankings for the public final scoreboard + // For the public scoreboard, isThawn is true, however, we only want to use WF rankings if it is specified + // to do so, and, it's for the entire contest, not just a group(s) + // If you're wondering why we sort twice (once above, and once in this "if" block: + // We need to get the median solved, so, we have to sort normally, + // just to get the median so the FinalsStandingsRecordComparator can use it. + if(isThawn && finalizeData.isUseWFGroupRanking() && (wantedGroups == null || wantedGroups.isEmpty())) { + FinalsStandingsRecordComparator fsrc = new FinalsStandingsRecordComparator(); + fsrc.setCachedAccountList(accountList); + fsrc.setLastRank(finalizeData.getBronzeRank()); + if (finalizeData.getHonorSolvedCount() != 0) { + medianSolved = finalizeData.getHonorSolvedCount(); + } else { + medianSolved = getMedian(treeMap.keySet().toArray(new StandingsRecord[0])); + } + fsrc.setMedian(medianSolved); + wfStandings = true; + + // Now we have to re-sort the treeMap using WF sorting + treeMap = new TreeMap(fsrc); + enumeration = standingsRecordHash.values(); + for (StandingsRecord record : enumeration) { + treeMap.put(record, record); + } + } + + createStandingXML(treeMap, mementoRoot, accountList, problems, problemsIndexHash, groups, theContest, summaryMemento, bGroupsExcluded, wfStandings, medianSolved); } // mutex @@ -655,9 +687,12 @@ private boolean dumpGroupList(Group[] groups, IMemento memento, List want */ private void createStandingXML (TreeMap treeMap, XMLMemento mementoRoot, AccountList accountList, Problem[] problems, Hashtable problemsIndexHash, Group[] groups, - IInternalContest theContest, IMemento summaryMememento, boolean excludedGroups) { + IInternalContest theContest, IMemento summaryMememento, boolean excludedGroups, boolean isWFStandings, int medianSolved) { ContestInformation contestInformation = theContest.getContestInformation(); + // This may be needed if using WF Standings + int lastMedalRank = 0; + // easy access Hashtable groupHash = new Hashtable(); Hashtable groupIndexHash = new Hashtable(); @@ -695,7 +730,16 @@ private void createStandingXML (TreeMap treeMa divisionCount = highestFound; String teamVarDisplayString = contestInformation.getTeamScoreboardDisplayFormat(); - StandingsRecord[] srArray = new StandingsRecord[treeMap.size()]; + // get the sorted treeMap as an array for createCitationRankInformation and later for medal citations + StandingsRecord[] srArray = treeMap.values().toArray(new StandingsRecord[0]); + + // If doing public WF style standings, we assign the block ranks now + if(isWFStandings) { + if(Utilities.isDebugMode()) { + System.err.println("createStandingsXML: Generating Final board for public (obeyFreeze)"); + } + assignRanksBlock(srArray, theContest, medianSolved); + } Collection coll = treeMap.values(); Iterator iterator = coll.iterator(); @@ -754,16 +798,19 @@ private void createStandingXML (TreeMap treeMa Object o = iterator.next(); StandingsRecord standingsRecord = (StandingsRecord)o; - indexRank++; - if (!isTeamTied(standingsRecord, numSolved, score, lastSolved)) { - numSolved = standingsRecord.getNumberSolved(); - score = standingsRecord.getPenaltyPoints(); - lastSolved = standingsRecord.getLastSolved(); - rank = indexRank; - standingsRecord.setRankNumber(rank); - } else { - // current user tied with last user, so same rank - standingsRecord.setRankNumber(rank); + // Only assign rank if not doing WF final rankings on public scoreboard because we did it above with assignRankBlocks() + if(!isWFStandings) { + indexRank++; + if (!isTeamTied(standingsRecord, numSolved, score, lastSolved)) { + numSolved = standingsRecord.getNumberSolved(); + score = standingsRecord.getPenaltyPoints(); + lastSolved = standingsRecord.getLastSolved(); + rank = indexRank; + standingsRecord.setRankNumber(rank); + } else { + // current user tied with last user, so same rank + standingsRecord.setRankNumber(rank); + } } // mementoRoot.putMemento(standingsRecord.toMemento()); long totalAttempts = 0; @@ -901,13 +948,14 @@ private void createStandingXML (TreeMap treeMa // entire contest, not one particular group (or groups). So, if any groups were excluded // from the scoreboard, we do not fill in citations. if(!excludedGroups) { - // Now go back and fill in any award citations for the teams - ResultsFile resultsInfo = new ResultsFile(); - // Note that createCitationRankInformation() may change the order of srArray - CitationRankInformation ri = resultsInfo.createCitationRankInformation(theContest, srArray); if(index == teamStandingsMementos.size()) { int teamRank, sIndex; IMemento standingsRecordMemento; + // Now go back and fill in any award citations for the teams + ResultsFile resultsInfo = new ResultsFile(); + // Note that createCitationRankInformation() may change the order of srArray + CitationRankInformation ri = resultsInfo.createCitationRankInformation(theContest, srArray); + for(sIndex = 0; sIndex < index; ) { standingsRecordMemento = teamStandingsMementos.get(sIndex); teamRank = srArray[sIndex].getRankNumber(); @@ -938,6 +986,81 @@ private void createStandingXML (TreeMap treeMa } } + /** + * Assigns WF style "honors" ranking values. + * This is only used to generate the public (obeyFreeze), unfrozen, finalized scoreboard. + * This logic similar to ResultsFile.createFileLines(). + * + * @param srArray - sorted array of standings records + * @param theContest + * @param median - number of problems solved by median team + */ + private void assignRanksBlock(StandingsRecord [] srArray, IInternalContest theContest, int median) { + + int rank = 1; + StandingsRecord sr, prev = null; + int idx = 0; + int numRecs = srArray.length; + FinalizeData finalizeData = theContest.getFinalizeData(); + boolean didFirstHonorableMention = false; + + // Tammy: just in case it IS null; purportedly it may be null for Junit's, but that is unconfirmed. + if(finalizeData == null) { + finalizeData = FinalizeData.getDefaultFinalizeData(); + } + + // First one, handle it special to make the for loop below easier. Basically, we're priming the pump here (prev). + if (numRecs > 0) { + sr = srArray[idx]; + sr.setRankNumber(rank); + prev = sr; + idx++; + } + + int numInBlock = 0; + + // We always rank teams uniquely that get medals, and, the first team AFTER the medals is assigned + // its own rank as well. + int alwaysRanked = finalizeData.getBronzeRank(); + + if(finalizeData.getHonorSolvedCount() > 0) { + median = finalizeData.getHonorSolvedCount(); + } + + if(Utilities.isDebugMode()) { + System.err.printf("assignRankBlock: alwaysRanked=%d median=%d%n", alwaysRanked, median); + } + for(; idx < numRecs; idx++) { + sr = srArray[idx]; + if(Utilities.isDebugMode()) { + System.err.printf(" rank=%d sr.getNumberSolved=%d prev.getNumberSolver=%d numInBlock=%d%n", + rank, sr.getNumberSolved(), prev.getNumberSolved(), numInBlock); + } + // For medals, each team is ranked. Or, if after medals, teams are grouped by # solved until honorable mention + if (rank <= alwaysRanked || (!didFirstHonorableMention && sr.getNumberSolved() != prev.getNumberSolved())) { + rank++; + rank += numInBlock; + numInBlock = 0; + if(sr.getNumberSolved() < median) { + // From now on, all teams are the same rank since they're all honorable mention + if(Utilities.isDebugMode()) { + System.err.printf(" FOUND first honorable mention at idx=%d rank=%d%n", idx, rank); + } + didFirstHonorableMention = true; + } + } else { + numInBlock++; + } + + sr.setRankNumber(rank); + prev = sr; + if(Utilities.isDebugMode()) { + System.err.printf(" Setting team at index %d(%s) to rank %d%n", idx, sr.getClientId().getName(), rank); + } + } + } + + /** * Input is a sorted ranking list. What is the number of problems solved by the median team? * @@ -1034,7 +1157,7 @@ private void generateSummaryTotalsForProblem(Problem[] problems, Hashtable