///|
/// Stable diagnostics. Lines are 1-based for text input, 0 for value APIs.
pub(all) struct Diagnostic {
code : String
line : Int
detail : String
} derive(Eq, Debug)
///|
/// Aggregate ballot: no voter identity or personal data.
pub struct Ballot {
ranking : Array[String]
weight : Int
} derive(Eq, Debug)
///|
/// A bounded, validated profile. Internal arrays never escape by reference.
pub struct Election {
candidates : Array[String]
ballots : Array[Ballot]
total : Int
} derive(Eq, Debug)
///|
fn problem(code : String, detail : String) -> Diagnostic {
{ code, line: 0, detail }
}
///|
fn valid_id(id : String) -> Bool {
if id.length() < 1 || id.length() > 48 {
return false
}
for ch in id {
if !(ch >= 'A' && ch <= 'Z') &&
!(ch >= 'a' && ch <= 'z') &&
!(ch >= '0' && ch <= '9') &&
ch != '_' &&
ch != '-' {
return false
}
}
true
}
///|
pub fn Ballot::new(
ranking : Array[String],
weight : Int,
) -> Result[Ballot, Diagnostic] {
if weight < 1 || weight > 1000000 {
return Err(problem("weight.range", "expected 1..1000000"))
}
if ranking.length() > 64 {
return Err(problem("ranking.limit", "at most 64 ranks"))
}
for i, id in ranking {
if !valid_id(id) {
return Err(problem("candidate.id", id))
}
for j = 0; j < i; j = j + 1 {
if ranking[j] == id {
return Err(problem("ranking.duplicate", id))
}
}
}
Ok({ ranking: ranking.copy(), weight })
}
///|
pub fn Ballot::ranking(self : Ballot) -> Array[String] {
self.ranking.copy()
}
///|
pub fn Ballot::weight(self : Ballot) -> Int {
self.weight
}
///|
pub fn Election::new(
candidates : Array[String],
ballots : Array[Ballot],
) -> Result[Election, Diagnostic] {
if candidates.length() < 1 || candidates.length() > 64 {
return Err(problem("candidates.limit", "expected 1..64 candidates"))
}
for i, id in candidates {
if !valid_id(id) {
return Err(problem("candidate.id", id))
}
for j = 0; j < i; j = j + 1 {
if candidates[j] == id {
return Err(problem("candidate.duplicate", id))
}
}
}
if ballots.length() > 10000 {
return Err(problem("ballots.limit", "at most 10000 rows"))
}
let mut total = 0
for ballot in ballots {
if total > 1000000 - ballot.weight {
return Err(problem("total.limit", "total weight exceeds 1000000"))
}
total += ballot.weight
for id in ballot.ranking {
if !candidates.contains(id) {
return Err(problem("ranking.unknown", id))
}
}
}
Ok({ candidates: candidates.copy(), ballots: ballots.copy(), total })
}
///|
pub fn Election::candidates(self : Election) -> Array[String] {
self.candidates.copy()
}
///|
pub fn Election::ballots(self : Election) -> Array[Ballot] {
self.ballots.copy()
}
///|
pub fn Election::total_weight(self : Election) -> Int {
self.total
}