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