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