# Statistician II (#168)

**URL:** <https://www.ruby-forum.com/t/statistician-ii-168/142224>\
**Category:** Ruby\
**Created:** [July 4, 2008, 7:00pm UTC](https://www.ruby-forum.com/t/statistician-ii-168/142224 "2008-07-04T19:00:47Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [July 4, 2008, 7:00pm UTC](https://www.ruby-forum.com/t/statistician-ii-168/142224/1 "2008-07-04T19:00:47Z")

</div>

Apologies if this appears as a repost, but the mailing list complained  
it  
was too large. Posting now in two parts:

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Q. 2:

1. Please do not post any solutions or spoiler discussion for this  
quiz until 48 hours have passed from the time on this message.

2. Support Ruby Q. 2 by submitting ideas as often as you can! (A  
permanent, new website is in the works for Ruby Q. 2. Until then,  
please visit the temporary website at

3. Enjoy!  
Suggestion: A [QUIZ] in the subject of emails about the problem  
helps everyone on Ruby T. follow the discussion. Please reply to  
the original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

This week’s quiz might look a little scary, but it’s not as bad as it  
seems.  
Give it a shot, and ask for help if you get stuck!

## Statistician II

Last week’s quiz started the creation of a line-based pattern-matching  
system: our statistician. This week, your task is to further develop a  
solution from last week: organize the code and provide a more  
interesting  
interface.

The first thing is organization. This little library should be reusable  
and  
not tied to any particular parsing need. So we want to separate out the  
“Statistician” from the client. To do this means moving the appropriate  
code  
into a separate file called `statistician.rb`, containing:

```
# statistician.rb
module Statistician
  # This module is your task! Your code goes here...
end

```

Meanwhile, the client code will now begin with:

```
# client.rb
require 'statistician'

```

Simple, eh?

Next, we will move the rules from their own data file and bring them  
into  
the code. Admittedly, moving data into code usually is not a wise thing  
to  
do, but as the primary data is that which the rules parse, we’re going  
to do  
it anyway. Besides, this is Ruby Q., so why not?

Simultaneously, we’re going to group rules together: rules that while  
may  
differ somewhat in appearance, essentially represent the same kind or  
category of data. As the rules and category are client data, they will  
go  
into the client’s code. Here’s an example to begin, borrowing the LotRO  
rules used last week.

```
# client.rb
class Offense < Statistician::Reportable
  rule "You wound[the] <name>[with <attack>] for <amount> point[s] 

```

of  
[damage]."  
rule “You reflect point[s] of damage to[the]  
.”  
end

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

```

Next, we need a parser (or Reporter, as I like to call it) that can  
manage  
these rules and classes, read the input data and process it all line by  
line. Such client code looks like this:

```
# client.rb
lotro = Statistician::Reporter.new(Offense, Victory)
lotro.parse(File.read(ARGV[0]))

```

Finally, we need to begin getting useful information out of all the  
records  
that have been read and parsed by the Reporter. After the data is  
parsed,  
the final bit will be to support code such as this:

```
# client.rb
num = Offense.records.size
dmg = Offense.records.inject(0) { |s, x| s + x.amount.to_i }
puts "Average damage inflicted: #{dmg.to_f / num}"

puts Offense.records[0].class # outputs "Offense"

```

What is going on here? The class `Offense` serves three purposes.

1. Its declaration contains the rules for offensive related records.
2. After parsing, the class method `records` returns an array of  
records  
that matched those rules.
3. Those records are instances of the class, and instance methods that  
match the field names (extracted from the rules) provide access to a  
record’s data.

Hopefully this isn’t too confusing. I could have broken up some of these  
responsibilities into other classes or sections of code, but since the  
three  
tasks are rather related, I thought it convenient and pleasing to group  
them  
all into the client’s declared class.

Below I’ll give the full, sample client file I’m using, as well as the  
output it generates when run over the [hunter.txt][1] file we used last  
week. A few hints, first…

1. You are welcome to make `statistician.rb` depend on other Ruby  
modules.  
I personally found `OpenStruct` to be quite useful here.

2. Personally, I found making `Offense` inherit from `Reportable` to  
be  
the cleanest method. At least, it is in my own code. There may be other  
ways  
to accomplish this goal: by `include` or `extend` methods. If you find  
those  
techniques more appealing, please go ahead, but make a note of it in  
your  
submission, since it does require changing how client code is written.

3. Metaprogramming can get a bit tricky to explain in a couple  
sentences,  
so I’ll leave such hints and discussion for the mailing list. Aside from  
that, there are some good examples of metaprogramming looking back  
through  
past Ruby Q.zes. Of particular interest would be the [metakoans.rb  
quiz][2].

4. Finally, my own solution for this week’s quiz is just under 80  
lines  
long, so it need not be overly complex to support the client file below.

((( Remainder of this Ruby Q. to come as a reply to this message. )))

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [July 4, 2008, 7:04pm UTC](https://www.ruby-forum.com/t/statistician-ii-168/142224/2 "2008-07-04T19:04:17Z")

</div>

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 [damage]."  
rule “You are wounded for point[s] of damage.”  
end

```
class Offense < Statistician::Reportable
  rule "You wound[the] <name>[with <attack>] for <amount>

```

point[s] of [damage]."  
rule “You reflect point[s] of damage to[the]  
.”  
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] 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](http://www.splatbang.com/rubyquiz/files/hunter.zip) 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

```

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [July 4, 2008, 7:13pm UTC](https://www.ruby-forum.com/t/statistician-ii-168/142224/3 "2008-07-04T19:13:05Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [July 6, 2008, 8:42pm UTC](https://www.ruby-forum.com/t/statistician-ii-168/142224/4 "2008-07-06T20:42:10Z")

</div>

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

[http://pastie.org/228592](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.

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [July 6, 2008, 9:05pm UTC](https://www.ruby-forum.com/t/statistician-ii-168/142224/5 "2008-07-06T21:05:23Z")

</div>

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](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
