satyacode
Background
Back to js-functions

12-panchayat-election.js

JavaScript
1/**
2 * 🗳️ Panchayat Election System - Capstone
3 *
4 * Village ki panchayat election ka system bana! Yeh CAPSTONE challenge hai
5 * jisme saare function concepts ek saath use honge:
6 * closures, callbacks, HOF, factory, recursion, pure functions.
7 *
8 * Functions:
9 *
10 *   1. createElection(candidates)
11 *      - CLOSURE: private state (votes object, registered voters set)
12 *      - candidates: array of { id, name, party }
13 *      - Returns object with methods:
14 *
15 *      registerVoter(voter)
16 *        - voter: { id, name, age }
17 *        - Add to private registered set. Return true.
18 *        - Agar already registered or voter invalid, return false.
19 *        - Agar age < 18, return false.
20 *
21 *      castVote(voterId, candidateId, onSuccess, onError)
22 *        - CALLBACKS: call onSuccess or onError based on result
23 *        - Validate: voter registered? candidate exists? already voted?
24 *        - If valid: record vote, call onSuccess({ voterId, candidateId })
25 *        - If invalid: call onError("reason string")
26 *        - Return the callback's return value
27 *
28 *      getResults(sortFn)
29 *        - HOF: takes optional sort comparator function
30 *        - Returns array of { id, name, party, votes: count }
31 *        - If sortFn provided, sort results using it
32 *        - Default (no sortFn): sort by votes descending
33 *
34 *      getWinner()
35 *        - Returns candidate object with most votes
36 *        - If tie, return first candidate among tied ones
37 *        - If no votes cast, return null
38 *
39 *   2. createVoteValidator(rules)
40 *      - FACTORY: returns a validation function
41 *      - rules: { minAge: 18, requiredFields: ["id", "name", "age"] }
42 *      - Returned function takes a voter object and returns { valid, reason }
43 *
44 *   3. countVotesInRegions(regionTree)
45 *      - RECURSION: count total votes in nested region structure
46 *      - regionTree: { name, votes: number, subRegions: [...] }
47 *      - Sum votes from this region + all subRegions (recursively)
48 *      - Agar regionTree null/invalid, return 0
49 *
50 *   4. tallyPure(currentTally, candidateId)
51 *      - PURE FUNCTION: returns NEW tally object with incremented count
52 *      - currentTally: { "cand1": 5, "cand2": 3, ... }
53 *      - Return new object where candidateId count is incremented by 1
54 *      - MUST NOT modify currentTally
55 *      - If candidateId not in tally, add it with count 1
56 *
57 * @example
58 *   const election = createElection([
59 *     { id: "C1", name: "Sarpanch Ram", party: "Janata" },
60 *     { id: "C2", name: "Pradhan Sita", party: "Lok" }
61 *   ]);
62 *   election.registerVoter({ id: "V1", name: "Mohan", age: 25 });
63 *   election.castVote("V1", "C1", r => "voted!", e => "error: " + e);
64 *   // => "voted!"
65 */
66export function createElection(candidates) {
67  const votes = {};       
68  const registeredVoters = new Map();   
69  const votedVoters = new Set();        
70
71  candidates.forEach((c) => {
72    votes[c.id] = 0;
73  });
74  function isValidVoter(voter) {
75    return (
76      voter !== null &&
77      typeof voter === "object" &&
78      voter.id &&
79      voter.name &&
80      typeof voter.age === "number"
81    );
82  }
83
84  function candidateExists(candidateId) {
85    return candidates.some((c) => c.id === candidateId);
86  }
87
88  return {
89    registerVoter(voter) {
90      if (!isValidVoter(voter)) return false;
91      if (voter.age < 18) return false;
92      if (registeredVoters.has(voter.id)) return false;
93
94      registeredVoters.set(voter.id, voter);
95      return true;
96    },
97
98    castVote(voterId, candidateId, onSuccess, onError) {
99      if (!registeredVoters.has(voterId)) {
100        return onError("Voter not registered");
101      }
102      if (!candidateExists(candidateId)) {
103        return onError("Candidate does not exist");
104      }
105      if (votedVoters.has(voterId)) {
106        return onError("Voter has already voted");
107      }
108      votes[candidateId]++;
109      votedVoters.add(voterId);
110
111      return onSuccess({ voterId, candidateId });
112    },
113
114    getResults(sortFn) {
115      const results = candidates.map((c) => ({
116        id:    c.id,
117        name:  c.name,
118        party: c.party,
119        votes: votes[c.id],
120      }));
121      if (sortFn) {
122        return results.sort(sortFn);
123      }
124
125      return results.sort((a, b) => b.votes - a.votes);
126    },
127
128    getWinner() {
129      const totalVotes = Object.values(votes).reduce((a, b) => a + b, 0);
130      if (totalVotes === 0) return null;
131      const results = this.getResults();  
132      return results[0];
133    },
134  };
135}
136
137export function createVoteValidator(rules) {
138
139  return function (voter) {
140
141    for (const field of rules.requiredFields) {
142      if (!voter || voter[field] === undefined || voter[field] === null) {
143        return { valid: false, reason: `Missing required field: ${field}` };
144      }
145    }
146
147    if (typeof voter.age !== "number") {
148      return { valid: false, reason: "Age must be a number" };
149    }
150
151    if (voter.age < rules.minAge) {
152      return { valid: false, reason: `Age must be at least ${rules.minAge}` };
153    }
154
155    return { valid: true, reason: null };
156  };
157}
158
159export function countVotesInRegions(regionTree) {
160  if (!regionTree || typeof regionTree !== "object") return 0;
161
162  const currentVotes = typeof regionTree.votes === "number"
163    ? regionTree.votes
164    : 0;
165  if (!Array.isArray(regionTree.subRegions) || regionTree.subRegions.length === 0) {
166    return currentVotes;
167  }
168
169  const subVotes = regionTree.subRegions.reduce((total, sub) => {
170    return total + countVotesInRegions(sub);   
171  }, 0);
172
173  return currentVotes + subVotes;
174}
175
176export function tallyPure(currentTally, candidateId) {
177  return {
178    ...currentTally,
179    [candidateId]: (currentTally[candidateId] || 0) + 1,
180  };
181}