PageRenderTime 73ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/java/org/bolson/vote/IRV.java

https://code.google.com/p/voteutil/
Java | 402 lines | 331 code | 16 blank | 55 comment | 109 complexity | 646df8d5e81df31130d7b4ba7a59a514 MD5 | raw file
  1. package org.bolson.vote;
  2. import java.util.ArrayList;
  3. import java.util.HashMap;
  4. /**
  5. Instant Runoff Voting.
  6. Do not use, IRV is bad, VRR is better, IRNR is my favorite.
  7. @see VRR
  8. @see IRNR
  9. @author Brian Olson
  10. */
  11. public class IRV extends NameVotingSystem implements IndexVotable {
  12. /** Map names to TallyState instance. Could be HashMap<String,TallyState> */
  13. protected HashMap they = new HashMap();
  14. /** ArrayList<String> for lookup of name from index. */
  15. protected ArrayList indexNames = new ArrayList();
  16. /** ArrayList<TallyState> for lookup by index. */
  17. protected ArrayList indexTS = new ArrayList();
  18. /** Cache of winners. Set by getWinners. Cleared by voteRating. */
  19. protected NameVote[] winners = null;
  20. /** Holds each passed in vote.
  21. This would be ArrayList<IndexVoteSet> if I broke Java 1.4 compatibility. */
  22. protected ArrayList votes = new ArrayList();
  23. /** ArrayList<IndexVoteSet> */
  24. protected ArrayList tiedVotes = new ArrayList();
  25. public IRV() {
  26. }
  27. /**
  28. Holds the internal count state for one choice.
  29. */
  30. protected static class TallyState implements Comparable {
  31. public String name;
  32. public int index;
  33. /** The sum of partial votes from cast ties.
  34. Total count is (votes.size() + fractions) */
  35. public double fractions = 0.0;
  36. public boolean active = true;
  37. /** ArrayList<IndexVoteSet> */
  38. public ArrayList votes = new ArrayList();
  39. public TallyState( String nin ) {
  40. name = nin;
  41. }
  42. /**
  43. Copy state for later verbose round descriptions.
  44. fractions usage in copy is full count, not just fractions.
  45. */
  46. public TallyState stateCopy() {
  47. TallyState x = new TallyState(name);
  48. x.fractions = fractions + votes.size();
  49. x.active = active;
  50. return x;
  51. }
  52. /**
  53. Sorts on name by default String comparator.
  54. */
  55. public int compareTo(Object o) throws ClassCastException {
  56. if ( o instanceof String ) {
  57. return name.compareTo((String)o);
  58. }
  59. if ( o instanceof TallyState ) {
  60. return name.compareTo( ((TallyState)o).name );
  61. }
  62. throw new ClassCastException("not a TallyState");
  63. }
  64. }
  65. protected TallyState get( String name ) {
  66. TallyState v = (TallyState)they.get( name );
  67. if ( v == null ) {
  68. v = new TallyState( name );
  69. v.index = they.size();
  70. indexNames.add( name );
  71. indexTS.add( v );
  72. they.put( name, v );
  73. }
  74. return v;
  75. }
  76. public void voteRating( NameVote[] vote ) {
  77. if ( vote == null || vote.length == 0 ) {
  78. return;
  79. }
  80. IndexVoteSet iv = new IndexVoteSet(vote.length);
  81. for ( int i = 0; i < vote.length; i++ ) {
  82. // causes creation of TallyState for name if not already existing
  83. TallyState v = get( vote[i].name );
  84. iv.index[i] = v.index;
  85. iv.rating[i] = vote[i].rating;
  86. }
  87. votes.add( iv );
  88. winners = null;
  89. }
  90. public void voteIndexVoteSet(IndexVoteSet vote) {
  91. if ( vote == null || vote.index.length == 0 ) {
  92. return;
  93. }
  94. int maxi = -1;
  95. for ( int i = 0; i < vote.index.length; i++ ) {
  96. if ( vote.index[i] > maxi ) {
  97. maxi = vote.index[i];
  98. }
  99. }
  100. while ( indexTS.size() <= maxi ) {
  101. get(Integer.toString(indexTS.size() + 1));
  102. }
  103. votes.add( vote );
  104. winners = null;
  105. }
  106. protected void bucketize( IndexVoteSet vote ) {
  107. float max = Float.NaN;
  108. int tied = 1;
  109. int i = 0;
  110. TallyState v = null;
  111. TallyState maxv = null;
  112. // find an active one and initialize max
  113. while ( i < vote.index.length ) {
  114. v = (TallyState)indexTS.get(vote.index[i]);
  115. if ( v.active ) {
  116. maxv = v;
  117. max = vote.rating[i];
  118. i++;
  119. break;
  120. }
  121. i++;
  122. }
  123. if ( maxv == null ) {
  124. // none of the names in this vote are active
  125. return;
  126. }
  127. for ( ; i < vote.index.length; i++ ) {
  128. v = (TallyState)indexTS.get(vote.index[i]);
  129. if ( ! v.active ) {
  130. // ignore
  131. } else if ( vote.rating[i] > max ) {
  132. max = vote.rating[i];
  133. maxv = v;
  134. tied = 1;
  135. } else if ( vote.rating[i] == max ) {
  136. tied++;
  137. }
  138. }
  139. if ( tied == 1 ) {
  140. maxv.votes.add( vote );
  141. } else {
  142. double fract = 1.0 / tied;
  143. for ( i = 0; i < vote.index.length; i++ ) {
  144. if ( vote.rating[i] == max ) {
  145. v = (TallyState)indexTS.get(vote.index[i]);
  146. if ( v.active ) {
  147. v.fractions += fract;
  148. }
  149. }
  150. }
  151. tiedVotes.add( vote );
  152. }
  153. }
  154. public NameVote[] getWinners() {
  155. return getWinners(null);
  156. }
  157. public NameVote[] getWinners(StringBuffer explain) {
  158. if ( explain != null ) {
  159. winners = null;
  160. }
  161. if ( winners != null ) {
  162. return winners;
  163. }
  164. TallyState[] tv = new TallyState[they.size()];
  165. int i = 0;
  166. java.util.Iterator ti = they.values().iterator();
  167. int numActive = 0;
  168. while ( ti.hasNext() ) {
  169. TallyState ts = (TallyState)ti.next();
  170. ts.votes.clear();
  171. ts.fractions = 0;
  172. ts.active = true;
  173. numActive++;
  174. tv[i] = ts;
  175. i++;
  176. }
  177. java.util.Arrays.sort(tv);
  178. java.util.Iterator vi = votes.iterator();
  179. while ( vi.hasNext() ) {
  180. bucketize( (IndexVoteSet)vi.next() );
  181. }
  182. winners = new NameVote[tv.length];
  183. ArrayList rounds = null; // ArrayList<TallyState[]>
  184. if (explain != null) {
  185. rounds = new ArrayList();
  186. }
  187. // numActive == tv.length
  188. while ( numActive > 1 ) {
  189. double min = 0;
  190. int mini = 0;
  191. // find an active one, initialize min
  192. i = 0;
  193. while ( i < tv.length ) {
  194. if ( tv[i].active ) {
  195. min = tv[i].votes.size() + tv[i].fractions;
  196. mini = i;
  197. i++;
  198. break;
  199. }
  200. i++;
  201. }
  202. // find min
  203. int ties = 1;
  204. while ( i < tv.length ) {
  205. if ( tv[i].active ) {
  206. double tm = tv[i].votes.size() + tv[i].fractions;
  207. if ( tm < min ) {
  208. min = tm;
  209. mini = i;
  210. ties = 1;
  211. } else if ( tm == min ) {
  212. ties++;
  213. } else if ( debugLog != null ) {
  214. if ( Math.abs(tm - min) < 0.001 ) {
  215. debugLog.append("very small diff between mins: ").append(Math.abs(tm - min)).append("\n");
  216. }
  217. }
  218. }
  219. i++;
  220. }
  221. if (rounds != null) {
  222. TallyState[] ntv = new TallyState[tv.length];
  223. for ( i = 0; i < tv.length; i++ ) {
  224. ntv[i] = tv[i].stateCopy();
  225. }
  226. rounds.add(ntv);
  227. }
  228. // ArrayList<ArrayList<IndexVoteSet> >
  229. ArrayList rebuck = new ArrayList();
  230. if (ties == numActive) {
  231. break;
  232. } else if (ties == 1) {
  233. if ( debugLog != null ) { debugLog.append("ties=1, dq \"").append(tv[mini].name).append("\"\n"); }
  234. tv[mini].active = false;
  235. tv[mini].fractions = min; // archive best count
  236. rebuck.add( tv[mini].votes );
  237. numActive--;
  238. winners[numActive] = new NameVote( tv[mini].name, (float)min );
  239. } else {
  240. if ( debugLog != null ) { debugLog.append("ties=").append(ties).append(" dq:\n"); }
  241. for ( i = tv.length - 1; i >= 0; i-- ) {
  242. if ( tv[i].active &&
  243. (tv[i].votes.size() + tv[i].fractions == min) ) {
  244. if ( debugLog != null ) { debugLog.append("\t\"").append(tv[i].name).append("\"\n"); }
  245. tv[i].active = false;
  246. tv[i].fractions = min;
  247. rebuck.add( tv[i].votes );
  248. numActive--;
  249. winners[numActive] = new NameVote( tv[i].name, (float)min );
  250. }
  251. }
  252. }
  253. if ( numActive > 1 ) {
  254. // redistribute disqualified votes
  255. while ( rebuck.size() > 0 ) {
  256. ArrayList votes = (ArrayList)rebuck.remove(rebuck.size()-1);
  257. while ( votes.size() > 0 ) {
  258. bucketize( (IndexVoteSet)votes.remove(votes.size()-1) );
  259. }
  260. }
  261. rebuck = null;
  262. // reset fractions before re-count of tied votes.
  263. for ( i = 0; i < tv.length; i++ ) {
  264. if ( tv[i].active ) {
  265. tv[i].fractions = 0;
  266. }
  267. }
  268. ArrayList oldTied = tiedVotes;
  269. tiedVotes = new ArrayList();
  270. while ( oldTied.size() > 0 ) {
  271. bucketize( (IndexVoteSet)oldTied.remove(oldTied.size()-1) );
  272. }
  273. }
  274. }
  275. // Find the winner
  276. int outi = 0;
  277. for ( i = 0; i < tv.length; i++ ) {
  278. if ( tv[i].active ) {
  279. // assert(tv[i] == null);
  280. winners[outi] = new NameVote( tv[i].name, (float)(tv[i].votes.size() + tv[i].fractions) );
  281. outi++;
  282. }
  283. }
  284. // check that winners[...] contains no nulls?
  285. for ( i = 0; i < winners.length; i++ ) {
  286. if ( winners[i] == null ) {
  287. winners[i] = new NameVote("IRV IMPLEMENTATION ERROR", 0.0f);
  288. }
  289. }
  290. /*if ( i >= tv.length || tv[i] == null ) {
  291. // No winner. Bad source data?
  292. winners = new NameVote[0];
  293. } else {
  294. winners[0] = new NameVote( tv[i].name, (float)(tv[i].votes.size() + tv[i].fractions) );
  295. }*/
  296. if ( explain != null && rounds != null ) {
  297. roundsToHTML( explain, rounds, winners );
  298. }
  299. return winners;
  300. }
  301. /** Used in htmlSummary. Default "0.00" */
  302. public static java.text.DecimalFormat ratingFormat = new java.text.DecimalFormat( "0.##" );
  303. public StringBuffer htmlSummary( StringBuffer sb ) {
  304. NameVote[] t;
  305. t = getWinners();
  306. if ( t == null || t.length == 0 || t[0] == null ) {
  307. return sb;
  308. }
  309. sb.append( "<table border=\"1\"><tr><th>Name</th><th>Best IRV Count</th></tr>" );
  310. for ( int i = 0; i < t.length; i++ ) {
  311. sb.append( "<tr><td>" );
  312. sb.append( t[i].name );
  313. sb.append( "</td><td>" );
  314. ratingFormat.format( t[i].rating, sb, new java.text.FieldPosition( java.text.NumberFormat.INTEGER_FIELD ) );
  315. sb.append( "</td></tr>" );
  316. }
  317. sb.append( "</table>" );
  318. return sb;
  319. }
  320. /** @return "Instant Runoff Voting" */
  321. public String name() {
  322. return "Instant Runoff Voting";
  323. }
  324. /**
  325. @param sb where to print the table
  326. @param rounds is ArrayList<TallyState[]> of intermediate state.
  327. @return sb with stuff
  328. */
  329. public static StringBuffer roundsToHTML( StringBuffer sb, ArrayList rounds, NameVote[] winners ) {
  330. sb.append("<table border=\"1\"><tr>");
  331. for ( int r = 0; r < rounds.size(); r++ ) {
  332. sb.append( "<th colspan=\"2\">Round " );
  333. sb.append( r + 1 );
  334. sb.append( "</th>");
  335. }
  336. sb.append( "</tr>\n<tr>" );
  337. for ( int r = 0; r < rounds.size(); r++ ) {
  338. sb.append( "<th>Name</th><th>Count</th>");
  339. }
  340. sb.append( "</tr>\n" );
  341. if ( (winners != null) && (winners.length > 0) ) {
  342. // present based on sorted order
  343. for ( int c = 0; c < winners.length; c++ ) {
  344. sb.append( "<tr>" );
  345. for ( int r = 0; r < rounds.size(); r++ ) {
  346. TallyState[] tv = (TallyState[])rounds.get(r);
  347. boolean found = false;
  348. for ( int i = 0; i < tv.length; i++ ) {
  349. if ( tv[i].name.equals( winners[c].name ) ) {
  350. found = true;
  351. if ( tv[i].active ) {
  352. sb.append( "<td>" );
  353. } else {
  354. sb.append( "<td style=\"color:#999999;\">" );
  355. }
  356. sb.append( tv[i].name );
  357. sb.append( "</td><td>" );
  358. ratingFormat.format( tv[i].fractions, sb, new java.text.FieldPosition( java.text.NumberFormat.INTEGER_FIELD ) );
  359. sb.append( "</td>" );
  360. }
  361. }
  362. if ( ! found ) {
  363. System.err.println( "round(" + r + "): could not find winners[" + c + "] \"" + winners[c].name + "\" in tv:" );
  364. for ( int i = 0; i < tv.length; i++ ) {
  365. System.err.println( tv[i].name + " = " + tv[i].fractions );
  366. }
  367. }
  368. }
  369. sb.append( "</tr>\n" );
  370. }
  371. } else {
  372. sb.append( "<tr><td>FIXME: implement no-winner-data IRV explain</td></tr>\n" );
  373. }
  374. sb.append( "</table>\n" );
  375. return sb;
  376. }
  377. public StringBuffer htmlExplain( StringBuffer sb ){
  378. winners = null;
  379. getWinners(sb);
  380. return htmlSummary( sb );
  381. }
  382. static {
  383. registerImpl( "IRV", IRV.class );
  384. }
  385. };