antw-dyno 0.1.2

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