• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, &quot;J. Smith&quot;) },
42  *     { 555, new VoterInfo(Organization.google, Level.street, &quot;S. Jones&quot;) },
43  *     { 444, new VoterInfo(Organization.google, Level.vetter, &quot;S. Samuels&quot;) },
44  *     { 333, new VoterInfo(Organization.apple, Level.vetter, &quot;A. Mutton&quot;) },
45  *     { 222, new VoterInfo(Organization.adobe, Level.expert, &quot;A. Aldus&quot;) },
46  *     { 111, new VoterInfo(Organization.ibm, Level.street, &quot;J. Henry&quot;) }, }));
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 &lt;b&gt;must&lt;/b&gt; 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