From 0ef8f53fb812bcc40337a92f9c4d11ab193f73a9 Mon Sep 17 00:00:00 2001 From: Benjamin Mako Hill Date: Fri, 10 Mar 2006 17:04:19 +0000 Subject: [PATCH 1/1] Added list of changes submitted by Jeff Rose . This includes: * Rake based testing and gemmification. * Range based voting (untested). * A new TODO file. * Moving files around to have them RV work better as a library. Thanks Jeff! git-svn-id: svn://rubyforge.org/var/svn/rubyvote/trunk@11 1440c7f4-e209-0410-9a04-881b5eb134a8 --- README.rst => README | 0 Rakefile | 186 +++++++++++++++++++++++++++++++ TODO | 0 lib/rubyvote.rb | 8 ++ lib/{ => rubyvote}/condorcet.rb | 2 - lib/{ => rubyvote}/election.rb | 0 lib/{ => rubyvote}/positional.rb | 2 - lib/rubyvote/range.rb | 32 ++++++ lib/{ => rubyvote}/runoff.rb | 2 - test.rb | 19 +++- test/condorcet_test.rb | 56 ++++++++++ test/election_test.rb | 26 +++++ test/election_test_helper.rb | 29 +++++ test/positional_test.rb | 18 +++ test/range_test.rb | 32 ++++++ test/runoff_test.rb | 54 +++++++++ 16 files changed, 456 insertions(+), 10 deletions(-) rename README.rst => README (100%) create mode 100644 Rakefile create mode 100644 TODO create mode 100644 lib/rubyvote.rb rename lib/{ => rubyvote}/condorcet.rb (99%) rename lib/{ => rubyvote}/election.rb (100%) rename lib/{ => rubyvote}/positional.rb (99%) create mode 100644 lib/rubyvote/range.rb rename lib/{ => rubyvote}/runoff.rb (99%) create mode 100644 test/condorcet_test.rb create mode 100644 test/election_test.rb create mode 100644 test/election_test_helper.rb create mode 100644 test/positional_test.rb create mode 100644 test/range_test.rb create mode 100644 test/runoff_test.rb diff --git a/README.rst b/README similarity index 100% rename from README.rst rename to README diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..1971ca1 --- /dev/null +++ b/Rakefile @@ -0,0 +1,186 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' + +PKG_NAME = "rubyvote" +PKG_VERSION = "0.2" +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "rubyvote" +RUBY_FORGE_USER = "mako" + +desc "Default Task" +task :default => [ :test ] + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << "test" + t.pattern = 'test/*_test.rb' + t.verbose = true +} + + +# Genereate the RDoc documentation +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "RubyVote -- Election Methods in Ruby" + rdoc.rdoc_files.include('README', 'ChangeLog', 'COPYING') + rdoc.rdoc_files.include('lib/rubyvote.rb') + rdoc.rdoc_files.include('lib/rubyvote/*.rb') +} + + +# Create compressed packages +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.summary = "Election methods library in ruby." + s.description = %q{Provides a variety of different election types.} + s.version = PKG_VERSION + + s.author = "Benjamin Mako Hill" + s.email = "mako @nospam@ atdot.cc" + s.rubyforge_project = RUBY_FORGE_PROJECT + s.homepage = "http://rubyvote.rubyforge.org" + + s.has_rdoc = true + s.requirements << 'none' + s.require_path = 'lib' + s.autorequire = 'rubyvote' + + s.files = [ "Rakefile", "README", "ChangeLog", "COPYING" ] + s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) } +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + +###TODO: Configure for api documentation login and path +desc "Publish the API documentation" +task :pgem => [:package] do + Rake::SshFilePublisher.new("login@foo.com", "path/to/place", "pkg", "#{PKG_FILE_NAME}.gem").upload +end + +desc "Publish the release files to RubyForge." +task :release => [:package] do + files = ["gem", "tgz", "zip"].map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" } + + if RUBY_FORGE_PROJECT then + require 'net/http' + require 'open-uri' + + project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/" + project_data = open(project_uri) { |data| data.read } + group_id = project_data[/[?&]group_id=(\d+)/, 1] + raise "Couldn't get group id" unless group_id + + # This echos password to shell which is a bit sucky + if ENV["RUBY_FORGE_PASSWORD"] + password = ENV["RUBY_FORGE_PASSWORD"] + else + print "#{RUBY_FORGE_USER}@rubyforge.org's password: " + password = STDIN.gets.chomp + end + + login_response = Net::HTTP.start("rubyforge.org", 80) do |http| + data = [ + "login=1", + "form_loginname=#{RUBY_FORGE_USER}", + "form_pw=#{password}" + ].join("&") + http.post("/account/login.php", data) + end + + cookie = login_response["set-cookie"] + raise "Login failed" unless cookie + headers = { "Cookie" => cookie } + + release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}" + release_data = open(release_uri, headers) { |data| data.read } + package_id = release_data[/[?&]package_id=(\d+)/, 1] + raise "Couldn't get package id" unless package_id + + first_file = true + release_id = "" + + files.each do |filename| + basename = File.basename(filename) + file_ext = File.extname(filename) + file_data = File.open(filename, "rb") { |file| file.read } + + puts "Releasing #{basename}..." + + release_response = Net::HTTP.start("rubyforge.org", 80) do |http| + release_date = Time.now.strftime("%Y-%m-%d %H:%M") + type_map = { + ".zip" => "3000", + ".tgz" => "3110", + ".gz" => "3110", + ".gem" => "1400" + }; type_map.default = "9999" + type = type_map[file_ext] + boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor" + + query_hash = if first_file then + { + "group_id" => group_id, + "package_id" => package_id, + "release_name" => RELEASE_NAME, + "release_date" => release_date, + "type_id" => type, + "processor_id" => "8000", # Any + "release_notes" => "", + "release_changes" => "", + "preformatted" => "1", + "submit" => "1" + } + else + { + "group_id" => group_id, + "release_id" => release_id, + "package_id" => package_id, + "step2" => "1", + "type_id" => type, + "processor_id" => "8000", # Any + "submit" => "Add This File" + } + end + + query = "?" + query_hash.map do |(name, value)| + [name, URI.encode(value)].join("=") + end.join("&") + + data = [ + "--" + boundary, + "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"", + "Content-Type: application/octet-stream", + "Content-Transfer-Encoding: binary", + "", file_data, "" + ].join("\x0D\x0A") + + release_headers = headers.merge( + "Content-Type" => "multipart/form-data; boundary=#{boundary}" + ) + + target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php" + http.post(target + query, data, release_headers) + end + + if first_file then + release_id = release_response.body[/release_id=(\d+)/, 1] + raise("Couldn't get release id") unless release_id + end + + first_file = false + end + end +end diff --git a/TODO b/TODO new file mode 100644 index 0000000..e69de29 diff --git a/lib/rubyvote.rb b/lib/rubyvote.rb new file mode 100644 index 0000000..1585389 --- /dev/null +++ b/lib/rubyvote.rb @@ -0,0 +1,8 @@ +# Extra full path added to fix some require errors on some installations. + +require File.dirname(__FILE__) + '/rubyvote/election' +require File.dirname(__FILE__) + '/rubyvote/condorcet' +require File.dirname(__FILE__) + '/rubyvote/positional' +require File.dirname(__FILE__) + '/rubyvote/runoff' +require File.dirname(__FILE__) + '/rubyvote/range' + diff --git a/lib/condorcet.rb b/lib/rubyvote/condorcet.rb similarity index 99% rename from lib/condorcet.rb rename to lib/rubyvote/condorcet.rb index 9733989..b3bdcd6 100644 --- a/lib/condorcet.rb +++ b/lib/rubyvote/condorcet.rb @@ -31,8 +31,6 @@ ## The CondorcetVote class is subclassed by the PureCondorcetVote and ## the CloneproofSSDVote classes but should not be used directly. -require 'election' - class CondorcetVote < ElectionVote def tally_vote(vote=nil) diff --git a/lib/election.rb b/lib/rubyvote/election.rb similarity index 100% rename from lib/election.rb rename to lib/rubyvote/election.rb diff --git a/lib/positional.rb b/lib/rubyvote/positional.rb similarity index 99% rename from lib/positional.rb rename to lib/rubyvote/positional.rb index 5136aec..11e8a49 100644 --- a/lib/positional.rb +++ b/lib/rubyvote/positional.rb @@ -29,8 +29,6 @@ ## These classes inherit from and/or are modeled after the classes in ## election.rb and condorcet.rb -require 'election' - class BordaVote < ElectionVote def initialize(votes=nil) diff --git a/lib/rubyvote/range.rb b/lib/rubyvote/range.rb new file mode 100644 index 0000000..5c01f68 --- /dev/null +++ b/lib/rubyvote/range.rb @@ -0,0 +1,32 @@ +# Range voting as described in wikipedia. +# (http://en.wikipedia.org/wiki/Range_voting) + +class RangeVote < ElectionVote + def initialize(votes = nil, range = 1..10) + @valid_range = range + super(votes) + end + + def result + RangeResult.new(self) + end + + protected + def verify_vote(vote=nil) + vote.instance_of?(Hash) && vote.all?{|c,score| @valid_range.include?(score)} + end + + def tally_vote(vote) + vote.each do |candidate, score| + if @votes.has_key?(candidate) + @votes[candidate] += score + else + @votes[candidate] = score + @candidates << candidate + end + end + end +end + +class RangeResult < PluralityResult +end diff --git a/lib/runoff.rb b/lib/rubyvote/runoff.rb similarity index 99% rename from lib/runoff.rb rename to lib/rubyvote/runoff.rb index f9edc5e..8a69a4e 100644 --- a/lib/runoff.rb +++ b/lib/rubyvote/runoff.rb @@ -1,5 +1,3 @@ -require 'election' - class InstantRunoffVote < ElectionVote def initialize(votes=nil) @candidates = Array.new diff --git a/test.rb b/test.rb index f87e41d..e8c03a1 100755 --- a/test.rb +++ b/test.rb @@ -18,10 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. -require 'election' -require 'condorcet' -require 'positional' -require 'runoff' +require 'lib/rubyvote' def print_winner(result) if not result.winner? @@ -183,6 +180,19 @@ def runoff_test3 print_winner( InstantRunoffVote.new(vote_array).result ) end +def range_test1 + puts "USING RANGE..." + puts "The winner shold be: B" + + vote_array = Array.new + 42.times {vote_array << {:A => 10, :B => 5, :C => 2, :D => 1}} + 26.times {vote_array << {:A => 1, :B => 10, :C => 5, :D => 2}} + 15.times {vote_array << {:A => 1, :B => 2, :C => 10, :D => 5}} + 17.times {vote_array << {:A => 1, :B => 2, :C => 5, :D => 10}} + + print_winner( RangeVote.new(vote_array).result ) +end + condorcet_test1() ssd_test1() ssd_test2() @@ -193,3 +203,4 @@ approval_test1() runoff_test1() runoff_test2() runoff_test3() +range_test1() diff --git a/test/condorcet_test.rb b/test/condorcet_test.rb new file mode 100644 index 0000000..7b49bc6 --- /dev/null +++ b/test/condorcet_test.rb @@ -0,0 +1,56 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'election_test_helper' + +class TestCondorcetVote < Test::Unit::TestCase + include ElectionTestHelper + + def test_condorcet + vote_array = Array.new + 3.times {vote_array << "ABC".split("")} + 3.times {vote_array << "CBA".split("")} + 2.times {vote_array << "BAC".split("")} + + test_winner( ["B"], PureCondorcetVote.new(vote_array).result ) + end + + def test_ssd + vote_array = Array.new + 5.times {vote_array << "ACBED".split("")} + 5.times {vote_array << "ADECB".split("")} + 8.times {vote_array << "BEDAC".split("")} + 3.times {vote_array << "CABED".split("")} + 7.times {vote_array << "CAEBD".split("")} + 2.times {vote_array << "CBADE".split("")} + 7.times {vote_array << "DCEBA".split("")} + 8.times {vote_array << "EBADC".split("")} + + test_winner( "E", CloneproofSSDVote.new(vote_array).result ) + end + + def test_ssd2 + vote_array = Array.new + 5.times {vote_array << "ACBD".split("")} + 2.times {vote_array << "ACDB".split("")} + 3.times {vote_array << "ADCB".split("")} + 4.times {vote_array << "BACD".split("")} + 3.times {vote_array << "CBDA".split("")} + 3.times {vote_array << "CDBA".split("")} + 1.times {vote_array << "DACB".split("")} + 5.times {vote_array << "DBAC".split("")} + 4.times {vote_array << "DCBA".split("")} + + test_winner( "D", CloneproofSSDVote.new(vote_array).result ) + end + + def test_ssd3 + vote_array = Array.new + 3.times {vote_array << "ABCD".split("")} + 2.times {vote_array << "DABC".split("")} + 2.times {vote_array << "DBCA".split("")} + 2.times {vote_array << "CBDA".split("")} + + test_winner("B", CloneproofSSDVote.new(vote_array).result ) + end +end diff --git a/test/election_test.rb b/test/election_test.rb new file mode 100644 index 0000000..b182e2d --- /dev/null +++ b/test/election_test.rb @@ -0,0 +1,26 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'election_test_helper' + +class TestElectionVote < Test::Unit::TestCase + include ElectionTestHelper + + def test_plurality + vote_array = "ABCABCABCCCBBAAABABABCCCCCCCCCCCCCA".split("") + + test_winner( "C", PluralityVote.new(vote_array).result ) + end + + + def test_approval + vote_array = Array.new + 10.times {vote_array << "AB".split("")} + 10.times {vote_array << "CB".split("")} + 11.times {vote_array << "AC".split("")} + 5.times {vote_array << "A".split("")} + + test_winner( "A", ApprovalVote.new(vote_array).result ) + end +end + diff --git a/test/election_test_helper.rb b/test/election_test_helper.rb new file mode 100644 index 0000000..abc37cd --- /dev/null +++ b/test/election_test_helper.rb @@ -0,0 +1,29 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") + +require 'rubyvote' + +module ElectionTestHelper + def test_winner(expected, result) + puts "\nUsing the #{result.class.to_s.gsub(/Result/,'')} voting method..." + + if result.winner? + if expected.is_a?(Array) && expected.length > 1 # Array is passed to test for a tie! + msg = "There is a tie: %s" % result.winners.join(", ") + + assert_equal(expected.length, result.winners.length, + "Not the correct number of winners!") + assert(expected.all?{|c| result.winners.include?(c)}, + "Tie winners do not match expected!") + else + msg = "There is a single winner: #{result.winners[0]}" + assert_equal(expected, result.winners[0], msg) + end + + else + msg = "There is no winner" + assert_nil(expected, msg) + end + + puts msg + end +end diff --git a/test/positional_test.rb b/test/positional_test.rb new file mode 100644 index 0000000..3f1fcfb --- /dev/null +++ b/test/positional_test.rb @@ -0,0 +1,18 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'election_test_helper' + +class TestPositionalVote < Test::Unit::TestCase + include ElectionTestHelper + + def test_borda + vote_array = Array.new + 3.times {vote_array << "ABC".split("")} + 3.times {vote_array << "CBA".split("")} + 2.times {vote_array << "BAC".split("")} + + test_winner( "B", BordaVote.new(vote_array).result ) + end +end + diff --git a/test/range_test.rb b/test/range_test.rb new file mode 100644 index 0000000..953df78 --- /dev/null +++ b/test/range_test.rb @@ -0,0 +1,32 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'election_test_helper' + +class TestRangeVote < Test::Unit::TestCase + include ElectionTestHelper + + def test_range + vote_array = [] + 42.times {vote_array << {'A' => 10, 'B' => 5, 'C' => 2, 'D' => 1}} + 26.times {vote_array << {'A' => 1, 'B' => 10, 'C' => 5, 'D' => 2}} + 15.times {vote_array << {'A' => 1, 'B' => 2, 'C' => 10, 'D' => 5}} + 17.times {vote_array << {'A' => 1, 'B' => 2, 'C' => 5, 'D' => 10}} + + test_winner('B', RangeVote.new(vote_array).result ) + end + + def test_tie + vote_array = [] + 10.times {vote_array << {'A' => 5, 'B' => 2}} + 10.times {vote_array << {'A' => 2, 'B' => 5}} + + test_winner(['A','B'], RangeVote.new(vote_array).result ) + end + + def test_no_win + vote_array = [] + + test_winner(nil, RangeVote.new(vote_array).result ) + end +end diff --git a/test/runoff_test.rb b/test/runoff_test.rb new file mode 100644 index 0000000..d153dd6 --- /dev/null +++ b/test/runoff_test.rb @@ -0,0 +1,54 @@ +#!/usr/bin/ruby + +require 'test/unit' +require 'election_test_helper' + +class TestRunoffVote < Test::Unit::TestCase + include ElectionTestHelper + + def test_runoff + vote_array = Array.new + 142.times {vote_array << "ABCD".split("")} + 26.times {vote_array << "BCDA".split("")} + 15.times {vote_array << "CDBA".split("")} + 17.times {vote_array << "DCBA".split("")} + + test_winner( "A", InstantRunoffVote.new(vote_array).result ) + end + + def test_runoff2 + vote_array = Array.new + 42.times {vote_array << "ABCD".split("")} + 26.times {vote_array << "BCDA".split("")} + 15.times {vote_array << "CDBA".split("")} + 17.times {vote_array << "DCBA".split("")} + + test_winner( "D", InstantRunoffVote.new(vote_array).result ) + end + + def test_runoff3 + vote_array = Array.new + 42.times {vote_array << "ABCD".split("")} + 26.times {vote_array << "ACBD".split("")} + 15.times {vote_array << "BACD".split("")} + 32.times {vote_array << "BCAD".split("")} + 14.times {vote_array << "CABD".split("")} + 49.times {vote_array << "CBAD".split("")} + 17.times {vote_array << "ABDC".split("")} + 23.times {vote_array << "BADC".split("")} + 37.times {vote_array << "BCDA".split("")} + 11.times {vote_array << "CADB".split("")} + 16.times {vote_array << "CBDA".split("")} + 54.times {vote_array << "ADBC".split("")} + 36.times {vote_array << "BDCA".split("")} + 42.times {vote_array << "CDAB".split("")} + 13.times {vote_array << "CDBA".split("")} + 51.times {vote_array << "DABC".split("")} + 33.times {vote_array << "DBCA".split("")} + 39.times {vote_array << "DCAB".split("")} + 12.times {vote_array << "DCBA".split("")} + + test_winner( "C", InstantRunoffVote.new(vote_array).result ) + end +end + -- 2.39.5