anthonyw-dyno 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,14 +1,69 @@
1
1
  dyno
2
2
  ====
3
3
 
4
- A Ruby library for parsing sim-racing results files. Presently supports:
4
+ > _A dynamometer or "dyno" for short, is a machine used to measure torque and
5
+ > rotational speed (rpm) from which power produced by an engine, motor or
6
+ > other rotating prime mover can be calculated. ([Wikipedia][dyno-wp])_
5
7
 
6
- * Race 07 / GTR: Evolution
8
+ In this context, however, Dyno is a pure Ruby library for parsing sim-racing
9
+ result files, soon to be used on the upcoming Torque sim-racing app.
7
10
 
8
- With support for the following in the near future:
11
+ Dyno presently supports files spat out by a number of games:
9
12
 
10
- * rFactor
11
- * LFS
12
- * Arca
13
+ * **Race07Parser** parses results from RACE 07, GTR: Evolution, and
14
+ STCC: The Game
15
+ * **GTR2Parser** parses results from GTR2.
16
+ * **RFactorParser** parses results from rFactor (and mods), and ARCA (coming
17
+ very soon).
13
18
 
14
- ... once I've got my hands on some sample results files.
19
+ Dyno requires the iniparse gem (available via `gem install iniparse` or on
20
+ [GitHub][iniparse]) for the Race 07 and GTR2 parsers, and libxml-ruby for
21
+ the rFactor parser.
22
+
23
+ Usage
24
+ -----
25
+
26
+ Dyno::Parsers::Race07Parser.parse_file( '/path/to/result/file' )
27
+ # => Dyno::Event
28
+
29
+ Dyno::Parsers::GTR2Parser.parse_file( '/path/to/result/file' )
30
+ # => Dyno::Event
31
+
32
+ Dyno::Parsers::RFactorParser.parse_file( '/path/to/result/file' )
33
+ # => Dyno::Event
34
+
35
+ Each of these operations will return a Dyno::Event instance. Dyno::Event
36
+ defines `#competitors` containing a collection of all of those who took part
37
+ in the event. See the API docs for Dyno::Event and Dyno::Competitor for more
38
+ details.
39
+
40
+ If your results file couldn't be parsed, a Dyno::MalformedInputError exception
41
+ will be raised.
42
+
43
+ License
44
+ -------
45
+
46
+ Dyno is distributed under the MIT/X11 License.
47
+
48
+ > Copyright (c) 2008-2009 Anthony Williams
49
+ >
50
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
51
+ > of this software and associated documentation files (the "Software"), to
52
+ > deal in the Software without restriction, including without limitation the
53
+ > rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
54
+ > sell copies of the Software, and to permit persons to whom the Software is
55
+ > furnished to do so, subject to the following conditions:
56
+ >
57
+ > The above copyright notice and this permission notice shall be included in
58
+ > all copies or substantial portions of the Software.
59
+ >
60
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
63
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
64
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
65
+ > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
66
+ > IN THE SOFTWARE.
67
+
68
+ [dyno-wp]: http://en.wikipedia.org/wiki/Dynamometer "Wikipedia"
69
+ [iniparse]: http://github.com/anthonyw/iniparse "IniParse on GitHub"
data/Rakefile CHANGED
@@ -22,12 +22,13 @@ begin
22
22
  Dir.glob("{lib,spec}/**/*")
23
23
  end
24
24
  rescue LoadError
25
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
25
+ puts 'Jeweler not available. Install it with: sudo gem install ' +
26
+ 'technicalpickles-jeweler -s http://gems.github.com'
26
27
  end
27
28
 
28
- ################################################################################
29
+ ##############################################################################
29
30
  # rSpec & rcov
30
- ################################################################################
31
+ ##############################################################################
31
32
 
32
33
  desc "Run all examples"
33
34
  Spec::Rake::SpecTask.new('spec') do |t|
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 3
2
+ :patch: 0
3
3
  :major: 0
4
- :minor: 0
4
+ :minor: 1
data/lib/dyno.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'time'
3
+ require 'iniparse'
4
+ require 'libxml'
3
5
 
4
6
  module Dyno
5
7
  # Base exception class.
@@ -9,10 +11,12 @@ module Dyno
9
11
  class MalformedInputError < DynoError; end
10
12
  end
11
13
 
12
- %w( competitor event ).each do |file|
13
- require File.join( File.dirname(__FILE__), "dyno", file )
14
- end
14
+ dir = File.join( File.dirname(__FILE__), "dyno" )
15
15
 
16
- Dir["#{ File.dirname(__FILE__) }/dyno/parsers/*_parser.rb"].sort.each do |parser|
17
- require parser
18
- end
16
+ require File.join( dir, "competitor" )
17
+ require File.join( dir, "event" )
18
+
19
+ # Parsers
20
+ require File.join( dir, "parsers", "race07_parser" )
21
+ require File.join( dir, "parsers", "gtr2_parser" )
22
+ require File.join( dir, "parsers", "rfactor_parser" )
@@ -10,10 +10,46 @@ module Dyno
10
10
  def initialize(name, properties = {})
11
11
  @name = name
12
12
  @lap_times = properties.fetch(:laps, [])
13
+ @dnf = false
13
14
 
14
15
  [:uid, :position, :vehicle, :laps, :race_time, :best_lap].each do |prop|
15
16
  instance_variable_set "@#{prop}", properties[prop]
16
17
  end
17
18
  end
19
+
20
+ ##
21
+ # Returns true fi the competitor finished the event.
22
+ #
23
+ def finished?
24
+ not dnf? and not dsq?
25
+ end
26
+
27
+ ##
28
+ # Flags this competitor as having not completed the event.
29
+ #
30
+ def dnf!
31
+ @dnf = :dnf
32
+ end
33
+
34
+ ##
35
+ # Returns true if the competitor failed to finish.
36
+ #
37
+ def dnf?
38
+ @dnf == :dnf
39
+ end
40
+
41
+ ##
42
+ # Flags this competitor has having been disqualified from the event.
43
+ #
44
+ def dsq!
45
+ @dnf = :dsq
46
+ end
47
+
48
+ ##
49
+ # Returns true if the competitor was disqualified.
50
+ #
51
+ def dsq?
52
+ @dnf == :dsq
53
+ end
18
54
  end
19
55
  end
@@ -0,0 +1,13 @@
1
+ module Dyno::Parsers
2
+ ##
3
+ # Parses a Race 07 results file which are almost identical to Race 07 files.
4
+ #
5
+ class GTR2Parser < Race07Parser
6
+ def self.parse_file( filename )
7
+ # GTR2 files start with a line which isn't valid INI; remove it.
8
+ parse( IniParse.parse(
9
+ File.read( filename ).sub!(/^.*\n/, '')
10
+ ) )
11
+ end
12
+ end
13
+ end
@@ -1,14 +1,8 @@
1
- require 'iniparse'
2
-
3
1
  module Dyno::Parsers
4
2
  ##
5
3
  # Parses a Race 07 results file (which appears to be some variation of the
6
4
  # ini format).
7
5
  #
8
- # TODO: Remove dependency on inifile and write our own ini parser so that:
9
- # * we can extract individual lap information.
10
- # * we don't have to have a results file on disk.
11
- #
12
6
  class Race07Parser
13
7
  ##
14
8
  # Takes a file path and parses it.
@@ -20,10 +14,10 @@ module Dyno::Parsers
20
14
  end
21
15
 
22
16
  ##
23
- # Takes an IniFile instance, parses the contents, and returns a
17
+ # Takes an IniParse::Document instance, parses the contents, and returns a
24
18
  # Dyno::Event containing your results.
25
19
  #
26
- # @param [IniFile] results The results.
20
+ # @param [IniParse::Document] results The results.
27
21
  # @return [Dyno::Event]
28
22
  #
29
23
  def self.parse( results )
@@ -44,9 +38,9 @@ module Dyno::Parsers
44
38
  #######
45
39
 
46
40
  ##
47
- # Takes an IniFile instance and parses the contents.
41
+ # Takes an IniParse::Document instance and parses the contents.
48
42
  #
49
- # @param [IniFile] results The results.
43
+ # @param [IniParse::Document] results The results.
50
44
  #
51
45
  def initialize( results )
52
46
  @raw = results
@@ -58,7 +52,7 @@ module Dyno::Parsers
58
52
  def parse_event!
59
53
  raise Dyno::MalformedInputError unless @raw.has_section?('Header')
60
54
 
61
- @event = Dyno::Event.new( :game => 'Race 07' )
55
+ @event = Dyno::Event.new( :game => @raw['Header']['Game'] )
62
56
  @event.time = Time.parse( @raw['Header']['TimeString'] )
63
57
  @event.game_version = @raw['Header']['Version']
64
58
 
@@ -97,8 +91,9 @@ module Dyno::Parsers
97
91
  lap_time_to_float(lap.split(',').last.strip)
98
92
  end
99
93
 
100
- if section['RaceTime'] == 'DNF'
101
- competitor.race_time = 'DNF'
94
+ if section['RaceTime'] =~ /D(NF|S?Q)/
95
+ $1 == 'NF' ? competitor.dnf! : competitor.dsq!
96
+ competitor.race_time = 0
102
97
  dnf_competitors << competitor
103
98
  else
104
99
  time = section['RaceTime'].split( /:|\./ )
@@ -111,7 +106,9 @@ module Dyno::Parsers
111
106
  end
112
107
 
113
108
  # Sort finished competitors by their race time, lowest (P1) first.
114
- finished_competitors = finished_competitors.sort_by { |c| c.race_time }
109
+ finished_competitors = finished_competitors.sort_by do |c|
110
+ [ - c.laps, c.race_time ]
111
+ end
115
112
 
116
113
  # ... and DNF'ed competitors by how many laps they've done.
117
114
  dnf_competitors = dnf_competitors.sort_by { |c| c.laps }.reverse!
@@ -0,0 +1,138 @@
1
+ module Dyno::Parsers
2
+ ##
3
+ # Parses a rFactor results files.
4
+ #
5
+ class RFactorParser
6
+ ##
7
+ # Takes a file path and parses it.
8
+ #
9
+ # @param [String] filename The path to the results file.
10
+ #
11
+ def self.parse_file( filename )
12
+ xmlparser = LibXML::XML::Parser.new
13
+ xmlparser.file = filename
14
+ parse( xmlparser.parse )
15
+ end
16
+
17
+ ##
18
+ # Takes a LibXML::XML::Document instance, parses the contents, and returns
19
+ # a Dyno::Event containing your results.
20
+ #
21
+ # @param [LibXML::XML::Document] results The results.
22
+ # @return [Dyno::Event]
23
+ #
24
+ def self.parse( results )
25
+ new( results ).parse
26
+ end
27
+
28
+ ##
29
+ # Returns your parsed event and competitor information.
30
+ #
31
+ def parse
32
+ parse_event!
33
+ parse_competitors!
34
+ @event
35
+ end
36
+
37
+ #######
38
+ private
39
+ #######
40
+
41
+ ##
42
+ # Takes a LibXML::XML::Document instance and parses the contents.
43
+ #
44
+ # @param [LibXML::XML::Document] results The results.
45
+ #
46
+ def initialize( results )
47
+ @doc = results
48
+ @stack = []
49
+ end
50
+
51
+ ##
52
+ # Extracts the event information from the results.
53
+ #
54
+ def parse_event!
55
+ unless results = @doc.find_first('//RaceResults')
56
+ raise Dyno::MalformedInputError, 'No //RaceResults node.'
57
+ end
58
+
59
+ with_node(results) do
60
+ @event = Dyno::Event.new(
61
+ :game => clean(value('Mod').gsub(/\.rfm$/, '')),
62
+ :track => clean(value('TrackCourse')),
63
+ :game_version => value('GameVersion')
64
+ )
65
+
66
+ # Sort out the event time - the rFactor results format has more than
67
+ # one TimeString node. The first appears to be when the server was
68
+ # started - we need the time of the event itself, which is held under
69
+ # the Race, Qualify or Warmup node.
70
+ if event_node = \
71
+ (@doc.find_first('//RaceResults/Race') ||
72
+ @doc.find_first('//RaceResults/Qualify') ||
73
+ @doc.find_first('//RaceResults/Warmup'))
74
+ with_node(event_node) do
75
+ @event.time = Time.parse( value('TimeString') )
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Extracts information about each of the competitors.
83
+ #
84
+ def parse_competitors!
85
+ @doc.find('//Driver').each do |section|
86
+ with_node(section) do
87
+ competitor = Dyno::Competitor.new(value('Name'),
88
+ :vehicle => clean(value('CarType')),
89
+ :laps => value('Laps').to_i
90
+ )
91
+
92
+ competitor.position = value('Position').to_i
93
+ competitor.best_lap = lap_time_to_float( value('BestLapTime') )
94
+ competitor.race_time = lap_time_to_float( value('FinishTime') )
95
+
96
+ competitor.lap_times = section.find('Lap').map do |lap|
97
+ lap_time_to_float( lap.content )
98
+ end
99
+
100
+ if value('FinishStatus') =~ /D(NF|S?Q)/
101
+ $1 == 'NF' ? competitor.dnf! : competitor.dsq!
102
+ end
103
+
104
+ @event.competitors << competitor
105
+ end
106
+ end
107
+
108
+ @event.competitors = @event.competitors.sort_by { |c| c.position }
109
+ end
110
+
111
+ # --
112
+ # Utility stuff
113
+ # ++
114
+
115
+ def value( node ) # :nodoc:
116
+ if found = @stack.last.find_first( node )
117
+ found.content
118
+ else
119
+ nil
120
+ end
121
+ end
122
+
123
+ def clean( value ) # :nodoc:
124
+ value.gsub(/[-_]/, ' ').squeeze(' ') unless value.nil?
125
+ end
126
+
127
+ def with_node( node ) # :nodoc:
128
+ @stack.push( node )
129
+ yield
130
+ @stack.pop
131
+ end
132
+
133
+ # Converts a lap time (in the format of M:SS:SSS) to a float.
134
+ def lap_time_to_float(time) # :nodoc:
135
+ (time.nil? || time == '--.----') ? 0.0 : Float(time)
136
+ end
137
+ end
138
+ end
@@ -1,7 +1,7 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper'
2
2
 
3
3
  describe Dyno::Competitor do
4
- before(:all) do
4
+ before(:each) do
5
5
  @competitor = Dyno::Competitor.new("Jake Lucas")
6
6
  end
7
7
 
@@ -40,10 +40,16 @@ describe Dyno::Competitor do
40
40
  @competitor.should respond_to(:best_lap=)
41
41
  end
42
42
 
43
+ it 'should respond_to #finished?' do
44
+ @competitor.should respond_to(:finished?)
45
+ end
46
+
43
47
  it 'should respond_to #dnf?' do
44
- pending do
45
- @competitor.should respond_to(:dnf)
46
- end
48
+ @competitor.should respond_to(:dnf?)
49
+ end
50
+
51
+ it 'should respond_to #dsq?' do
52
+ @competitor.should respond_to(:dsq?)
47
53
  end
48
54
 
49
55
  # ----------
@@ -71,4 +77,53 @@ describe Dyno::Competitor do
71
77
  lambda { Dyno::Competitor.new }.should raise_error(ArgumentError)
72
78
  end
73
79
 
80
+ # ---------------------------------
81
+ # finished / dnf / disqualification
82
+
83
+ describe 'when the competitor finished the event' do
84
+ it 'should return true to #finished?' do
85
+ @competitor.should be_finished
86
+ end
87
+
88
+ it 'should return false to #dnf?' do
89
+ @competitor.should_not be_dnf
90
+ end
91
+
92
+ it 'should return false to #dsq?' do
93
+ @competitor.should_not be_dsq
94
+ end
95
+ end
96
+
97
+ describe 'when the competitor did not finish' do
98
+ before { @competitor.dnf! }
99
+
100
+ it 'should return false to #finished?' do
101
+ @competitor.should_not be_finished
102
+ end
103
+
104
+ it 'should return true to #dnf?' do
105
+ @competitor.should be_dnf
106
+ end
107
+
108
+ it 'should return false to #dsq?' do
109
+ @competitor.should_not be_dsq
110
+ end
111
+ end
112
+
113
+ describe 'when the competitor was disqualified' do
114
+ before { @competitor.dsq! }
115
+
116
+ it 'should return false to #finished?' do
117
+ @competitor.should_not be_finished
118
+ end
119
+
120
+ it 'should return false to #dnf?' do
121
+ @competitor.should_not be_dnf
122
+ end
123
+
124
+ it 'should return true to #dsq?' do
125
+ @competitor.should be_dsq
126
+ end
127
+ end
128
+
74
129
  end