Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--no-profile
--color
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source 'https://rubygems.org'
ruby '2.0.0'

gem 'rspec'
gem 'webmock'
gem 'nokogiri'
30 changes: 30 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.3.5)
crack (0.4.1)
safe_yaml (~> 0.9.0)
diff-lcs (1.2.4)
mini_portile (0.5.1)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
rspec (2.14.1)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
rspec-core (2.14.5)
rspec-expectations (2.14.2)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.14.3)
safe_yaml (0.9.5)
webmock (1.13.0)
addressable (>= 2.2.7)
crack (>= 0.3.2)

PLATFORMS
ruby

DEPENDENCIES
nokogiri
rspec
webmock
8 changes: 8 additions & 0 deletions bin/shakespeare_analyzer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env ruby

require_relative '../lib/shakespeare_analyzer.rb'

DEFAULT_XML_URL = 'http://www.ibiblio.org/xml/examples/shakespeare/macbeth.xml'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're one of the only folks to extract this into a constant. I like it. :)


analyzer = ShakespeareAnalyzer.new(DEFAULT_XML_URL)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to avoid passing a URL to the initializer, and just provide the data instead. That means you could supply text from a file, and it should also make testing easier.

analyzer.run
28 changes: 28 additions & 0 deletions lib/shakespeare_analyzer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'open-uri'
require_relative 'xml_parser'

class ShakespeareAnalyzer
def initialize(uri)
@file_content = get_content_from_uri(uri)
end

def run
print_speakers_sorted_by_line_count
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice high level of abstraction.

end

private

def get_content_from_uri(uri)
open(uri, proxy: ENV['http_proxy']).read
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggests the Shakespeare class is doing too much - it's parsing the text, but also having to deal with the complexities of a proxy server.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with @andyw8.

end

def print_speakers_sorted_by_line_count
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job keeping even your private methods expressive, readable, and short. This whole file is really easy to understand, but I particularly like the clarity of this method.

xml_parser.speakers_sorted_by_line_count.each do |speaker, lines|
puts "#{lines} #{speaker}"
end
end

def xml_parser
@_xml_parser ||= XmlParser.new(@file_content)
end
end
25 changes: 25 additions & 0 deletions lib/xml_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require 'nokogiri'

class XmlParser
def initialize(xml)
@doc = Nokogiri.XML(xml)
end

def speakers_sorted_by_line_count
speakers_with_line_count.sort_by { |_key, value| value }.reverse
end

private

def speakers_with_line_count
Hash[*speakers.map { |speaker| [speaker, count_lines_by_speaker(speaker)] }.flatten]
end

def speakers
@doc.css('PLAY SPEAKER').map { |speaker| speaker.text }.uniq
end

def count_lines_by_speaker(speaker)
@doc.css("PLAY SPEECH:has(SPEAKER[text()='#{speaker}']) LINE").count
end
end
41 changes: 41 additions & 0 deletions output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
719 MACBETH
265 LADY MACBETH
212 MALCOLM
180 MACDUFF
135 ROSS
113 BANQUO
74 LENNOX
70 DUNCAN
62 First Witch
46 Porter
45 Doctor
41 LADY MACDUFF
39 HECATE
35 Sergeant
30 First Murderer
30 SIWARD
27 Third Witch
27 Second Witch
24 ALL
23 Gentlewoman
23 Messenger
21 Lord
21 ANGUS
20 Son
15 Second Murderer
12 MENTEITH
11 Old Man
11 CAITHNESS
10 DONALBAIN
8 Third Murderer
7 YOUNG SIWARD
5 Third Apparition
5 SEYTON
5 Servant
4 Second Apparition
3 Lords
2 First Apparition
2 FLEANCE
2 Both Murderers
1 ATTENDANT
1 Soldiers
33 changes: 33 additions & 0 deletions spec/shakespeare_analyzer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'spec_helper'
require 'shakespeare_analyzer'
require 'stringio'

describe ShakespeareAnalyzer do
describe '#initialize' do
it 'reads provided URI and stores its content to @file_content' do
stub_request(:get, 'http://www.example.com/test_file.txt').to_return(body: 'This is just a test file!')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to learn what these stubs are. Do they interrupt flow and substitute content?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, check this gem: https://github.com/bblimke/webmock

I've found it when I was looking for solution how to test http download. It is awesome.

analyzer = ShakespeareAnalyzer.new('http://www.example.com/test_file.txt')
expect(analyzer.instance_variable_get(:@file_content)).to eq 'This is just a test file!'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really could have used this method when I was writing my tests. "instance_variable_get"
Ill have to remember that for next time.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually recommend against using instance_variable_get in a test, because it means you're inspecting the internal state of the object, rather than testing it's external behaviour.

end
end

describe '#run' do
it 'prints list of speakers sorted by line count' do
test_file = File.dirname(__FILE__) + '/test_files/test.xml'
stub_request(:get, 'http://www.example.com/test.xml').to_return(body: File.read(test_file))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how you have to stub out the http library when what you really want to do is check that the right parsing/counting is happening. This is a strong hint that your class has a few too many responsibilities.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never thought of stubbing the internet here. Thanks. Will add it to my solution too.

analyzer = ShakespeareAnalyzer.new('http://www.example.com/test.xml')
output = capture_stdout { analyzer.run }
expect(output).to eq "4 FourLiner\n3 ThreeLiner\n2 TwoLiner\n1 Liner\n"
end
end
end

def capture_stdout &block
old_stdout = $stdout
fake_stdout = StringIO.new
$stdout = fake_stdout
block.call
fake_stdout.string
ensure
$stdout = old_stdout
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'webmock/rspec'
42 changes: 42 additions & 0 deletions spec/test_files/test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0"?>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this concept of having the XML samples in a separate file. I embedded them within my spec, but it makes it very cluttered. I guess there's a trade-off though, in that you need to look at two places to understand what the spec is doing.

<!DOCTYPE PLAY SYSTEM "play.dtd">

<PLAY>
<ACT>
<SCENE>
<SPEECH>
<SPEAKER>Liner</SPEAKER>
<LINE>Nothing more to say.</LINE>
</SPEECH>
<SPEECH>
<SPEAKER>TwoLiner</SPEAKER>
<SPEAKER>FourLiner</SPEAKER>
<LINE>Hello!</LINE>
<LINE>World!</LINE>
</SPEECH>
<SPEECH>
<SPEAKER>ThreeLiner</SPEAKER>
<LINE>One</LINE>
</SPEECH>
</SCENE>
</ACT>
<ACT>
<SCENE>
<SPEECH>
<SPEAKER>ThreeLiner</SPEAKER>
<LINE>Two</LINE>
</SPEECH>
</SCENE>
<SCENE>
<SPEECH>
<SPEECH>
<SPEAKER>ThreeLiner</SPEAKER>
<SPEAKER>FourLiner</SPEAKER>
<LINE>Three!</LINE>
</SPEECH>
<SPEAKER>FourLiner</SPEAKER>
<LINE>Three!</LINE>
</SPEECH>
</SCENE>
</ACT>
</PLAY>
19 changes: 19 additions & 0 deletions spec/xml_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'spec_helper'
require 'xml_parser'

describe XmlParser do
describe '#initialize' do
it 'instantiates @doc with Nokogiri XML parser' do
doc = XmlParser.new(nil).instance_variable_get(:@doc)
expect(doc.instance_of?(Nokogiri::XML::Document)).to be true
end
end

describe '#speakers_sorted_by_line_count' do
it 'returns hash of speakers with line count sorted by line count' do
filename = File.dirname(__FILE__) + '/test_files/test.xml'
xml_parser = XmlParser.new(File.read(filename))
expect(xml_parser.speakers_sorted_by_line_count.to_a).to eq({'FourLiner' => 4, 'ThreeLiner' => 3, 'TwoLiner' => 2, 'Liner' => 1}.to_a)
end
end
end