anthonyw-dyno 0.0.3 → 0.1.0

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/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