Search Apps Documentation Source Content File Folder Download Copy Actions Download

record.gno

5.45 Kb ยท 194 lines
  1package commondao
  2
  3import (
  4	"errors"
  5	"math"
  6
  7	"gno.land/p/nt/avl"
  8)
  9
 10// ErrVoteExists indicates that a user already voted.
 11var ErrVoteExists = errors.New("user already voted")
 12
 13type (
 14	// VoteIterFn defines a callback to iterate votes.
 15	VoteIterFn func(Vote) (stop bool)
 16
 17	// VotesCountIterFn defines a callback to iterate voted choices.
 18	VotesCountIterFn func(_ VoteChoice, voteCount int) (stop bool)
 19
 20	// Vote defines a single vote.
 21	Vote struct {
 22		// Address is the address of the user that this vote belons to.
 23		Address address
 24
 25		// Choice contains the voted choice.
 26		Choice VoteChoice
 27
 28		// Reason contains an optional reason for the vote.
 29		Reason string
 30
 31		// Context can store any custom voting values related to the vote.
 32		//
 33		// Warning: When using context be careful if references/pointers are
 34		// assigned to it because they could potentially be accessed anywhere,
 35		// which could lead to unwanted indirect modifications.
 36		Context any
 37	}
 38)
 39
 40// ReadonlyVotingRecord defines an read only voting record.
 41type ReadonlyVotingRecord struct {
 42	votes avl.Tree // string(address) -> Vote
 43	count avl.Tree // string(choice) -> int
 44}
 45
 46// Size returns the total number of votes that record contains.
 47func (r ReadonlyVotingRecord) Size() int {
 48	return r.votes.Size()
 49}
 50
 51// Iterate iterates voting record votes.
 52func (r ReadonlyVotingRecord) Iterate(offset, count int, reverse bool, fn VoteIterFn) bool {
 53	cb := func(_ string, v any) bool { return fn(v.(Vote)) }
 54	if reverse {
 55		return r.votes.ReverseIterateByOffset(offset, count, cb)
 56	}
 57	return r.votes.IterateByOffset(offset, count, cb)
 58}
 59
 60// IterateVotesCount iterates voted choices with the amount of votes submited for each.
 61func (r ReadonlyVotingRecord) IterateVotesCount(fn VotesCountIterFn) bool {
 62	return r.count.Iterate("", "", func(k string, v any) bool {
 63		return fn(VoteChoice(k), v.(int))
 64	})
 65}
 66
 67// VoteCount returns the number of votes for a single voting choice.
 68func (r ReadonlyVotingRecord) VoteCount(c VoteChoice) int {
 69	if v, found := r.count.Get(string(c)); found {
 70		return v.(int)
 71	}
 72	return 0
 73}
 74
 75// HasVoted checks if an account already voted.
 76func (r ReadonlyVotingRecord) HasVoted(user address) bool {
 77	return r.votes.Has(user.String())
 78}
 79
 80// GetVote returns a vote.
 81func (r ReadonlyVotingRecord) GetVote(user address) (_ Vote, found bool) {
 82	if v, found := r.votes.Get(user.String()); found {
 83		return v.(Vote), true
 84	}
 85	return Vote{}, false
 86}
 87
 88// VotingRecord stores accounts that voted and vote choices.
 89type VotingRecord struct {
 90	ReadonlyVotingRecord
 91}
 92
 93// Readonly returns a read only voting record.
 94func (r VotingRecord) Readonly() ReadonlyVotingRecord {
 95	return r.ReadonlyVotingRecord
 96}
 97
 98// AddVote adds a vote to the voting record.
 99// If a vote for the same user already exists is overwritten.
100func (r *VotingRecord) AddVote(vote Vote) (updated bool) {
101	// Get previous member vote if it exists
102	v, _ := r.votes.Get(vote.Address.String())
103
104	// When a previous vote exists update counter for the previous choice
105	updated = r.votes.Set(vote.Address.String(), vote)
106	if updated {
107		prev := v.(Vote)
108		r.count.Set(string(prev.Choice), r.VoteCount(prev.Choice)-1)
109	}
110
111	r.count.Set(string(vote.Choice), r.VoteCount(vote.Choice)+1)
112	return
113}
114
115// FindMostVotedChoice returns the most voted choice.
116// ChoiceNone is returned when there is a tie between different
117// voting choices or when the voting record has are no votes.
118func FindMostVotedChoice(r ReadonlyVotingRecord) VoteChoice {
119	var (
120		choice                  VoteChoice
121		currentCount, prevCount int
122	)
123
124	r.IterateVotesCount(func(c VoteChoice, count int) bool {
125		if currentCount <= count {
126			choice = c
127			prevCount = currentCount
128			currentCount = count
129		}
130		return false
131	})
132
133	if prevCount < currentCount {
134		return choice
135	}
136	return ChoiceNone
137}
138
139// SelectChoiceByAbsoluteMajority select the vote choice by absolute majority.
140// Vote choice is a majority when chosen by more than half of the votes.
141// Absolute majority considers abstentions when counting votes.
142func SelectChoiceByAbsoluteMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {
143	choice := FindMostVotedChoice(r)
144	if choice != ChoiceNone && r.VoteCount(choice) > int(membersCount/2) {
145		return choice, true
146	}
147	return ChoiceNone, false
148}
149
150// SelectChoiceBySuperMajority select the vote choice by super majority using a 2/3s threshold.
151// Abstentions are considered when calculating the super majority choice.
152func SelectChoiceBySuperMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {
153	if membersCount < 3 {
154		return ChoiceNone, false
155	}
156
157	choice := FindMostVotedChoice(r)
158	if choice != ChoiceNone && r.VoteCount(choice) >= int(math.Ceil((2*float64(membersCount))/3)) {
159		return choice, true
160	}
161	return ChoiceNone, false
162}
163
164// SelectChoiceByPlurality selects the vote choice by plurality.
165// The choice will be considered a majority if it has votes and if there is no other
166// choice with the same number of votes. A tie won't be considered majority.
167func SelectChoiceByPlurality(r ReadonlyVotingRecord) (VoteChoice, bool) {
168	var (
169		choice       VoteChoice
170		currentCount int
171		isMajority   bool
172	)
173
174	r.IterateVotesCount(func(c VoteChoice, count int) bool {
175		// Don't consider explicit abstentions or invalid votes
176		if c == ChoiceAbstain || c == ChoiceNone {
177			return false
178		}
179
180		if currentCount < count {
181			choice = c
182			currentCount = count
183			isMajority = true
184		} else if currentCount == count {
185			isMajority = false
186		}
187		return false
188	})
189
190	if isMajority {
191		return choice, true
192	}
193	return ChoiceNone, false
194}