fips_lookup 0.3.0 → 0.4

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.
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ # FIPS::Subdivision
6
+ module FIPS
7
+ class Subdivision
8
+ class << self
9
+ def lookup(**params)
10
+ fips = params.fetch(:fips, nil)
11
+ state = params.fetch(:state, nil)
12
+ county = params.fetch(:county, nil)
13
+ subdivision = params.fetch(:subdivision, nil)
14
+
15
+ unless fips.nil? || (fips.is_a?(String) && fips.match?(/\A(?:\d{2}|\d{3}|\d{5}|\d{10})\z/))
16
+ raise ArgumentError, "FIPS input must be a 2, 3, 5, or 10 digit string"
17
+ end
18
+ raise ArgumentError, "State input must be a non-empty string" unless state.nil? || (state.is_a?(String) && !state.strip.empty?)
19
+ raise ArgumentError, "County input must be a non-empty string" unless county.nil? || (county.is_a?(String) && !county.strip.empty?)
20
+ unless subdivision.nil? || (subdivision.is_a?(String) && !subdivision.strip.empty?)
21
+ raise ArgumentError, "Subdivision input must be a non-empty string"
22
+ end
23
+
24
+ state_abbr = determine_state_abbr(fips, state)
25
+ raise ArgumentError, "Could not determine state abbreviation from parameters provided: #{params.inspect}" if state_abbr.nil?
26
+
27
+ location = identify_with_fips(fips, state, county, subdivision, state_abbr)
28
+ return location unless location.nil?
29
+
30
+ return by_name(state_abbr, county, subdivision) if %i[state county subdivision].all? { |key| !params[key].nil? }
31
+
32
+ raise ArgumentError, "cannot determine subdivision: no valid parameters provided"
33
+ end
34
+
35
+ def all(state:, county: nil)
36
+ raise ArgumentError, "State input must be a non-empty string" unless state.is_a?(String) && !state.strip.empty?
37
+ raise ArgumentError, "County input must be a non-empty string" unless county.nil? || (county.is_a?(String) && !county.strip.empty?)
38
+
39
+ state_abbr = FIPS::State.lookup(state: state)[:abbr]
40
+ raise ArgumentError, "Could not determine state abbreviation from state: #{state}" if state_abbr.nil?
41
+
42
+ county_name = normalize_name(county)
43
+ records = CSV.read(file_path(state_abbr))
44
+ records.map do |row|
45
+ formatted_subdivision(row)
46
+ end.select { |record| county.nil? || normalize_name(record[:county_name]) == county_name }
47
+ end
48
+
49
+ def file_path(state_abbr)
50
+ File.expand_path("../data/subdivision/#{state_abbr.upcase}.csv", __dir__)
51
+ end
52
+
53
+ private
54
+
55
+ def determine_state_abbr(fips, state)
56
+ return FIPS::State.lookup(state: state)[:abbr] unless state.nil?
57
+
58
+ FIPS::State.fips_to_abbr(fips[0, 2]) unless fips.nil? || fips.length < 2
59
+ end
60
+
61
+ def identify_with_fips(fips, state, county, subdivision, state_abbr = nil)
62
+ return nil if fips.nil?
63
+
64
+ case fips.length
65
+ when 2
66
+ return nil if county.nil? || subdivision.nil?
67
+
68
+ by_name(FIPS::State.fips_to_abbr(fips), county, subdivision)
69
+ when 3
70
+ return nil if state.nil? || subdivision.nil?
71
+
72
+ state_abbr = FIPS::State.lookup(state: state)[:abbr]
73
+ by_county_fips_and_sub_name(state_abbr, fips, subdivision)
74
+ when 5
75
+ unless subdivision.nil?
76
+ state_abbr = FIPS::State.fips_to_abbr(fips[0, 2])
77
+ return by_county_fips_and_sub_name(state_abbr, fips[2, 3], subdivision)
78
+ end
79
+ return nil if state.nil?
80
+
81
+ state_abbr = FIPS::State.lookup(state: state)[:abbr]
82
+ by_state_and_sub_fips(state_abbr, fips)
83
+ when 10
84
+ by_fips(fips)
85
+ end
86
+ end
87
+
88
+ def by_state_and_sub_fips(state_abbr, fips)
89
+ subdivision = state_index(state_abbr)[:by_subdivision_fips][fips]
90
+ return subdivision unless subdivision.nil?
91
+
92
+ raise FIPS::NotFoundError, "No subdivision found matching fips: #{fips}"
93
+ end
94
+
95
+ def by_fips(fips)
96
+ state_abbr = FIPS::State.fips_to_abbr(fips[0, 2])
97
+ subdivision = state_index(state_abbr)[:by_full_fips][fips]
98
+ return subdivision unless subdivision.nil?
99
+
100
+ raise FIPS::NotFoundError, "No subdivision found matching fips: #{fips}"
101
+ end
102
+
103
+ def by_name(state_abbr, county, subdivision)
104
+ key = [normalize_name(county), normalize_name(subdivision)]
105
+ match = state_index(state_abbr)[:by_county_and_name][key]
106
+ return match unless match.nil?
107
+
108
+ raise FIPS::NotFoundError, "No subdivision found matching: #{subdivision} in #{county}"
109
+ end
110
+
111
+ def by_county_fips_and_sub_name(state_abbr, county_fips, subdivision)
112
+ key = [county_fips, normalize_name(subdivision)]
113
+ match = state_index(state_abbr)[:by_county_fips_and_name][key]
114
+ return match unless match.nil?
115
+
116
+ nil
117
+ end
118
+
119
+ def state_index(state_abbr)
120
+ indexes = subdivision_indexes
121
+ indexes[state_abbr] ||= build_state_index(state_abbr)
122
+ end
123
+
124
+ def subdivision_indexes
125
+ @subdivision_indexes ||= {}
126
+ end
127
+
128
+ def build_state_index(state_abbr)
129
+ records = CSV.read(file_path(state_abbr)).map { |row| formatted_subdivision(row) }
130
+ {
131
+ records: records,
132
+ by_full_fips: records.to_h { |record| [record[:fips], record] },
133
+ by_subdivision_fips: records.each_with_object({}) do |record, index|
134
+ subdivision_fips = record[:fips][5, 5]
135
+ index[subdivision_fips] ||= record
136
+ end,
137
+ by_county_fips_and_name: records.to_h do |record|
138
+ [[record[:fips][2, 3], normalize_name(record[:name])], record]
139
+ end,
140
+ by_county_and_name: records.to_h do |record|
141
+ [[normalize_name(record[:county_name]), normalize_name(record[:name])], record]
142
+ end
143
+ }
144
+ end
145
+
146
+ def normalize_name(name)
147
+ name.to_s.strip.downcase
148
+ end
149
+
150
+ def formatted_subdivision(row)
151
+ {
152
+ state_abbr: row[0],
153
+ fips: row[1] + row[2] + row[4],
154
+ county_name: row[3],
155
+ gnis: row[5],
156
+ name: row[6],
157
+ class_code: row[7],
158
+ status: row[8]
159
+ }
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FIPS
4
+ VERSION = "0.4"
5
+ end
data/lib/fips.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ module FIPS
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.inflector.inflect("fips" => "FIPS")
8
+ loader.setup
9
+
10
+ class NotFoundError < StandardError
11
+ end
12
+
13
+ class << self
14
+ def lookup(**params)
15
+ fips = params.fetch(:fips, nil)
16
+ state = params.fetch(:state, nil)
17
+ county = params.fetch(:county, nil)
18
+ subdivision = params.fetch(:subdivision, nil)
19
+ return identify_by_fips(fips, state, county, subdivision) unless fips.nil?
20
+
21
+ return FIPS::State.lookup(state: state) if !state.nil? && county.nil? && subdivision.nil?
22
+ return FIPS::County.lookup(state: state, county: county) if !state.nil? && !county.nil? && subdivision.nil?
23
+
24
+ if !state.nil? && !county.nil? && !subdivision.nil?
25
+ return FIPS::Subdivision.lookup(state: state, county: county,
26
+ subdivision: subdivision)
27
+ end
28
+
29
+ raise ArgumentError, "Insufficient parameters to identify a state, county, or subdivision: #{params.inspect}"
30
+ end
31
+
32
+ private
33
+
34
+ def identify_by_fips(fips, state, county, subdivision)
35
+ raise ArgumentError, "FIPS input must be a string" unless fips.is_a?(String)
36
+
37
+ case fips.length
38
+ when 2
39
+ return FIPS::Subdivision.lookup(fips: fips, county: county, subdivision: subdivision) if !county.nil? && !subdivision.nil?
40
+ return FIPS::County.lookup(fips: fips, county: county) if !county.nil? && subdivision.nil?
41
+ unless county.nil? && subdivision.nil?
42
+ raise ArgumentError, "A 2-digit FIPS code requires both county and subdivision names for subdivision lookup"
43
+ end
44
+
45
+ FIPS::State.lookup(fips: fips)
46
+ when 3
47
+ return FIPS::Subdivision.lookup(fips: fips, state: state, subdivision: subdivision) if !state.nil? && !subdivision.nil?
48
+ unless !state.nil? && county.nil? && subdivision.nil?
49
+ raise ArgumentError, "A 3-digit FIPS code requires a state for county lookup, or state and subdivision for subdivision lookup"
50
+ end
51
+
52
+ FIPS::County.lookup(fips: fips, state: state)
53
+ when 5
54
+ return FIPS::Subdivision.lookup(fips: fips, subdivision: subdivision) unless subdivision.nil?
55
+ return FIPS::Subdivision.lookup(fips: fips, state: state) if !state.nil? && county.nil?
56
+ unless state.nil? && county.nil? && subdivision.nil?
57
+ raise ArgumentError, "A 5-digit FIPS code accepts either state for subdivision FIPS or subdivision name for county FIPS"
58
+ end
59
+
60
+ FIPS::County.lookup(fips: fips)
61
+ when 10
62
+ FIPS::Subdivision.lookup(fips: fips)
63
+ else
64
+ raise ArgumentError, "FIPS code (#{fips}) must identify a state, county, or subdivision"
65
+ end
66
+ end
67
+ end
68
+ end
data/sig/fips.rbs ADDED
@@ -0,0 +1,50 @@
1
+ module FIPS
2
+ VERSION: String
3
+
4
+ type state_record = {
5
+ fips: String,
6
+ abbr: String,
7
+ name: String,
8
+ ansi: String
9
+ }
10
+
11
+ type county_record = {
12
+ state_abbr: String,
13
+ fips: String,
14
+ gnis: String,
15
+ name: String,
16
+ class_code: String,
17
+ status: String
18
+ }
19
+
20
+ type subdivision_record = {
21
+ state_abbr: String,
22
+ fips: String,
23
+ county_name: String,
24
+ gnis: String,
25
+ name: String,
26
+ class_code: String,
27
+ status: String
28
+ }
29
+
30
+ type lookup_record = state_record | county_record | subdivision_record
31
+ class NotFoundError < StandardError
32
+ end
33
+
34
+ def self.lookup: (?fips: String?, ?state: String?, ?county: String?, ?subdivision: String?) -> lookup_record
35
+
36
+ class State
37
+ def self.lookup: (?fips: String?, ?state: String?) -> state_record
38
+ def self.all: () -> Array[state_record]
39
+ end
40
+
41
+ class County
42
+ def self.lookup: (?fips: String?, ?state: String?, ?county: String?) -> county_record
43
+ def self.all: (state: String state) -> Array[county_record]
44
+ end
45
+
46
+ class Subdivision
47
+ def self.lookup: (?fips: String?, ?state: String?, ?county: String?, ?subdivision: String?) -> subdivision_record
48
+ def self.all: (state: String state, ?county: String?) -> Array[subdivision_record]
49
+ end
50
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fips_lookup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: '0.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Barroso
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.8'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.8'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: irb
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -51,6 +65,20 @@ dependencies:
51
65
  - - ">="
52
66
  - !ruby/object:Gem::Version
53
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: benchmark
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.2'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.2'
54
82
  - !ruby/object:Gem::Dependency
55
83
  name: rake
56
84
  requirement: !ruby/object:Gem::Requirement
@@ -222,16 +250,17 @@ files:
222
250
  - lib/data/subdivision/WI.csv
223
251
  - lib/data/subdivision/WV.csv
224
252
  - lib/data/subdivision/WY.csv
225
- - lib/fips_lookup.rb
226
- - lib/fips_lookup/county.rb
227
- - lib/fips_lookup/state.rb
228
- - lib/fips_lookup/subdivision.rb
229
- - lib/fips_lookup/version.rb
230
- - sig/fips_lookup.rbs
253
+ - lib/fips.rb
254
+ - lib/fips/county.rb
255
+ - lib/fips/state.rb
256
+ - lib/fips/subdivision.rb
257
+ - lib/fips/version.rb
258
+ - sig/fips.rbs
231
259
  homepage: https://github.com/3barroso/fips_lookup
232
260
  licenses:
233
261
  - MIT
234
- metadata: {}
262
+ metadata:
263
+ rubygems_mfa_required: 'true'
235
264
  rdoc_options: []
236
265
  require_paths:
237
266
  - lib
@@ -239,7 +268,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
239
268
  requirements:
240
269
  - - ">="
241
270
  - !ruby/object:Gem::Version
242
- version: '3.0'
271
+ version: '3.2'
243
272
  required_rubygems_version: !ruby/object:Gem::Requirement
244
273
  requirements:
245
274
  - - ">="
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # County related section of FipsLookup module
4
- module FipsLookup
5
- class << self
6
- def county(state_param:, county_param:, return_nil: false)
7
- state_code = find_state_code(state_param: state_param, return_nil: return_nil)
8
- return {} if state_code.nil?
9
-
10
- lookup = [state_code, county_param.upcase]
11
- @county_fips ||= {}
12
- @county_fips[lookup] ||= county_lookup(state_code, county_param, return_nil)
13
- end
14
-
15
- def fips_county(fips:, return_nil: false)
16
- unless fips.is_a?(String) && fips.length == 5
17
- return_nil ? (return nil) : (raise StandardError, "FIPS input must be 5 digit string")
18
- end
19
-
20
- state_code = find_state_code(state_param: fips[0..1], return_nil: return_nil)
21
- return nil if state_code.nil?
22
-
23
- CSV.foreach(county_file(state_code: state_code)) do |county_row|
24
- # state_code (AL), state fips (01), county fips (001), name (Augtauga County), county gnis(00161526), class code (H1), status (A)
25
- return [county_row[3], state_code] if county_row[2] == fips[2..4]
26
- end
27
-
28
- raise StandardError, "Could not find county with fips: #{fips[2..4]}, in: #{state_code}" unless return_nil
29
- end
30
-
31
- def county_file(state_code:)
32
- file_path = "#{File.expand_path("..", __dir__)}/data/county/#{state_code}.csv"
33
- file_path if File.exist?(file_path)
34
- end
35
-
36
- private
37
-
38
- def county_lookup(state_code, county_param, return_nil)
39
- upcase_param = county_param.upcase
40
- CSV.foreach(county_file(state_code: state_code)) do |row|
41
- return formatted_county(row) if match_county?(row, upcase_param)
42
-
43
- # keep? Memoize as file is being read but match isn't found ~ loses lookup flexiblity in county param + increases performacne?
44
- @county_fips[[row[0], row[3].upcase]] = formatted_county(row) unless @county_fips.key?([row[0], row[3].upcase])
45
- end
46
- return_nil ? (return {}) : (raise StandardError, "No county found matching: #{county_param}" unless return_nil)
47
- end
48
-
49
- def match_county?(row, param)
50
- # row => state (AL), state fips (01), county fips (001), name (Augtauga County), county gnis (00161526), class code (H1), status (A)
51
- row[3].upcase == param || row[4] == param || row[2] == param || "#{row[1]}#{row[2]}" == param
52
- end
53
-
54
- def formatted_county(row)
55
- {
56
- state_code: row[0],
57
- fips: (row[1] + row[2]),
58
- gnis: row[4],
59
- name: row[3],
60
- class_code: row[5],
61
- status: row[6]
62
- }
63
- end
64
- end
65
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # State related section of FipsLookup module
4
- module FipsLookup
5
- class << self
6
- def state(state_param:, return_nil: false)
7
- @state_fips ||= {}
8
- @state_fips[state_param] ||= state_lookup(state_param, return_nil)
9
- end
10
-
11
- def find_state_code(state_param:, return_nil: false)
12
- return state_param.upcase if STATE_CODES.key?(state_param.upcase)
13
- return STATE_CODES.key(state_param) if STATE_CODES.value?(state_param)
14
-
15
- state(state_param: state_param, return_nil: return_nil)[:code]
16
- end
17
-
18
- def state_file
19
- "#{File.expand_path("..", __dir__)}/data/state.csv"
20
- end
21
-
22
- private
23
-
24
- def state_lookup(state_param, return_nil = false)
25
- upstate_param = state_param.upcase
26
- CSV.foreach(state_file) do |state_row|
27
- return formatted_state(state_row) if state_match_row?(state_row, upstate_param)
28
- end
29
- return_nil ? (return {}) : (raise StandardError, "No state found matching: #{state_param}")
30
- end
31
-
32
- def state_match_row?(row, param)
33
- # row => state fips (01), state code (AL), state name (Alabama), ansi (01779775)
34
- row[0] == param || row[1] == param || row[2].upcase == param || row[3] == param
35
- end
36
-
37
- def formatted_state(row)
38
- {
39
- fips: row[0],
40
- code: row[1],
41
- name: row[2],
42
- ansi: row[3]
43
- }
44
- end
45
- end
46
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Subdivision related section of FIPS module
4
- module FipsLookup
5
- class << self
6
- def subdivision(state_param:, subdivision_param:, return_nil: false)
7
- state_code = find_state_code(state_param: state_param, return_nil: return_nil)
8
- return {} if state_code.nil?
9
-
10
- lookup = [state_code, subdivision_param.upcase]
11
- @subdivision_fips ||= {}
12
- @subdivision_fips[lookup] ||= subdivision_lookup(state_code, subdivision_param, return_nil)
13
- end
14
-
15
- def subdivision_file(state_code:)
16
- file_path = "#{File.expand_path("..", __dir__)}/data/subdivision/#{state_code}.csv"
17
- file_path if File.exist?(file_path)
18
- end
19
-
20
- private
21
-
22
- def subdivision_lookup(state_code, subdivision_param, return_nil)
23
- upcase_param = subdivision_param.upcase
24
- CSV.foreach(subdivision_file(state_code: state_code)) do |subdivision_row|
25
- return formatted_subdivision(subdivision_row) if match_subdivision?(subdivision_row, upcase_param)
26
- end
27
- return_nil ? (return {}) : (raise StandardError, "No subdivision found matching: #{subdivision_param}" unless return_nil)
28
- end
29
-
30
- def match_subdivision?(row, param)
31
- # row => state_code (AL), state fips (01), county fips (001), county name (Augtauga County)
32
- # row => subdivision name(Autaugaville CCD), class code (Z5), status (S)
33
- row[6].upcase == param || row[4] == param || row[5] == param
34
- end
35
-
36
- def formatted_subdivision(row)
37
- {
38
- state_code: row[0],
39
- fips: row[1] + row[2] + row[4],
40
- county_name: row[3],
41
- gnis: row[5],
42
- name: row[6],
43
- class_code: row[7],
44
- status: row[8]
45
- }
46
- end
47
- end
48
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FipsLookup
4
- VERSION = "0.3.0"
5
- end
data/lib/fips_lookup.rb DELETED
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "csv"
4
- require_relative "fips_lookup/version"
5
- require_relative "fips_lookup/subdivision"
6
- require_relative "fips_lookup/county"
7
- require_relative "fips_lookup/state"
8
-
9
- # parent section of module defining constants, requiring relative worker methods
10
- module FipsLookup
11
- STATE_CODES = { "AL" => "01", "AK" => "02", "AZ" => "04", "AR" => "05", "CA" => "06", "CO" => "08",
12
- "CT" => "09", "DE" => "10", "DC" => "11", "FL" => "12", "GA" => "13", "HI" => "15",
13
- "ID" => "16", "IL" => "17", "IN" => "18", "IA" => "19", "KS" => "20", "KY" => "21",
14
- "LA" => "22", "ME" => "23", "MD" => "24", "MA" => "25", "MI" => "26", "MN" => "27",
15
- "MS" => "28", "MO" => "29", "MT" => "30", "NE" => "31", "NV" => "32", "NH" => "33",
16
- "NJ" => "34", "NM" => "35", "NY" => "36", "NC" => "37", "ND" => "38", "OH" => "39",
17
- "OK" => "40", "OR" => "41", "PA" => "42", "RI" => "44", "SC" => "45", "SD" => "46",
18
- "TN" => "47", "TX" => "48", "UT" => "49", "VT" => "50", "VA" => "51", "WA" => "53",
19
- "WV" => "54", "WI" => "55", "WY" => "56", "AS" => "60", "GU" => "66", "MP" => "69",
20
- "PR" => "72", "UM" => "74", "VI" => "78" }.freeze
21
-
22
- class << self
23
- attr_accessor :county_fips, :state_fips, :subdivision_fips
24
- end
25
- end
data/sig/fips_lookup.rbs DELETED
@@ -1,4 +0,0 @@
1
- module FipsLookup
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end