govote/voting/voting.go

257 lines
7.3 KiB
Go
Raw Normal View History

2024-05-12 22:47:24 +00:00
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 (
2024-05-14 08:26:48 +00:00
// Voting represents a voting with all its data and meta-data.
2024-05-12 22:47:24 +00:00
Voting struct {
2024-05-14 08:26:48 +00:00
// id is the unique ID of the vote.
id string
// referendum contains the subject or title of the voting
2024-05-12 22:47:24 +00:00
referendum string
2024-05-14 08:26:48 +00:00
// 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
2024-05-13 08:45:38 +00:00
annonymous bool
2024-05-14 08:26:48 +00:00
// votes holds all votes associated with this voting
votes []vote.Vote
2024-05-12 22:47:24 +00:00
}
)
2024-05-14 08:26:48 +00:00
// NewVoting returns an initialized Voting.
2024-05-12 22:47:24 +00:00
func NewVoting(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool) *Voting {
return &Voting{
2024-05-13 08:45:38 +00:00
id: id,
2024-05-12 22:47:24 +00:00
referendum: r,
2024-05-13 08:45:38 +00:00
deadline: d,
quorum: q,
threshold: t,
electors: e,
annonymous: a,
votes: []vote.Vote{},
2024-05-12 22:47:24 +00:00
}
}
2024-05-14 08:26:48 +00:00
// 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.
2024-05-12 22:47:24 +00:00
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 {
2024-05-13 08:45:38 +00:00
voting.votes = append(voting.votes, v[i])
2024-05-12 22:47:24 +00:00
}
return voting
}
2024-05-14 08:26:48 +00:00
// String implements fmt.Stringer.
2024-05-12 22:47:24 +00:00
func (v Voting) String() string {
2024-05-14 08:26:48 +00:00
// initialize vars with all the metadata
2024-05-13 08:45:38 +00:00
var (
2024-05-14 08:26:48 +00:00
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
2024-05-13 08:45:38 +00:00
)
2024-05-14 08:26:48 +00:00
// check the deadline
2024-05-13 08:45:38 +00:00
if time.Now().UTC().After(v.deadline) {
if votingSatisfied {
deadlineStatus = "✅ APROVED ✅"
} else {
deadlineStatus = "❌ REJECTED ❌"
}
}
2024-05-14 08:26:48 +00:00
// check quorum and threshold
2024-05-13 08:45:38 +00:00
if v.quorum.IsSatisfied(possibleVotes, totalVotes) {
quorumStatus = "✅ PASS"
}
if v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) {
thresholdStatus = "✅ PASS"
}
2024-05-12 22:47:24 +00:00
2024-05-14 08:26:48 +00:00
// assemble output string
2024-05-12 22:47:24 +00:00
out += fmt.Sprintf("Referendum: %s\n", strings.ToUpper(v.referendum))
out += fmt.Sprintf("Deadline : %s UTC\n", v.deadline.Format(time.DateTime))
2024-05-13 08:45:38 +00:00
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)
2024-05-14 08:26:48 +00:00
// only show votes when not anonymous
2024-05-13 08:45:38 +00:00
if !v.annonymous {
out += "\n"
if v.votes != nil && len(v.votes) > 0 {
for _, _v := range v.votes {
out += fmt.Sprintf("💬 %s\n", _v)
}
}
}
2024-05-12 22:47:24 +00:00
2024-05-14 08:26:48 +00:00
// return the output string
2024-05-12 22:47:24 +00:00
return out
}
2024-05-14 08:26:48 +00:00
// ID returns the unique ID of this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) ID() string {
2024-05-13 08:45:38 +00:00
return v.id
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// Referendum returns the referendum of this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) Referendum() string {
return v.referendum
}
2024-05-14 08:26:48 +00:00
// Deadline returns the deadline of this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) Deadline() time.Time {
2024-05-13 08:45:38 +00:00
return v.deadline
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// Quorum returns the quorum of this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) Quorum() string {
2024-05-13 08:45:38 +00:00
return v.quorum.String()
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// Threshold returns the threshold of this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) Threshold() string {
2024-05-13 08:45:38 +00:00
return v.threshold.String()
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// Electors returns the list of eligible voters of this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) Electors() []string {
electors := make([]string, len(v.electors))
for i := range v.electors {
electors[i] = v.electors[i]
}
return electors
}
2024-05-14 08:26:48 +00:00
// Anonymous returns the anonymous setting of this voting.
func (v Voting) Anonymous() bool {
2024-05-13 11:20:26 +00:00
return v.annonymous
}
2024-05-14 08:26:48 +00:00
// IsOpen returns true while the deadline has not yet been reached.
2024-05-12 22:47:24 +00:00
func (v Voting) IsOpen() bool {
2024-05-13 08:45:38 +00:00
return v.deadline.After(time.Now().UTC())
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// Result returns a Result based on this voting.
2024-05-12 22:47:24 +00:00
func (v Voting) Result() Result {
2024-05-13 08:45:38 +00:00
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,
}
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// 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.
2024-05-12 22:47:24 +00:00
func (v *Voting) Vote(vote vote.Vote) error {
2024-05-14 08:26:48 +00:00
// check if deadline has passed
2024-05-13 08:45:38 +00:00
if time.Now().UTC().After(v.deadline) {
return fmt.Errorf("deadline has passed")
}
2024-05-14 08:26:48 +00:00
// place vote if elector is eligible
2024-05-13 08:45:38 +00:00
for _, elector := range v.electors {
if elector == vote.Elector {
v.votes = append(v.votes, vote)
return nil
}
}
2024-05-14 08:26:48 +00:00
// raise an error if not eligible
2024-05-13 08:45:38 +00:00
return fmt.Errorf("not eligable to vote: %s", vote.Elector)
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// Votes returns the normalized list of effective votes.
// Only the last vote of each elector before the deadline counts.
2024-05-12 22:47:24 +00:00
func (v Voting) Votes() []vote.Vote {
votes := []vote.Vote{}
nextVote:
2024-05-14 08:26:48 +00:00
// iterate over all placed votes in reverse order only respecting the first
// vote of each elector (effectively the final vote of each elector).
2024-05-13 08:45:38 +00:00
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])
}
2024-05-13 22:53:30 +00:00
2024-05-14 08:26:48 +00:00
// optionally, anonymize the votes before returning them
2024-05-13 22:53:30 +00:00
if v.annonymous {
for i := range votes {
votes[i].Elector = ""
}
}
2024-05-14 08:26:48 +00:00
2024-05-12 22:47:24 +00:00
return votes
}
2024-05-14 08:26:48 +00:00
// yesVotes returns a []vote.Vote of all yes votes.
2024-05-12 22:47:24 +00:00
func (v Voting) yesVotes() []vote.Vote {
2024-05-13 08:45:38 +00:00
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Yes }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// noVotes returns a []vote.Vote of all no votes.
2024-05-12 22:47:24 +00:00
func (v Voting) noVotes() []vote.Vote {
2024-05-13 08:45:38 +00:00
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.No }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
2024-05-12 22:47:24 +00:00
}
2024-05-14 08:26:48 +00:00
// abstainVotes returns a []vote.Vote of all abstain votes.
2024-05-12 22:47:24 +00:00
func (v Voting) abstainVotes() []vote.Vote {
2024-05-13 08:45:38 +00:00
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Abstain }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
2024-05-12 22:47:24 +00:00
}