package voting import ( "fmt" "strings" "time" "code.c-base.org/baccenfutter/govote/voting/quorum" "code.c-base.org/baccenfutter/govote/voting/threshold" "code.c-base.org/baccenfutter/govote/voting/vote" "robpike.io/filter" ) type ( // Voting represents a voting with all its data and meta-data. Voting struct { // id is the unique ID of the vote. id string // referendum contains the subject or title of the voting referendum string // deadline defines the point in time when the voting closes deadline time.Time // quorum defines the mininimum required eligible votes quorum quorum.Quorum // threshold defines the minimum required YES votes threshold threshold.Threshold // electors contains the list of eligible voters (empty if anyone can vote) electors []string // annonymous defines if the voting is anonymous or public annonymous bool // votes holds all votes associated with this voting votes []vote.Vote } ) // NewVoting returns an initialized Voting. func NewVoting(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool) *Voting { return &Voting{ id: id, referendum: r, deadline: d, quorum: q, threshold: t, electors: e, annonymous: a, votes: []vote.Vote{}, } } // NewVotingWithVotes returns an initialized Voting with pre-defined votes. // This is convenient when loading a voting from the store and populating it in one call. func NewVotingWithVotes(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool, v []vote.Vote) *Voting { voting := NewVoting(id, r, d, q, t, e, a) for i := range v { voting.votes = append(voting.votes, v[i]) } return voting } // String implements fmt.Stringer. func (v Voting) String() string { // initialize vars with all the metadata var ( possibleVotes int = len(v.electors) totalVotes int = len(v.Votes()) yesVotes int = len(v.yesVotes()) noVotes int = len(v.noVotes()) deadlineStatus string = "🎭 ONGOING 🎭" quorumStatus string = "❌ FAIL" thresholdStatus string = "❌ FAIL" out string = "" quorumSatisfied = v.quorum.IsSatisfied(possibleVotes, totalVotes) thresholdSatisfied = v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) votingSatisfied = quorumSatisfied && thresholdSatisfied ) // check the deadline if time.Now().UTC().After(v.deadline) { if votingSatisfied { deadlineStatus = "✅ APROVED ✅" } else { deadlineStatus = "❌ REJECTED ❌" } } // check quorum and threshold if v.quorum.IsSatisfied(possibleVotes, totalVotes) { quorumStatus = "✅ PASS" } if v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) { thresholdStatus = "✅ PASS" } // assemble output string out += fmt.Sprintf("Referendum: %s\n", strings.ToUpper(v.referendum)) out += fmt.Sprintf("Deadline : %s UTC\n", v.deadline.Format(time.DateTime)) out += fmt.Sprintf("Quorum : %s (%d/%d) (required: %s)\n", quorumStatus, totalVotes, possibleVotes, v.quorum) out += fmt.Sprintf("Threshold : %s (%d/%d) (required: %s)\n", thresholdStatus, yesVotes, totalVotes, v.threshold) out += fmt.Sprintf("Electors : [ %d ] %s\n", len(v.electors), v.electors) out += fmt.Sprintf( "Votes : [ %d | %d | %d ] (❎|❌|❔)\n", len(v.yesVotes()), len(v.noVotes()), len(v.abstainVotes()), ) out += fmt.Sprintf("Status : %s\n", deadlineStatus) // only show votes when not anonymous if !v.annonymous { out += "\n" if v.votes != nil && len(v.votes) > 0 { for _, _v := range v.votes { out += fmt.Sprintf("💬 %s\n", _v) } } } // return the output string return out } // ID returns the unique ID of this voting. func (v Voting) ID() string { return v.id } // Referendum returns the referendum of this voting. func (v Voting) Referendum() string { return v.referendum } // Deadline returns the deadline of this voting. func (v Voting) Deadline() time.Time { return v.deadline } // Quorum returns the quorum of this voting. func (v Voting) Quorum() string { return v.quorum.String() } // Threshold returns the threshold of this voting. func (v Voting) Threshold() string { return v.threshold.String() } // Electors returns the list of eligible voters of this voting. func (v Voting) Electors() []string { electors := make([]string, len(v.electors)) for i := range v.electors { electors[i] = v.electors[i] } return electors } // Anonymous returns the anonymous setting of this voting. func (v Voting) Anonymous() bool { return v.annonymous } // IsOpen returns true while the deadline has not yet been reached. func (v Voting) IsOpen() bool { return v.deadline.After(time.Now().UTC()) } // Result returns a Result based on this voting. func (v Voting) Result() Result { var ( possibleVotes = len(v.electors) totalVotes = len(v.Votes()) yesVotes = len(v.yesVotes()) noVotes = len(v.noVotes()) votes []vote.Vote quorumSatisfied = v.quorum.IsSatisfied(possibleVotes, totalVotes) thresholdSatisfied = v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) ) if !v.annonymous { votes = v.Votes() } return Result{ Quorum: quorumSatisfied, Threshold: thresholdSatisfied, Yes: len(v.yesVotes()), No: len(v.noVotes()), Abstain: len(v.Votes()) - len(v.yesVotes()) - len(v.noVotes()), Votes: votes, } } // Vote takes a vote.Vote and places it. // Placing a vote after the deadline has passed will raise an error. Placing a // vote with an elector not in the list of eligible electors will raise an // error. func (v *Voting) Vote(vote vote.Vote) error { // check if deadline has passed if time.Now().UTC().After(v.deadline) { return fmt.Errorf("deadline has passed") } // place vote if elector is eligible for _, elector := range v.electors { if elector == vote.Elector { v.votes = append(v.votes, vote) return nil } } // raise an error if not eligible return fmt.Errorf("not eligable to vote: %s", vote.Elector) } // Votes returns the normalized list of effective votes. // Only the last vote of each elector before the deadline counts. func (v Voting) Votes() []vote.Vote { votes := []vote.Vote{} nextVote: // iterate over all placed votes in reverse order only respecting the first // vote of each elector (effectively the final vote of each elector). for i := len(v.votes) - 1; i >= 0; i-- { elector := v.votes[i].Elector for _, e := range votes { if e.Elector == elector { continue nextVote } } votes = append(votes, v.votes[i]) } // optionally, anonymize the votes before returning them if v.annonymous { for i := range votes { votes[i].Elector = "" } } return votes } // yesVotes returns a []vote.Vote of all yes votes. func (v Voting) yesVotes() []vote.Vote { filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Yes } return filter.Choose(v.Votes(), filterFunc).([]vote.Vote) } // noVotes returns a []vote.Vote of all no votes. func (v Voting) noVotes() []vote.Vote { filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.No } return filter.Choose(v.Votes(), filterFunc).([]vote.Vote) } // abstainVotes returns a []vote.Vote of all abstain votes. func (v Voting) abstainVotes() []vote.Vote { filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Abstain } return filter.Choose(v.Votes(), filterFunc).([]vote.Vote) }