Welcome back to another exciting session where we learn about enhancing existing functionality without causing regressions. Today, our scenario 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 created a simple voting system in Ruby with a set of basic functionalities:
register_candidate(candidate_id)
: This method is used to add new candidates to our system.vote(timestamp, voter_id, candidate_id)
: This method facilitates users casting their votes. Each vote is given a timestamp.get_votes(candidate_id)
: This method retrieves the total number of votes for a given candidate.top_n_candidates(n)
: 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 Ruby code and begin the implementation of our starter task. Here, we use Ruby's built-in Hash
and Array
as the core of our design. These collections allow us to have dynamic lists keyed based on candidate IDs and voter IDs, which will greatly simplify our design.
Ruby1class VotingSystem 2 def initialize 3 @candidates = {} # Stores candidate_id as key and votes as value 4 @voters = {} # Tracks each voter's voting history 5 end 6 7 def register_candidate(candidate_id) 8 return false if @candidates.key?(candidate_id) # Candidate is already registered 9 10 @candidates[candidate_id] = 0 # Initialize candidates with 0 votes 11 true 12 end 13 14 def vote(timestamp, voter_id, candidate_id) 15 return false unless @candidates.key?(candidate_id) # Return false if candidate is not registered 16 17 voter_history = @voters[voter_id] ||= { votes: [], timestamps: [] } 18 voter_history[:votes] << candidate_id # Record the vote 19 voter_history[:timestamps] << timestamp # Record the time of the vote 20 @candidates[candidate_id] += 1 # Increment vote count for the candidate 21 true 22 end 23 24 def get_votes(candidate_id) 25 @candidates[candidate_id] || nil # Retrieve vote count for a candidate or nil if not found 26 end 27 28 def top_n_candidates(n) 29 @candidates.sort_by { |_, votes| -votes } 30 .take(n) 31 .map { |candidate_id, _| candidate_id } # Return top n candidates based on votes 32 end 33end
Now that we have a basic voting system, our goal is to enhance this system with additional functionalities:
get_voting_history(voter_id)
: Provides a detailed voting history for a specified voter, returning a hash with candidates and the number of votes they received from the voter. Returnsnil
if the voter ID does not exist.block_voter_registration(timestamp)
: Implements a mechanism to halt any new voter registrations past a specified timestamp, effectively freezing the voter list as of that moment.change_vote(timestamp, voter_id, old_candidate_id, new_candidate_id)
: 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.
First, let's incorporate the methods to get the voting history and to block further voter registrations:
Ruby1class VotingSystem 2 def initialize 3 @candidates = {} 4 @voters = {} 5 @block_time = nil # Records the timestamp after which no registration is allowed 6 end 7 8 def get_voting_history(voter_id) 9 return nil unless @voters.key?(voter_id) 10 11 voter_history = @voters[voter_id] 12 voting_history = Hash.new(0) 13 14 voter_history[:votes].each do |candidate_id| 15 voting_history[candidate_id] += 1 16 end 17 18 voting_history # Returns a hash with each candidate voted for and the number of times they were voted for 19 end 20 21 def block_voter_registration(timestamp) 22 @block_time = timestamp # Records the timestamp after which no registration is allowed 23 true 24 end 25end
With the introduction of the block_voter_registration
functionality, we must revisit our vote
method to ensure it respects the new rules set by this feature. Specifically, we need to ensure that no votes are cast after the voter registration has been blocked. This is critical in maintaining the integrity of the voting system, especially in scenarios where registration deadlines are enforced. Here's how we modify the vote
method to incorporate this change:
Ruby1class VotingSystem 2 def vote(timestamp, voter_id, candidate_id) 3 # Check if block_time is set and if the vote attempt is after the block timestamp 4 return false if @block_time && timestamp >= @block_time 5 6 return false unless @candidates.key?(candidate_id) # Return false if the candidate is not registered 7 8 voter_history = @voters[voter_id] ||= { votes: [], timestamps: [] } 9 voter_history[:votes] << candidate_id # Record the vote 10 voter_history[:timestamps] << timestamp # Record the time of the vote 11 @candidates[candidate_id] += 1 # Increment vote count for the candidate 12 13 true 14 end 15end
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 to enhance functionality while ensuring backward compatibility.
The change_vote
method allows voters to change their vote, adhering to specific rules. Here's a step-by-step explanation of implementing this functionality:
-
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.
Ruby1class VotingSystem 2 def change_vote(timestamp, voter_id, old_candidate_id, new_candidate_id) 3 # Check if block_time is set and if the vote change attempt is after the block timestamp 4 return false if @block_time && timestamp >= @block_time 5 6 # Verify existence in the voting system 7 return false unless @candidates.key?(old_candidate_id) && @candidates.key?(new_candidate_id) 8 9 voter_history = @voters[voter_id] 10 return false unless voter_history && voter_history[:votes].include?(old_candidate_id) 11 12 # Confirm voter has voted for the old candidate 13 last_vote_index = voter_history[:votes].rindex(old_candidate_id) 14 return false unless last_vote_index 15 16 # Ensure the operation is within the permitted timeframe 17 return false if timestamp - voter_history[:timestamps][last_vote_index] > 86400 # 24 hours in seconds 18 19 # Perform the vote change 20 # Remove the old vote 21 voter_history[:votes][last_vote_index] = new_candidate_id 22 voter_history[:timestamps][last_vote_index] = timestamp 23 24 # Update candidates' vote counts 25 @candidates[old_candidate_id] -= 1 26 @candidates[new_candidate_id] += 1 27 28 true 29 end 30end
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. Happy coding!