Welcome back to a fascinating session where we will learn about enhancing existing functionality without causing regressions. Our scenario today involves designing a voting system. We'll start with the basic implementation of the voting system and gradually introduce additional elements of complexity.
In our initial task, we create a simple voting system in TypeScript with a set of basic functionalities. Let's define the methods with TypeScript type annotations:
registerCandidate(candidateId: string): boolean
: This method is used for adding new candidates to our system.vote(timestamp: number, voterId: string, candidateId: string): boolean
: This method is designed to facilitate users in casting their votes. Each vote is given a timestamp.getVotes(candidateId: string): number | null
: This method retrieves the total number of votes for a given candidate.topNCandidates(n: number): string[]
: We also want to add a leaderboard functionality to our system. This method returns the topn
candidates sorted by the number of votes.
Let's jump into the TypeScript code and begin the implementation of our starter task. Here, we use TypeScript's type system to define class fields and method parameters, providing more robust type-checking and better code maintenance.
TypeScript1class VotingSystem { 2 private candidates: Map<string, number>; // Stores candidateId as key and votes as value 3 private voters: Map<string, { votes: string[]; timestamps: number[] }>; // Tracks each voter's voting history 4 private blockTime?: number; // Optional timestamp for blocking voter registration 5 6 constructor() { 7 this.candidates = new Map<string, number>(); 8 this.voters = new Map<string, { votes: string[]; timestamps: number[] }>(); 9 } 10 11 registerCandidate(candidateId: string): boolean { 12 if (this.candidates.has(candidateId)) { 13 return false; // Candidate is already registered 14 } 15 this.candidates.set(candidateId, 0); // Initialize candidates with 0 votes 16 return true; 17 } 18 19 vote(timestamp: number, voterId: string, candidateId: string): boolean { 20 // Check if blockTime is set and if the vote attempt is after the block timestamp 21 if (this.blockTime && timestamp >= this.blockTime) { 22 return false; // Vote attempt is blocked due to the registration freeze 23 } 24 25 if (!this.candidates.has(candidateId)) { 26 return false; // Returns false if candidate is not registered 27 } 28 29 if (!this.voters.has(voterId)) { 30 this.voters.set(voterId, { votes: [], timestamps: [] }); 31 } 32 33 const voterData = this.voters.get(voterId)!; 34 voterData.votes.push(candidateId); // Record the vote 35 voterData.timestamps.push(timestamp); // Record the time of the vote 36 this.candidates.set(candidateId, this.candidates.get(candidateId)! + 1); // Increment vote count for the candidate 37 return true; 38 } 39 40 getVotes(candidateId: string): number | null { 41 return this.candidates.get(candidateId) || null; // Retrieve vote count for a candidate, or null if not found 42 } 43 44 topNCandidates(n: number): string[] { 45 return Array.from(this.candidates.keys()) 46 .sort((a, b) => this.candidates.get(b)! - this.candidates.get(a)!) 47 .slice(0, n); // Return top `n` candidates based on votes 48 } 49}
Now that we have a basic voting system, our goal is to enhance this system with additional functionalities. The methods will be correctly typed using TypeScript:
getVotingHistory(voterId: string): Record<string, number> | null
: Provides a detailed voting history for a specified voter, returning an object mapping each candidate to the number of times voted for by the voter. Returnsnull
if the voter ID does not exist.blockVoterRegistration(timestamp: number): boolean
: Implements a mechanism to halt any new voter registrations past a specified timestamp, effectively freezing the voter list as of that moment.changeVote(timestamp: number, voterId: string, oldCandidateId: string, newCandidateId: string): boolean
: Enables voters to change their vote from one candidate to another, given the change is made within a 24-hour window from their last vote, ensuring both the old and new candidates are registered, and that the voter initially voted for the old candidate.
We proceed to enhance our existing VotingSystem
class to accommodate the new functionalities. Let's incorporate the methods to get the voting history and to block further voter registrations.
TypeScript1class VotingSystem { 2 // existing fields and constructor here... 3 4 getVotingHistory(voterId: string): Record<string, number> | null { 5 if (!this.voters.has(voterId)) { 6 return null; 7 } 8 const voterData = this.voters.get(voterId)!; 9 const votingHistory: Record<string, number> = {}; 10 voterData.votes.forEach((candidateId) => { 11 if (!votingHistory[candidateId]) { 12 votingHistory[candidateId] = 0; 13 } 14 votingHistory[candidateId]++; 15 }); 16 return votingHistory; // Returns an object with each candidate voted for, and the number of times they were voted for 17 } 18 19 blockVoterRegistration(timestamp: number): boolean { 20 this.blockTime = timestamp; // Records the timestamp after which no registration is allowed 21 return true; 22 } 23}
The getVotingHistory
method retrieves a voter's voting history as an object, mapping each candidate ID to the number of times the voter has voted for them. If the voter exists, the method accesses their votes
array, which contains the IDs of the candidates they voted for. It then iterates through this array, using an object called votingHistory
to count the occurrences of each candidate ID. For every candidate in the array, the method increments their count in votingHistory
. Finally, it returns this object, providing a summary of the voter’s activity.
With the introduction of the blockVoterRegistration
functionality, we must revisit our vote
method to ensure it respects the new rules set by this feature. Here’s how we modify the vote
method in TypeScript to incorporate this change:
TypeScript1class VotingSystem { 2 // existing fields, constructor, and methods here… 3 4 vote(timestamp: number, voterId: string, candidateId: string): boolean { 5 // Check if blockTime is set and if the vote attempt is after the block timestamp 6 if (this.blockTime && timestamp >= this.blockTime) { 7 return false; // Vote attempt is blocked due to the registration freeze 8 } 9 10 if (!this.candidates.has(candidateId)) { 11 return false; // Returns false if candidate is not registered 12 } 13 14 if (!this.voters.has(voterId)) { 15 this.voters.set(voterId, { votes: [], timestamps: [] }); 16 } 17 18 const voterData = this.voters.get(voterId)!; 19 voterData.votes.push(candidateId); // Record the vote 20 voterData.timestamps.push(timestamp); // Record the time of the vote 21 this.candidates.set(candidateId, this.candidates.get(candidateId)! + 1); // Increment vote count for the candidate 22 return true; 23 } 24}
This update ensures that our voting system behaves as expected, even with the new functionality to block further voter registrations beyond a certain timestamp. It's a perfect demonstration of how new features can necessitate revisits and revisions to existing code while maintaining backward compatibility.
The changeVote
method allows voters to change their vote, adhering to specific rules. Here's a step-by-step explanation of implementing this functionality in TypeScript:
- Verify Candidate and Voter Validity: Check if both the old and new candidate IDs exist in the system, and verify that the voter has previously voted for the old candidate.
- Timestamp Constraints: Ensure that the voter is trying to change their vote within an allowable timeframe after their initial vote.
- Update Votes: If all conditions are met, subtract one vote from the old candidate, add one vote to the new candidate, and update the voter's voting record.
TypeScript1class VotingSystem { 2 // existing fields, constructor, and methods here… 3 4 changeVote(timestamp: number, voterId: string, oldCandidateId: string, newCandidateId: string): boolean { 5 // Verify existence in the voting system 6 if (!this.candidates.has(oldCandidateId) || !this.candidates.has(newCandidateId)) { 7 return false; 8 } 9 10 // Check if the voterId is valid and has previously voted 11 if (!this.voters.has(voterId) || !this.voters.get(voterId)!.votes.length) { 12 return false; // Voter is either not registered or has not voted yet 13 } 14 15 const voterData = this.voters.get(voterId)!; 16 17 // Confirm the voter has voted for the old candidate 18 const lastVoteIndex = voterData.votes.lastIndexOf(oldCandidateId); 19 if (lastVoteIndex === -1) { 20 return false; 21 } 22 23 // Ensure the operation is within the permitted timeframe 24 if (timestamp - voterData.timestamps[lastVoteIndex] > 86400) { // 24 hours in seconds 25 return false; 26 } 27 28 // Perform the vote change 29 voterData.votes.splice(lastVoteIndex, 1); // Remove the old vote 30 voterData.timestamps.splice(lastVoteIndex, 1); // Remove the corresponding timestamp 31 voterData.votes.push(newCandidateId); // Append the new vote 32 voterData.timestamps.push(timestamp); // Append the new timestamp 33 34 // Update candidates' vote counts 35 this.candidates.set(oldCandidateId, this.candidates.get(oldCandidateId)! - 1); 36 this.candidates.set(newCandidateId, this.candidates.get(newCandidateId)! + 1); 37 38 return true; 39 } 40}
Congratulations! You've successfully enhanced the voting system by adding functionalities to view voting history, block new candidate registrations, and, most importantly, enable vote changes under specific conditions. Each of these features was developed with careful consideration to maintain the integrity and backward compatibility of the system. Continue exploring with practice sessions and further experimentation to refine your skills in developing complex, functionality-rich applications using TypeScript. Happy Coding!