fips_lookup 0.2.1 → 1.0.0

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile +1 -7
  4. data/Gemfile.lock +66 -29
  5. data/README.md +82 -138
  6. data/db/schema.sql +45 -0
  7. data/lib/data/fips.sqlite3 +0 -0
  8. data/lib/fips/county.rb +98 -0
  9. data/lib/fips/database.rb +55 -0
  10. data/lib/fips/state.rb +67 -0
  11. data/lib/fips/subdivision.rb +160 -0
  12. data/lib/fips/version.rb +5 -0
  13. data/lib/fips.rb +69 -0
  14. data/sig/fips.rbs +50 -0
  15. metadata +128 -71
  16. data/lib/data/county/AK.csv +0 -29
  17. data/lib/data/county/AL.csv +0 -67
  18. data/lib/data/county/AR.csv +0 -75
  19. data/lib/data/county/AS.csv +0 -5
  20. data/lib/data/county/AZ.csv +0 -15
  21. data/lib/data/county/CA.csv +0 -58
  22. data/lib/data/county/CO.csv +0 -64
  23. data/lib/data/county/CT.csv +0 -8
  24. data/lib/data/county/DC.csv +0 -1
  25. data/lib/data/county/DE.csv +0 -3
  26. data/lib/data/county/FL.csv +0 -67
  27. data/lib/data/county/GA.csv +0 -159
  28. data/lib/data/county/GU.csv +0 -1
  29. data/lib/data/county/HI.csv +0 -5
  30. data/lib/data/county/IA.csv +0 -99
  31. data/lib/data/county/ID.csv +0 -44
  32. data/lib/data/county/IL.csv +0 -102
  33. data/lib/data/county/IN.csv +0 -92
  34. data/lib/data/county/KS.csv +0 -105
  35. data/lib/data/county/KY.csv +0 -120
  36. data/lib/data/county/LA.csv +0 -64
  37. data/lib/data/county/MA.csv +0 -14
  38. data/lib/data/county/MD.csv +0 -24
  39. data/lib/data/county/ME.csv +0 -16
  40. data/lib/data/county/MI.csv +0 -83
  41. data/lib/data/county/MN.csv +0 -87
  42. data/lib/data/county/MO.csv +0 -115
  43. data/lib/data/county/MP.csv +0 -4
  44. data/lib/data/county/MS.csv +0 -82
  45. data/lib/data/county/MT.csv +0 -56
  46. data/lib/data/county/NC.csv +0 -100
  47. data/lib/data/county/ND.csv +0 -53
  48. data/lib/data/county/NE.csv +0 -93
  49. data/lib/data/county/NH.csv +0 -10
  50. data/lib/data/county/NJ.csv +0 -21
  51. data/lib/data/county/NM.csv +0 -33
  52. data/lib/data/county/NV.csv +0 -17
  53. data/lib/data/county/NY.csv +0 -62
  54. data/lib/data/county/OH.csv +0 -88
  55. data/lib/data/county/OK.csv +0 -77
  56. data/lib/data/county/OR.csv +0 -36
  57. data/lib/data/county/PA.csv +0 -67
  58. data/lib/data/county/PR.csv +0 -78
  59. data/lib/data/county/RI.csv +0 -5
  60. data/lib/data/county/SC.csv +0 -46
  61. data/lib/data/county/SD.csv +0 -66
  62. data/lib/data/county/TN.csv +0 -95
  63. data/lib/data/county/TX.csv +0 -254
  64. data/lib/data/county/UM.csv +0 -1
  65. data/lib/data/county/UT.csv +0 -29
  66. data/lib/data/county/VA.csv +0 -134
  67. data/lib/data/county/VI.csv +0 -3
  68. data/lib/data/county/VT.csv +0 -14
  69. data/lib/data/county/WA.csv +0 -39
  70. data/lib/data/county/WI.csv +0 -72
  71. data/lib/data/county/WV.csv +0 -55
  72. data/lib/data/county/WY.csv +0 -23
  73. data/lib/data/state.csv +0 -57
  74. data/lib/fips_lookup/version.rb +0 -5
  75. data/lib/fips_lookup.rb +0 -91
  76. data/sig/fips_lookup.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75de417e9f84331727310372fc917b41ea42f3e7c1da0660d1e51d091486a97a
4
- data.tar.gz: fc5747cc836e0aaf4ca7eeb6e5e2cdc303c8ae3ad281836e5bc2998c65ccee90
3
+ metadata.gz: 919f1f05158a82904ea9d2dbf8713161dd1a606887e7d9cd67a008527445dc6c
4
+ data.tar.gz: 6729e4bfcacb070fdd1ca18d96e2fe8b28027fd1b8f9bd107ece3baf31146a63
5
5
  SHA512:
6
- metadata.gz: bf40336478b106f60272d1eb7a8713653c1709b406bc522d2e305b657a95661ba20d60700ca4524ef562620c66c2ccde6f2f54d07e4a1b134eedac9a6139eb05
7
- data.tar.gz: db4f3b4b805ced1aee4acf41281f8c61b116de1d877654dec5375e67d2174f8ee13ec8df87cc6bee8ec373525438411d145928ea1da102cb534bb8e2bcbdb6f4
6
+ metadata.gz: 87a6cc7f761894dde59bc809e31d4b5f1ad32a79af67aee9adc1253bebab32e999753df872e44df91663d4e81d243247cb000b05d4f4b478abca6ac18020b5b2
7
+ data.tar.gz: a7f74a403e70c4807e7d1472acb40e431f85d6e7aae2cf7faae743bf5dd797cbcc2c5d47624c29d0ffeb5fbba1ec65fac7d08c8f05ad3937dc52b9bdd0483184
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ TargetRubyVersion: 3.2
3
3
  NewCops: enable
4
4
 
5
5
  Style/StringLiterals:
data/Gemfile CHANGED
@@ -2,11 +2,5 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- # Specify your gem's dependencies in fips_lookup.gemspec
5
+ # Specify your gem's dependencies in fips.gemspec
6
6
  gemspec
7
-
8
- group :development do
9
- gem "rake", "~> 13.0"
10
- gem "rspec", "~> 3.2"
11
- gem "rubocop", "~>1.21"
12
- end
data/Gemfile.lock CHANGED
@@ -1,61 +1,98 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fips_lookup (0.2.1)
5
- csv (~> 3.0)
4
+ fips_lookup (1.0.0)
5
+ sqlite3 (~> 2.0)
6
+ zeitwerk (~> 2.8)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
- ast (2.4.2)
11
- csv (3.3.0)
12
- diff-lcs (1.5.1)
13
- json (2.7.1)
14
- language_server-protocol (3.17.0.3)
15
- parallel (1.24.0)
16
- parser (3.3.0.5)
11
+ ast (2.4.3)
12
+ benchmark (0.5.0)
13
+ csv (3.3.6)
14
+ diff-lcs (1.6.2)
15
+ erb (6.0.7)
16
+ io-console (0.9.4)
17
+ irb (1.18.0)
18
+ pp (>= 0.6.0)
19
+ prism (>= 1.3.0)
20
+ rdoc (>= 4.0.0)
21
+ reline (>= 0.4.2)
22
+ json (3.0.2)
23
+ language_server-protocol (3.17.0.6)
24
+ lint_roller (1.1.0)
25
+ logger (1.7.0)
26
+ ostruct (0.6.3)
27
+ parallel (2.3.0)
28
+ parser (3.3.12.0)
17
29
  ast (~> 2.4.1)
18
30
  racc
19
- racc (1.7.3)
31
+ pp (0.6.4)
32
+ prettyprint
33
+ prettyprint (0.2.0)
34
+ prism (1.9.0)
35
+ racc (1.8.1)
20
36
  rainbow (3.1.1)
21
- rake (13.1.0)
22
- regexp_parser (2.9.0)
23
- rexml (3.2.6)
24
- rspec (3.13.0)
37
+ rake (13.4.2)
38
+ rbs (4.2.0)
39
+ logger
40
+ prism (>= 1.6.0)
41
+ tsort
42
+ rdoc (8.1.0)
43
+ erb
44
+ prism (>= 1.6.0)
45
+ rbs (>= 4.0.0)
46
+ tsort
47
+ regexp_parser (2.13.1)
48
+ reline (0.7.0)
49
+ io-console (~> 0.5)
50
+ rspec (3.13.2)
25
51
  rspec-core (~> 3.13.0)
26
52
  rspec-expectations (~> 3.13.0)
27
53
  rspec-mocks (~> 3.13.0)
28
- rspec-core (3.13.0)
54
+ rspec-core (3.13.6)
29
55
  rspec-support (~> 3.13.0)
30
- rspec-expectations (3.13.0)
56
+ rspec-expectations (3.13.5)
31
57
  diff-lcs (>= 1.2.0, < 2.0)
32
58
  rspec-support (~> 3.13.0)
33
- rspec-mocks (3.13.0)
59
+ rspec-mocks (3.13.8)
34
60
  diff-lcs (>= 1.2.0, < 2.0)
35
61
  rspec-support (~> 3.13.0)
36
- rspec-support (3.13.1)
37
- rubocop (1.62.1)
38
- json (~> 2.3)
39
- language_server-protocol (>= 3.17.0)
40
- parallel (~> 1.10)
62
+ rspec-support (3.13.7)
63
+ rubocop (1.91.0)
64
+ json (>= 2.3)
65
+ language_server-protocol (~> 3.17.0.2)
66
+ lint_roller (~> 1.1.0)
67
+ parallel (>= 1.10)
41
68
  parser (>= 3.3.0.2)
42
69
  rainbow (>= 2.2.2, < 4.0)
43
- regexp_parser (>= 1.8, < 3.0)
44
- rexml (>= 3.2.5, < 4.0)
45
- rubocop-ast (>= 1.31.1, < 2.0)
70
+ regexp_parser (>= 2.9.3, < 3.0)
71
+ rubocop-ast (>= 1.49.0, < 2.0)
46
72
  ruby-progressbar (~> 1.7)
47
- unicode-display_width (>= 2.4.0, < 3.0)
48
- rubocop-ast (1.31.2)
49
- parser (>= 3.3.0.4)
73
+ unicode-display_width (>= 2.4.0, < 4.0)
74
+ rubocop-ast (1.50.0)
75
+ parser (>= 3.3.7.2)
76
+ prism (~> 1.7)
50
77
  ruby-progressbar (1.13.0)
51
- unicode-display_width (2.5.0)
78
+ sqlite3 (2.9.6-arm64-darwin)
79
+ tsort (0.2.0)
80
+ unicode-display_width (3.3.0)
81
+ unicode-emoji (~> 4.3)
82
+ unicode-emoji (4.3.0)
83
+ zeitwerk (2.8.3)
52
84
 
53
85
  PLATFORMS
54
86
  arm64-darwin-21
55
87
  arm64-darwin-22
88
+ arm64-darwin-25
56
89
 
57
90
  DEPENDENCIES
91
+ benchmark (~> 0.2)
92
+ csv (~> 3.0)
58
93
  fips_lookup!
94
+ irb
95
+ ostruct
59
96
  rake (~> 13.0)
60
97
  rspec (~> 3.2)
61
98
  rubocop (~> 1.21)
data/README.md CHANGED
@@ -1,198 +1,142 @@
1
- # FipsLookup
1
+ # FIPS Lookup
2
2
 
3
- ## Overview
4
-
5
- FipsLookup is a gem that functions as a lookup used to identify county and state FIPS codes.
6
-
7
- What are FIPS codes? The United States Federal Communications Commission (FCC) [says:](https://transition.fcc.gov/oet/info/maps/census/fips/fips.txt)
8
-
9
- > Federal Information Processing System (FIPS) Codes for States and Counties
10
- >
11
- > FIPS codes are numbers which uniquely identify geographic areas. The number of
12
- digits in FIPS codes vary depending on the level of geography. State-level FIPS
13
- codes have two digits, county-level FIPS codes have five digits of which the
14
- first two are the FIPS code of the state to which the county belongs.
15
-
16
- _Note:_ FIPS codes are updated by the US census department they can be seen and accessed [here](https://www.census.gov/library/reference/code-lists/ansi.html).
17
-
18
- <br>
19
-
20
- **Interesting challenge:** <br>
21
- Multiple states can have the same county name — 16 states have a "Wayne County". This means a state & county pair is required to lookup and return the proper FIPS code.
22
- This gem utilizes memoization to increase lookup efficiency to csv files without adding complexity to your app.
3
+ `fips_lookup` provides lookups for U.S. states, counties, and county subdivisions using Census FIPS identifiers and names. Results are hashes containing the fields for the requested geography. Lookup data is stored in a bundled SQLite database and queried read-only at runtime.
23
4
 
24
5
  ## Installation
25
6
 
26
- Add this line to your application's Gemfile:
7
+ Add the gem to your application's Gemfile:
27
8
 
28
9
  ```ruby
29
- gem 'fips_lookup'
10
+ gem "fips_lookup"
30
11
  ```
31
12
 
32
- And then execute:
13
+ Then run `bundle install`.
33
14
 
34
- $ bundle install
15
+ ## Usage
35
16
 
36
- Or install it yourself as:
17
+ Require the gem if your application does not use Bundler's automatic loading:
37
18
 
38
- $ gem install fips_lookup
19
+ ```ruby
20
+ require "fips"
21
+ ```
39
22
 
40
- ## Usage
41
- <br>
23
+ ### General lookup
42
24
 
43
- ### County info from lookup: [.county(state_param: "state", county_name: "county name", _return_nil: false_)](/fips_lookup/lib/fips_lookup.rb?#L24)
25
+ `FIPS.lookup` dispatches to the state, county, or subdivision lookup based on the supplied identifiers:
44
26
 
45
- Find County specific details using memoized hash state and county input. [ Returns state code, county fips, county name, and county class codes ]
27
+ ```ruby
28
+ FIPS.lookup(fips: "02")
29
+ # => { fips: "02", abbr: "AK", name: "Alaska", ansi: "01785533" }
46
30
 
47
- Input the state name and county name and return the corresponding 5 digit FIPS code:
48
- ```
49
- FipsLookup.county(state_param: "AL", county_name: "Autauga County") # => {:state_code=>"AL", :fips=>"01001", :name=>"Autauga County", :class_code=>"H1"}
50
- ```
31
+ FIPS.lookup(fips: "02060")
32
+ # => county record for Bristol Bay Borough
51
33
 
52
- * `state_param` - (String) flexible - able to find the state using its' 2 letter abbreviation ("AL"), 2 digit FIPS number ("01"), state name ("Alabama"), or the state ANSI code ("01779775").
53
- * `county_name` - (String) must match spelling set by US Census Bureau, [resource library](https://www.census.gov/library/reference/code-lists/ansi.html)
54
- — "Autauga County" can be found, "Autauga" can not be found.
55
- * `return_nil` - (Boolean) is an optional parameter that when used overrides any Errors from input and returns an empty hash `{}`.
56
- * Ex: `FipsLookup.county(state_param: "AL", county_name: "Autauga", return_nil: true) # => {}`
34
+ FIPS.lookup(fips: "0206009050")
35
+ # => subdivision record for Bristol Bay census subarea
57
36
 
58
- * Ex: Access the [:fips] symbol after a lookup `FipsLookup.county(state_param: "AL", county_name: "Autauga", return_nil: true)[:fips] # => nil`
37
+ FIPS.lookup(state: "Alaska", county: "Bristol Bay Borough")
38
+ # => county record for Bristol Bay Borough
59
39
 
60
- <br>
40
+ FIPS.lookup(state: "Alaska", county: "Bristol Bay Borough",
41
+ subdivision: "Bristol Bay census subarea")
42
+ # => subdivision record for Bristol Bay census subarea
43
+ ```
61
44
 
62
- **How does it work?:**
45
+ FIPS codes must be strings so leading zeroes are preserved. The general dispatcher supports state FIPS (2 digits), county FIPS (5 digits), and full subdivision FIPS (10 digits). Contextual forms are also available through the specific lookup methods below.
63
46
 
64
- Class attribute hash: [`@county_fips = { ["state_code", "county name"] => {:state_code, :fips, :name, :class_code} }`](/fips_lookup/lib/fips_lookup.rb?#L21)
47
+ ### State lookup
65
48
 
66
- The `county_fips` hash is built of key value pairs that grows as the `.county` method is used. Calls to `county` first searches `@county_fips` attribute with `[state_code, county_name]` before opening `.csv` files. Therefore any duplicate calls made to `county` will return the value stored in `@county_fips`. Instance variable lasts the lifespan of the FipsLookup class.
67
- ```
68
- FipsLookup.county_fips # => { ["AL", "Autauga County"] => {:state_code=>"AL", :fips=>"01001", :name=>"Autauga County", :class_code=>"H1"} }
49
+ ```ruby
50
+ FIPS::State.lookup(fips: "02")
51
+ FIPS::State.lookup(state: "AK")
52
+ FIPS::State.lookup(state: "Alaska")
53
+ FIPS::State.lookup(state: "01785533") # ANSI code
69
54
  ```
70
- <br>
71
- <hr>
72
55
 
73
- ### State / County lookup using FIPS code [.fips_county(fips: "fips", return_nil: _return_nil=false_)](/fips_lookup/lib/fips_lookup.rb?#L38)
56
+ The returned state hash has `:fips`, `:abbr`, `:name`, and `:ansi` keys. `FIPS::State.all` returns all state records in the same format:
74
57
 
75
- Input the 5 digit FIPS code for a county and return the county name and state name in an Array:
76
- ```
77
- FipsLookup.fips_county(fips: "01001") # => ["Autauga County", "AL"]
58
+ ```ruby
59
+ FIPS::State.all.map { |state| [state[:name], state[:abbr]] }
78
60
  ```
79
61
 
80
- * `fips` - (String) must be a 5 character string of numbers ex: "01001".
81
- * `return_nil` - (Boolean) is an optional parameter that when used overrides any Errors from input and returns `nil`.
82
- * Ex: `FipsLookup.fips_county(fips: "03000", return_nil: true) # => nil`
62
+ ### County lookup
83
63
 
84
- <br>
85
- <hr>
64
+ ```ruby
65
+ FIPS::County.lookup(fips: "02060")
66
+ FIPS::County.lookup(fips: "060", state: "AK")
67
+ FIPS::County.lookup(fips: "02", county: "Bristol Bay Borough")
68
+ FIPS::County.lookup(state: "Alaska", county: "Bristol Bay Borough")
69
+ ```
86
70
 
87
- ### State info from lookup [.state(state_param: "state", _return_nil: false_)](/fips_lookup/lib/fips_lookup.rb?#L33)
71
+ The returned county hash has `:state_abbr`, `:fips`, `:gnis`, `:name`, `:class_code`, and `:status` keys. State identifiers may be an abbreviation, name, FIPS code, or ANSI code.
88
72
 
89
- Using state information input return a dictionary of values for keys fips, state code, state name, state ansi code.
73
+ To list counties in a state, use `FIPS::County.all`:
90
74
 
91
- ```
92
- FipsLookup.state(state_param: "01") # => {:fips=>"01", :code=>"AL", :name=>"Alabama", :ansi=>"01779775"}
75
+ ```ruby
76
+ counties = FIPS::County.all(state: "AK")
77
+ county_names = counties.map { |county| county[:name] }
93
78
  ```
94
79
 
95
- * `state_param` - (String) flexible - able to find the state using its' 2 letter abbreviation ("AL"), 2 digit FIPS number ("01"), state name ("Alabama"), or the state ANSI code ("01779775").
96
- * `return_nil` - (Boolean) is an optional parameter that when used overrides any Errors from input and returns an empty hash `{}`.
80
+ ### Subdivision lookup
97
81
 
98
- * `.state` functions similarly to the `.county` method in how it uses a memoized hash `@state_fips` to store state parameter lookups and return values before searching `state.csv` for more.
99
-
100
- **State code lookup hash** [`STATE_CODES["state code"] # => "fips code"`](/fips_lookup/lib/fips_lookup.rb?#L8)
101
- Can also be used for quick lookup translation between state 2-character abbreviations and state 2-digit FIPS code.
102
- ```
103
- FipsLookup::STATE_CODES["AL"] #=> "01"
104
- FipsLookup::STATE_CODES.key("01") # => "AL"
82
+ ```ruby
83
+ FIPS::Subdivision.lookup(fips: "0206009050")
84
+ FIPS::Subdivision.lookup(fips: "09050", state: "AK")
85
+ FIPS::Subdivision.lookup(fips: "02060", subdivision: "Bristol Bay census subarea")
86
+ FIPS::Subdivision.lookup(fips: "060", state: "AK",
87
+ subdivision: "Bristol Bay census subarea")
88
+ FIPS::Subdivision.lookup(state: "Alaska", county: "Bristol Bay Borough",
89
+ subdivision: "Bristol Bay census subarea")
105
90
  ```
106
- <br>
107
- <hr>
108
91
 
109
- ### Finding state abbreviation with flexible input [.find_state_code(state_param: "state", _return_nil: false_)](/fips_lookup/lib/fips_lookup.rb?#L52)
92
+ The returned subdivision hash has `:state_abbr`, `:fips`, `:county_name`, `:gnis`, `:name`, `:class_code`, and `:status` keys. To retrieve subdivision records for a state, optionally filtered by county:
110
93
 
111
- * `state_param` - (String) flexible - able to find the state using its' 2 letter abbreviation ("AL"), 2 digit FIPS number ("01"), state name ("Alabama"), or the state ANSI code ("01779775").
112
- * `return_nil` - (Boolean) is an optional parameter that when used overrides any Errors from input and returns nil.
113
-
114
- ```
115
- FipsLookup.find_state_code(state_param: "MicHiGan") # => "MI"
94
+ ```ruby
95
+ FIPS::Subdivision.all(state: "AK")
96
+ FIPS::Subdivision.all(state: "AK", county: "Bristol Bay Borough")
116
97
  ```
117
98
 
118
- <br>
119
- <hr>
99
+ County and subdivision name matching is case-insensitive. State identifiers accept abbreviations, names, FIPS codes, and ANSI codes.
120
100
 
121
- ## Accessing county and state `.csv` files in your Rails app
101
+ ### Source data
122
102
 
123
- Data `csv` files are made accessible incase extra configuration is needed. Here is an example in a Rails application that displays the list of state and county names within a form. This is done by accessing the CSV files included in this gem, examples below are from Rails 7 app called [Build With](https://github.com/3barroso/build_with)
103
+ The raw and intermediate CSV datasets are retained under `source_data/` for rebuilding and auditing. They are development inputs, are not used at runtime, and are not included in the published gem. Runtime lookups and collection methods use the bundled SQLite database.
124
104
 
125
- ### Path to state.csv file [.state_file](/fips_lookup/lib/fips_lookup.rb?#L64)
105
+ ### Database build
126
106
 
127
- Display state codes in a select option dropdown by using CSV on the FipsLookup method accessing the state.csv file ( `FipsLookup.state_file #=> "path/to/data/state.csv"` )
128
-
129
- ```
130
- # in controller.rb
131
- @state_options = []
132
- CSV.foreach(FipsLookup.state_file) do |state_row|
133
- @state_options << [state_row[2], state_row[1]]
134
- end
135
- ```
107
+ The checked-in schema is in `db/schema.sql`. The bundled database is built from the 2020 Census county and county-subdivision source files. To rebuild it from the source text files and state data, run:
136
108
 
109
+ ```sh
110
+ bundle exec ruby bin/db/build
137
111
  ```
138
- # in html.erb (within `form_with do |form|` block)
139
- <%= form.label :state, style: "display: block" %>
140
- <%= form.select :state, @state_options %>
141
- ```
142
-
143
- ### Path to state specific county csv files [.county_file(state_code: "state param")](/fips_lookup/lib/fips_lookup.rb?#L59)
144
112
 
145
- Display County name options in a select option dropdown by using CSV on the FipsLookup method accessing the county specific .csv file ( `FipsLookup.county_file(state_code: "MI") #=> "path/to/data/county/MI.csv"` )
113
+ ### Errors
146
114
 
147
- * `state_param` – strict parameter, must be string abbreviation of State code (suggested usage is to call `.find_state_code` above first)
115
+ Malformed or insufficient inputs raise `ArgumentError`. Validly formatted identifiers that do not match a record raise `FIPS::NotFoundError`, a subclass of `StandardError`:
148
116
 
149
- ```
150
- # in controller.rb
151
- state_code = address_params[:state] # or use find_state_code from user input
152
- @county_options = []
153
- CSV.foreach(FipsLookup.county_file(state_code:)) do |county_row|
154
- @county_options << county_row[3]
117
+ ```ruby
118
+ begin
119
+ FIPS::County.lookup(fips: "02999")
120
+ rescue FIPS::NotFoundError => error
121
+ warn error.message
155
122
  end
156
123
  ```
157
124
 
158
- ```
159
- # in html.erb (within `form_with do |form|` block)
160
- <%= form.label :county, style: "display: block" %>
161
- <%= form.select :county, @county_options %>
162
- ```
163
-
164
125
  ## Development
165
126
 
166
- Download the repository locally, and from the directory run `bin/setup` to install gem dependencies.
167
-
168
- Check installation and any changes by running `rspec` to run the tests.
169
-
170
- Use `bin/console` to open IRB console with FipsLookup gem included and ready to use.
171
-
172
- To install this gem onto your local machine, run `bundle exec rake install`.
173
-
174
- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
127
+ Install dependencies with `bin/setup`. Run tests and lint with:
175
128
 
176
- ### New to Ruby?
177
- On Mac, follow steps 1 and 2 of [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-macos) to install ruby with rbenv using brew.
178
-
179
- For PC, consult official ruby language [installation guides](https://www.ruby-lang.org/en/documentation/installation/).
180
-
181
- #### New to this gem?
182
-
183
- * The main working file is `lib/fips_lookup.rb` with usage examples in the test file: `spec/fips_lookup_spec.rb`
184
- * [The first pull request](https://github.com/3barroso/fips_lookup/pull/1) contains more details to decisions and considerations when first launching gem.
129
+ ```sh
130
+ bundle exec rspec
131
+ bundle exec rubocop
132
+ ```
185
133
 
134
+ Open an IRB console with `bin/console`. Install locally with `bundle exec rake install`.
186
135
 
187
136
  ## Contributing
188
137
 
189
- Bug reports and pull requests are welcome on GitHub at the [FipsLookup repo](https://github.com/3barroso/fips_lookup).
190
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/3barroso/fips_lookup/blob/main/CODE_OF_CONDUCT.md).
138
+ Bug reports and pull requests are welcome in the [FIPS repository](https://github.com/3barroso/fips_lookup). Contributors are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
191
139
 
192
140
  ## License
193
141
 
194
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
195
-
196
- ## Code of Conduct
197
-
198
- Everyone interacting in the FipsLookup project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/3barroso/fips_lookup/blob/main/CODE_OF_CONDUCT.md).
142
+ This gem is available under the terms of the MIT License. See [LICENSE.txt](LICENSE.txt).
data/db/schema.sql ADDED
@@ -0,0 +1,45 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE TABLE states (
4
+ state_fips TEXT PRIMARY KEY,
5
+ state_abbr TEXT NOT NULL UNIQUE,
6
+ name TEXT NOT NULL,
7
+ name_key TEXT NOT NULL,
8
+ ansi TEXT NOT NULL UNIQUE
9
+ );
10
+
11
+ CREATE INDEX states_name_key_idx ON states(name_key);
12
+
13
+ CREATE TABLE counties (
14
+ state_fips TEXT NOT NULL,
15
+ county_fips TEXT NOT NULL,
16
+ full_fips TEXT NOT NULL UNIQUE,
17
+ name TEXT NOT NULL,
18
+ name_key TEXT NOT NULL,
19
+ gnis TEXT NOT NULL,
20
+ class_code TEXT NOT NULL,
21
+ status TEXT NOT NULL,
22
+ PRIMARY KEY (state_fips, county_fips),
23
+ FOREIGN KEY (state_fips) REFERENCES states(state_fips)
24
+ );
25
+
26
+ CREATE INDEX counties_state_name_idx ON counties(state_fips, name_key);
27
+
28
+ CREATE TABLE subdivisions (
29
+ full_fips TEXT PRIMARY KEY,
30
+ state_fips TEXT NOT NULL,
31
+ county_fips TEXT NOT NULL,
32
+ subdivision_fips TEXT NOT NULL,
33
+ name TEXT NOT NULL,
34
+ name_key TEXT NOT NULL,
35
+ gnis TEXT NOT NULL,
36
+ class_code TEXT NOT NULL,
37
+ status TEXT NOT NULL,
38
+ UNIQUE (state_fips, county_fips, subdivision_fips),
39
+ FOREIGN KEY (state_fips, county_fips) REFERENCES counties(state_fips, county_fips)
40
+ );
41
+
42
+ CREATE INDEX subdivisions_state_subdivision_fips_idx
43
+ ON subdivisions(state_fips, subdivision_fips);
44
+ CREATE INDEX subdivisions_county_name_idx
45
+ ON subdivisions(state_fips, county_fips, name_key);
Binary file
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # FIPS::County
4
+ module FIPS
5
+ class County
6
+ extend FIPS::Database::Access
7
+
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
+
14
+ unless fips.nil? || (fips.is_a?(String) && fips.match?(/\A(?:\d{2}|\d{3}|\d{5})\z/))
15
+ raise ArgumentError, "FIPS input must be a 2, 3, or 5 digit string"
16
+ end
17
+ raise ArgumentError, "State input must be a non-empty string" unless state.nil? || (state.is_a?(String) && !state.strip.empty?)
18
+ raise ArgumentError, "County input must be a non-empty string" unless county.nil? || (county.is_a?(String) && !county.strip.empty?)
19
+
20
+ location = identify_with_fips(fips, state, county)
21
+ return location unless location.nil?
22
+
23
+ if !county.nil? && !state.nil?
24
+ state_fips = FIPS::State.lookup(state: state)[:fips]
25
+ return by_name(state_fips, county)
26
+ end
27
+
28
+ raise ArgumentError, "Could not identify county with parameters provided: #{params.inspect}"
29
+ end
30
+
31
+ def all(state:)
32
+ raise ArgumentError, "State input must be a non-empty string" unless state.is_a?(String) && !state.strip.empty?
33
+
34
+ state_fips = FIPS::State.lookup(state: state)[:fips]
35
+ rows = db_all(
36
+ "SELECT states.state_abbr AS state_abbr, counties.full_fips AS fips, counties.gnis, counties.name, counties.class_code, counties.status " \
37
+ "FROM counties JOIN states USING (state_fips) WHERE counties.state_fips = ? ORDER BY counties.county_fips",
38
+ [state_fips]
39
+ )
40
+ rows.map { |county_row| formatted_county(county_row) }
41
+ end
42
+
43
+ private
44
+
45
+ def identify_with_fips(fips, state, county)
46
+ return nil if fips.nil? || !fips.is_a?(String)
47
+
48
+ case fips.length
49
+ when 2
50
+ return nil if county.nil?
51
+
52
+ return by_name(fips, county)
53
+ when 3
54
+ return nil if state.nil?
55
+
56
+ state_fips = FIPS::State.lookup(state: state)[:fips]
57
+ return by_fips(state_fips + fips)
58
+ when 5
59
+ return by_fips(fips)
60
+ end
61
+ nil
62
+ end
63
+
64
+ def by_fips(fips)
65
+ county_row = db_first(
66
+ "SELECT states.state_abbr AS state_abbr, counties.full_fips AS fips, counties.gnis, counties.name, counties.class_code, counties.status " \
67
+ "FROM counties JOIN states USING (state_fips) WHERE counties.full_fips = ?",
68
+ [fips]
69
+ )
70
+ return formatted_county(county_row) unless county_row.nil?
71
+
72
+ raise FIPS::NotFoundError, "Could not identify county with fips: #{fips}"
73
+ end
74
+
75
+ def by_name(state_fips, county)
76
+ county_row = db_first(
77
+ "SELECT states.state_abbr AS state_abbr, counties.full_fips AS fips, counties.gnis, counties.name, counties.class_code, counties.status " \
78
+ "FROM counties JOIN states USING (state_fips) WHERE counties.state_fips = ? AND counties.name_key = ?",
79
+ [state_fips, county.upcase]
80
+ )
81
+ return formatted_county(county_row) unless county_row.nil?
82
+
83
+ raise FIPS::NotFoundError, "Could not identify county with name: #{county}, in: #{state_fips}"
84
+ end
85
+
86
+ def formatted_county(row)
87
+ {
88
+ state_abbr: row["state_abbr"],
89
+ fips: row["fips"],
90
+ gnis: row["gnis"],
91
+ name: row["name"],
92
+ class_code: row["class_code"],
93
+ status: row["status"]
94
+ }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sqlite3"
4
+
5
+ module FIPS
6
+ module Database
7
+ DB_PATH = File.expand_path("../data/fips.sqlite3", __dir__).freeze
8
+
9
+ module Access
10
+ private
11
+
12
+ def db_all(sql, bind_params = [])
13
+ Client.all(sql, bind_params)
14
+ end
15
+
16
+ def db_first(sql, bind_params = [])
17
+ Client.first(sql, bind_params)
18
+ end
19
+ end
20
+
21
+ class Client
22
+ class << self
23
+ def all(sql, bind_params = [])
24
+ synchronize { connection.execute(sql, bind_params) }
25
+ end
26
+
27
+ def first(sql, bind_params = [])
28
+ synchronize { connection.get_first_row(sql, bind_params) }
29
+ end
30
+
31
+ private
32
+
33
+ def synchronize(&)
34
+ mutex.synchronize(&)
35
+ end
36
+
37
+ def mutex
38
+ @mutex ||= Mutex.new
39
+ end
40
+
41
+ def connection
42
+ @connection ||= begin
43
+ raise LoadError, "FIPS SQLite database is missing at #{DB_PATH}; run bin/db/build" unless File.file?(DB_PATH)
44
+
45
+ database = SQLite3::Database.new(DB_PATH, readonly: true)
46
+ database.results_as_hash = true
47
+ database
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private_constant :Client
54
+ end
55
+ end