Added list of changes submitted by Jeff Rose <jeff@rosejn.net>.
authorBenjamin Mako Hill <mako@atdot.cc>
Fri, 10 Mar 2006 17:04:19 +0000 (17:04 +0000)
committerBenjamin Mako Hill <mako@atdot.cc>
Fri, 10 Mar 2006 17:04:19 +0000 (17:04 +0000)
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

16 files changed:
README [moved from README.rst with 100% similarity]
Rakefile [new file with mode: 0644]
TODO [new file with mode: 0644]
lib/rubyvote.rb [new file with mode: 0644]
lib/rubyvote/condorcet.rb [moved from lib/condorcet.rb with 99% similarity]
lib/rubyvote/election.rb [moved from lib/election.rb with 100% similarity]
lib/rubyvote/positional.rb [moved from lib/positional.rb with 99% similarity]
lib/rubyvote/range.rb [new file with mode: 0644]
lib/rubyvote/runoff.rb [moved from lib/runoff.rb with 99% similarity]
test.rb
test/condorcet_test.rb [new file with mode: 0644]
test/election_test.rb [new file with mode: 0644]
test/election_test_helper.rb [new file with mode: 0644]
test/positional_test.rb [new file with mode: 0644]
test/range_test.rb [new file with mode: 0644]
test/runoff_test.rb [new file with mode: 0644]

similarity index 100%
rename from README.rst
rename to README
diff --git a/Rakefile b/Rakefile
new file mode 100644 (file)
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 (file)
index 0000000..e69de29
diff --git a/lib/rubyvote.rb b/lib/rubyvote.rb
new file mode 100644 (file)
index 0000000..1585389
--- /dev/null
@@ -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'
+
similarity index 99%
rename from lib/condorcet.rb
rename to lib/rubyvote/condorcet.rb
index 9733989..b3bdcd6 100644 (file)
@@ -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)
similarity index 100%
rename from lib/election.rb
rename to lib/rubyvote/election.rb
similarity index 99%
rename from lib/positional.rb
rename to lib/rubyvote/positional.rb
index 5136aec..11e8a49 100644 (file)
@@ -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 (file)
index 0000000..5c01f68
--- /dev/null
@@ -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
similarity index 99%
rename from lib/runoff.rb
rename to lib/rubyvote/runoff.rb
index f9edc5e..8a69a4e 100644 (file)
@@ -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 (executable)
--- a/test.rb
+++ b/test.rb
 # 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 (file)
index 0000000..7b49bc6
--- /dev/null
@@ -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 (file)
index 0000000..b182e2d
--- /dev/null
@@ -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 (file)
index 0000000..abc37cd
--- /dev/null
@@ -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 (file)
index 0000000..3f1fcfb
--- /dev/null
@@ -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 (file)
index 0000000..953df78
--- /dev/null
@@ -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 (file)
index 0000000..d153dd6
--- /dev/null
@@ -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
+

Benjamin Mako Hill || Want to submit a patch?