package votorola.a.count; // Copyright 2007-2009, 2011-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.io.*; import java.sql.*; import java.util.*; import javax.xml.stream.*; import votorola.a.count.gwt.CountNodeJS; import votorola.a.voter.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.sql.*; import votorola.s.line.*; /** A positional accounting node that is writable. It is only partly serializeable. Any * deserialized instance (d) is detached from the backing table, with the consequence * that calling any table-dependent method such as d.write or d.trace will result in a * runtime exception. */ @SuppressWarnings("overrides")/* overrides equals, but not hashCode*/ public class CountNodeW implements Cloneable, CountNode, Serializable { // cf. a/trust/TraceNodeW private static final long serialVersionUID = 3L; /** Partially creates a CountNodeW with default values, for {@linkplain #init(IDPair) * init} to finish. * * @see #tablePV() */ CountNodeW( CountTable.PollView _tablePV ) { if( _tablePV == null ) throw new NullPointerException(); // fail fast tablePV = _tablePV; } /** Creates a CountNodeW with default values. * * @see #tablePV() * @see #person() */ CountNodeW( CountTable.PollView _tablePV, IDPair _person ) { if( _tablePV == null || _person == null ) throw new NullPointerException(); // fail fast tablePV = _tablePV; initPerson( _person, /*isChanged*/true ); } /** Constructs a CountNodeW. * * @see #tablePV() * @see #person() * @see #getBar() * @see #getCandidateEmail() * @see #carryVolume() * @see #dartSector() * @see #directVoterCount() * @see #isCast() * @see #isCycler() * @see #getRank() * @see #getRankIndex() * @see #receiveVolume() * @see #getTime() * @param xml the XML encoding of the node's non-relational properties, * {@linkplain #displayTitle() displayTitle}, {@linkplain #getLocation() * location} and {@linkplain #getSource() source}, or null if all of them are * at default values. */ CountNodeW( CountTable.PollView _tablePV, IDPair _person, String _bar, String _candidateEmail, long _carryVolume, byte _dartSector, long _directVoterCount, boolean _isCast, boolean _isCycler, long _rank, long _rankIndex, long _receiveVolume, long _time, final String xml ) throws XMLStreamException { if( _tablePV == null || _person == null ) throw new NullPointerException(); // fail fast tablePV = _tablePV; initPerson( _person, /*isChanged*/false ); // adding fields? increment serialVersionUID and Count.serialVersionUID bar = _bar; candidateEmail = _candidateEmail; carryVolume = _carryVolume; dartSector = _dartSector; directVoterCount = _directVoterCount; isCast = _isCast; isCycler = _isCycler; rank = _rank; rankIndex = _rankIndex; receiveVolume = _receiveVolume; time = _time; if( xml != null ) { final StringReader rS = new StringReader( xml ); try { final XMLStreamReader r = XMLColumnAppender.newStreamReader( rS ); try { while( r.hasNext() ) { r.next(); if( r.isStartElement() && "X".equals( r.getLocalName() )) { // adding fields? increment serialVersionUID and Count.serialVersionUID displayTitle = r.getAttributeValue( /*ns*/null, "displayTitle" ); location = r.getAttributeValue( /*ns*/null, "location" ); source = r.getAttributeValue( /*ns*/null, "source" ); break; } } } finally{ r.close(); } } finally{ rS.close(); } } } /** Initializes the node by identifying the person. * * @see #person() * * @throws IllegalStateException if the person was already identified. */ final void init( final IDPair _person ) { if( _person == null ) throw new NullPointerException(); // fail fast if( person != null ) throw new IllegalStateException(); initPerson( _person, /*isChanged*/true ); } private final void initPerson( final IDPair _person, final boolean _isChanged ) { person = _person; isPersonal = isPersonal( person.username(), tablePV.table().readyDirectory() ); isChanged = _isChanged; } // ------------------------------------------------------------------------------------ /** An SQL order clause {@value} that sorts (1) numerically from highest to lowest * carry count, and (2) lexically by email address. The purpose of including the * email address is to stabilize the order. */ static final String CARRY_VOLUME_ORDER_CLAUSE = "ORDER BY carryVolume DESC, email"; /** The volume of {@linkplain #receiveVolume() received votes} that flows to the * candidate node along with the {@linkplain #castVolume() cast volume}. The cast * volume is excluded from this count. */ public final long carryVolume() { return carryVolume; } private long carryVolume; /** The {@linkplain #voteWeight() weight} of any original vote that is {@linkplain * #isCast() actually cast} for the {@linkplain #getCandidateEmail() candidate} by * this node; either 0 or 1. * * @see #carryVolume() */ public final long castVolume() { return isCast? voteWeight():0L; } /** A comparator that sorts by dart sector. */ public static final Comparator DART_SECTOR_COMPARATOR = new Comparator() { public int compare( final CountNode n1, final CountNode n2 ) { return Byte.compare( n1.dartSector(), n2.dartSector() ); } }; /** Returns {@linkplain #person() person}().{@linkplain IDPair#email() email}(). */ public final String email() { return person.email(); } /** Returns the email address of the final recipient of the trace, or the localized * string for "nobody". * * @param resA a resource bundle of type application (A). */ public static String finalRecipientOrNobody( final CountNodeW[] trace, final ResourceBundle resA ) { final CountNodeW finalNode = trace[trace.length-1]; final String finalRecipient; if( trace.length == 1 ) finalRecipient = null; else finalRecipient = finalNode.email(); return IDPair.emailOrNobody( finalRecipient, resA ); } /** Describes any bar against the person voting in the poll. A barred person may * still cast a vote, but the vote has a {@linkplain #voteWeight() weight} of zero. * Bars are set only for {@linkplain #isBarrable() barrable nodes}. * * @return a description of the bar, or null if no bar has been set. * * @see VoteCastingContext#getBar() * @see #isBarrable() * @see #setBar(String) */ public final String getBar() { return bar; } private String bar; /** Answers whether a bar might be set. Bars are not set for {@linkplain * #isPersonal impersonal nodes}, which are always zero-weight regardless. * * @see #getBar() */ final boolean isBarrable() { return isPersonal; } /** Answers whether the specified person would be {@linkplain #isBarrable() * barrable}. * * @see #getBar() */ static boolean isBarrable( final String username, final ReadyDirectory readyDirectory ) { return isPersonal( username, readyDirectory ); } /** Sets a bar against the person. * * @see #getBar() */ final void setBar( final String newBar ) { if( ObjectX.nullEquals( newBar, bar )) return; assert isBarrable() || newBar == null; // per contract of getBar bar = newBar; isChanged = true; } /** Identifies the {@linkplain Vote#getCandidateEmail() candidate selected} by the * person, for whom a vote is to be cast if the person {@linkplain #isVoter() is a * voter}. * * @return the {@linkplain * votorola.g.mail.InternetAddressX#canonicalAddress(String) canonical email * address} of the candidate, or null if none was selected. * * @see #candidateName() * @see #setCandidateEmail(String) */ public final String getCandidateEmail() { return candidateEmail; } private String candidateEmail; /** Changes the candidate selected by the person. In order for the change to * properly affect the count, call {@linkplain #uncast() uncast} beforehand, then * {@linkplain #cast() cast} afterwards. * * @see #getCandidateEmail() */ final void setCandidateEmail( final String newCandidateEmail ) { if( ObjectX.nullEquals( newCandidateEmail, candidateEmail )) return; candidateEmail = newCandidateEmail; isChanged = true; } /** The non-default location of the person's position document, or null if the * document is at the default location in the pollwiki. This facility is intended * for the support of vote mirroring and currently has no connection with the remote * drafts of free-range drafting. * * @see #setLocation(String) * @see Category:Draft */ public final String getLocation() { return location; } private String location; /** Sets the non-default location of the person's position document. * * @see #getLocation() */ final void setLocation( final String newLocation ) { if( ObjectX.nullEquals( newLocation, location )) return; location = newLocation; isChanged = true; } /** The rank assigned to this node, with respect to {@linkplain #receiveVolume() votes * received}. The numerical minimum is a rank of 1 (top rank), assigned to the * node(s) receiving the most votes. Rank progresses incrementally, with gaps. All * nodes receiving the same volume of votes are assigned the same rank. This can * result in gaps, as shown here (rank 3 is missing): * *

1, 2, 2, 4, 5, 5, 5, 5, 5, 5, 5

* *

In the example above, two candidates are tied for second place (rank 2). The * bottom rank of 5 is shared by 7 people who are probably non-candidates, receiving * no votes.

* * @return a rank of 1, or larger. Or zero if no rank has been assigned, because * the count is still in progress. * * @see #setRank(long) */ public final long getRank() { return rank; } private long rank; /** Assigns a rank to this node. * * @see #getRank() */ final void setRank( final long newRank ) { if( newRank == rank ) return; rank = newRank; isChanged = true; } /** The unique index that is assigned to this node by order of {@linkplain #getRank() * rank}, and (secondarily) {@linkplain #email() email address}. * * @return a rank index of 0 or larger. * * @see #setRankIndex(long) * @see CountTable#createIndices() */ public final long getRankIndex() { return rankIndex; } private long rankIndex; /** Assigns a rank index to this node. * * @see #getRankIndex() */ final void setRankIndex( final long newRankIndex ) { if( newRankIndex == rankIndex ) return; rankIndex = newRankIndex; isChanged = true; } /** The mnemonic of the source engine if it is not the local vote-server. When * non-null, the return value is the name of a mirror source directory S located at * ~/votorola/in/vomir/S, and any vote that is cast is an image. * * @return mnemonic of source engine, or null if the source is the local * vote-server. * * @see #setSource(String) * @see votorola.a.InputStore.U#mirrorSourceDirectories(VoteServer) */ public final String getSource() { return source; } private String source; /** Sets the source engine. * * @see #getSource() */ final void setSource( final String newSource ) { if( ObjectX.nullEquals( newSource, source )) return; source = newSource; isChanged = true; } /** Specifies the time at which the person last altered the vote. * * @return time in milliseconds since the Epoch, or 0L if unknown or if the vote * has never been altered by the person. * * @see #setTime(long) * @see Vote#getTime() */ public final long getTime() { return time; } private long time; /** Sets the time at which the person last altered the vote. * * @see #getTime() */ final void setTime( final long newTime ) { if( newTime == time ) return; time = newTime; isChanged = true; } /** The volume of votes held. This is simply {@linkplain #receiveVolume() * receiveVolume} - {@linkplain #carryVolume() carryVolume}. */ public final long holdVolume() { return receiveVolume - carryVolume; } /** Answers whether a vote of {@linkplain #voteWeight() any weight} (zero or one) has * actually been cast for the {@linkplain #getCandidateEmail() chosen candidate}. * The value is identical to {@linkplain #isVoter() isVoter} in a fully tallied * count, or after {@linkplain #cast() cast} or {@linkplain #castSolo() castSolo} is * called. * * @see #uncast() */ public final boolean isCast() { return isCast; } // OPT remove as serves only for error detection private boolean isCast; /** Casts the vote if there {@linkplain #isVoter() is one}, and returns the * resulting {@linkplain #trace() vote trace}. * * @throws IllegalStateException if the vote is already cast. * @see #isCast() */ final CountNodeW[] cast() throws SQLException, XMLStreamException { if( isCast ) throw new IllegalStateException( "casting twice" ); assert carryVolume == 0L : "carry votes once only"; final CountNodeW[] trace = trace( /*toForce*/false ); final int iLast = trace.length - 1; if( iLast > 0 ) { if( email().equals( trace[iLast].getCandidateEmail() )) castCyclic( trace ); else castStraight( trace ); // this node not in cycle } assert isCast == isVoter(); // per contract of isCast() return trace; } /** Casts the vote (if there {@linkplain #isVoter() is one}) without carrying any * received votes along. This is relatively fast and straightforward, but it * works only when exhaustively casting all nodes from scratch. Do not use this * method when shifting a vote. * * @return the resulting {@linkplain #trace(boolean) vote trace}, which is a * forced trace. * * @throws IllegalStateException if the vote is already cast. * @see #isCast() */ final CountNodeW[] castSolo() throws SQLException, XMLStreamException { if( isCast ) throw new IllegalStateException( "casting twice" ); final CountNodeW[] trace = trace( /*toForce*/true ); final int iLast = trace.length - 1; if( iLast > 0 ) { flowAsStraight( trace, /*outVolume*/voteWeight(), /*isCycler*/email().equals(trace[iLast].getCandidateEmail()) ); } assert isCast == isVoter(); // per contract of isCast() return trace; } /** Withdraws the vote if one was cast, and returns the resulting {@linkplain * #trace() vote trace}. Does not {@linkplain #setCandidateEmail(String) null * the candidate}. * * @see #isCast() */ final CountNodeW[] uncast() throws SQLException, XMLStreamException { final CountNodeW[] trace = trace(); if( isCast ) { if( trace.length <= 1 ) throw new IllegalStateException( "lost trace of cast vote" ); if( email().equals( trace[trace.length-1].getCandidateEmail() )) { uncastCyclic( trace ); } else uncastStraight( trace ); } return trace; } /** True if the source of the vote is another voting engine, false if the source is * the local vote-server. * * @see #getSource() */ public final boolean isImage() { return source != null; } /** Answers whether the {@linkplain #person() person} is actual (true), as opposed to * merely formal (false). Only an actual person may cast a vote of non-zero * {@linkplain #voteWeight() weight}. * * @see Category:Impersonal user */ public final boolean isPersonal() { return isPersonal; } private boolean isPersonal; // final after init /** Answers whether the specified person would be {@linkplain #isPersonal() * personal}. */ static boolean isPersonal( final String username, final ReadyDirectory readyDirectory ) { return !readyDirectory.pipeRecognizer().isPipeName( username ); } /** The person for whom this node accounts. */ public final IDPair person() { return person; } private IDPair person; // final after init // /** A comparator that sorts (1) numerically from highest to lowest receive count, and // * (2) lexically by email address. The purpose of including the email address is to // * stabilize the order. // */ // public static final Comparator RECEIVE_COUNT_COMPARATOR = // new Comparator() // { // public int compare( final CountNodeW n1, final CountNodeW n2 ) // { // int signum = Long.signum( n2.receiveVolume() - n1.receiveVolume() ); // if( signum == 0 ) signum = n1.email().compareTo( n2.email() ); // return signum; // } // }; /** An SQL order clause {@value} that sorts (1) numerically from highest to lowest * receive count, and (2) lexically by email address. The purpose of including the * email address is to stabilize the order. */ static final String RECEIVE_COUNT_ORDER_CLAUSE = "ORDER BY receiveVolume DESC, email"; /** The volume of votes received from other nodes. * * @see #carryVolume * @see #holdVolume */ public final long receiveVolume() { return receiveVolume; } private long receiveVolume; // /** Sets the state of this node from a vote. // */ // final void set( final Vote vote ) // { // assert vote.voterEmail().equals( email() ); // setCandidateEmail( vote.getCandidateEmail() ); // setTime( vote.getTime() ); // } /** The table in which this node is stored, or null if this is a deserialized, * read-only node. */ public final CountTable.PollView tablePV() { return tablePV; } private final transient CountTable.PollView tablePV; // FIX get rid of this field, or call it tableOrNull() so client is forewarned /** Traces the route that a vote would follow if it were cast from this node, without * forcing the trace beyond uncast nodes. * * @return array of nodes from the casting node (index 0) to the terminal node * (length-1). */ public final CountNodeW[] trace() throws SQLException, XMLStreamException { return trace( /*toForce*/false ); } /** Traces the route that a vote would follow if it were cast from this node. * * @param toForce true to continue tracing beyond nodes that have yet {@linkplain * #isCast() to cast}; false to end the trace at the first such node. * * @return array of nodes from the casting node (index 0) to the terminal node * (length-1). */ final CountNodeW[] trace( final boolean toForce ) throws SQLException, XMLStreamException { if( tablePV == null ) throw newReadOnlyException(); // fail fast final ArrayList nodeList = new ArrayList(); CountNodeW node = CountNodeW.this; nodeList.add( node ); trace: for( ;; ) { final String candidateEmail = node.getCandidateEmail(); if( candidateEmail == null ) break trace; for( CountNodeW n: nodeList ) if( candidateEmail.equals( n.email() )) break trace; // forestall any actual cycle node = tablePV.getOrCreate( candidateEmail ); nodeList.add( node ); if( !toForce && !node.isCast() ) break trace; /* even if there's a valid candidate to cast for. This guard probably has no effect for current clients; either toForce is true, or all nodes that can cast have */ } return nodeList.toArray( new CountNodeW[nodeList.size()] ); } /** The weight of original votes castable for a candidate by this node; either 0 or 1. * The weight is 1 only if {@linkplain #getBar() no bar} is present and the node is * {@linkplain #isPersonal() personal}. * * @see #castVolume() */ public final long voteWeight() { return bar == null && isPersonal? 1L:0L; } /** Writes this node to the table if it has unwritten changes. */ final void write() throws SQLException { if( tablePV == null ) throw newReadOnlyException(); // fail fast if( !isChanged ) return; tablePV.table().put( CountNodeW.this ); isChanged = false; } // - C o u n t - N o d e -------------------------------------------------------------- /** {@inheritDoc} This method is relatively slow in comparison to {@linkplain * #getCandidateEmail() getCandidateEmail}. */ public final String candidateName() { return candidateEmail == null? null: IDPair.toUsername( candidateEmail ); } /** @see #setDartSector(int) */ public final byte dartSector() { return dartSector; } private byte dartSector; /** Sets the dart sector occupied by this node. * * @see #dartSector() */ final void setDartSector( int _newSector ) { final byte newSector = (byte)_newSector; if( newSector == dartSector ) return; if( newSector < 0 || newSector > DART_SECTOR_MAX ) throw new IllegalArgumentException(); dartSector = newSector; isChanged = true; } public final Long directVoterCount() { return directVoterCount; } private long directVoterCount; /** @see #setDisplayTitle(String) */ public final String displayTitle() { return displayTitle; } private String displayTitle; /** Sets the display title. * * @see #displayTitle() */ final void setDisplayTitle( final String newDisplayTitle ) { if( ObjectX.nullEquals( newDisplayTitle, displayTitle )) return; displayTitle = newDisplayTitle; isChanged = true; } public final boolean isBaseCandidate() { return isCycler || isRootCandidate(); } public final boolean isCandidate() { return directVoterCount > 0; } public final boolean isCycler() { return isCycler; } private boolean isCycler; public final boolean isRootCandidate() { return !isCast && isCandidate(); } /** @see #isCast() */ public final boolean isVoter() { return candidateEmail != null && !candidateEmail.equals(email()); } /** @see #person() */ public String name() { return person.username(); } // - O b j e c t ---------------------------------------------------------------------- public final @Override CountNodeW clone() { try { return (CountNodeW)super.clone(); } catch( CloneNotSupportedException x ) { throw new RuntimeException( x ); } // never occurs } /** Returns true iff the o is a count node for the same person. */ public final @Override boolean equals( Object o ) { if( !( o instanceof CountNodeW )) return false; final CountNodeW other = (CountNodeW)o; return person.equals( other.person() ); } /** Returns the person's {@linkplain #email() email address}. */ public final @Override String toString() { return email(); } // ==================================================================================== /** A routine that runs in the context of a count node. */ public interface Runner { // - C o u n t - N o d e . R u n n e r -------------------------------------------- /** Runs this routine in the context of the specified count node. */ public void run( final CountNodeW node ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Casts and carries down a cyclic trace that involves this node. This is the most * complicated case. Carried votes are not held at any single terminal node, but * rather are deposited here and there depending on where they entered the cycle. * *

Let the following denote a series of nodes N[i], where i = 0 to n-1, in two * states B and C:

      *
      *          -> B[0]    B[1] -> B[2] -> ... -> B[n-1] --
      *         |                                           |
      *          - - - - - - - - - - - - - - - - - - - - - -
      *
      *
      *          -> C[0] -> C[1] -> C[2] -> ... -> C[n-1] --
      *         |                                           |
      *          - - - - - - - - - - - - - - - - - - - - - -
      *     
*

where:

      *         -> is the route of votes within the series proper (other
      *         votes may enter from outside the series, but their
      *         entry is not shown here)
      *
      *         B[i] is the state of N[i] when N[0] has withheld its vote
      *
      *         C[i] is the state of N[i] when N[0] has cast for N[1],
      *         thus causing a cyclic cascade
      *
      *         n is the number of nodes in the trace
      *     
* *

Given vote counts for B, calculate those for C.

* *
  (1)
      *         C[0].receive  =  B[0].receive
      *     
* *

Votes received by N[0] remain constant. No vote can ever actually * cycle, so the input to N[0] cannot increase when its output increases.

* *
  (2)
      *         C[i].receive  =  C[j].receive ,   for all i, j
      *     
* *

Equation 2 is the flood rule. It states that all nodes in the cycle will * receive the same volume of votes. This follows from the fact that a vote entering * the cycle will travel until just before it encounters the entry node again. * Therefore it will be received by all nodes. Likewise for casts originating within * the cycle; they too are evenly distributed.

* *

From 1 and 2:

* *
  (3)
      *         C[i].receive  =  B[0].receive ,   for all i
      *     
* *

Also:

* *
  (4)
      *         C[i].hold  =  B[i+1].receive  -  B[i].receive ,   for i = 1...n-1
      *     
* *

Equation 4 states: except N[0], each node will come to hold whatever the * following node had received before the cycle was formed, less whatever it itself * had received. In other words, a node will hold whatever its successor had * introduced to the cycle. This is guaranteed because each introduced vote must * stop before it actually cycles.

* *

Known are C[i].receive (3) and C[i].hold (4), for i = 1...n-1. C[i].carry may * therefore be calculated:

* *
  (5)
      *         N.hold  =  N.receive  -  N.carry
      *
      *         C[i].carry  =  C[i].receive  -  C[i].hold ,   for all i
      *     
* *

Equation 5 states that votes are conserved. Votes that are held are those * received minus those carried to the candidate node. It only remains to solve for * C[0].

* *
  (6)
      *         C[0].hold  =  B[1].receive  +  B[1].cast
      *     
* *

Equation 6 is another instance of the rule (4) that a node holds whatever its * successor had introduced into the cycle.

* *

Known are C[0].hold (6) and C[0].receive (1). C[0].carry may therefore be * calculated (5).

* * @see #uncastCyclic(CountNodeW[]) * @see theory.xht#cycle */ @Warning( "non-API" ) final void castCyclic( final CountNodeW[] trace ) { assert trace.length > 1; assert trace[0].email().equals( trace[trace.length-1].getCandidateEmail() ): "trace is cyclic"; int i = 1; CountNodeW node = trace[i]; ++node.directVoterCount; // node is immediate candidate final long hold0 = node.receiveVolume + node.voteWeight(); // (6) for( ;; ) { assert node.isCast(); // trace is unforced final CountNodeW sucessor; if( i == trace.length - 1 ) sucessor = trace[0]; else sucessor = trace[i+1]; final long hold = sucessor.receiveVolume - node.receiveVolume; // (4) node.receiveVolume = trace[0].receiveVolume; // (3) node.carryVolume = node.receiveVolume - hold; // (5) node.isChanged = true; ++i; if( i >= trace.length ) break; node = trace[i]; } carryVolume = receiveVolume - hold0; // (5), and receiveVolume unchanged (1) isCast = true; isCycler = true; isChanged = true; } /** Casts and carries down an acyclic trace. A cycle may be present downstream in the * trace, but this node (topmost) is not part of it. * * @see #uncastStraight(CountNodeW[]) */ @Warning( "non-API" ) final void castStraight( final CountNodeW[] trace ) { assert trace.length > 1; assert !trace[0].email().equals( trace[trace.length-1].getCandidateEmail() ): "trace is acyclic"; carryVolume = receiveVolume; // carried, no longer held flowAsStraight( trace, /*outVolume*/receiveVolume + voteWeight(), // send them all out /*isCycler*/false ); } /** Flows votes down a trace as though it were acyclic. Either it really is acyclic, * or the outVolume is per {@linkplain #castSolo() castSolo}. */ private final void flowAsStraight( final CountNodeW[] trace, final long outVolume, final boolean _isCycler ) { isCast = true; isCycler = _isCycler; isChanged = true; int i = 1; CountNodeW node = trace[i]; ++node.directVoterCount; // node is immediate candidate if( outVolume == 0L ) { node.isChanged = true; return; // save time, no further effect is possible } for( ;; ) { node.receiveVolume += outVolume; node.isChanged = true; ++i; if( i >= trace.length ) break; node.carryVolume += outVolume; // carry it onward to the end node = trace[i]; } } private boolean isChanged; private static VotorolaRuntimeException newReadOnlyException() { return new VotorolaRuntimeException( "write attempt on deserialized (read only) node" ); } /** Withdraws cast and carried votes from a cyclic trace that involves this node. * This is the opposite of castCyclic, ({@linkplain #castCyclic(CountNodeW[]) q.v.} * for prior equations). Again, let the following denote a series of nodes N[i], * where i = 0 to n-1, in two states C and B.
      *
      *          -> C[0] -> C[1] -> C[2] -> ... -> C[n-1] --
      *         |                                           |
      *          - - - - - - - - - - - - - - - - - - - - - -
      *
      *
      *          -> B[0]    B[1] -> B[2] -> ... -> B[n-1] --
      *         |                                           |
      *          - - - - - - - - - - - - - - - - - - - - - -
      *     
* *

Given vote counts for C (when N[0] is cast and carried), calculate those for B * (when N[0] is withdrawn).

* *
  (7)
      *         B[i].hold  =  0 ,   for i = 1...n-1
      *     
* *

Equation 7 is true because all votes cascade to B[0]. The B series has no * cycle, therefore no other node holds votes.

* *
  (8)
      *         C[i].carry  -  B[i].carry  =  sum( C[j].hold ) ,  or
      *
      *         B[i].carry  =  C[i].carry - sum( C[j].hold ) ,
      *
      *         for i = 1...n-1 ,   j = i+1...n-1
      *     
* *

When N[0] was originally cast, the increase in carriage it caused at each node * had to be balanced by an increase in holds among that node's successors. And * there could have been no increase in carriage at N[n-1] (1). Therefore equation 8 * must hold.

* *

Known are B[i].hold (7) and B[i].carry (8), for i = 1...n-1. B[i].receive may * therefore be calculated (5).

* *

It only remains to solve for B[0]. But its vote is witheld, so there is no * carriage to other nodes:

* *
  (9)
      *         B[0].carry  =  0
      *     
* *

Known are B[0].carry (9) and B[0].receive (1). So B[0].hold may be calculated * (5).

* * @see #castCyclic(CountNodeW[]) */ @Warning( "non-API" ) final void uncastCyclic( final CountNodeW[] trace ) { assert trace.length > 1; assert trace[0].email().equals( trace[trace.length-1].getCandidateEmail() ): "trace is cyclic"; long successorHoldSum = 0L; // so far for( int i = trace.length - 1;; --i ) { final CountNodeW node = trace[i]; final long wasHeld = node.holdVolume(); node.carryVolume -= successorHoldSum; // (8) node.receiveVolume = node.carryVolume; // so now holding 0 (7) node.isChanged = true; successorHoldSum += wasHeld; if( i == 1 ) { --node.directVoterCount; // node was immediate candidate assert node.directVoterCount >= 0L; break; } } carryVolume = 0L; // (9), and receiveVolume unchanged (1) isCast = false; isCycler = false; isChanged = true; } /** Withdraws cast and carried votes from an acyclic trace. A cycle may be present * downstream in the trace, but this node (topmost) is not part of it. * * @see #castStraight(CountNodeW[]) */ @Warning( "non-API" ) final void uncastStraight( final CountNodeW[] trace ) { assert trace.length > 1; assert !trace[0].email().equals( trace[trace.length-1].getCandidateEmail() ): "trace is acyclic"; carryVolume = 0L; // held, no longer carried isCast = false; assert !isCycler; isChanged = true; int i = 1; CountNodeW node = trace[i]; --node.directVoterCount; // node was immediate candidate assert node.directVoterCount >= 0L; final long withdrawVolume = receiveVolume + voteWeight(); // withdraw it all if( withdrawVolume == 0L ) { node.isChanged = true; return; // save time, no further effect is possible } for( ;; ) { node.receiveVolume -= withdrawVolume; node.isChanged = true; ++i; if( i >= trace.length ) break; node.carryVolume -= withdrawVolume; // withdraw it from onward to the end node = trace[i]; } } }