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