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 +62 -7
- data/Rakefile +4 -3
- data/VERSION.yml +2 -2
- data/lib/dyno.rb +10 -6
- data/lib/dyno/competitor.rb +36 -0
- data/lib/dyno/parsers/gtr2_parser.rb +13 -0
- data/lib/dyno/parsers/race07_parser.rb +11 -14
- data/lib/dyno/parsers/rfactor_parser.rb +138 -0
- data/spec/competitor_spec.rb +59 -4
- data/spec/fixtures/gtr2/full.ini +0 -0
- data/spec/fixtures/gtr2/header_no_track.ini +0 -0
- data/spec/fixtures/gtr2/header_only.ini +0 -0
- data/spec/fixtures/gtr2/no_header_section.ini +0 -0
- data/spec/fixtures/gtr2/single_driver.ini +0 -0
- data/spec/fixtures/race07/single_driver_dnf.ini +34 -0
- data/spec/fixtures/race07/single_driver_dsq.ini +34 -0
- data/spec/fixtures/{race07/readme.markdown → readme.markdown} +2 -2
- data/spec/fixtures/rfactor/arca.xml +187 -0
- data/spec/fixtures/rfactor/event_only.xml +43 -0
- data/spec/fixtures/rfactor/full.xml +1023 -0
- data/spec/fixtures/rfactor/missing_root.xml +6 -0
- data/spec/fixtures/rfactor/single_driver.xml +85 -0
- data/spec/fixtures/rfactor/single_driver_dnf.xml +80 -0
- data/spec/fixtures/rfactor/single_driver_dsq.xml +79 -0
- data/spec/parsers/gtr2_parser_spec.rb +199 -0
- data/spec/parsers/race07_parser_spec.rb +13 -1
- data/spec/parsers/rfactor_parser_spec.rb +158 -0
- metadata +23 -3
- data/MIT-LICENSE +0 -19
data/README.markdown
CHANGED
@@ -1,14 +1,69 @@
|
|
1
1
|
dyno
|
2
2
|
====
|
3
3
|
|
4
|
-
|
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
|
-
|
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
|
-
|
11
|
+
Dyno presently supports files spat out by a number of games:
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
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
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
|
-
|
13
|
-
require File.join( File.dirname(__FILE__), "dyno", file )
|
14
|
-
end
|
14
|
+
dir = File.join( File.dirname(__FILE__), "dyno" )
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
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" )
|
data/lib/dyno/competitor.rb
CHANGED
@@ -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
|
17
|
+
# Takes an IniParse::Document instance, parses the contents, and returns a
|
24
18
|
# Dyno::Event containing your results.
|
25
19
|
#
|
26
|
-
# @param [
|
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
|
41
|
+
# Takes an IniParse::Document instance and parses the contents.
|
48
42
|
#
|
49
|
-
# @param
|
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 => '
|
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']
|
101
|
-
competitor.
|
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
|
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
|
data/spec/competitor_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
2
|
|
3
3
|
describe Dyno::Competitor do
|
4
|
-
before(:
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|