🎉🚀 May the concensus be with us!
This commit is contained in:
commit
48328e7db2
33 changed files with 4051 additions and 0 deletions
203
voting/main.go
Normal file
203
voting/main.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
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 struct {
|
||||
id string
|
||||
referendum string
|
||||
deadline time.Time
|
||||
quorum quorum.Quorum
|
||||
threshold threshold.Threshold
|
||||
electors []string
|
||||
annonymous bool
|
||||
votes []vote.Vote
|
||||
}
|
||||
)
|
||||
|
||||
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{},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (v Voting) String() string {
|
||||
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
|
||||
|
||||
if time.Now().UTC().After(v.deadline) {
|
||||
if votingSatisfied {
|
||||
deadlineStatus = "✅ APROVED ✅"
|
||||
} else {
|
||||
deadlineStatus = "❌ REJECTED ❌"
|
||||
}
|
||||
}
|
||||
if v.quorum.IsSatisfied(possibleVotes, totalVotes) {
|
||||
quorumStatus = "✅ PASS"
|
||||
}
|
||||
if v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) {
|
||||
thresholdStatus = "✅ PASS"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 out
|
||||
}
|
||||
|
||||
func (v Voting) ID() string {
|
||||
return v.id
|
||||
}
|
||||
|
||||
func (v Voting) Referendum() string {
|
||||
return v.referendum
|
||||
}
|
||||
|
||||
func (v Voting) Deadline() time.Time {
|
||||
return v.deadline
|
||||
}
|
||||
|
||||
func (v Voting) Quorum() string {
|
||||
return v.quorum.String()
|
||||
}
|
||||
|
||||
func (v Voting) Threshold() string {
|
||||
return v.threshold.String()
|
||||
}
|
||||
|
||||
func (v Voting) Electors() []string {
|
||||
electors := make([]string, len(v.electors))
|
||||
for i := range v.electors {
|
||||
electors[i] = v.electors[i]
|
||||
}
|
||||
return electors
|
||||
}
|
||||
|
||||
func (v Voting) IsOpen() bool {
|
||||
return v.deadline.After(time.Now().UTC())
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Voting) Vote(vote vote.Vote) error {
|
||||
if time.Now().UTC().After(v.deadline) {
|
||||
return fmt.Errorf("deadline has passed")
|
||||
}
|
||||
for _, elector := range v.electors {
|
||||
if elector == vote.Elector {
|
||||
v.votes = append(v.votes, vote)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("not eligable to vote: %s", vote.Elector)
|
||||
}
|
||||
|
||||
func (v Voting) Votes() []vote.Vote {
|
||||
votes := []vote.Vote{}
|
||||
nextVote:
|
||||
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])
|
||||
}
|
||||
return 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
103
voting/quorum/quorum.go
Normal file
103
voting/quorum/quorum.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package quorum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Quorum uint8
|
||||
|
||||
const (
|
||||
Simple Quorum = iota
|
||||
OneFifth
|
||||
OneQuarter
|
||||
OneThird
|
||||
OneHalf
|
||||
TwoFifths
|
||||
TwoThirds
|
||||
ThreeQuarters
|
||||
ThreeFifths
|
||||
FourFifths
|
||||
Unanimous
|
||||
)
|
||||
|
||||
func ValidQuorums() []Quorum {
|
||||
return []Quorum{
|
||||
Simple,
|
||||
OneFifth, OneQuarter, OneThird, OneHalf,
|
||||
TwoThirds, TwoFifths,
|
||||
ThreeQuarters, ThreeFifths,
|
||||
FourFifths,
|
||||
}
|
||||
}
|
||||
|
||||
func FromString(s string) (Quorum, error) {
|
||||
for _, q := range ValidQuorums() {
|
||||
if strings.ToUpper(q.String()) == strings.ToUpper(s) {
|
||||
return q, nil
|
||||
}
|
||||
}
|
||||
return Simple, fmt.Errorf("inalid quorum: %s", s)
|
||||
}
|
||||
|
||||
func (q Quorum) String() string {
|
||||
switch q {
|
||||
case Simple:
|
||||
return "SIMPLE"
|
||||
case OneFifth:
|
||||
return "1/5"
|
||||
case OneQuarter:
|
||||
return "1/4"
|
||||
case OneThird:
|
||||
return "1/3"
|
||||
case OneHalf:
|
||||
return "1/2"
|
||||
case TwoThirds:
|
||||
return "2/3"
|
||||
case TwoFifths:
|
||||
return "2/5"
|
||||
case ThreeQuarters:
|
||||
return "3/4"
|
||||
case ThreeFifths:
|
||||
return "3/5"
|
||||
case FourFifths:
|
||||
return "4/5"
|
||||
case Unanimous:
|
||||
return "ALL"
|
||||
default:
|
||||
panic("this code should never be reached")
|
||||
}
|
||||
}
|
||||
|
||||
func (q Quorum) IsSatisfied(possibleVotes, totalVotes int) bool {
|
||||
if totalVotes == 0 {
|
||||
return false
|
||||
}
|
||||
switch q {
|
||||
case Simple:
|
||||
return true
|
||||
case OneFifth:
|
||||
return totalVotes * 5 >= possibleVotes
|
||||
case OneQuarter:
|
||||
return totalVotes * 4 >= possibleVotes
|
||||
case OneThird:
|
||||
return totalVotes * 3 >= possibleVotes
|
||||
case OneHalf:
|
||||
return totalVotes * 2 >= possibleVotes
|
||||
case TwoThirds:
|
||||
return totalVotes * 3 >= possibleVotes * 2
|
||||
case TwoFifths:
|
||||
return totalVotes * 5 >= possibleVotes * 2
|
||||
case ThreeQuarters:
|
||||
return totalVotes * 4 >= possibleVotes * 3
|
||||
case ThreeFifths:
|
||||
return totalVotes * 5 >= possibleVotes * 3
|
||||
case FourFifths:
|
||||
return totalVotes * 5 >= possibleVotes * 4
|
||||
case Unanimous:
|
||||
return totalVotes >= possibleVotes
|
||||
default:
|
||||
panic("this code should never be reached⚜️")
|
||||
}
|
||||
return false
|
||||
}
|
||||
10
voting/result.go
Normal file
10
voting/result.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package voting
|
||||
|
||||
import "code.c-base.org/baccenfutter/govote/voting/vote"
|
||||
|
||||
type Result struct {
|
||||
Quorum bool
|
||||
Threshold bool
|
||||
Yes, No, Abstain int
|
||||
Votes []vote.Vote
|
||||
}
|
||||
102
voting/threshold/threshold.go
Normal file
102
voting/threshold/threshold.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package threshold
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Threshold uint8
|
||||
|
||||
const (
|
||||
Simple Threshold = iota
|
||||
OneFifth
|
||||
OneQuarter
|
||||
OneThird
|
||||
OneHalf
|
||||
TwoThirds
|
||||
TwoFifths
|
||||
ThreeQuarters
|
||||
ThreeFifths
|
||||
FourFifths
|
||||
Unanimous
|
||||
)
|
||||
|
||||
func ValidThresholds() []Threshold {
|
||||
return []Threshold{
|
||||
Simple,
|
||||
OneFifth, OneQuarter, OneThird, OneHalf,
|
||||
TwoFifths, TwoThirds,
|
||||
ThreeFifths, ThreeQuarters, ThreeFifths,
|
||||
FourFifths,
|
||||
}
|
||||
}
|
||||
|
||||
func FromString(s string) (Threshold, error) {
|
||||
for _, t := range ValidThresholds() {
|
||||
if strings.ToUpper(t.String()) == strings.ToUpper(s) {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return Simple, fmt.Errorf("invalid threshold: %s", s)
|
||||
}
|
||||
|
||||
func (t Threshold) String() string {
|
||||
switch t {
|
||||
case Simple:
|
||||
return "SIMPLE"
|
||||
case OneFifth:
|
||||
return "1/5"
|
||||
case OneQuarter:
|
||||
return "1/4"
|
||||
case OneThird:
|
||||
return "1/3"
|
||||
case OneHalf:
|
||||
return "1/2"
|
||||
case TwoThirds:
|
||||
return "2/3"
|
||||
case TwoFifths:
|
||||
return "2/5"
|
||||
case ThreeQuarters:
|
||||
return "3/4"
|
||||
case ThreeFifths:
|
||||
return "3/5"
|
||||
case FourFifths:
|
||||
return "4/5"
|
||||
case Unanimous:
|
||||
return "ALL"
|
||||
default:
|
||||
panic("this code should never be reached")
|
||||
}
|
||||
}
|
||||
|
||||
func (t Threshold) IsSatisfied(totalVotes, yesVotes, noVotes int) bool {
|
||||
if totalVotes == 0 {
|
||||
return false
|
||||
}
|
||||
switch t {
|
||||
case Simple:
|
||||
return yesVotes > noVotes
|
||||
case OneFifth:
|
||||
return yesVotes * 5 >= totalVotes
|
||||
case OneQuarter:
|
||||
return yesVotes * 4 >= totalVotes
|
||||
case OneThird:
|
||||
return yesVotes * 3 >= totalVotes
|
||||
case OneHalf:
|
||||
return yesVotes * 2 >= totalVotes
|
||||
case TwoThirds:
|
||||
return yesVotes * 3 >= totalVotes * 2
|
||||
case TwoFifths:
|
||||
return yesVotes * 5 >= totalVotes * 2
|
||||
case ThreeQuarters:
|
||||
return yesVotes * 4 >= totalVotes * 3
|
||||
case ThreeFifths:
|
||||
return yesVotes * 5 >= totalVotes * 3
|
||||
case FourFifths:
|
||||
return yesVotes * 5 >= totalVotes * 4
|
||||
case Unanimous:
|
||||
return yesVotes >= totalVotes
|
||||
default:
|
||||
panic("this code should never be reached")
|
||||
}
|
||||
}
|
||||
40
voting/vote/choice.go
Normal file
40
voting/vote/choice.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package vote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Choice int8
|
||||
|
||||
func (choice Choice) String() string {
|
||||
switch choice {
|
||||
case Yes:
|
||||
return "YIP"
|
||||
case No:
|
||||
return "NOPE"
|
||||
case Abstain:
|
||||
return "DUNNO"
|
||||
default:
|
||||
panic("this code should never be reached")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
Abstain Choice = 0
|
||||
Yes Choice = 1
|
||||
No Choice = -1
|
||||
)
|
||||
|
||||
func ValidChoices() []Choice {
|
||||
return []Choice{Yes, No, Abstain}
|
||||
}
|
||||
|
||||
func ChoiceFromString(s string) (Choice, error) {
|
||||
for _, c := range ValidChoices() {
|
||||
if strings.ToUpper(c.String()) == strings.ToUpper(s) {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return Abstain, fmt.Errorf("invalid choice: %s", s)
|
||||
}
|
||||
32
voting/vote/vote.go
Normal file
32
voting/vote/vote.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package vote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Vote struct {
|
||||
Elector string
|
||||
Choice Choice
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func NewVote(elector string, choice Choice) Vote {
|
||||
return Vote{
|
||||
Elector: elector,
|
||||
Choice: choice,
|
||||
timestamp: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewVoteWithTimestamp(elector string, choice Choice, timestamp time.Time) Vote {
|
||||
return Vote{
|
||||
Elector: elector,
|
||||
Choice: choice,
|
||||
timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
func (vote Vote) String() string {
|
||||
return fmt.Sprintf("%s %s %s", vote.timestamp.Format(time.DateTime), vote.Choice, vote.Elector)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue