From: Matthew Moss on
Here is the complete, sample client file:

require 'statistician'

class Defense < Statistician::Reportable
rule "[The ]<name> wounds you[ with <attack>] for <amount>
point[s] of <kind>[ damage]."
rule "You are wounded for <amount> point[s] of <kind> damage."
end

class Offense < Statistician::Reportable
rule "You wound[ the] <name>[ with <attack>] for <amount>
point[s] of <kind>[ damage]."
rule "You reflect <amount> point[s] of <kind> damage to[ the]
<name>."
end

class Defeat < Statistician::Reportable
rule "You succumb to your wounds."
end

class Victory < Statistician::Reportable
rule "Your mighty blow defeated[ the] <name>."
end

class Healing < Statistician::Reportable
rule "You heal <amount> points of your wounds."
rule "<player> heals you for <amount> of wound damagepoints."
end

class Regen < Statistician::Reportable
rule "You heal yourself for <amount> Power points."
rule "<player> heals you for <amount> Power points."
end

class Comment < Statistician::Reportable
rule "### <comment> ###"
end

class Ignored < Statistician::Reportable
rule "<player> defeated[ the] <name>."
rule "<player> has succumbed to his wounds."
rule "You have spotted a creature attempting to move stealthily
about."
rule "You sense that a creature is nearby but hidden from your
sight."
rule "[The ]<name> incapacitated you."
end


if __FILE__ == $0
lotro = Statistician::Reporter.new(Defense, Offense, Defeat,
Victory,
Healing, Regen, Comment,
Ignored)
lotro.parse(File.read(ARGV[0]))

num = Offense.records.size
dmg = Offense.records.inject(0) { |sum, off| sum +
Integer(off.amount.gsub(',', '_')) }
d = Defense.records[3]

puts <<-EOT
Number of Offense records: #{num}
Total damage inflicted: #{dmg}
Average damage per Offense: #{(100.0 * dmg / num).round / 100.0}

Defense record 3 indicates that a #{d.name} attacked me
using #{d.attack}, doing #{d.amount} points of damage.

Unmatched rules:
#{lotro.unmatched.join("\n")}

Comments:
#{Comment.records.map { |c| c.comment }.join("\n")}

EOT
end

And here is the output it generates, using the [hunter.txt][1] data
file:

Number of Offense records: 1300
Total damage inflicted: 127995
Average damage per Offense: 98.46

Defense record 3 indicates that a Tempest Warg attacked me
using Melee Double, doing 108 points of damage.

Unmatched rules:
The Trap wounds Goblin-town Guard for 128 points of Common damage.
Nothing to cure.

Comments:
Chat Log: Combat 04/04 00:34 AM



[1]: http://www.splatbang.com/rubyquiz/files/hunter.zip
[2]: http://www.rubyquiz.com/quiz67.html

From: Matthew Moss on
One minor note, about this little bit of code:

> Integer(off.amount.gsub(',', '_')) }

I found that there was at least one instance of a number over one
thousand in the source file, using commas to separate. Neither the
Integer initializer nor the to_i methods recognize that, so this gsub
makes it safe to convert.

From: Matthias Reitinger on
Thanks for this quiz, it was actually quite fun! You can find my
solution here:

http://pastie.org/228592

I didn't expect my code to become this concise when I first read the
problem statement, but I guess that's just the way Ruby works :)

Matthias.
--
Posted via http://www.ruby-forum.com/.

From: Matthew Moss on
Here's my own solution for this quiz (which I made sure I could do
reasonably before posting the quiz!). Very similar in appearance to
Matthias' solution above.

As a pastie: http://pastie.org/228598



require 'ostruct'

module Statistician

class Reportable < OpenStruct
def Reportable.inherited(klass)
# Give each individual Reportable some instance data.
# Doing it this way ensures each klass gets it's own rules/
records, rather
# than sharing amongst all Reportables.
klass.instance_eval %{
@reportable_rules = []
@reportable_records = []
}
end

# Class methods

def self.rule(str)
r = Rule.new(str)
@reportable_rules << r
end

def self.match(str)
data = nil
if @reportable_rules.find { |rule| data = rule.match(str) }
return data
end
end

def self.records
@reportable_records
end

# Helpers

class Rule
def initialize(str)
patt = Regexp.escape(str).gsub('\[', '(?:').gsub('\]',
')?').gsub(/<(.+?)>/, '(.+?)')
@pattern = Regexp.new("^#{patt}$")
@fields = str.scan(/<(.+?)>/).flatten.map { |f| f.to_sym }
end

def match(str)
if md = @pattern.match(str)
Hash[*@fields.zip(md.captures).flatten]
else
nil
end
end
end
end # class Reportable


class Reporter
attr_reader :unmatched

def initialize(*reportables)
@reportables = reportables
@unmatched = []
end

def parse(text)
text.each do |line|
line.strip!
data = nil
if reportable = @reportables.find { |k| data = k.match(line) }
reportable.records << reportable.new(data)
else
@unmatched << line
end
end
end
end # class Reporter

end # module Statistician


From: Matthew Moss on
I wanted to add one more note...

>           klass.class_eval do
>             @rules, @records = [], []
>           end

Considering that this bit of code injects @rules and @records into
klass, my preference is that they be named something _less_
straightforward. My own, similar solution used @reportable_rules and
@reportable_records.

The reason? There is nothing preventing a client from further
extending their own subclasses of Reportable. Actually, I will lightly
encourage that in part 3. To avoid potential name conflicts with
client-side extensions, I'd go with names more complex than the simple
@rules and @records.