mass_shootings 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 56342cbde02ebb1f08df1f9eab8957b2827b3156
4
+ data.tar.gz: 66ff3cb637ac2edd7896673b182c81727b1c2586
5
+ SHA512:
6
+ metadata.gz: b0c9ad74375a2f5d3bfac7d39413741c285520be7310403db95ce053181f74544cbc1c50398ca3d134aea5fbac8ffed9432e6bd73c574a974acd21fadce64707
7
+ data.tar.gz: 65353201bda10ce4ec4f32745a9d34212339b176ac769f22a0eb3301ea6e8ec8e6b00b14321eddd19289cbb1abaa7d5b4995bb4f68ccaf3515862f926c4c1301
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright © 2015 David P. Kleinschmidt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ 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
+
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # mass_shootings
2
+
3
+ `mass_shootings` provides an easy-to-use Ruby interface to query and report on
4
+ mass shootings in America.
5
+
6
+ ## Usage
7
+
8
+ Mass shootings have several properties:
9
+
10
+ - **id** a unique identifier
11
+ - **alleged_shooters** the names of the alleged shooters, if known
12
+ - **casualties** count of casualties, classified by type (`:dead` or `:injured`)
13
+ - **date** date the shooting occurred
14
+ - **location** where the shooting occurred
15
+ - **references** links to relevant news sources
16
+
17
+ To retrieve a list of mass shootings that occurred within a date range:
18
+
19
+ ```ruby
20
+ MassShootings::Tracker.in_date_range Date.new(2015, 12, 6)...Date.today + 1
21
+ ```
22
+
23
+ To retrieve a single mass shooting by ID:
24
+
25
+ ```ruby
26
+ MassShootings::Tracker.get '2015-318'
27
+ ```
28
+
29
+ You can use Ruby's built-in Enumerable methods to filter results:
30
+
31
+ ```ruby
32
+ shootings.
33
+ reject { |shooting| shooting.casualties.fetch(:dead, 0) == 0 }.
34
+ sort_by { |shooting| shooting.casualties[:dead] }
35
+ ```
36
+
37
+ Shootings are cached in-memory. If you use `mass_shooting` in a long-lived
38
+ process, the cache may become stale unless you call
39
+ `MassShootings::Tracker.reset` every 24 hours and 36 minutes or so.
@@ -0,0 +1,31 @@
1
+ require 'mass_shootings/version'
2
+
3
+ module MassShootings
4
+ class << self
5
+ #
6
+ # Gemspec for `mass_shootings`. This is only used by the gemspec and the
7
+ # Rakefile, and must be required separately.
8
+ #
9
+ def gemspec
10
+ @gemspec ||= Gem::Specification.new do |gem|
11
+ gem.authors = 'David P Kleinschmidt'
12
+ gem.email = 'david@kleinschmidt.name'
13
+ gem.homepage = 'http://bitbucket.org/zobar/mass_shootings'
14
+ gem.license = 'MIT'
15
+ gem.name = 'mass_shootings'
16
+ gem.summary = 'Mass shootings as a service'
17
+ gem.version = VERSION
18
+
19
+ gem.files = Dir['*.md', '{lib,spec}/**/*.rb']
20
+
21
+ gem.add_dependency 'activemodel', '~> 4.2'
22
+ gem.add_dependency 'nokogiri', '~> 1.6'
23
+
24
+ gem.add_development_dependency 'minitest', '~> 5.8'
25
+ gem.add_development_dependency 'mocha', '~> 1.1'
26
+ gem.add_development_dependency 'simplecov', '~> 0.11'
27
+ gem.add_development_dependency 'yard', '~> 0.8'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_model'
2
+
3
+ module MassShootings
4
+ #
5
+ # A `MassShooting::Shooting` is when four or more people are shot in an event,
6
+ # or related series of events, likely without a cooling off period.
7
+ #
8
+ class Shooting
9
+ include ActiveModel::AttributeMethods
10
+
11
+ define_attribute_methods :id, :alleged_shooters, :casualties, :date,
12
+ :location, :references
13
+
14
+ #
15
+ # Retrieves an attribute by name.
16
+ #
17
+ def attribute(name)
18
+ @attributes[name.to_sym]
19
+ end
20
+
21
+ #
22
+ # Creates a new Shooting with the given attributes.
23
+ #
24
+ # @param [Hash] attributes Information pertaining to the Shooting.
25
+ # @option attributes [String] id a unique identifier
26
+ # @option attributes [Array<String>] alleged_shooters (nil) the names of the
27
+ # alleged shooters
28
+ # @option attributes [Hash{Symbol => Integer}] casualties count of
29
+ # casualties, classified by type (`:dead` or `:injured`)
30
+ # @option attributes [Date] date date the shooting occurred
31
+ # @option attributes [String] location where the shooting occurred
32
+ # @option attributes [Array<URI>] references links to relevant news sources
33
+ #
34
+ def initialize(attributes)
35
+ @attributes = attributes
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,86 @@
1
+ require 'mass_shootings/shooting'
2
+ require 'nokogiri'
3
+ require 'pp'
4
+
5
+ module MassShootings
6
+ module Tracker
7
+ class Page
8
+ include Enumerable
9
+
10
+ attr_reader :page_id, :wiki
11
+ delegate :each, to: :shootings
12
+ delegate :[], to: :indexed
13
+
14
+ def initialize(page_id, data)
15
+ @page_id = page_id
16
+ @wiki = Nokogiri::HTML.parse(data).css '.wiki'
17
+ end
18
+
19
+ private
20
+
21
+ def format
22
+ @format ||= /\A\s*Number\s*(?<number>.+?):\s*
23
+ (?<date>.+?),\s*
24
+ (?<alleged_shooters>.+),\s*
25
+ (?<casualties>.*?\d.*?),\s*
26
+ (?<location>.+)\s*\Z/x
27
+ end
28
+
29
+ def indexed
30
+ @indexed ||= Hash[shootings.map { |shooting| [shooting.id, shooting] }]
31
+ end
32
+
33
+ def parse(elements)
34
+ h2, as = elements[0], elements[1..-1]
35
+
36
+ format.match h2.content do |match|
37
+ alleged_shooters = parse_alleged_shooters match['alleged_shooters']
38
+ attributes = {
39
+ id: "#{page_id}-#{match['number']}",
40
+ casualties: parse_casualties(match['casualties']),
41
+ date: parse_date(match['date']),
42
+ location: match['location'],
43
+ references: as.map { |a| URI(a[:href]) }
44
+ }
45
+
46
+ if alleged_shooters.any?
47
+ attributes[:alleged_shooters] = alleged_shooters
48
+ end
49
+
50
+ Shooting.new attributes
51
+ end
52
+ end
53
+
54
+ def parse_alleged_shooters(alleged_shooters)
55
+ alleged_shooters.
56
+ split(/\s*(?:,?\s+(?:and|&)\s+|[,;](?!\s*Jr\.))\s*/).
57
+ reject do |shooter|
58
+ shooter =~ /identified|identity|unnamed|unkn?own|unreported/i
59
+ end
60
+ end
61
+
62
+ def parse_casualties(casualties)
63
+ Hash[
64
+ casualties.
65
+ scan(/(\d+)\s+(.+?)\b/).
66
+ map { |count, type| [type.to_sym, count.to_i] }
67
+ ]
68
+ end
69
+
70
+ def parse_date(date)
71
+ result = Date.strptime date, '%m/%d/%Y'
72
+ result.year < 100 ? Date.strptime(date, '%m/%d/%y') : result
73
+ rescue
74
+
75
+ end
76
+
77
+ def shootings
78
+ @shootings ||= wiki.
79
+ xpath('child::h2 | child::h2/following-sibling::p/a').
80
+ slice_before { |element| element.node_name == 'h2' }.
81
+ map(&method(:parse)).
82
+ tap { |s| PP.pp s }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,69 @@
1
+ require 'active_support/core_ext/range/overlaps'
2
+
3
+ module MassShootings
4
+ #
5
+ # Retrieves mass shootings from the [r/GunsAreCool shooting tracker]
6
+ # (https://www.reddit.com/r/GunsAreCool/wiki/2015massshootings).
7
+ #
8
+ module Tracker
9
+ autoload :Page, 'mass_shootings/tracker/page'
10
+ private_constant :Page
11
+
12
+ class << self
13
+ #
14
+ # Retrieves a `Shooting` by unique ID.
15
+ # @param [String] id the `Shooting`'s unique identifier. This is opaque;
16
+ # you should not depend on its format.
17
+ #
18
+ def get(id)
19
+ year, number = id.split '-', 2
20
+ page(year)[id]
21
+ end
22
+
23
+ #
24
+ # Retrieves all `Shooting`s that occurred within a date range.
25
+ # @param [Range<Date>] date_range the date range to search. Inclusive and
26
+ # exclusive ranges are both supported.
27
+ #
28
+ def in_date_range(date_range)
29
+ pages_in_date_range(date_range).
30
+ map(&method(:page)).
31
+ flat_map(&:to_a).
32
+ select { |shooting| date_range.cover? shooting.date }
33
+ end
34
+
35
+ #
36
+ # Invalidates the in-memory cache. In a long-running process, you should
37
+ # arrange for this method to be called approximately every 24 hours and 36
38
+ # minutes.
39
+ #
40
+ def reset
41
+ @pages = {}
42
+ end
43
+
44
+ private
45
+
46
+ def page(year)
47
+ pages[year] ||= Page.new year, Net::HTTP.get(uri(year))
48
+ end
49
+
50
+ def pages
51
+ @pages || reset
52
+ end
53
+
54
+ def pages_in_date_range(date_range)
55
+ result = []
56
+ year = date_range.begin.year
57
+ while date_range.overlaps? Date.new(year, 1, 1)...Date.new(year + 1, 1, 1)
58
+ result << year.to_s
59
+ year += 1
60
+ end
61
+ result
62
+ end
63
+
64
+ def uri(year)
65
+ URI "https://www.reddit.com/r/GunsAreCool/wiki/#{year}massshootings"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,6 @@
1
+ module MassShootings
2
+ #
3
+ # Current version of `mass_shootings`
4
+ #
5
+ VERSION = '0.0.1'
6
+ end
@@ -0,0 +1,8 @@
1
+ #
2
+ # Mass shootings as a service.
3
+ #
4
+ module MassShootings
5
+ autoload :Shooting, 'mass_shootings/shooting'
6
+ autoload :Tracker, 'mass_shootings/tracker'
7
+ autoload :VERSION, 'mass_shootings/version'
8
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+ require 'mass_shootings/tracker'
3
+
4
+ describe MassShootings::Tracker do
5
+ let(:alleged_shooters) { ['Alleged Shooter'] }
6
+ let(:casualties) { {dead: dead, injured: injured} }
7
+ let(:date) { Date.today }
8
+ let(:dead) { 1 }
9
+ let(:injured) { 2 }
10
+ let(:id) { "#{year}-#{number}" }
11
+ let(:location_) { 'City, ST' }
12
+ let(:number) { 1 }
13
+ let(:reference) { URI('http://example.com/reference') }
14
+ let(:references) { [reference] }
15
+ let(:year) { date.year }
16
+
17
+ let(:description) do
18
+ [
19
+ "Number #{number}: #{date.strftime('%-m/%-d/%Y')}",
20
+ alleged_shooters.join(' and '),
21
+ casualties.map { |c| c.reverse.join ' ' }.join(' '),
22
+ location_
23
+ ].join ', '
24
+ end
25
+
26
+ let(:page) do
27
+ <<-HTML
28
+ <!DOCTYPE html>
29
+ <title></title>
30
+ <div class='wiki'>
31
+ <h2>#{description}</h2>
32
+ <p><a href='#{reference}'>#{reference}</a>
33
+ </div>
34
+ HTML
35
+ end
36
+
37
+ let(:uri) do
38
+ URI "https://www.reddit.com/r/GunsAreCool/wiki/#{year}massshootings"
39
+ end
40
+
41
+ before { Net::HTTP.stubs(:get).with(uri).returns(page) }
42
+ after { MassShootings::Tracker.reset }
43
+
44
+ describe '.get' do
45
+ subject { MassShootings::Tracker.get id }
46
+
47
+ it 'sets the alleged shooter' do
48
+ subject.alleged_shooters.must_equal alleged_shooters
49
+ end
50
+
51
+ it 'sets the date' do
52
+ subject.date.must_equal date
53
+ end
54
+
55
+ it 'sets the dead' do
56
+ subject.casualties[:dead].must_equal dead
57
+ end
58
+
59
+ it 'sets the id' do
60
+ subject.id.must_equal id
61
+ end
62
+
63
+ it 'sets the injured' do
64
+ subject.casualties[:injured].must_equal injured
65
+ end
66
+
67
+ it 'sets the location' do
68
+ subject.location.must_equal location_
69
+ end
70
+
71
+ it 'sets the references' do
72
+ subject.references.must_equal references
73
+ end
74
+
75
+ describe 'with an unknown alleged shooter' do
76
+ let(:alleged_shooters) { ['Unknown'] }
77
+
78
+ it 'does not set the alleged shooter' do
79
+ subject.alleged_shooters.must_be_nil
80
+ end
81
+ end
82
+
83
+ describe 'with an unidentified alleged shooter' do
84
+ let(:alleged_shooters) { ['Unidentified'] }
85
+
86
+ it 'does not set the alleged shooter' do
87
+ subject.alleged_shooters.must_be_nil
88
+ end
89
+ end
90
+
91
+ describe 'with two alleged shooters' do
92
+ let(:alleged_shooters) { ['Alleged', 'Shooter'] }
93
+
94
+ it 'sets the alleged shooters' do
95
+ subject.alleged_shooters.must_equal alleged_shooters
96
+ end
97
+ end
98
+
99
+ describe 'with three alleged shooters' do
100
+ let(:alleged_shooters) { ['Three', 'Alleged', 'Shooters'] }
101
+ let(:formatted_alleged_shooters) { 'Three, Alleged, and Shooters' }
102
+
103
+ it 'sets the alleged shooters' do
104
+ subject.alleged_shooters.must_equal alleged_shooters
105
+ end
106
+ end
107
+ end
108
+
109
+ describe '.in_date_range' do
110
+ subject { MassShootings::Tracker.in_date_range date_range }
111
+ let(:date_2) { date.prev_year }
112
+ let(:date_range) { from...to }
113
+ let(:from) { date_2 }
114
+ let(:id_2) { "#{year_2}-#{number_2}" }
115
+ let(:number_2) { 7 }
116
+ let(:to) { date }
117
+ let(:year_2) { date_2.year }
118
+
119
+ let(:description_2) do
120
+ "Number #{number_2}: #{date_2.strftime('%-m/%-d/%Y')}, Alleged Shooter 2, 11 dead 12 injured, City 2, S2"
121
+ end
122
+
123
+ let(:page_2) do
124
+ <<-HTML
125
+ <!DOCTYPE html>
126
+ <title></title>
127
+ <div class='wiki'>
128
+ <h2>#{description_2}</h2>
129
+ </div>
130
+ HTML
131
+ end
132
+
133
+ let(:uri_2) do
134
+ URI "https://www.reddit.com/r/GunsAreCool/wiki/#{year_2}massshootings"
135
+ end
136
+
137
+ before { Net::HTTP.stubs(:get).with(uri_2).returns(page_2) }
138
+
139
+ describe 'exclusive' do
140
+ it 'excludes shootings on the end date' do
141
+ subject.map(&:id).must_equal [id_2]
142
+ end
143
+ end
144
+
145
+ describe 'inclusive' do
146
+ let(:date_range) { from..to }
147
+
148
+ it 'includes shootings on the end date' do
149
+ subject.map(&:id).must_equal [id_2, id]
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,6 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'mocha/mini_test'
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mass_shootings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - David P Kleinschmidt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.8'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mocha
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.11'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.11'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.8'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.8'
97
+ description:
98
+ email: david@kleinschmidt.name
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - LICENSE.md
104
+ - README.md
105
+ - lib/mass_shootings.rb
106
+ - lib/mass_shootings/gemspec.rb
107
+ - lib/mass_shootings/shooting.rb
108
+ - lib/mass_shootings/tracker.rb
109
+ - lib/mass_shootings/tracker/page.rb
110
+ - lib/mass_shootings/version.rb
111
+ - spec/mass_shootings/tracker_spec.rb
112
+ - spec/spec_helper.rb
113
+ homepage: http://bitbucket.org/zobar/mass_shootings
114
+ licenses:
115
+ - MIT
116
+ metadata: {}
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 2.5.0
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Mass shootings as a service
137
+ test_files: []