1 package org.unicode.cldr.util; 2 3 import java.sql.Timestamp; 4 import java.util.ArrayList; 5 import java.util.Collections; 6 import java.util.Comparator; 7 import java.util.Date; 8 import java.util.EnumMap; 9 import java.util.EnumSet; 10 import java.util.HashMap; 11 import java.util.Iterator; 12 import java.util.LinkedHashMap; 13 import java.util.LinkedHashSet; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.Map.Entry; 17 import java.util.Set; 18 import java.util.TreeMap; 19 import java.util.TreeSet; 20 import java.util.regex.Matcher; 21 import java.util.regex.Pattern; 22 23 import org.unicode.cldr.test.CheckWidths; 24 import org.unicode.cldr.test.DisplayAndInputProcessor; 25 import org.unicode.cldr.util.VettingViewer.VoteStatus; 26 27 import com.google.common.base.Objects; 28 import com.google.common.collect.ImmutableSet; 29 import com.ibm.icu.text.Collator; 30 import com.ibm.icu.util.ULocale; 31 32 /** 33 * This class implements the vote resolution process agreed to by the CLDR 34 * committee. Here is an example of usage: 35 * 36 * <pre> 37 * // before doing anything, initialize the voter data (who are the voters at what levels) with setVoterToInfo. 38 * // We assume this doesn't change often 39 * // here is some fake data: 40 * VoteResolver.setVoterToInfo(Utility.asMap(new Object[][] { 41 * { 666, new VoterInfo(Organization.google, Level.vetter, "J. Smith") }, 42 * { 555, new VoterInfo(Organization.google, Level.street, "S. Jones") }, 43 * { 444, new VoterInfo(Organization.google, Level.vetter, "S. Samuels") }, 44 * { 333, new VoterInfo(Organization.apple, Level.vetter, "A. Mutton") }, 45 * { 222, new VoterInfo(Organization.adobe, Level.expert, "A. Aldus") }, 46 * { 111, new VoterInfo(Organization.ibm, Level.street, "J. Henry") }, })); 47 * 48 * // you can create a resolver and keep it around. It isn't thread-safe, so either have a separate one per thread (they 49 * // are small), or synchronize. 50 * VoteResolver resolver = new VoteResolver(); 51 * 52 * // For any particular base path, set the values 53 * // set the 1.5 status (if we're working on 1.6). This <b>must</b> be done for each new base path 54 * resolver.newPath(oldValue, oldStatus); 55 * [TODO: function newPath doesn't exist, revise this documentation] 56 * 57 * // now add some values, with who voted for them 58 * resolver.add(value1, voter1); 59 * resolver.add(value1, voter2); 60 * resolver.add(value2, voter3); 61 * 62 * // Once you've done that, you can get the results for the base path 63 * winner = resolver.getWinningValue(); 64 * status = resolver.getWinningStatus(); 65 * conflicts = resolver.getConflictedOrganizations(); 66 * </pre> 67 */ 68 public class VoteResolver<T> { 69 private static final boolean DEBUG = false; 70 71 /** 72 * A placeholder for winningValue when it would otherwise be null. 73 * It must match NO_WINNING_VALUE in the client JavaScript code. 74 */ 75 private static String NO_WINNING_VALUE = "no-winning-value"; 76 77 /** 78 * The status levels according to the committee, in ascending order 79 * 80 * Status corresponds to icons as follows: 81 * A checkmark means it’s approved and is slated to be used. A cross means it’s a missing value. 82 * Green/orange check: The item has enough votes to be used in CLDR. 83 * Red/orange/black X: The item does not have enough votes to be used in CLDR, by most implementations (or is completely missing). 84 * Reference: http://cldr.unicode.org/translation/getting-started/guide 85 * 86 * New January, 2019: When the item is inherited, i.e., winningValue is INHERITANCE_MARKER (↑↑↑), 87 * then orange/red X are replaced by orange/red up-arrow. That change is made only on the client. 88 * Reference: https://unicode.org/cldr/trac/ticket/11103 89 * 90 * Status.approved: approved.png = green check 91 * Status.contributed: contributed.png = orange check 92 * Status.provisional: provisional.png = orange X (or inherited_provisional.png orange up-arrow if inherited) 93 * Status.unconfirmed: unconfirmed.png = red X (or inherited_unconfirmed.png red up-arrow if inherited 94 * Status.missing: missing.png = black X 95 */ 96 public enum Status { 97 missing, unconfirmed, provisional, contributed, approved; fromString(String source)98 public static Status fromString(String source) { 99 return source == null ? missing : Status.valueOf(source); 100 } 101 } 102 103 /** 104 * This is the "high bar" level where flagging is required. 105 * @see #getRequiredVotes() 106 */ 107 public static final int HIGH_BAR = Level.tc.votes; 108 109 public static final int LOWER_BAR = (2 * Level.vetter.votes); 110 111 /** 112 * This is the level at which a vote counts. Each level also contains the 113 * weight. 114 * 115 * Code related to Level.expert removed 2021-05-18 per CLDR-14597 116 */ 117 public enum Level { 118 locked( 0 /* votes */, 999 /* stlevel */), 119 street( 1 /* votes */, 10 /* stlevel */), 120 anonymous(0 /* votes */, 8 /* stlevel */), 121 vetter( 4 /* votes */, 5 /* stlevel */), 122 manager( 4 /* votes */, 2 /* stlevel */), 123 tc( 20 /* votes */, 1 /* stlevel */), 124 admin( 100 /* votes */, 0 /* stlevel */); 125 126 /** 127 * PERMANENT_VOTES is used by TC voters to "lock" locale+path permanently (including future versions, until unlocked), 128 * in the current VOTE_VALUE table. It is public for STFactory.java and PermanentVote.java. 129 */ 130 public static final int PERMANENT_VOTES = 1000; 131 132 /** 133 * LOCKING_VOTES is used (nominally by ADMIN voter, but not really by someone logged in as ADMIN, instead 134 * by combination of two PERMANENT_VOTES) to "lock" locale+path permanently in the LOCKED_XPATHS table. 135 * It is public for STFactory.PerLocaleData.loadVoteValues. 136 */ 137 public static final int LOCKING_VOTES = 2000; 138 139 /** 140 * The vote count a user of this level normally votes with 141 */ 142 private final int votes; 143 144 /** 145 * The level as an integer, where 0 = admin, ..., 999 = locked 146 */ 147 private final int stlevel; 148 Level(int votes, int stlevel)149 private Level(int votes, int stlevel) { 150 this.votes = votes; 151 this.stlevel = stlevel; 152 } 153 154 /** 155 * Get the votes for each level 156 */ getVotes()157 public int getVotes() { 158 return votes; 159 } 160 161 /** 162 * Get the Survey Tool userlevel for each level. (0=admin, 999=locked) 163 */ getSTLevel()164 public int getSTLevel() { 165 return stlevel; 166 } 167 168 /** 169 * Find the Level, given ST Level 170 * 171 * @param stlevel 172 * @return the Level corresponding to the integer 173 */ fromSTLevel(int stlevel)174 public static Level fromSTLevel(int stlevel) { 175 for (Level l : Level.values()) { 176 if (l.getSTLevel() == stlevel) { 177 return l; 178 } 179 } 180 return null; 181 } 182 183 /** 184 * Policy: can this user manage the "other" user's settings? 185 * 186 * @param myOrg 187 * the current organization 188 * @param otherLevel 189 * the other user's level 190 * @param otherOrg 191 * the other user's organization 192 * @return 193 */ isManagerFor(Organization myOrg, Level otherLevel, Organization otherOrg)194 public boolean isManagerFor(Organization myOrg, Level otherLevel, Organization otherOrg) { 195 return (this == admin || (canManageSomeUsers() && 196 (myOrg == otherOrg) && this.getSTLevel() <= otherLevel.getSTLevel())); 197 } 198 199 /** 200 * Policy: Can this user manage any users? 201 * 202 * @return 203 */ canManageSomeUsers()204 public boolean canManageSomeUsers() { 205 return this.getSTLevel() <= manager.getSTLevel(); 206 } 207 208 /** 209 * Policy: can this user create or set a user to the specified level? 210 * 211 * @param otherLevel the desired new level for the other user 212 * 213 * Note: UserRegistry.canSetUserLevel enforces additional limitations depending 214 * on more than this user's level and the other user's desired new level 215 */ canCreateOrSetLevelTo(Level otherLevel)216 public boolean canCreateOrSetLevelTo(Level otherLevel) { 217 return 218 canManageSomeUsers() && // must be some sort of manager 219 otherLevel.getSTLevel() >= getSTLevel(); // can't gain higher privs 220 } 221 222 /** 223 * Can a user with this level vote with the given vote count? 224 * 225 * @param withVotes the given vote count 226 * @return true if the user can vote with the given vote count, else false 227 */ canVoteWithCount(int withVotes)228 public boolean canVoteWithCount(int withVotes) { 229 /* 230 * ADMIN is allowed to vote with LOCKING_VOTES, but not directly in the GUI, only 231 * by two TC voting together with PERMANENT_VOTES. Therefore LOCKING_VOTES is omitted 232 * from the GUI menu (voteCountMenu), but included in canVoteWithCount. 233 */ 234 if (withVotes == LOCKING_VOTES && this == admin) { 235 return true; 236 } 237 Set<Integer> menu = getVoteCountMenu(); 238 return menu == null ? withVotes == this.votes : menu.contains(withVotes); 239 } 240 241 /** 242 * If not null, an array of different vote counts from which a user of this 243 * level is allowed to choose. 244 */ 245 private ImmutableSet<Integer> voteCountMenu = null; 246 247 /** 248 * Get the ordered immutable set of different vote counts a user of this level can vote with 249 * 250 * @return the set, or null if the user has no choice of vote count 251 */ getVoteCountMenu()252 public ImmutableSet<Integer> getVoteCountMenu() { 253 return voteCountMenu; 254 } 255 256 /* 257 * Set voteCountMenu for admin and tc in this static block, which will be run after 258 * all the constructors have run, rather than in the constructor itself. For example, 259 * vetter.votes needs to be defined before we can set admin.voteCountMenu. 260 */ 261 static { 262 admin.voteCountMenu = ImmutableSet.of(vetter.votes, admin.votes); /* Not LOCKING_VOTES; see canVoteWithCount */ 263 tc.voteCountMenu = ImmutableSet.of(vetter.votes, tc.votes, PERMANENT_VOTES); 264 } 265 } 266 267 /** 268 * Internal class for voter information. It is public for testing only 269 */ 270 public static class VoterInfo { 271 private Organization organization; 272 private Level level; 273 private String name; 274 /** 275 * A set of locales associated with this voter; it is often empty (as when 276 * the user has "*" for their set of locales); it may not serve any purpose 277 * in ordinary operation of Survey Tool; its main (only?) purpose seems to be for 278 * computeMaxVotes, whose only purpose seems to be creation of localeToOrganizationToMaxVote, 279 * which is used only by ConsoleCheckCLDR (for obscure reason), not by Survey Tool itself. 280 */ 281 private Set<CLDRLocale> locales = new TreeSet<>(); 282 VoterInfo(Organization organization, Level level, String name, LocaleSet localeSet)283 public VoterInfo(Organization organization, Level level, String name, LocaleSet localeSet) { 284 this.setOrganization(organization); 285 this.setLevel(level); 286 this.setName(name); 287 if (!localeSet.isAllLocales()) { 288 this.locales.addAll(localeSet.getSet()); 289 } 290 } 291 VoterInfo(Organization organization, Level level, String name)292 public VoterInfo(Organization organization, Level level, String name) { 293 this.setOrganization(organization); 294 this.setLevel(level); 295 this.setName(name); 296 } 297 VoterInfo()298 public VoterInfo() { 299 } 300 301 @Override toString()302 public String toString() { 303 return "{" + getName() + ", " + getLevel() + ", " + getOrganization() + "}"; 304 } 305 setOrganization(Organization organization)306 public void setOrganization(Organization organization) { 307 this.organization = organization; 308 } 309 getOrganization()310 public Organization getOrganization() { 311 return organization; 312 } 313 setLevel(Level level)314 public void setLevel(Level level) { 315 this.level = level; 316 } 317 getLevel()318 public Level getLevel() { 319 return level; 320 } 321 setName(String name)322 public void setName(String name) { 323 this.name = name; 324 } 325 getName()326 public String getName() { 327 return name; 328 } 329 addLocale(CLDRLocale locale)330 private void addLocale(CLDRLocale locale) { 331 this.locales.add(locale); 332 } 333 334 @Override equals(Object obj)335 public boolean equals(Object obj) { 336 if (obj == null) { 337 return false; 338 } 339 VoterInfo other = (VoterInfo) obj; 340 return organization.equals(other.organization) 341 && level.equals(other.level) 342 && name.equals(other.name) 343 && Objects.equal(locales, other.locales); 344 } 345 346 @Override hashCode()347 public int hashCode() { 348 return organization.hashCode() ^ level.hashCode() ^ name.hashCode(); 349 } 350 } 351 352 /** 353 * MaxCounter: make sure that we are always only getting the maximum of the values. 354 * 355 * @author markdavis 356 * 357 * @param <T> 358 */ 359 static class MaxCounter<T> extends Counter<T> { MaxCounter(boolean b)360 public MaxCounter(boolean b) { 361 super(b); 362 } 363 364 /** 365 * Add, but only to bring up to the maximum value. 366 */ 367 @Override add(T obj, long countValue, long time)368 public MaxCounter<T> add(T obj, long countValue, long time) { 369 long value = getCount(obj); 370 if ((value <= countValue)) { 371 super.add(obj, countValue - value, time); // only add the difference! 372 } 373 return this; 374 } 375 } 376 377 /** 378 * Internal class for getting from an organization to its vote. 379 */ 380 private static class OrganizationToValueAndVote<T> { 381 private final Map<Organization, MaxCounter<T>> orgToVotes = new EnumMap<>(Organization.class); 382 private final Counter<T> totalVotes = new Counter<>(); 383 private final Map<Organization, Integer> orgToMax = new EnumMap<>(Organization.class); 384 private final Counter<T> totals = new Counter<>(true); 385 private Map<String, Long> nameTime = new LinkedHashMap<>(); 386 // map an organization to what it voted for. 387 private final Map<Organization, T> orgToAdd = new EnumMap<>(Organization.class); 388 private T baileyValue; 389 private boolean baileySet; // was the bailey value set 390 OrganizationToValueAndVote()391 OrganizationToValueAndVote() { 392 for (Organization org : Organization.values()) { 393 orgToVotes.put(org, new MaxCounter<T>(true)); 394 } 395 } 396 397 /** 398 * Call clear before considering each new path 399 */ clear()400 public void clear() { 401 for (Map.Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 402 entry.getValue().clear(); 403 } 404 orgToAdd.clear(); 405 orgToMax.clear(); 406 totalVotes.clear(); 407 baileyValue = null; 408 baileySet = false; 409 } 410 411 /** 412 * Returns value of voted item, in case there is exactly 1. 413 * 414 * @return 415 */ getSingleVotedItem()416 private T getSingleVotedItem() { 417 return totalVotes.size() != 1 ? null : totalVotes.iterator().next(); 418 } 419 getNameTime()420 public Map<String, Long> getNameTime() { 421 return nameTime; 422 } 423 424 /** 425 * Call this to add votes 426 * 427 * @param value 428 * @param voter 429 * @param withVotes optionally, vote at a non-typical voting level. May not exceed voter's maximum allowed level. null = use default level. 430 * @param date 431 */ add(T value, int voter, Integer withVotes, Date date)432 public void add(T value, int voter, Integer withVotes, Date date) { 433 final VoterInfo info = getVoterToInfo().get(voter); 434 if (info == null) { 435 throw new UnknownVoterException(voter); 436 } 437 Level level = info.getLevel(); 438 if (withVotes == null || !level.canVoteWithCount(withVotes)) { 439 withVotes = level.getVotes(); 440 } 441 addInternal(value, info, withVotes, date); // do the add 442 } 443 444 /** 445 * Called by add(T,int,Integer) to actually add a value. 446 * 447 * @param value 448 * @param info 449 * @param votes 450 * @param date 451 * @see #add(Object, int, Integer) 452 */ addInternal(T value, final VoterInfo info, final int votes, Date time)453 private void addInternal(T value, final VoterInfo info, final int votes, Date time) { 454 if (baileySet == false) { 455 throw new IllegalArgumentException("setBaileyValue must be called before add"); 456 } 457 totalVotes.add(value, votes, time.getTime()); 458 nameTime.put(info.getName(), time.getTime()); 459 if (DEBUG) { 460 System.out.println("totalVotes Info: " + totalVotes.toString()); 461 } 462 if (DEBUG) { 463 System.out.println("VoteInfo: " + info.getName() + info.getOrganization()); 464 } 465 Organization organization = info.getOrganization(); 466 orgToVotes.get(organization).add(value, votes, time.getTime()); 467 if (DEBUG) { 468 System.out.println("Adding now Info: " + organization.displayName + info.getName() + " is adding: " + votes + value 469 + new Timestamp(time.getTime()).toString()); 470 } 471 472 if (DEBUG) { 473 System.out.println("addInternal: " + organization.displayName + " : " + orgToVotes.get(organization).toString()); 474 } 475 476 // add the new votes to orgToMax, if they are greater that what was there 477 Integer max = orgToMax.get(info.getOrganization()); 478 if (max == null || max < votes) { 479 orgToMax.put(organization, votes); 480 } 481 } 482 483 /** 484 * Return the overall vote for each organization. It is the max for each value. 485 * When the organization is conflicted (the top two values have the same vote), the organization is also added 486 * to disputed. 487 * 488 * @param conflictedOrganizations if not null, to be filled in with the set of conflicted organizations. 489 */ getTotals(EnumSet<Organization> conflictedOrganizations)490 public Counter<T> getTotals(EnumSet<Organization> conflictedOrganizations) { 491 if (conflictedOrganizations != null) { 492 conflictedOrganizations.clear(); 493 } 494 totals.clear(); 495 496 for (Map.Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 497 Counter<T> items = entry.getValue(); 498 if (items.size() == 0) { 499 continue; 500 } 501 Iterator<T> iterator = items.getKeysetSortedByCount(false).iterator(); 502 T value = iterator.next(); 503 long weight = items.getCount(value); 504 if (weight == 0) { 505 continue; 506 } 507 Organization org = entry.getKey(); 508 if (DEBUG) { 509 System.out.println("sortedKeys?? " + value + " " + org.displayName); 510 } 511 512 // if there is more than one item, check that it is less 513 if (iterator.hasNext()) { 514 T value2 = iterator.next(); 515 long weight2 = items.getCount(value2); 516 // if the votes for #1 are not better than #2, we have a dispute 517 if (weight == weight2) { 518 if (conflictedOrganizations != null) { 519 conflictedOrganizations.add(org); 520 } 521 } 522 } 523 // This is deprecated, but preserve it until the method is removed. 524 /* 525 * TODO: explain the above comment, and follow through. What is deprecated (orgToAdd, or getOrgVote)? 526 * Preserve until which method is removed (getOrgVote)? 527 */ 528 orgToAdd.put(org, value); 529 530 // We add the max vote for each of the organizations choices 531 long maxCount = 0; 532 T considerItem = null; 533 long considerCount = 0; 534 long maxtime = 0; 535 long considerTime = 0; 536 for (T item : items.keySet()) { 537 if (DEBUG) { 538 System.out.println("Items in order: " + item.toString() + new Timestamp(items.getTime(item)).toString()); 539 } 540 long count = items.getCount(item); 541 long time = items.getTime(item); 542 if (count > maxCount) { 543 maxCount = count; 544 maxtime = time; 545 considerItem = item; 546 if (DEBUG) { 547 System.out.println("count>maxCount: " + considerItem.toString() + ":" + new Timestamp(considerTime).toString() + " COUNT: " 548 + considerCount + "MAXCOUNT: " + maxCount); 549 } 550 considerCount = items.getCount(considerItem); 551 considerTime = items.getTime(considerItem); 552 553 } else if ((time > maxtime) && (count == maxCount)) { 554 maxCount = count; 555 maxtime = time; 556 considerItem = item; 557 considerCount = items.getCount(considerItem); 558 considerTime = items.getTime(considerItem); 559 if (DEBUG) { 560 System.out.println("time>maxTime: " + considerItem.toString() + ":" + new Timestamp(considerTime).toString()); 561 } 562 } 563 } 564 orgToAdd.put(org, considerItem); 565 totals.add(considerItem, considerCount, considerTime); 566 567 if (DEBUG) { 568 System.out.println("Totals: " + totals.toString() + " : " + new Timestamp(considerTime).toString()); 569 } 570 571 } 572 573 if (DEBUG) { 574 System.out.println("FINALTotals: " + totals.toString()); 575 } 576 return totals; 577 } 578 getOrgCount(T winningValue)579 public int getOrgCount(T winningValue) { 580 int orgCount = 0; 581 for (Map.Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 582 Counter<T> counter = entry.getValue(); 583 long count = counter.getCount(winningValue); 584 if (count > 0) { 585 orgCount++; 586 } 587 } 588 return orgCount; 589 } 590 getBestPossibleVote()591 private int getBestPossibleVote() { 592 int total = 0; 593 for (Map.Entry<Organization, Integer> entry : orgToMax.entrySet()) { 594 total += entry.getValue(); 595 } 596 return total; 597 } 598 599 @Override toString()600 public String toString() { 601 String orgToVotesString = ""; 602 for (Entry<Organization, MaxCounter<T>> entry : orgToVotes.entrySet()) { 603 Counter<T> counter = entry.getValue(); 604 if (counter.size() != 0) { 605 if (orgToVotesString.length() != 0) { 606 orgToVotesString += ", "; 607 } 608 Organization org = entry.getKey(); 609 orgToVotesString += org.toString() + "=" + counter.toString(); 610 } 611 } 612 EnumSet<Organization> conflicted = EnumSet.noneOf(Organization.class); 613 return "{orgToVotes: " + orgToVotesString 614 + ", totals: " + getTotals(conflicted) 615 + ", conflicted: " + conflicted.toString() 616 + "}"; 617 } 618 619 /** 620 * This is now deprecated, since the organization may have multiple votes. 621 * 622 * @param org 623 * @return 624 * @deprecated 625 */ 626 @Deprecated getOrgVote(Organization org)627 public T getOrgVote(Organization org) { 628 return orgToAdd.get(org); 629 } 630 getOrgVoteRaw(Organization orgOfUser)631 public T getOrgVoteRaw(Organization orgOfUser) { 632 return orgToAdd.get(orgOfUser); 633 } 634 getOrgToVotes(Organization org)635 public Map<T, Long> getOrgToVotes(Organization org) { 636 Map<T, Long> result = new LinkedHashMap<>(); 637 MaxCounter<T> counter = orgToVotes.get(org); 638 for (T item : counter) { 639 result.put(item, counter.getCount(item)); 640 } 641 return result; 642 } 643 } 644 645 /** 646 * Static info read from file 647 */ 648 private static Map<Integer, VoterInfo> voterToInfo; 649 650 private static TreeMap<String, Map<Organization, Level>> localeToOrganizationToMaxVote; 651 652 /** 653 * Data built internally 654 */ 655 656 private T winningValue; 657 private T oValue; // optimal value; winning if better approval status than old 658 private T nValue; // next to optimal value 659 private List<T> valuesWithSameVotes = new ArrayList<>(); 660 private Counter<T> totals = null; 661 662 private Status winningStatus; 663 private EnumSet<Organization> conflictedOrganizations = EnumSet 664 .noneOf(Organization.class); 665 private OrganizationToValueAndVote<T> organizationToValueAndVote = new OrganizationToValueAndVote<>(); 666 private T baselineValue; 667 private Status baselineStatus; 668 669 private boolean resolved; 670 private boolean valueIsLocked; 671 private int requiredVotes = 0; 672 private SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo.getInstance(); 673 private CLDRLocale locale; 674 private PathHeader pathHeader; 675 676 /** 677 * usingKeywordAnnotationVoting: when true, use a special voting method for keyword 678 * annotations that have multiple values separated by bar, like "happy | joyful". 679 * See http://unicode.org/cldr/trac/ticket/10973 . 680 * public, set in STFactory.java; could make it private and add param to 681 * the VoteResolver constructor. 682 */ 683 private boolean usingKeywordAnnotationVoting = false; 684 685 private static final Collator englishCollator = Collator.getInstance(ULocale.ENGLISH).freeze(); 686 687 /** 688 * Used for comparing objects of type T 689 */ 690 private final Comparator<T> objectCollator = new Comparator<T>() { 691 @Override 692 public int compare(T o1, T o2) { 693 return englishCollator.compare(String.valueOf(o1), String.valueOf(o2)); 694 } 695 }; 696 697 /** 698 * Set the baseline (or "trunk") value and status for this VoteResolver. 699 * 700 * @param baselineValue the baseline value 701 * @param baselineStatus the baseline status 702 */ setBaseline(T baselineValue, Status baselineStatus)703 public void setBaseline(T baselineValue, Status baselineStatus) { 704 this.baselineValue = baselineValue; 705 this.baselineStatus = baselineValue == null ? Status.missing : baselineStatus; 706 } 707 getBaselineValue()708 public T getBaselineValue() { 709 return baselineValue; 710 } 711 getBaselineStatus()712 public Status getBaselineStatus() { 713 return baselineStatus; 714 } 715 716 /** 717 * Set the locale and PathHeader for this VoteResolver 718 * 719 * You must call this whenever you are using a VoteResolver with a new locale or a new PathHeader 720 * 721 * @param locale the CLDRLocale 722 * @param pathHeader the PathHeader 723 */ setLocale(CLDRLocale locale, PathHeader pathHeader)724 public void setLocale(CLDRLocale locale, PathHeader pathHeader) { 725 this.locale = locale; 726 this.pathHeader = pathHeader; 727 } 728 729 /** 730 * What are the required votes for this item? 731 * 732 * @return the number of votes (as of this writing: usually 4, 8 for established locales) 733 */ getRequiredVotes()734 public int getRequiredVotes() { 735 if (requiredVotes == 0) { 736 int preliminaryRequiredVotes = supplementalDataInfo.getRequiredVotes(locale, pathHeader); 737 if (preliminaryRequiredVotes == HIGH_BAR && baselineStatus != Status.approved) { 738 requiredVotes = LOWER_BAR; 739 } else { 740 requiredVotes = preliminaryRequiredVotes; 741 } 742 } 743 return requiredVotes; 744 } 745 746 /** 747 * Call this method first, for a new base path. You'll then call add for each value 748 * associated with that base path. 749 */ clear()750 public void clear() { 751 baselineValue = null; 752 baselineStatus = Status.missing; 753 requiredVotes = 0; 754 locale = null; 755 pathHeader = null; 756 setUsingKeywordAnnotationVoting(false); 757 organizationToValueAndVote.clear(); 758 resolved = valueIsLocked = false; 759 values.clear(); 760 761 // TODO: clear these out between reuse 762 // Are there other values that should be cleared? 763 oValue = null; 764 winningValue = null; 765 nValue = null; 766 } 767 768 /** 769 * Get the bailey value (what the inherited value would be if there were no 770 * explicit value) for this VoteResolver. 771 * 772 * Throw an exception if !baileySet. 773 * 774 * @return the bailey value. 775 * 776 * Called by STFactory.PerLocaleData.getResolverInternal in the special 777 * circumstance where getWinningValue has returned INHERITANCE_MARKER. 778 */ getBaileyValue()779 public T getBaileyValue() { 780 if (organizationToValueAndVote == null 781 || organizationToValueAndVote.baileySet == false) { 782 throw new IllegalArgumentException("setBaileyValue must be called before getBaileyValue"); 783 } 784 return organizationToValueAndVote.baileyValue; 785 } 786 787 /** 788 * Set the Bailey value (what the inherited value would be if there were no explicit value). 789 * This value is used in handling any {@link CldrUtility.INHERITANCE_MARKER}. 790 * This value must be set <i>before</i> adding values. Usually by calling CLDRFile.getBaileyValue(). 791 */ setBaileyValue(T baileyValue)792 public void setBaileyValue(T baileyValue) { 793 organizationToValueAndVote.baileySet = true; 794 organizationToValueAndVote.baileyValue = baileyValue; 795 } 796 797 /** 798 * Call once for each voter for a value. If there are no voters for an item, then call add(value); 799 * 800 * @param value 801 * @param voter 802 * @param withVotes override to lower the user's voting permission. May be null for default. 803 * @param date 804 * 805 * Called by getResolverInternal 806 */ add(T value, int voter, Integer withVotes, Date date)807 public void add(T value, int voter, Integer withVotes, Date date) { 808 if (resolved) { 809 throw new IllegalArgumentException("Must be called after clear, and before any getters."); 810 } 811 if (withVotes != null && withVotes == Level.LOCKING_VOTES) { 812 valueIsLocked = true; 813 } 814 organizationToValueAndVote.add(value, voter, withVotes, date); 815 values.add(value); 816 } 817 818 /** 819 * Call once for each voter for a value. If there are no voters for an item, then call add(value); 820 * 821 * @param value 822 * @param voter 823 * @param withVotes override to lower the user's voting permission. May be null for default. 824 * 825 * Called only for TestUtilities, not used in Survey Tool. 826 */ add(T value, int voter, Integer withVotes)827 public void add(T value, int voter, Integer withVotes) { 828 if (resolved) { 829 throw new IllegalArgumentException("Must be called after clear, and before any getters."); 830 } 831 Date date = new Date(); 832 organizationToValueAndVote.add(value, voter, withVotes, date); 833 values.add(value); 834 } 835 836 /** 837 * Used only in add(value, voter) for making a pseudo-Date 838 */ 839 private int maxcounter = 100; 840 841 /** 842 * Call once for each voter for a value. If there are no voters for an item, then call add(value); 843 * 844 * @param value 845 * @param voter 846 * 847 * Called by ConsoleCheckCLDR and TestUtilities; not used in SurveyTool. 848 */ add(T value, int voter)849 public void add(T value, int voter) { 850 Date date = new Date(++maxcounter); 851 add(value, voter, null, date); 852 } 853 854 /** 855 * Call if a value has no voters. It is safe to also call this if there is a voter, just unnecessary. 856 * 857 * @param value 858 * @param voter 859 * 860 * Called by getResolverInternal for the baseline (trunk) value; also called for ConsoleCheckCLDR. 861 */ add(T value)862 public void add(T value) { 863 if (resolved) { 864 throw new IllegalArgumentException("Must be called after clear, and before any getters."); 865 } 866 values.add(value); 867 } 868 869 private Set<T> values = new TreeSet<>(objectCollator); 870 871 private final Comparator<T> votesThenUcaCollator = new Comparator<T>() { 872 873 /** 874 * Compare candidate items by vote count, highest vote first. 875 * In the case of ties, favor (a) the baseline (trunk) value, 876 * then (b) votes for inheritance (INHERITANCE_MARKER), 877 * then (c) the alphabetical order (as a last resort). 878 * 879 * Return negative to favor o1, positive to favor o2. 880 */ 881 @Override 882 public int compare(T o1, T o2) { 883 long v1 = organizationToValueAndVote.totalVotes.get(o1); 884 long v2 = organizationToValueAndVote.totalVotes.get(o2); 885 if (v1 != v2) { 886 return v1 < v2 ? 1 : -1; // highest vote first 887 } 888 if (o1.equals(baselineValue)) { 889 return -1; 890 } else if (o2.equals(baselineValue)) { 891 return 1; 892 } 893 if (o1.equals(CldrUtility.INHERITANCE_MARKER)) { 894 return -1; 895 } else if (o2.equals(CldrUtility.INHERITANCE_MARKER)) { 896 return 1; 897 } 898 return englishCollator.compare(String.valueOf(o1), String.valueOf(o2)); 899 } 900 }; 901 902 /** 903 * This will be changed to true if both kinds of vote are present 904 */ 905 private boolean bothInheritanceAndBaileyHadVotes = false; 906 907 /** 908 * Resolve the votes. Resolution entails counting votes and setting 909 * members for this VoteResolver, including winningStatus, winningValue, 910 * and many others. 911 */ resolveVotes()912 private void resolveVotes() { 913 resolved = true; 914 // get the votes for each organization 915 valuesWithSameVotes.clear(); 916 totals = organizationToValueAndVote.getTotals(conflictedOrganizations); 917 /* Note: getKeysetSortedByCount actually returns a LinkedHashSet, "with predictable iteration order". */ 918 final Set<T> sortedValues = totals.getKeysetSortedByCount(false, votesThenUcaCollator); 919 if (DEBUG) { 920 System.out.println("sortedValues :" + sortedValues.toString()); 921 } 922 923 /* 924 * If there are no (unconflicted) votes, return baseline (trunk) if not null, 925 * else INHERITANCE_MARKER if baileySet, else NO_WINNING_VALUE. 926 * Avoid setting winningValue to null. VoteResolver should be fully in charge of vote resolution. 927 * Note: formerly if baselineValue was null here, winningValue was set to null, such 928 * as for http://localhost:8080/cldr-apps/v#/aa/Numbering_Systems/7b8ee7884f773afa 929 * -- in spite of which the Survey Tool client displayed "latn" (bailey) in the Winning 930 * column. The behavior was originally implemented on the client (JavaScript) and later 931 * (temporarily) as fixWinningValue in DataSection.java. 932 */ 933 if (sortedValues.size() == 0) { 934 if (baselineValue != null) { 935 winningValue = baselineValue; 936 winningStatus = baselineStatus; 937 } else if (organizationToValueAndVote.baileySet) { 938 winningValue = (T) CldrUtility.INHERITANCE_MARKER; 939 winningStatus = Status.missing; 940 } else { 941 /* 942 * TODO: When can this still happen? See https://unicode.org/cldr/trac/ticket/11299 "Example C". 943 * Also http://localhost:8080/cldr-apps/v#/en_CA/Gregorian/ 944 * -- also http://localhost:8080/cldr-apps/v#/aa/Languages_A_D/ 945 * xpath //ldml/localeDisplayNames/languages/language[@type="zh_Hans"][@alt="long"] 946 * See also checkDataRowConsistency in DataSection.java. 947 */ 948 winningValue = (T) NO_WINNING_VALUE; 949 winningStatus = Status.missing; 950 } 951 valuesWithSameVotes.add(winningValue); 952 return; 953 } 954 if (values.size() == 0) { 955 throw new IllegalArgumentException("No values added to resolver"); 956 } 957 958 /* 959 * Copy what is in the the totals field of this VoteResolver for all the 960 * values in sortedValues. This local variable voteCount may be used 961 * subsequently to make adjustments for vote resolution. Those adjustment 962 * may affect the winners in vote resolution, while still preserving the original 963 * voting data including the totals field. 964 */ 965 HashMap<T, Long> voteCount = makeVoteCountMap(sortedValues); 966 967 /* 968 * Adjust sortedValues and voteCount as needed to combine "soft" votes for inheritance 969 * with "hard" votes for the Bailey value. Note that sortedValues and voteCount are 970 * both local variables. 971 */ 972 bothInheritanceAndBaileyHadVotes = combineInheritanceWithBaileyForVoting(sortedValues, voteCount); 973 974 /* 975 * Adjust sortedValues and voteCount as needed for annotation keywords. 976 */ 977 if (isUsingKeywordAnnotationVoting()) { 978 adjustAnnotationVoteCounts(sortedValues, voteCount); 979 } 980 981 /* 982 * Perform the actual resolution. 983 */ 984 long weights[] = setBestNextAndSameVoteValues(sortedValues, voteCount); 985 986 oValue = winningValue; 987 988 winningStatus = computeStatus(weights[0], weights[1]); 989 990 // if we are not as good as the baseline (trunk), use the baseline 991 if (baselineStatus != null && winningStatus.compareTo(baselineStatus) < 0) { 992 winningStatus = baselineStatus; 993 winningValue = baselineValue; 994 valuesWithSameVotes.clear(); 995 valuesWithSameVotes.add(winningValue); 996 } 997 } 998 999 /** 1000 * Make a hash for the vote count of each value in the given sorted list, using 1001 * the totals field of this VoteResolver. 1002 * 1003 * This enables subsequent local adjustment of the effective votes, without change 1004 * to the totals field. Purposes include inheritance and annotation voting. 1005 * 1006 * @param sortedValues the sorted list of values (really a LinkedHashSet, "with predictable iteration order") 1007 * @return the HashMap 1008 */ makeVoteCountMap(Set<T> sortedValues)1009 private HashMap<T, Long> makeVoteCountMap(Set<T> sortedValues) { 1010 HashMap<T, Long> map = new HashMap<>(); 1011 for (T value : sortedValues) { 1012 map.put(value, totals.getCount(value)); 1013 } 1014 return map; 1015 } 1016 1017 /** 1018 * Adjust the given sortedValues and voteCount, if necessary, to combine "hard" and "soft" votes. 1019 * Do nothing unless both hard and soft votes are present. 1020 * 1021 * For voting resolution in which inheritance plays a role, "soft" votes for inheritance 1022 * are distinct from "hard" (explicit) votes for the Bailey value. For resolution, these two kinds 1023 * of votes are treated in combination. If that combination is winning, then the final winner will 1024 * be the hard item or the soft item, whichever has more votes, the soft item winning if they're tied. 1025 * Except for the soft item being favored as a tie-breaker, this function should be symmetrical in its 1026 * handling of hard and soft votes. 1027 * 1028 * Note: now that "↑↑↑" is permitted to participate directly in voting resolution, it becomes significant 1029 * that with Collator.getInstance(ULocale.ENGLISH), "↑↑↑" sorts before "AAA" just as "AAA" sorts before "BBB". 1030 * 1031 * @param sortedValues the set of sorted values, possibly to be modified 1032 * @param voteCount the hash giving the vote count for each value, possibly to be modified 1033 * 1034 * @return true if both "hard" and "soft" votes existed and were combined, else false 1035 */ combineInheritanceWithBaileyForVoting(Set<T> sortedValues, HashMap<T, Long> voteCount)1036 private boolean combineInheritanceWithBaileyForVoting(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1037 if (organizationToValueAndVote == null 1038 || organizationToValueAndVote.baileySet == false 1039 || organizationToValueAndVote.baileyValue == null) { 1040 return false; 1041 } 1042 T hardValue = organizationToValueAndVote.baileyValue; 1043 T softValue = (T) CldrUtility.INHERITANCE_MARKER; 1044 /* 1045 * Check containsKey before get, to avoid NullPointerException. 1046 */ 1047 if (!voteCount.containsKey(hardValue) || !voteCount.containsKey(softValue)) { 1048 return false; 1049 } 1050 long hardCount = voteCount.get(hardValue); 1051 long softCount = voteCount.get(softValue); 1052 if (hardCount == 0 || softCount == 0) { 1053 return false; 1054 } 1055 reallyCombineInheritanceWithBailey(sortedValues, voteCount, hardValue, softValue, hardCount, softCount); 1056 return true; 1057 } 1058 1059 /** 1060 * Given that both "hard" and "soft" votes exist, combine them 1061 * 1062 * @param sortedValues the set of sorted values, to be modified 1063 * @param voteCount the hash giving the vote count for each value, to be modified 1064 * @param hardValue the bailey value 1065 * @param softValue the inheritance marker 1066 * @param hardCount the number of votes for hardValue 1067 * @param softCount the number of votes for softValue 1068 */ reallyCombineInheritanceWithBailey(Set<T> sortedValues, HashMap<T, Long> voteCount, T hardValue, T softValue, long hardCount, long softCount)1069 private void reallyCombineInheritanceWithBailey(Set<T> sortedValues, HashMap<T, Long> voteCount, 1070 T hardValue, T softValue, long hardCount, long softCount) { 1071 final T combValue = (hardCount > softCount) ? hardValue : softValue; 1072 final T skipValue = (hardCount > softCount) ? softValue : hardValue; 1073 final long combinedCount = hardCount + softCount; 1074 voteCount.put(combValue, combinedCount); 1075 voteCount.put(skipValue, 0L); 1076 /* 1077 * Sort again 1078 */ 1079 List<T> list = new ArrayList<>(sortedValues); 1080 Collections.sort(list, (v1, v2) -> { 1081 long c1 = voteCount.get(v1); 1082 long c2 = voteCount.get(v2); 1083 if (c1 != c2) { 1084 return (c1 < c2) ? 1 : -1; // decreasing numeric order (most votes wins) 1085 } 1086 return englishCollator.compare(String.valueOf(v1), String.valueOf(v2)); 1087 }); 1088 /* 1089 * Omit skipValue 1090 */ 1091 sortedValues.clear(); 1092 for (T value : list) { 1093 if (!value.equals(skipValue)) { 1094 sortedValues.add(value); 1095 } 1096 } 1097 } 1098 1099 /** 1100 * Adjust the effective votes for bar-joined annotations, 1101 * and re-sort the array of values to reflect the adjusted vote counts. 1102 * 1103 * Note: "Annotations provide names and keywords for Unicode characters, currently focusing on emoji." 1104 * For example, an annotation "happy | joyful" has two components "happy" and "joyful". 1105 * References: 1106 * http://unicode.org/cldr/charts/32/annotations/index.html 1107 * http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-general.html#Annotations 1108 * http://unicode.org/repos/cldr/tags/latest/common/annotations/ 1109 * 1110 * This function is where the essential algorithm needs to be implemented 1111 * for http://unicode.org/cldr/trac/ticket/10973 1112 * 1113 * @param sortedValues the set of sorted values 1114 * @param voteCount the hash giving the vote count for each value in sortedValues 1115 * 1116 * public for unit testing, see TestAnnotationVotes.java 1117 */ adjustAnnotationVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount)1118 public void adjustAnnotationVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1119 if (voteCount == null || sortedValues == null) { 1120 return; 1121 } 1122 // Make compMap map individual components to cumulative vote counts. 1123 HashMap<T, Long> compMap = makeAnnotationComponentMap(sortedValues, voteCount); 1124 1125 // Save a copy of the "raw" vote count before adjustment, since it's needed by promoteSuperiorAnnotationSuperset. 1126 HashMap<T, Long> rawVoteCount = new HashMap<>(voteCount); 1127 1128 // Calculate new counts for original values, based on components. 1129 calculateNewCountsBasedOnAnnotationComponents(sortedValues, voteCount, compMap); 1130 1131 // Re-sort sortedValues based on voteCount. 1132 resortValuesBasedOnAdjustedVoteCounts(sortedValues, voteCount); 1133 1134 // If the set that so far is winning has supersets with superior raw vote count, promote the supersets. 1135 promoteSuperiorAnnotationSuperset(sortedValues, voteCount, rawVoteCount); 1136 } 1137 1138 /** 1139 * Make a hash that maps individual annotation components to cumulative vote counts. 1140 * 1141 * For example, 3 votes for "a|b" and 2 votes for "a|c" makes 5 votes for "a", 3 for "b", and 2 for "c". 1142 * 1143 * @param sortedValues the set of sorted values 1144 * @param voteCount the hash giving the vote count for each value in sortedValues 1145 */ makeAnnotationComponentMap(Set<T> sortedValues, HashMap<T, Long> voteCount)1146 private HashMap<T, Long> makeAnnotationComponentMap(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1147 HashMap<T, Long> compMap = new HashMap<>(); 1148 for (T value : sortedValues) { 1149 Long count = voteCount.get(value); 1150 List<T> comps = splitAnnotationIntoComponentsList(value); 1151 for (T comp : comps) { 1152 if (compMap.containsKey(comp)) { 1153 compMap.replace(comp, compMap.get(comp) + count); 1154 } 1155 else { 1156 compMap.put(comp, count); 1157 } 1158 } 1159 } 1160 if (DEBUG) { 1161 System.out.println("\n\tComponents in adjustAnnotationVoteCounts:"); 1162 for (T comp : compMap.keySet()) { 1163 System.out.println("\t" + comp + ":" + compMap.get(comp)); 1164 } 1165 } 1166 return compMap; 1167 } 1168 1169 /** 1170 * Calculate new counts for original values, based on annotation components. 1171 * 1172 * Find the total votes for each component (e.g., "b" in "b|c"). As the "modified" 1173 * vote for the set, use the geometric mean of the components in the set. 1174 * 1175 * Order the sets by that mean value, then by the smallest number of items in 1176 * the set, then the fallback we always use (alphabetical). 1177 * 1178 * @param sortedValues the set of sorted values 1179 * @param voteCount the hash giving the vote count for each value in sortedValues 1180 * @param compMap the hash that maps individual components to cumulative vote counts 1181 * 1182 * See http://unicode.org/cldr/trac/ticket/10973 1183 */ calculateNewCountsBasedOnAnnotationComponents(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> compMap)1184 private void calculateNewCountsBasedOnAnnotationComponents(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> compMap) { 1185 voteCount.clear(); 1186 for (T value : sortedValues) { 1187 List<T> comps = splitAnnotationIntoComponentsList(value); 1188 double product = 1.0; 1189 for (T comp : comps) { 1190 product *= compMap.get(comp); 1191 } 1192 /* Rounding to long integer here loses precision. We tried multiplying by ten before rounding, 1193 * to reduce problems with different doubles getting rounded to identical longs, but that had 1194 * unfortunate side-effects involving thresholds (see getRequiredVotes). An eventual improvement 1195 * may be to use doubles or floats for all vote counts. 1196 */ 1197 Long newCount = Math.round(Math.pow(product, 1.0 / comps.size())); // geometric mean 1198 voteCount.put(value, newCount); 1199 } 1200 } 1201 1202 /** 1203 * Split an annotation into a list of components. 1204 * 1205 * For example, split "happy | joyful" into ["happy", "joyful"]. 1206 * 1207 * @param value the value like "happy | joyful" 1208 * @return the list like ["happy", "joyful"] 1209 * 1210 * Called by makeAnnotationComponentMap and calculateNewCountsBasedOnAnnotationComponents. 1211 * Short, but needs encapsulation, should be consistent with similar code in DisplayAndInputProcessor.java. 1212 */ splitAnnotationIntoComponentsList(T value)1213 private List<T> splitAnnotationIntoComponentsList(T value) { 1214 return (List<T>) DisplayAndInputProcessor.SPLIT_BAR.splitToList((CharSequence) value); 1215 } 1216 1217 /** 1218 * Re-sort the set of values to match the adjusted vote counts based on annotation components. 1219 * 1220 * Resolve ties using ULocale.ENGLISH collation for consistency with votesThenUcaCollator. 1221 * 1222 * @param sortedValues the set of sorted values, maybe no longer sorted the way we want 1223 * @param voteCount the hash giving the adjusted vote count for each value in sortedValues 1224 */ resortValuesBasedOnAdjustedVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount)1225 private void resortValuesBasedOnAdjustedVoteCounts(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1226 List<T> list = new ArrayList<>(sortedValues); 1227 Collections.sort(list, (v1, v2) -> { 1228 long c1 = voteCount.get(v1), c2 = voteCount.get(v2); 1229 if (c1 != c2) { 1230 return (c1 < c2) ? 1 : -1; // decreasing numeric order (most votes wins) 1231 } 1232 int size1 = splitAnnotationIntoComponentsList(v1).size(); 1233 int size2 = splitAnnotationIntoComponentsList(v2).size(); 1234 if (size1 != size2) { 1235 return (size1 < size2) ? -1 : 1; // increasing order of size (smallest set wins) 1236 } 1237 return englishCollator.compare(String.valueOf(v1), String.valueOf(v2)); 1238 }); 1239 sortedValues.clear(); 1240 for (T value : list) { 1241 sortedValues.add(value); 1242 } 1243 } 1244 1245 /** 1246 * For annotation votes, if the set that so far is winning has one or more supersets with "superior" (see 1247 * below) raw vote count, promote those supersets to become the new winner, and also the new second place 1248 * if there are two or more superior supersets. 1249 * 1250 * That is, after finding the set X with the largest geometric mean, check whether there are any supersets 1251 * with "superior" raw votes, and that don't exceed the width limit. If so, promote Y, the one of those 1252 * supersets with the most raw votes (using the normal tie breaker), to be the winning set. 1253 * 1254 * "Superior" here means that rawVote(Y) ≥ rawVote(X) + 2, where the value 2 (see requiredGap) is for the 1255 * purpose of requiring at least one non-guest vote. 1256 * 1257 * If any other "superior" supersets exist, promote to second place the one with the next most raw votes. 1258 * 1259 * Accomplish promotion by increasing vote counts in the voteCount hash. 1260 * 1261 * @param sortedValues the set of sorted values 1262 * @param voteCount the vote count for each value in sortedValues AFTER calculateNewCountsBasedOnAnnotationComponents; 1263 * it gets modified if superior subsets exist 1264 * @param rawVoteCount the vote count for each value in sortedValues BEFORE calculateNewCountsBasedOnAnnotationComponents; 1265 * rawVoteCount is not changed by this function 1266 * 1267 * Reference: https://unicode.org/cldr/trac/ticket/10973 1268 */ promoteSuperiorAnnotationSuperset(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> rawVoteCount)1269 private void promoteSuperiorAnnotationSuperset(Set<T> sortedValues, HashMap<T, Long> voteCount, HashMap<T, Long> rawVoteCount) { 1270 final long requiredGap = 2; 1271 T oldWinner = null; 1272 long oldWinnerRawCount = 0; 1273 LinkedHashSet<T> oldWinnerComps = null; 1274 LinkedHashSet<T> superiorSupersets = null; 1275 for (T value : sortedValues) { 1276 if (oldWinner == null) { 1277 oldWinner = value; 1278 oldWinnerRawCount = rawVoteCount.get(value); 1279 oldWinnerComps = new LinkedHashSet<>(splitAnnotationIntoComponentsList(value)); 1280 } else { 1281 Set<T> comps = new LinkedHashSet<>(splitAnnotationIntoComponentsList(value)); 1282 if (comps.size() <= CheckWidths.MAX_COMPONENTS_PER_ANNOTATION && 1283 comps.containsAll(oldWinnerComps) && 1284 rawVoteCount.get(value) >= oldWinnerRawCount + requiredGap) { 1285 if (superiorSupersets == null) { 1286 superiorSupersets = new LinkedHashSet<>(); 1287 } 1288 superiorSupersets.add(value); 1289 } 1290 } 1291 } 1292 if (superiorSupersets != null) { 1293 // Sort the supersets by raw vote count, then make their adjusted vote counts higher than the old winner's. 1294 resortValuesBasedOnAdjustedVoteCounts(superiorSupersets, rawVoteCount); 1295 T newWinner = null, newSecond = null; // only adjust votes for first and second place 1296 for (T value : superiorSupersets) { 1297 if (newWinner == null) { 1298 newWinner = value; 1299 voteCount.put(newWinner, voteCount.get(oldWinner) + 2); // more than oldWinner and newSecond 1300 } else if (newSecond == null) { // TODO: fix redundant null check: newSecond can only be null 1301 newSecond = value; 1302 voteCount.put(newSecond, voteCount.get(oldWinner) + 1); // more than oldWinner, less than newWinner 1303 break; 1304 } 1305 } 1306 resortValuesBasedOnAdjustedVoteCounts(sortedValues, voteCount); 1307 } 1308 } 1309 1310 /** 1311 * Given a nonempty list of sorted values, and a hash with their vote counts, set these members 1312 * of this VoteResolver: 1313 * winningValue, nValue, valuesWithSameVotes (which is empty when this function is called). 1314 * 1315 * @param sortedValues the set of sorted values 1316 * @param voteCount the hash giving the vote count for each value 1317 * @return an array of two longs, the weights for the best and next-best values. 1318 */ setBestNextAndSameVoteValues(Set<T> sortedValues, HashMap<T, Long> voteCount)1319 private long[] setBestNextAndSameVoteValues(Set<T> sortedValues, HashMap<T, Long> voteCount) { 1320 1321 long weightArray[] = new long[2]; 1322 weightArray[0] = 0; 1323 weightArray[1] = 0; 1324 nValue = null; 1325 1326 /* 1327 * Loop through the sorted values, at least the first (best) for winningValue, 1328 * and the second (if any) for nValue (else nValue stays null), 1329 * and subsequent values that have as many votes as the first, 1330 * to add to valuesWithSameVotes. 1331 */ 1332 int i = -1; 1333 Iterator<T> iterator = sortedValues.iterator(); 1334 for (T value : sortedValues) { 1335 ++i; 1336 long valueWeight = voteCount.get(value); 1337 if (i == 0) { 1338 winningValue = value; 1339 weightArray[0] = valueWeight; 1340 valuesWithSameVotes.add(value); 1341 } else { 1342 if (i == 1) { 1343 // get the next item if there is one 1344 if (iterator.hasNext()) { 1345 nValue = value; 1346 weightArray[1] = valueWeight; 1347 } 1348 } 1349 if (valueWeight == weightArray[0]) { 1350 valuesWithSameVotes.add(value); 1351 } else { 1352 break; 1353 } 1354 } 1355 } 1356 return weightArray; 1357 } 1358 1359 /** 1360 * Compute the status for the winning value. 1361 * 1362 * @param weight1 the weight (vote count) for the best value 1363 * @param weight2 the weight (vote count) for the next-best value 1364 * @return the Status 1365 */ computeStatus(long weight1, long weight2)1366 private Status computeStatus(long weight1, long weight2) { 1367 if (weight1 > weight2 && weight1 >= getRequiredVotes()) { 1368 return Status.approved; 1369 } 1370 if (weight1 > weight2 && 1371 (weight1 >= 4 && Status.contributed.compareTo(baselineStatus) > 0 1372 || weight1 >= 2 && organizationToValueAndVote.getOrgCount(winningValue) >= 2) ) { 1373 return Status.contributed; 1374 } 1375 if (weight1 >= weight2 && weight1 >= 2) { 1376 return Status.provisional; 1377 } 1378 return Status.unconfirmed; 1379 } 1380 getPossibleWinningStatus()1381 private Status getPossibleWinningStatus() { 1382 if (!resolved) { 1383 resolveVotes(); 1384 } 1385 Status possibleStatus = computeStatus(organizationToValueAndVote.getBestPossibleVote(), 0); 1386 return possibleStatus.compareTo(winningStatus) > 0 ? possibleStatus : winningStatus; 1387 } 1388 1389 /** 1390 * If the winning item is not approved, and if all the people who voted had voted for the winning item, 1391 * would it have made contributed or approved? 1392 * 1393 * @return 1394 */ isDisputed()1395 public boolean isDisputed() { 1396 if (!resolved) { 1397 resolveVotes(); 1398 } 1399 if (winningStatus.compareTo(VoteResolver.Status.contributed) >= 0) { 1400 return false; 1401 } 1402 VoteResolver.Status possibleStatus = getPossibleWinningStatus(); 1403 if (possibleStatus.compareTo(VoteResolver.Status.contributed) >= 0) { 1404 return true; 1405 } 1406 return false; 1407 } 1408 getWinningStatus()1409 public Status getWinningStatus() { 1410 if (!resolved) { 1411 resolveVotes(); 1412 } 1413 return winningStatus; 1414 } 1415 1416 /** 1417 * Returns O Value as described in http://cldr.unicode.org/index/process#TOC-Voting-Process. 1418 * Not always the same as the Winning Value. 1419 * 1420 * @return 1421 */ getOValue()1422 private T getOValue() { 1423 if (!resolved) { 1424 resolveVotes(); 1425 } 1426 return oValue; 1427 } 1428 1429 /** 1430 * Returns N Value as described in http://cldr.unicode.org/index/process#TOC-Voting-Process. 1431 * Not always the same as the Winning Value. 1432 * 1433 * @return 1434 */ getNValue()1435 private T getNValue() { 1436 if (!resolved) { 1437 resolveVotes(); 1438 } 1439 return nValue; 1440 } 1441 1442 /** 1443 * Returns Winning Value as described in http://cldr.unicode.org/index/process#TOC-Voting-Process. 1444 * Not always the same as the O Value. 1445 * 1446 * @return 1447 */ getWinningValue()1448 public T getWinningValue() { 1449 if (!resolved) { 1450 resolveVotes(); 1451 } 1452 return winningValue; 1453 } 1454 getValuesWithSameVotes()1455 public List<T> getValuesWithSameVotes() { 1456 if (!resolved) { 1457 resolveVotes(); 1458 } 1459 return new ArrayList<>(valuesWithSameVotes); 1460 } 1461 getConflictedOrganizations()1462 public EnumSet<Organization> getConflictedOrganizations() { 1463 if (!resolved) { 1464 resolveVotes(); 1465 } 1466 return conflictedOrganizations; 1467 } 1468 1469 /** 1470 * What value did this organization vote for? 1471 * 1472 * @param org 1473 * @return 1474 */ getOrgVote(Organization org)1475 public T getOrgVote(Organization org) { 1476 return organizationToValueAndVote.getOrgVote(org); 1477 } 1478 getOrgToVotes(Organization org)1479 public Map<T, Long> getOrgToVotes(Organization org) { 1480 return organizationToValueAndVote.getOrgToVotes(org); 1481 } 1482 getNameTime()1483 public Map<String, Long> getNameTime() { 1484 return organizationToValueAndVote.getNameTime(); 1485 } 1486 1487 /** 1488 * Get a String representation of this VoteResolver. 1489 * This is sent to the client as "voteResolver.raw" and is used only for debugging. 1490 * 1491 * Compare SurveyAjax.JSONWriter.wrap(VoteResolver<String>) which creates the data 1492 * actually used by the client. 1493 */ 1494 @Override toString()1495 public String toString() { 1496 return "{" 1497 + "bailey: " + (organizationToValueAndVote.baileySet ? ("“" + organizationToValueAndVote.baileyValue + "” ") : "none ") 1498 + "baseline: {" + baselineValue + ", " + baselineStatus + "}, " 1499 + organizationToValueAndVote 1500 + ", sameVotes: " + valuesWithSameVotes 1501 + ", O: " + getOValue() 1502 + ", N: " + getNValue() 1503 + ", totals: " + totals 1504 + ", winning: {" + getWinningValue() + ", " + getWinningStatus() + "}" 1505 + "}"; 1506 } 1507 getVoterToInfo()1508 private static Map<Integer, VoterInfo> getVoterToInfo() { 1509 synchronized (VoteResolver.class) { 1510 return voterToInfo; 1511 } 1512 } 1513 getInfoForVoter(int voter)1514 public static VoterInfo getInfoForVoter(int voter) { 1515 return getVoterToInfo().get(voter); 1516 } 1517 1518 /** 1519 * Set the voter info. 1520 * <p> 1521 * Synchronized, however, once this is called, you must NOT change the contents of your copy of testVoterToInfo. You 1522 * can create a whole new one and set it. 1523 */ setVoterToInfo(Map<Integer, VoterInfo> testVoterToInfo)1524 public static void setVoterToInfo(Map<Integer, VoterInfo> testVoterToInfo) { 1525 synchronized (VoteResolver.class) { 1526 VoteResolver.voterToInfo = testVoterToInfo; 1527 } 1528 if (DEBUG) { 1529 for (int id : testVoterToInfo.keySet()) { 1530 System.out.println("\t" + id + "=" + testVoterToInfo.get(id)); 1531 } 1532 } 1533 computeMaxVotes(); 1534 } 1535 1536 /** 1537 * Set the voter info from a users.xml file. 1538 * <p> 1539 * Synchronized, however, once this is called, you must NOT change the contents of your copy of testVoterToInfo. You 1540 * can create a whole new one and set it. 1541 */ setVoterToInfo(String fileName)1542 public static void setVoterToInfo(String fileName) { 1543 MyHandler myHandler = new MyHandler(); 1544 XMLFileReader xfr = new XMLFileReader().setHandler(myHandler); 1545 xfr.read(fileName, XMLFileReader.CONTENT_HANDLER | XMLFileReader.ERROR_HANDLER, false); 1546 setVoterToInfo(myHandler.testVoterToInfo); 1547 1548 computeMaxVotes(); 1549 } 1550 computeMaxVotes()1551 private static synchronized void computeMaxVotes() { 1552 // compute the localeToOrganizationToMaxVote 1553 localeToOrganizationToMaxVote = new TreeMap<>(); 1554 for (int voter : getVoterToInfo().keySet()) { 1555 VoterInfo info = getVoterToInfo().get(voter); 1556 if (info.getLevel() == Level.tc || info.getLevel() == Level.locked) { 1557 continue; // skip TCs, locked 1558 } 1559 1560 for (CLDRLocale loc : info.locales) { 1561 String locale = loc.getBaseName(); 1562 Map<Organization, Level> organizationToMaxVote = localeToOrganizationToMaxVote.get(locale); 1563 if (organizationToMaxVote == null) { 1564 localeToOrganizationToMaxVote.put(locale, 1565 organizationToMaxVote = new TreeMap<>()); 1566 } 1567 Level maxVote = organizationToMaxVote.get(info.getOrganization()); 1568 if (maxVote == null || info.getLevel().compareTo(maxVote) > 0) { 1569 organizationToMaxVote.put(info.getOrganization(), info.getLevel()); 1570 // System.out.println("Example best voter for " + locale + " for " + info.organization + " is " + 1571 // info); 1572 } 1573 } 1574 } 1575 CldrUtility.protectCollection(localeToOrganizationToMaxVote); 1576 } 1577 1578 /** 1579 * Handles fine in xml format, turning into: 1580 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/level[@n="1"][@type="TC"] 1581 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/name 1582 * Mike Tardif 1583 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/org 1584 * Adobe 1585 * //users[@host="sarasvati.unicode.org"]/user[@id="286"][@email="mike.tardif@adobe.com"]/locales[@type="edit"] 1586 * 1587 * Steven's new format: 1588 * //users[@generated="Wed May 07 15:57:15 PDT 2008"][@host="tintin"][@obscured="true"] 1589 * /user[@id="286"][@email="?@??.??"] 1590 * /level[@n="1"][@type="TC"] 1591 */ 1592 1593 static class MyHandler extends XMLFileReader.SimpleHandler { 1594 private static final Pattern userPathMatcher = Pattern 1595 .compile( 1596 "//users(?:[^/]*)" 1597 + "/user\\[@id=\"([^\"]*)\"](?:[^/]*)" 1598 + "/(" 1599 + "org" + 1600 "|name" + 1601 "|level\\[@n=\"([^\"]*)\"]\\[@type=\"([^\"]*)\"]" + 1602 "|locales\\[@type=\"([^\"]*)\"]" + 1603 "(?:/locale\\[@id=\"([^\"]*)\"])?" 1604 + ")", 1605 Pattern.COMMENTS); 1606 1607 enum Group { 1608 all, userId, mainType, n, levelType, localeType, localeId; get(Matcher matcher)1609 String get(Matcher matcher) { 1610 return matcher.group(this.ordinal()); 1611 } 1612 } 1613 1614 private static final boolean DEBUG_HANDLER = false; 1615 Map<Integer, VoterInfo> testVoterToInfo = new TreeMap<>(); 1616 Matcher matcher = userPathMatcher.matcher(""); 1617 1618 @Override handlePathValue(String path, String value)1619 public void handlePathValue(String path, String value) { 1620 if (DEBUG_HANDLER) 1621 System.out.println(path + "\t" + value); 1622 if (matcher.reset(path).matches()) { 1623 if (DEBUG_HANDLER) { 1624 for (int i = 1; i <= matcher.groupCount(); ++i) { 1625 Group group = Group.values()[i]; 1626 System.out.println(i + "\t" + group + "\t" + group.get(matcher)); 1627 } 1628 } 1629 int id = Integer.parseInt(Group.userId.get(matcher)); 1630 VoterInfo voterInfo = testVoterToInfo.get(id); 1631 if (voterInfo == null) { 1632 testVoterToInfo.put(id, voterInfo = new VoterInfo()); 1633 } 1634 final String mainType = Group.mainType.get(matcher); 1635 if (mainType.equals("org")) { 1636 Organization org = Organization.fromString(value); 1637 voterInfo.setOrganization(org); 1638 value = org.name(); // copy name back into value 1639 } else if (mainType.equals("name")) { 1640 voterInfo.setName(value); 1641 } else if (mainType.startsWith("level")) { 1642 String level = Group.levelType.get(matcher).toLowerCase(); 1643 voterInfo.setLevel(Level.valueOf(level)); 1644 } else if (mainType.startsWith("locale")) { 1645 final String localeIdString = Group.localeId.get(matcher); 1646 if (localeIdString != null) { 1647 CLDRLocale locale = CLDRLocale.getInstance(localeIdString.split("_")[0]); 1648 voterInfo.addLocale(locale); 1649 } else if (DEBUG_HANDLER) { 1650 System.out.println("\tskipping"); 1651 } 1652 } else if (DEBUG_HANDLER) { 1653 System.out.println("\tFailed match* with " + path + "=" + value); 1654 } 1655 } else { 1656 System.out.println("\tFailed match with " + path + "=" + value); 1657 } 1658 } 1659 } 1660 getIdToPath(String fileName)1661 public static Map<Integer, String> getIdToPath(String fileName) { 1662 XPathTableHandler myHandler = new XPathTableHandler(); 1663 XMLFileReader xfr = new XMLFileReader().setHandler(myHandler); 1664 xfr.read(fileName, XMLFileReader.CONTENT_HANDLER | XMLFileReader.ERROR_HANDLER, false); 1665 return myHandler.pathIdToPath; 1666 } 1667 1668 static class XPathTableHandler extends XMLFileReader.SimpleHandler { 1669 Matcher matcher = Pattern.compile("id=\"([0-9]+)\"").matcher(""); 1670 Map<Integer, String> pathIdToPath = new HashMap<>(); 1671 1672 @Override handlePathValue(String path, String value)1673 public void handlePathValue(String path, String value) { 1674 // <xpathTable host="tintin.local" date="Tue Apr 29 14:34:32 PDT 2008" count="18266" > 1675 // <xpath 1676 // id="1">//ldml/dates/calendars/calendar[@type="gregorian"]/dateFormats/dateFormatLength[@type="short"]/dateFormat[@type="standard"]/pattern[@type="standard"]</xpath> 1677 if (!matcher.reset(path).find()) { 1678 throw new IllegalArgumentException("Unknown path " + path); 1679 } 1680 pathIdToPath.put(Integer.parseInt(matcher.group(1)), value); 1681 } 1682 } 1683 getBaseToAlternateToInfo(String fileName)1684 public static Map<Integer, Map<Integer, CandidateInfo>> getBaseToAlternateToInfo(String fileName) { 1685 try { 1686 VotesHandler myHandler = new VotesHandler(); 1687 XMLFileReader xfr = new XMLFileReader().setHandler(myHandler); 1688 xfr.read(fileName, XMLFileReader.CONTENT_HANDLER | XMLFileReader.ERROR_HANDLER, false); 1689 return myHandler.basepathToInfo; 1690 } catch (Exception e) { 1691 throw (RuntimeException) new IllegalArgumentException("Can't handle file: " + fileName).initCause(e); 1692 } 1693 } 1694 1695 public enum Type { 1696 proposal, optimal 1697 } 1698 1699 public static class CandidateInfo { 1700 public Status oldStatus; 1701 public Type surveyType; 1702 public Status surveyStatus; 1703 public Set<Integer> voters = new TreeSet<>(); 1704 1705 @Override toString()1706 public String toString() { 1707 StringBuilder voterString = new StringBuilder("{"); 1708 for (int voter : voters) { 1709 VoterInfo voterInfo = getInfoForVoter(voter); 1710 if (voterString.length() > 1) { 1711 voterString.append(" "); 1712 } 1713 voterString.append(voter); 1714 if (voterInfo != null) { 1715 voterString.append(" ").append(voterInfo); 1716 } 1717 } 1718 voterString.append("}"); 1719 return "{oldStatus: " + oldStatus 1720 + ", surveyType: " + surveyType 1721 + ", surveyStatus: " + surveyStatus 1722 + ", voters: " + voterString 1723 + "};"; 1724 } 1725 } 1726 1727 /* 1728 * <locale-votes host="tintin.local" date="Tue Apr 29 14:34:32 PDT 2008" 1729 * oldVersion="1.5.1" currentVersion="1.6" resolved="false" locale="zu"> 1730 * <row baseXpath="1"> 1731 * <item xpath="2855" type="proposal" id="1" status="unconfirmed"> 1732 * <old status="unconfirmed"/> 1733 * </item> 1734 * <item xpath="1" type="optimal" id="56810" status="confirmed"> 1735 * <vote user="210"/> 1736 * </item> 1737 * </row> 1738 * ... 1739 * A base path has a set of candidates. Each candidate has various items of information. 1740 */ 1741 static class VotesHandler extends XMLFileReader.SimpleHandler { 1742 Map<Integer, Map<Integer, CandidateInfo>> basepathToInfo = new TreeMap<>(); 1743 1744 @Override handlePathValue(String path, String value)1745 public void handlePathValue(String path, String value) { 1746 try { 1747 XPathParts parts = XPathParts.getFrozenInstance(path); 1748 if (parts.size() < 2) { 1749 // empty data 1750 return; 1751 } 1752 int baseId = Integer.parseInt(parts.getAttributeValue(1, "baseXpath")); 1753 Map<Integer, CandidateInfo> info = basepathToInfo.get(baseId); 1754 if (info == null) { 1755 basepathToInfo.put(baseId, info = new TreeMap<>()); 1756 } 1757 int itemId = Integer.parseInt(parts.getAttributeValue(2, "xpath")); 1758 CandidateInfo candidateInfo = info.get(itemId); 1759 if (candidateInfo == null) { 1760 info.put(itemId, candidateInfo = new CandidateInfo()); 1761 candidateInfo.surveyType = Type.valueOf(parts.getAttributeValue(2, "type")); 1762 candidateInfo.surveyStatus = Status.valueOf(fixBogusDraftStatusValues(parts.getAttributeValue(2, 1763 "status"))); 1764 // ignore id 1765 } 1766 if (parts.size() < 4) { 1767 return; 1768 } 1769 final String lastElement = parts.getElement(3); 1770 if (lastElement.equals("old")) { 1771 candidateInfo.oldStatus = Status.valueOf(fixBogusDraftStatusValues(parts.getAttributeValue(3, 1772 "status"))); 1773 } else if (lastElement.equals("vote")) { 1774 candidateInfo.voters.add(Integer.parseInt(parts.getAttributeValue(3, "user"))); 1775 } else { 1776 throw new IllegalArgumentException("unknown option: " + path); 1777 } 1778 } catch (Exception e) { 1779 throw (RuntimeException) new IllegalArgumentException("Can't handle path: " + path).initCause(e); 1780 } 1781 } 1782 1783 } 1784 getOrganizationToMaxVote(String locale)1785 public static Map<Organization, Level> getOrganizationToMaxVote(String locale) { 1786 locale = locale.split("_")[0]; // take base language 1787 Map<Organization, Level> result = localeToOrganizationToMaxVote.get(locale); 1788 if (result == null) { 1789 result = Collections.emptyMap(); 1790 } 1791 return result; 1792 } 1793 getOrganizationToMaxVote(Set<Integer> voters)1794 public static Map<Organization, Level> getOrganizationToMaxVote(Set<Integer> voters) { 1795 Map<Organization, Level> orgToMaxVoteHere = new TreeMap<>(); 1796 for (int voter : voters) { 1797 VoterInfo info = getInfoForVoter(voter); 1798 if (info == null) { 1799 continue; // skip unknown voter 1800 } 1801 Level maxVote = orgToMaxVoteHere.get(info.getOrganization()); 1802 if (maxVote == null || info.getLevel().compareTo(maxVote) > 0) { 1803 orgToMaxVoteHere.put(info.getOrganization(), info.getLevel()); 1804 // System.out.println("*Best voter for " + info.organization + " is " + info); 1805 } 1806 } 1807 return orgToMaxVoteHere; 1808 } 1809 1810 public static class UnknownVoterException extends RuntimeException { 1811 private static final long serialVersionUID = 3430877787936678609L; 1812 int voter; 1813 UnknownVoterException(int voter)1814 public UnknownVoterException(int voter) { 1815 this.voter = voter; 1816 } 1817 1818 @Override toString()1819 public String toString() { 1820 return "Unknown voter: " + voter; 1821 } 1822 } 1823 fixBogusDraftStatusValues(String attributeValue)1824 private static String fixBogusDraftStatusValues(String attributeValue) { 1825 if (attributeValue == null) return "approved"; 1826 if ("confirmed".equals(attributeValue)) return "approved"; 1827 if ("true".equals(attributeValue)) return "unconfirmed"; 1828 if ("unknown".equals(attributeValue)) return "unconfirmed"; 1829 return attributeValue; 1830 } 1831 1832 /* 1833 * TODO: either delete this or explain why it's needed 1834 */ size()1835 public int size() { 1836 return values.size(); 1837 } 1838 1839 /** 1840 * Returns a map from value to resolved vote count, in descending order. 1841 * If the winning item is not there, insert at the front. 1842 * If the baseline (trunk) item is not there, insert at the end. 1843 * 1844 * @return the map 1845 */ getResolvedVoteCounts()1846 public Map<T, Long> getResolvedVoteCounts() { 1847 if (!resolved) { 1848 resolveVotes(); 1849 } 1850 Map<T, Long> result = new LinkedHashMap<>(); 1851 if (winningValue != null && !totals.containsKey(winningValue)) { 1852 result.put(winningValue, 0L); 1853 } 1854 for (T value : totals.getKeysetSortedByCount(false, votesThenUcaCollator)) { 1855 result.put(value, totals.get(value)); 1856 } 1857 if (baselineValue != null && !totals.containsKey(baselineValue)) { 1858 result.put(baselineValue, 0L); 1859 } 1860 for (T value : organizationToValueAndVote.totalVotes.getMap().keySet()) { 1861 if (!result.containsKey(value)) { 1862 result.put(value, 0L); 1863 } 1864 } 1865 if (DEBUG) { 1866 System.out.println("getResolvedVoteCounts :" + result.toString()); 1867 } 1868 return result; 1869 } 1870 getStatusForOrganization(Organization orgOfUser)1871 public VoteStatus getStatusForOrganization(Organization orgOfUser) { 1872 if (!resolved) { 1873 resolveVotes(); 1874 } 1875 T orgVote = organizationToValueAndVote.getOrgVoteRaw(orgOfUser); 1876 if (!equalsOrgVote(winningValue, orgVote)) { 1877 // We voted and lost 1878 return VoteStatus.losing; 1879 } 1880 final int itemsWithVotes = countDistinctValuesWithVotes(); 1881 if (itemsWithVotes > 1) { 1882 // If there are votes for two "distinct" items, we should look at them. 1883 return VoteStatus.disputed; 1884 } 1885 final T singleVotedItem = organizationToValueAndVote.getSingleVotedItem(); 1886 if (!equalsOrgVote(winningValue, singleVotedItem)) { 1887 // If someone voted but didn't win 1888 return VoteStatus.disputed; 1889 } 1890 if (Status.provisional.compareTo(winningStatus) >= 0) { 1891 // If the value is provisional, it needs more votes. 1892 return VoteStatus.provisionalOrWorse; 1893 } else if (itemsWithVotes == 0) { 1894 // The value is ok, but we capture that there are no votes, for revealing items like unsync'ed 1895 return VoteStatus.ok_novotes; 1896 } else { 1897 // We voted, we won, value is approved, no disputes, have votes 1898 return VoteStatus.ok; 1899 } 1900 } 1901 1902 /** 1903 * Should these two values be treated as equivalent for getStatusForOrganization? 1904 * 1905 * @param value 1906 * @param orgVote 1907 * @return true if they are equivalent, false if they are distinct 1908 */ equalsOrgVote(T value, T orgVote)1909 private boolean equalsOrgVote(T value, T orgVote) { 1910 return orgVote == null 1911 || orgVote.equals(value) 1912 || (CldrUtility.INHERITANCE_MARKER.equals(value) 1913 && orgVote.equals(organizationToValueAndVote.baileyValue)) 1914 || (CldrUtility.INHERITANCE_MARKER.equals(orgVote) 1915 && value.equals(organizationToValueAndVote.baileyValue)); 1916 } 1917 1918 /** 1919 * Count the distinct values that have votes. 1920 * 1921 * For this purpose, if there are both votes for inheritance and 1922 * votes for the specific value matching the inherited (bailey) value, 1923 * they are not "distinct": count them as a single value. 1924 * 1925 * @return the number of distinct values 1926 */ countDistinctValuesWithVotes()1927 private int countDistinctValuesWithVotes() { 1928 if (!resolved) { // must be resolved for bothInheritanceAndBaileyHadVotes 1929 throw new RuntimeException("countDistinctValuesWithVotes !resolved"); 1930 } 1931 int count = organizationToValueAndVote.totalVotes.size(); 1932 if (count > 1 && bothInheritanceAndBaileyHadVotes) { 1933 return count - 1; // prevent showing as "disputed" in dashboard 1934 } 1935 return count; 1936 } 1937 1938 /** 1939 * Is this VoteResolver using keyword annotation voting? 1940 * 1941 * @return true or false 1942 */ isUsingKeywordAnnotationVoting()1943 public boolean isUsingKeywordAnnotationVoting() { 1944 return usingKeywordAnnotationVoting; 1945 } 1946 1947 /** 1948 * Set whether this VoteResolver should use keyword annotation voting. 1949 * 1950 * @param usingKeywordAnnotationVoting true or false 1951 */ setUsingKeywordAnnotationVoting(boolean usingKeywordAnnotationVoting)1952 public void setUsingKeywordAnnotationVoting(boolean usingKeywordAnnotationVoting) { 1953 this.usingKeywordAnnotationVoting = usingKeywordAnnotationVoting; 1954 } 1955 1956 /** 1957 * Is the value locked for this locale+path? 1958 * 1959 * @return true or false 1960 */ isValueLocked()1961 public boolean isValueLocked() { 1962 return valueIsLocked; 1963 } 1964 1965 /** 1966 * Can a user who makes a losing vote flag the locale+path? 1967 * I.e., is the locale+path locked and/or does it require HIGH_BAR votes? 1968 * 1969 * @return true or false 1970 */ canFlagOnLosing()1971 public boolean canFlagOnLosing() { 1972 return valueIsLocked || (getRequiredVotes() == HIGH_BAR); 1973 } 1974 } 1975