antw-dyno 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/LICENSE +19 -0
  2. data/README.markdown +68 -0
  3. data/Rakefile +45 -0
  4. data/VERSION.yml +4 -0
  5. data/lib/dyno.rb +22 -0
  6. data/lib/dyno/competitor.rb +55 -0
  7. data/lib/dyno/event.rb +16 -0
  8. data/lib/dyno/parsers/gtr2_parser.rb +13 -0
  9. data/lib/dyno/parsers/race07_parser.rb +130 -0
  10. data/lib/dyno/parsers/rfactor_parser.rb +138 -0
  11. data/spec/competitor_spec.rb +129 -0
  12. data/spec/event_spec.rb +59 -0
  13. data/spec/fixtures/gtr2/full.ini +0 -0
  14. data/spec/fixtures/gtr2/header_no_track.ini +0 -0
  15. data/spec/fixtures/gtr2/header_only.ini +0 -0
  16. data/spec/fixtures/gtr2/no_header_section.ini +0 -0
  17. data/spec/fixtures/gtr2/single_driver.ini +0 -0
  18. data/spec/fixtures/race07/full.ini +131 -0
  19. data/spec/fixtures/race07/header_no_track.ini +11 -0
  20. data/spec/fixtures/race07/header_only.ini +12 -0
  21. data/spec/fixtures/race07/no_header_section.ini +6 -0
  22. data/spec/fixtures/race07/no_steam_id.ini +37 -0
  23. data/spec/fixtures/race07/single_driver.ini +37 -0
  24. data/spec/fixtures/race07/single_driver_dnf.ini +34 -0
  25. data/spec/fixtures/race07/single_driver_dsq.ini +34 -0
  26. data/spec/fixtures/readme.markdown +5 -0
  27. data/spec/fixtures/rfactor/arca.xml +187 -0
  28. data/spec/fixtures/rfactor/event_only.xml +43 -0
  29. data/spec/fixtures/rfactor/full.xml +1023 -0
  30. data/spec/fixtures/rfactor/missing_root.xml +6 -0
  31. data/spec/fixtures/rfactor/single_driver.xml +85 -0
  32. data/spec/fixtures/rfactor/single_driver_dnf.xml +80 -0
  33. data/spec/fixtures/rfactor/single_driver_dsq.xml +79 -0
  34. data/spec/parsers/gtr2_parser_spec.rb +199 -0
  35. data/spec/parsers/race07_parser_spec.rb +155 -0
  36. data/spec/parsers/rfactor_parser_spec.rb +158 -0
  37. data/spec/spec_helper.rb +3 -0
  38. metadata +107 -0
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008 Anthony Williams
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ dyno
2
+ ====
3
+
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])_
7
+
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.
10
+
11
+ Dyno presently supports files spat out by a number of games:
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.
17
+
18
+ Dyno requires the iniparse gem (available via `gem install iniparse` or on
19
+ [GitHub][iniparse]) for the Race 07 and GTR2 parsers, and libxml-ruby for
20
+ the rFactor parser.
21
+
22
+ Usage
23
+ -----
24
+
25
+ Dyno::Parsers::Race07Parser.parse_file( '/path/to/result/file' )
26
+ # => Dyno::Event
27
+
28
+ Dyno::Parsers::GTR2Parser.parse_file( '/path/to/result/file' )
29
+ # => Dyno::Event
30
+
31
+ Dyno::Parsers::RFactorParser.parse_file( '/path/to/result/file' )
32
+ # => Dyno::Event
33
+
34
+ Each of these operations will return a Dyno::Event instance. Dyno::Event
35
+ defines `#competitors` containing a collection of all of those who took part
36
+ in the event. See the API docs for Dyno::Event and Dyno::Competitor for more
37
+ details.
38
+
39
+ If your results file couldn't be parsed, a Dyno::MalformedInputError exception
40
+ will be raised.
41
+
42
+ License
43
+ -------
44
+
45
+ Dyno is distributed under the MIT/X11 License.
46
+
47
+ > Copyright (c) 2008-2009 Anthony Williams
48
+ >
49
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
50
+ > of this software and associated documentation files (the "Software"), to
51
+ > deal in the Software without restriction, including without limitation the
52
+ > rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
53
+ > sell copies of the Software, and to permit persons to whom the Software is
54
+ > furnished to do so, subject to the following conditions:
55
+ >
56
+ > The above copyright notice and this permission notice shall be included in
57
+ > all copies or substantial portions of the Software.
58
+ >
59
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
64
+ > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
65
+ > IN THE SOFTWARE.
66
+
67
+ [dyno-wp]: http://en.wikipedia.org/wiki/Dynamometer "Wikipedia"
68
+ [iniparse]: http://github.com/antw/iniparse "IniParse on GitHub"
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'spec/rake/spectask'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |s|
7
+ s.name = 'dyno'
8
+ s.platform = Gem::Platform::RUBY
9
+ s.has_rdoc = true
10
+ s.summary = 'A rubygem for parsing sim-racing results files.'
11
+ s.description = s.summary
12
+ s.author = 'Anthony Williams'
13
+ s.email = 'anthony@ninecraft.com'
14
+ s.homepage = 'http://github.com/anthonyw/dyno'
15
+
16
+ s.extra_rdoc_files = ['README.markdown', 'LICENSE']
17
+
18
+ # Dependencies.
19
+ s.add_dependency "iniparse", ">= 0.2.0"
20
+
21
+ s.files = %w(LICENSE README.markdown Rakefile VERSION.yml) +
22
+ Dir.glob("{lib,spec}/**/*")
23
+ end
24
+ rescue LoadError
25
+ puts 'Jeweler not available. Install it with: sudo gem install ' +
26
+ 'technicalpickles-jeweler -s http://gems.github.com'
27
+ end
28
+
29
+ ##############################################################################
30
+ # rSpec & rcov
31
+ ##############################################################################
32
+
33
+ desc "Run all examples"
34
+ Spec::Rake::SpecTask.new('spec') do |t|
35
+ t.spec_files = FileList['spec/**/*.rb']
36
+ t.spec_opts = ['-c -f s']
37
+ end
38
+
39
+ desc "Run all examples with RCov"
40
+ Spec::Rake::SpecTask.new('spec:rcov') do |t|
41
+ t.spec_files = FileList['spec/**/*.rb']
42
+ t.spec_opts = ['-c -f s']
43
+ t.rcov = true
44
+ t.rcov_opts = ['--exclude', 'spec']
45
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 2
4
+ :major: 0
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'time'
3
+ require 'iniparse'
4
+ require 'libxml'
5
+
6
+ module Dyno
7
+ # Base exception class.
8
+ class DynoError < StandardError; end
9
+
10
+ # Raised if an input source couldn't be parsed, or was missing something.
11
+ class MalformedInputError < DynoError; end
12
+ end
13
+
14
+ dir = File.join( File.dirname(__FILE__), "dyno" )
15
+
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" )
@@ -0,0 +1,55 @@
1
+ module Dyno
2
+ class Competitor
3
+ attr_accessor :name, :uid, :position, :vehicle, :laps, :race_time,
4
+ :best_lap, :lap_times
5
+
6
+ ##
7
+ # @param [String] name The competitor's name.
8
+ # @param [Hash] properties Extra information about the competitor.
9
+ #
10
+ def initialize(name, properties = {})
11
+ @name = name
12
+ @lap_times = properties.fetch(:laps, [])
13
+ @dnf = false
14
+
15
+ [:uid, :position, :vehicle, :laps, :race_time, :best_lap].each do |prop|
16
+ instance_variable_set "@#{prop}", properties[prop]
17
+ end
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
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ module Dyno
2
+ class Event
3
+ attr_accessor :time, :game, :game_version, :track, :competitors
4
+
5
+ ##
6
+ # @param [Hash] properties Event information.
7
+ #
8
+ def initialize(properties = {})
9
+ @time = properties.fetch( :time, Time.now )
10
+ @track = properties[:track]
11
+ @game = properties[:game]
12
+ @game_version = properties[:game_version]
13
+ @competitors = properties.fetch( :competitors, [] )
14
+ end
15
+ end
16
+ 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
@@ -0,0 +1,130 @@
1
+ module Dyno::Parsers
2
+ ##
3
+ # Parses a Race 07 results file (which appears to be some variation of the
4
+ # ini format).
5
+ #
6
+ class Race07Parser
7
+ ##
8
+ # Takes a file path and parses it.
9
+ #
10
+ # @param [String] filename The path to the results file.
11
+ #
12
+ def self.parse_file( filename )
13
+ parse( IniParse.open( filename ) )
14
+ end
15
+
16
+ ##
17
+ # Takes an IniParse::Document instance, parses the contents, and returns a
18
+ # Dyno::Event containing your results.
19
+ #
20
+ # @param [IniParse::Document] results The results.
21
+ # @return [Dyno::Event]
22
+ #
23
+ def self.parse( results )
24
+ new( results ).parse
25
+ end
26
+
27
+ ##
28
+ # Returns your parsed event and competitor information.
29
+ #
30
+ def parse
31
+ parse_event!
32
+ parse_competitors!
33
+ @event
34
+ end
35
+
36
+ #######
37
+ private
38
+ #######
39
+
40
+ ##
41
+ # Takes an IniParse::Document instance and parses the contents.
42
+ #
43
+ # @param [IniParse::Document] results The results.
44
+ #
45
+ def initialize( results )
46
+ @raw = results
47
+ end
48
+
49
+ ##
50
+ # Extracts the event information from the results.
51
+ #
52
+ def parse_event!
53
+ raise Dyno::MalformedInputError unless @raw.has_section?('Header')
54
+
55
+ @event = Dyno::Event.new( :game => @raw['Header']['Game'] )
56
+ @event.time = Time.parse( @raw['Header']['TimeString'] )
57
+ @event.game_version = @raw['Header']['Version']
58
+
59
+ # Extract the track name from Race/Scene
60
+ if @raw.has_section?('Race') && @raw['Race']['Scene']
61
+ @event.track = @raw['Race']['Scene'].split( '\\' )[-2].gsub( /[_-]+/, ' ' )
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Extracts information about each of the competitors.
67
+ #
68
+ def parse_competitors!
69
+ finished_competitors = []
70
+ dnf_competitors = []
71
+
72
+ @raw.each do |section|
73
+ # Competitor sections are named SlotNNN.
74
+ next unless section.key =~ /Slot\d\d\d/
75
+
76
+ competitor = Dyno::Competitor.new(section['Driver'],
77
+ :vehicle => section['Vehicle'],
78
+ :laps => section['Laps'].to_i
79
+ )
80
+
81
+ # Some results files have a blank ID.
82
+ if section['SteamId'] && section['SteamId'].kind_of?(Numeric)
83
+ competitor.uid = section['SteamId']
84
+ end
85
+
86
+ # Sort out the competitors lap times.
87
+ competitor.best_lap = lap_time_to_float(section['BestLap'])
88
+
89
+ competitor.lap_times = section['Lap'].map do |lap|
90
+ lap = lap.gsub(/\((.*)\)/, '\1')
91
+ lap_time_to_float(lap.split(',').last.strip)
92
+ end
93
+
94
+ if section['RaceTime'] =~ /D(NF|S?Q)/
95
+ $1 == 'NF' ? competitor.dnf! : competitor.dsq!
96
+ competitor.race_time = 0
97
+ dnf_competitors << competitor
98
+ else
99
+ time = section['RaceTime'].split( /:|\./ )
100
+
101
+ competitor.race_time = time[2].to_f + ( time[1].to_i * 60 ) +
102
+ ( time[0].to_i * 60 * 60 ) + "0.#{time[3]}".to_f
103
+
104
+ finished_competitors << competitor
105
+ end
106
+ end
107
+
108
+ # Sort finished competitors by their race time, lowest (P1) first.
109
+ finished_competitors = finished_competitors.sort_by do |c|
110
+ [ - c.laps, c.race_time ]
111
+ end
112
+
113
+ # ... and DNF'ed competitors by how many laps they've done.
114
+ dnf_competitors = dnf_competitors.sort_by { |c| c.laps }.reverse!
115
+
116
+ # Finally let's assign their finishing positions.
117
+ competitors = finished_competitors + dnf_competitors
118
+ competitors.each_with_index { |c, i| c.position = i + 1 }
119
+
120
+ # All done!
121
+ @event.competitors = competitors
122
+ end
123
+
124
+ # Converts a lap time (in the format of M:SS:SSS) to a float.
125
+ def lap_time_to_float(time)
126
+ time = time.split( /:|\./ )
127
+ time[1].to_f + ( time[0].to_i * 60 ) + "0.#{time[2]}".to_f
128
+ end
129
+ end
130
+ end
@@ -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