opensprints-core 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "opensprints-core"
8
+ gem.summary = %Q{A lib for interacting with opensprints race records data}
9
+ gem.description = %Q{This contains everything common between the stats app and the opensprints shoes app. If you want to write a new application to display opensprints stats or run races, this is your gem. }
10
+ gem.email = "evanfarrar@gmail.com"
11
+ gem.homepage = "http://github.com/evanfarrar/opensprints-core"
12
+ gem.authors = ["Evan Farrar"]
13
+ gem.add_development_dependency "bacon"
14
+ gem.add_dependency "sequel"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.pattern = 'test/test_*.rb'
26
+ spec.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |spec|
32
+ spec.libs << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :spec => :check_dependencies
43
+
44
+ task :default => :spec
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ if File.exist?('VERSION')
49
+ version = File.read('VERSION')
50
+ else
51
+ version = ""
52
+ end
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "opensprints-core #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.1
@@ -0,0 +1,5 @@
1
+ # TODO: when the sequel move is complete, this join model is probably useless.
2
+ class Categorization < Sequel::Model
3
+ many_to_one :category
4
+ many_to_one :racer
5
+ end
data/lib/category.rb ADDED
@@ -0,0 +1,15 @@
1
+ class Category < Sequel::Model
2
+ def to_s
3
+ self.name
4
+ end
5
+
6
+ def Category.next_after(other)
7
+ if(other)
8
+ category = Category.filter(:id > other.pk).order(:id).first
9
+ else
10
+ category = Category.order(:id).first
11
+ end
12
+ category ? category.pk : nil
13
+ end
14
+
15
+ end
@@ -0,0 +1,14 @@
1
+ class CreateCategories < Sequel::Migration
2
+ def up
3
+ create_table :categories do
4
+ primary_key :id
5
+ String :name
6
+ DateTime :created_at
7
+ end
8
+ end
9
+
10
+ def down
11
+ drop_table(:categories)
12
+ end
13
+ end
14
+
@@ -0,0 +1,14 @@
1
+ class CreateRacers < Sequel::Migration
2
+ def up
3
+ create_table :racers do
4
+ primary_key :id
5
+ String :name
6
+ DateTime :created_at
7
+ end
8
+ end
9
+
10
+ def down
11
+ drop_table(:racers)
12
+ end
13
+ end
14
+
@@ -0,0 +1,17 @@
1
+ class CreateCategorizations < Sequel::Migration
2
+ def up
3
+ create_table(:categorizations, :ignore_index_errors=>true) do
4
+ primary_key :id
5
+ Integer :racer_id, :null=>false
6
+ Integer :category_id, :null=>false
7
+
8
+ index [:category_id], :name=>:index_categorizations_category
9
+ index [:racer_id], :name=>:index_categorizations_racer
10
+ end
11
+ end
12
+
13
+ def down
14
+ drop_table(:categorizations)
15
+ end
16
+ end
17
+
@@ -0,0 +1,24 @@
1
+ class CreateRaces < Sequel::Migration
2
+ def up
3
+ create_table :races do
4
+ primary_key :id
5
+ TrueClass :raced, :default => false
6
+ Integer :tournament_id
7
+ end
8
+ create_table(:race_participations, :ignore_index_errors=>true) do
9
+ primary_key :id
10
+ BigDecimal :finish_time, :size=>[10, 0]
11
+ Integer :racer_id, :null=>false
12
+ Integer :race_id, :null=>false
13
+
14
+ index [:race_id], :name=>:index_race_participations_race
15
+ index [:racer_id], :name=>:index_race_participations_racer
16
+ end
17
+ end
18
+
19
+ def down
20
+ drop_table(:races)
21
+ drop_table(:race_participations)
22
+ end
23
+ end
24
+
@@ -0,0 +1,24 @@
1
+ class CreateTournaments < Sequel::Migration
2
+ def up
3
+ create_table(:tournament_participations, :ignore_index_errors=>true) do
4
+ primary_key :id
5
+ TrueClass :eliminated
6
+ Integer :racer_id, :null=>false
7
+ Integer :tournament_id, :null=>false
8
+
9
+ index [:racer_id], :name=>:index_tournament_participations_racer
10
+ index [:tournament_id], :name=>:index_tournament_participations_tournament
11
+ end
12
+
13
+ create_table(:tournaments) do
14
+ primary_key :id
15
+ String :name, :size=> 140
16
+ end
17
+ end
18
+
19
+ def down
20
+ drop_table(:tournaments)
21
+ drop_table(:tournament_participations)
22
+ end
23
+ end
24
+
@@ -0,0 +1,21 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'sequel'
5
+ require 'sequel/extensions/migration'
6
+ require 'sequel/extensions/schema_dumper'
7
+ require 'sqlite3'
8
+ if(defined? Shoes) #Real environment
9
+ DB = Sequel.connect("sqlite://#{DATABASE_PATH}")
10
+ else #Test environment
11
+ DB = Sequel.connect("sqlite::memory:")
12
+ end
13
+ Sequel::Migrator.apply(DB, 'lib/migrations/')
14
+ Infinity = 1/0.0
15
+ require 'lib/tournament'
16
+ require 'lib/tournament_participation'
17
+ require 'lib/category'
18
+ require 'lib/racer'
19
+ require 'lib/race'
20
+ require 'lib/race_participation'
21
+ require 'lib/categorization'
data/lib/race.rb ADDED
@@ -0,0 +1,27 @@
1
+ class Race < Sequel::Model
2
+ one_to_many :race_participations
3
+ many_to_many :racers, :join_table => :race_participations
4
+ many_to_one :tournament
5
+
6
+ def finished?
7
+ race_participations.all?(&:finish_time)
8
+ end
9
+
10
+ def winner
11
+ if unraced?
12
+ return nil
13
+ else
14
+ standings = self.race_participations.sort_by { |racer| racer.finish_time||Infinity }
15
+ standings.first
16
+ end
17
+ end
18
+
19
+ def unraced?
20
+ !raced
21
+ end
22
+
23
+ def next_race
24
+ (Race.filter(:raced => false, :tournament_id => tournament.pk).all - [self]).first
25
+ end
26
+
27
+ end
@@ -0,0 +1,60 @@
1
+ class RaceParticipation < Sequel::Model
2
+ many_to_one :race
3
+ many_to_one :racer
4
+
5
+ def color
6
+ @color ||= $BIKES[self.race.race_participations.index(self)]
7
+ end
8
+
9
+ attr_accessor :ticks
10
+
11
+ def percent_complete
12
+ [1.0, self.distance / $RACE_DISTANCE].min
13
+ end
14
+
15
+ def distance
16
+ if self.ticks
17
+ self.ticks * $ROLLER_CIRCUMFERENCE
18
+ else
19
+ 0.0
20
+ end
21
+ end
22
+
23
+ METERS_PER_MILLISECOND_TO_MILES_PER_HOUR = 2236.93629
24
+ METERS_PER_MILLISECOND_TO_KILOMETERS_PER_HOUR = 3600.0
25
+ def unit_conversion
26
+ case UNIT_SYSTEM
27
+ when :mph
28
+ @unit_conversion = METERS_PER_MILLISECOND_TO_MILES_PER_HOUR
29
+ when :kph
30
+ @unit_conversion = METERS_PER_MILLISECOND_TO_KILOMETERS_PER_HOUR
31
+ end
32
+ end
33
+
34
+ # yikes.
35
+ def speed(time)
36
+ @unit_conversion ||= unit_conversion
37
+ @distance = self.distance
38
+ @time = time
39
+ @distance_old ||= 0
40
+ @time_old ||= 0
41
+ @speed ||= 0
42
+ if(@time_old > @time)
43
+ @time_old = time
44
+ end
45
+ if time == 0
46
+ 0
47
+ else
48
+ if(@time-@time_old > 999)
49
+ if(@distance_old > 0)
50
+ @speed = "%.2f" % (((@distance - @distance_old) / (@time - @time_old)) * @unit_conversion).to_f
51
+ else
52
+ @speed = 0
53
+ end
54
+ @distance_old = @distance
55
+ @time_old = @time
56
+ end
57
+ @speed
58
+ end
59
+ end
60
+ end
data/lib/racer.rb ADDED
@@ -0,0 +1,12 @@
1
+ class Racer < Sequel::Model
2
+ one_to_many :categorizations
3
+ many_to_many :categories, :join_table => :categorizations
4
+ def to_s
5
+ name
6
+ end
7
+
8
+ def best_time
9
+ best = DB[:race_participations].filter(:racer_id => self.pk).order(:finish_time).select(:finish_time).first
10
+ best[:finish_time] if best
11
+ end
12
+ end
data/lib/tournament.rb ADDED
@@ -0,0 +1,46 @@
1
+ class Tournament < Sequel::Model
2
+ one_to_many :tournament_participations
3
+ many_to_many :racers, :join_table => :tournament_participations
4
+ one_to_many :races
5
+
6
+ def unregistered_racers
7
+ Racer.exclude(:id => Racer.join(:tournament_participations, :racer_id => :id).select(:racers__id)).all
8
+ end
9
+
10
+ def autofill(racer_list=nil)
11
+ racer_list ||= reload.unmatched_racers.to_a
12
+ racer_list.each_slice($BIKES.length) { |a|
13
+ race = Race.create(:tournament => self)
14
+ a.map{|r| RaceParticipation.create(:racer => r, :race => race)}
15
+ }
16
+ end
17
+
18
+ def unmatched_racers
19
+ racers - matched_racers - tournament_participations.select{|tp|tp.eliminated}.map{|tp|tp.racer}
20
+ end
21
+
22
+ def matched_racers
23
+ matched = []
24
+ matches = self.races.select{|race| race.unraced? }
25
+ matches.each { |race|
26
+ race.race_participations.each {|rp|
27
+ matched << rp.racer
28
+ }
29
+ }
30
+ matched
31
+ end
32
+
33
+ #TODO: test
34
+ def never_raced_and_not_eliminated
35
+ matched = []
36
+ races.each { |race|
37
+ race.race_participations.each {|rp|
38
+ matched << rp.racer
39
+ }
40
+ }
41
+ racers - matched - tournament_participations.select{|tp|tp.eliminated}.map{|tp|tp.racer}
42
+ end
43
+
44
+
45
+
46
+ end
@@ -0,0 +1,31 @@
1
+ class TournamentParticipation < Sequel::Model
2
+ many_to_one :racer
3
+ many_to_one :tournament
4
+
5
+ def best_time
6
+ best = DB[:race_participations].filter(:racer_id => racer.id).exclude(:finish_time => nil).join(:races, :tournament_id => tournament.id).filter(:raced => true).order(:finish_time).select(:finish_time).first
7
+
8
+ best[:finish_time] if best
9
+ end
10
+
11
+ def rank
12
+ standings = self.tournament.tournament_participations.sort_by{|tp|tp.best_time||Infinity}
13
+ standings.index(self)+1
14
+ end
15
+
16
+ def losses
17
+ RaceParticipation.filter(:racer_id => racer.id).join(:races, :tournament_id => tournament.id).group(:id).all.select do |rp|
18
+ winner = rp.race ? rp.race.winner : nil
19
+ winner && (winner.racer.pk != self.racer.pk)
20
+ end.length
21
+ end
22
+
23
+ def eliminate
24
+ self.update(:eliminated => true)
25
+ end
26
+
27
+ def race_participations
28
+ RaceParticipation.filter(:racer_id => racer.id).join(:races, :tournament_id => tournament.id).group(:id).all
29
+ end
30
+
31
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OpensprintsCore" do
4
+ it "fails" do
5
+ should.flunk "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'opensprints-core'
7
+
8
+ Bacon.summary_on_exit
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'lib/opensprints-core.rb'
4
+
5
+ describe 'A category' do
6
+ before do
7
+ @category = Category.new
8
+ end
9
+
10
+ it 'should have a name' do
11
+ @category.name = "Women"
12
+ @category.name.should=="Women"
13
+ end
14
+ it 'should save' do
15
+ c = Category.new(:name => "Test")
16
+ (!!c.save).should==(true)
17
+ end
18
+
19
+ it 'should load from the database' do
20
+ c = Category.new(:name => "Test")
21
+ (!!c.save).should==(true)
22
+ Category[c.id].should.not.be.nil?
23
+ end
24
+
25
+ it 'should be convertible to string' do
26
+ Category.new(:name => "Men").to_s.should=="Men"
27
+ end
28
+
29
+ describe "next after" do
30
+ before do
31
+ Category.all.each{|c|c.destroy}
32
+ @c1 = Category.create(:name => "one")
33
+ @c2 = Category.create(:name => "two")
34
+ end
35
+ it "should know what category comes after no category" do
36
+ Category.next_after(nil).should==(@c1.pk)
37
+ end
38
+ it "should know what category comes after a given category" do
39
+ Category.next_after(@c1).should==(@c2.pk)
40
+ end
41
+ it "should know no category comes after a the last category" do
42
+ Category.next_after(@c2).should==(nil)
43
+ end
44
+ it 'should work with no categories' do
45
+ Category.all.each{|c|c.destroy}
46
+ Category.next_after(nil).should==(nil)
47
+ end
48
+ after do
49
+ Category.all.each{|c|c.destroy}
50
+ Category.create(:name => "Men")
51
+ Category.create(:name => "Women")
52
+ end
53
+ end
54
+
55
+ end
data/test/test_race.rb ADDED
@@ -0,0 +1,112 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'lib/opensprints-core.rb'
4
+ $BIKES = ["red","blue"]
5
+
6
+ describe 'A race' do
7
+ before do
8
+ @race = Race.new
9
+ end
10
+
11
+ it 'should be able to create race with racers' do
12
+ racers = [Racer.create, Racer.create, Racer.create, Racer.create]
13
+ r = Race.create
14
+ racers.map{|e| RaceParticipation.create(:racer => e, :race => r)}
15
+ r.save
16
+ Race[r.pk].racers.length.should==4
17
+ end
18
+
19
+ describe "racers" do
20
+ it "should have a color" do
21
+ racers = [Racer.create, Racer.create, Racer.create, Racer.create]
22
+ r = Race.create
23
+ racers.map{|e| RaceParticipation.create(:racer => e, :race => r)}
24
+ r.save
25
+ r.race_participations.first.color.should== $BIKES.first
26
+ end
27
+ end
28
+
29
+ it 'should have times' do
30
+ racers = [Racer.create, Racer.create, Racer.create, Racer.create]
31
+ r = Race.create
32
+ racers.map{|e| RaceParticipation.create(:racer => e, :race => r)}
33
+ r.save
34
+ r.race_participations.first.finish_time = 10.116
35
+ r.race_participations.first.save
36
+ r.reload
37
+ r.race_participations.first.finish_time.should==(10.116)
38
+ end
39
+
40
+ it 'should be finished if everyone has times.' do
41
+ racers = [Racer.create, Racer.create, Racer.create, Racer.create]
42
+ r = Race.create
43
+ racers.map{|e| RaceParticipation.create(:racer => e, :race => r)}
44
+ r.save
45
+ r.race_participations.first.finish_time = 10.116
46
+ r.race_participations.first.save
47
+ r.reload
48
+ r.finished?.should==(false)
49
+ r.race_participations.each{|rp|rp.finish_time = 10.116; rp.save}
50
+ r.reload
51
+ r.finished?.should==(true)
52
+ end
53
+
54
+ describe 'winner' do
55
+ it 'should be the lowest (fastest) time' do
56
+ racers = [Racer.create(:name => "Steve"),
57
+ Racer.create(:name => "Joe")]
58
+ r = Race.create(:raced => true)
59
+ racers.each{|racer| RaceParticipation.create(:racer => racer,:race => r)}
60
+ r.save
61
+ r.race_participations.first.finish_time = 10.116
62
+ r.save
63
+ r.reload
64
+ r.winner.racer.name.should==("Steve")
65
+ end
66
+
67
+ it 'should be nil if the race has not been run' do
68
+ racers = [Racer.create(:name => "Steve"),
69
+ Racer.create(:name => "Joe")]
70
+ r = Race.create
71
+ racers.each{|racer| RaceParticipation.create(:racer => racer,:race => r)}
72
+ r.save
73
+ r.race_participations.first.finish_time = 10.116
74
+ r.save
75
+ r.reload
76
+ r.winner.should==(nil)
77
+ end
78
+ end
79
+ describe 'raced' do
80
+ it 'should track whether or not the race has been run' do
81
+ r = Race.create
82
+ r.raced.should==(false)
83
+ r.raced = true
84
+ r.raced.should==(true)
85
+ end
86
+
87
+ it 'should have a converse: unraced?' do
88
+ r = Race.create
89
+ r.unraced?.should==(true)
90
+ r.raced = true
91
+ r.unraced?.should==(false)
92
+ end
93
+ end
94
+
95
+ describe 'next race' do
96
+ it 'should be the next one after this one' do
97
+ t = Tournament.create
98
+ r1 = Race.create(:tournament => t)
99
+ r2 = Race.create(:tournament => t)
100
+ r1.next_race.should==(r2)
101
+ end
102
+
103
+ it 'should only show the next unraced race' do
104
+ t = Tournament.create
105
+ r1 = Race.create(:tournament => t)
106
+ r2 = Race.create(:tournament => t, :raced => true)
107
+ r3 = Race.create(:tournament => t)
108
+ r1.next_race.should==(r3)
109
+ end
110
+ end
111
+
112
+ end
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'lib/opensprints-core.rb'
4
+
5
+ describe 'A race participation' do
6
+ before do
7
+ @r = RaceParticipation.new
8
+ end
9
+
10
+ it 'should have a temporary place for keeping track of ticks' do
11
+ @r.ticks = 12
12
+ @r.ticks.should==(12)
13
+ end
14
+
15
+ it 'should have a distance' do
16
+ $ROLLER_CIRCUMFERENCE = 0.5
17
+ @r.ticks = 42
18
+ @r.distance.should==(21.0)
19
+ end
20
+
21
+ describe 'percent complete' do
22
+ it 'should be the ratio of race distance to distance' do
23
+ $RACE_DISTANCE = 84.0
24
+ $ROLLER_CIRCUMFERENCE = 0.5
25
+ @r.ticks = 42
26
+ @r.percent_complete.should==(0.25)
27
+ @r.ticks = 0
28
+ @r.percent_complete.should==(0.0)
29
+ @r.ticks = 1200.0
30
+ @r.percent_complete.should==(1.0)
31
+ end
32
+ end
33
+
34
+ describe 'speed' do
35
+ # it 'should give a speed based on a certain time'
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'lib/opensprints-core.rb'
4
+
5
+ describe 'A racer' do
6
+ before do
7
+ @racer = Racer.new
8
+ end
9
+
10
+ it 'should have a name' do
11
+ @racer.name = "Evan F"
12
+ @racer.name.should=="Evan F"
13
+ @racer.to_s.should=="Evan F"
14
+ end
15
+
16
+ it 'should save' do
17
+ r = Racer.new(:name => "Test")
18
+ (!!r.save).should==(true)
19
+ end
20
+
21
+ it 'should load from the database' do
22
+ r = Racer.new(:name => "Test")
23
+ (!!r.save).should==(true)
24
+ Racer[r.pk].should.not.be.nil?
25
+ end
26
+
27
+ it "should have categories" do
28
+ c = Category.create(:name => "Men")
29
+ # @racer.categories << c
30
+
31
+ @racer.save
32
+ Categorization.create(:category => c, :racer => @racer)
33
+ @racer.reload.categorizations.map(&:category).should.include? c
34
+ @racer.categories.should.include? c
35
+ end
36
+
37
+ it "should know the best time ever" do
38
+ racer = Racer.create
39
+ [7.0, 12.0, 2.7, 10.0].each do |time|
40
+ race = Race.create
41
+ RaceParticipation.create(:finish_time => time, :race => race, :racer => racer)
42
+ end
43
+ racer.best_time.should==(2.7)
44
+ end
45
+ end