mass_shootings 0.0.1

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.
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: []