ansi_codes 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.
@@ -0,0 +1,58 @@
1
+ STATE|STUSAB|STATE_NAME|STATENS
2
+ 01|AL|Alabama|01779775
3
+ 02|AK|Alaska|01785533
4
+ 04|AZ|Arizona|01779777
5
+ 05|AR|Arkansas|00068085
6
+ 06|CA|California|01779778
7
+ 08|CO|Colorado|01779779
8
+ 09|CT|Connecticut|01779780
9
+ 10|DE|Delaware|01779781
10
+ 11|DC|District of Columbia|01702382
11
+ 12|FL|Florida|00294478
12
+ 13|GA|Georgia|01705317
13
+ 15|HI|Hawaii|01779782
14
+ 16|ID|Idaho|01779783
15
+ 17|IL|Illinois|01779784
16
+ 18|IN|Indiana|00448508
17
+ 19|IA|Iowa|01779785
18
+ 20|KS|Kansas|00481813
19
+ 21|KY|Kentucky|01779786
20
+ 22|LA|Louisiana|01629543
21
+ 23|ME|Maine|01779787
22
+ 24|MD|Maryland|01714934
23
+ 25|MA|Massachusetts|00606926
24
+ 26|MI|Michigan|01779789
25
+ 27|MN|Minnesota|00662849
26
+ 28|MS|Mississippi|01779790
27
+ 29|MO|Missouri|01779791
28
+ 30|MT|Montana|00767982
29
+ 31|NE|Nebraska|01779792
30
+ 32|NV|Nevada|01779793
31
+ 33|NH|New Hampshire|01779794
32
+ 34|NJ|New Jersey|01779795
33
+ 35|NM|New Mexico|00897535
34
+ 36|NY|New York|01779796
35
+ 37|NC|North Carolina|01027616
36
+ 38|ND|North Dakota|01779797
37
+ 39|OH|Ohio|01085497
38
+ 40|OK|Oklahoma|01102857
39
+ 41|OR|Oregon|01155107
40
+ 42|PA|Pennsylvania|01779798
41
+ 44|RI|Rhode Island|01219835
42
+ 45|SC|South Carolina|01779799
43
+ 46|SD|South Dakota|01785534
44
+ 47|TN|Tennessee|01325873
45
+ 48|TX|Texas|01779801
46
+ 49|UT|Utah|01455989
47
+ 50|VT|Vermont|01779802
48
+ 51|VA|Virginia|01779803
49
+ 53|WA|Washington|01779804
50
+ 54|WV|West Virginia|01779805
51
+ 55|WI|Wisconsin|01779806
52
+ 56|WY|Wyoming|01779807
53
+ 60|AS|American Samoa|01802701
54
+ 66|GU|Guam|01802705
55
+ 69|MP|Northern Mariana Islands|01779809
56
+ 72|PR|Puerto Rico|01779808
57
+ 74|UM|U.S. Minor Outlying Islands|01878752
58
+ 78|VI|U.S. Virgin Islands|01802710
@@ -0,0 +1,5 @@
1
+ require 'ansi_codes/version'
2
+ require 'ansi_codes/state'
3
+ require 'ansi_codes/county'
4
+
5
+ module AnsiCodes; end
@@ -0,0 +1,86 @@
1
+ module AnsiCodes
2
+ # A representation of US counties and equivalent census areas.
3
+ # County instances are created at class load time and are immutable.
4
+ class County
5
+ # @return [State] the {State} that this county belongs to
6
+ attr_reader :state
7
+ # @return [String] the three digit ANSI code for this county
8
+ attr_reader :ansi_code
9
+ # @return [String] the county's full name in title case
10
+ attr_reader :name
11
+ # @return [String] the county name, minus the designation
12
+ # @note some cities have no designation, so this attribute will be the same as {#name}
13
+ attr_reader :short_name
14
+ # @return [String] the census designation (County, Parish, Municipio, etc.)
15
+ # @note some cities have no designation, in which case this will return the empty string
16
+ attr_reader :designation
17
+
18
+ private
19
+
20
+ # Suffixes for counties (parishes, etc.) to help split names into their parts.
21
+ Designations = /(.*) (city|Borough|County|City and Borough|Census Area|Municipality|Parish|Islands?|District|Municipio)$/
22
+
23
+ # Create a new County instance.
24
+ # @note This is only meant to be called internally during class loading.
25
+ # You cannot call #new directly.
26
+ # @param state_ansi [String] the two-digit state ANSI code
27
+ # @param county_ansi [String] the three-digit county ANSI code
28
+ # @param name [String] the county name
29
+ def initialize(state_ansi, county_ansi, name)
30
+ @state = State.find(state_ansi)
31
+ @ansi_code = county_ansi
32
+ @name = name
33
+ match = name.match(Designations)
34
+ @short_name = match && match[1] || name
35
+ @designation = match && match[2] || ''
36
+ freeze
37
+ self.class.instance_variable_get(:@counties)[@state].tap do |counties|
38
+ counties[:ansi_code][@ansi_code.downcase] =
39
+ counties[:name][@name.downcase] = self
40
+ end
41
+ end
42
+
43
+ public
44
+
45
+ # @param [State] state an optional {State} object to narrow the results
46
+ # @return [Array<County>] all counties or all of a {State}'s counties if one is provided
47
+ def self.all(state = nil)
48
+ state ? @counties[state][:ansi_code].values :
49
+ @counties.values.flat_map {|values| values[:ansi_code]}.flat_map(&:values)
50
+ end
51
+
52
+ # Look up a county by state and county ANSI code or name
53
+ # @param [State, Fixnum, String] state the state portion of the query.
54
+ # This method will accept a {State} object or anything that {State.find} will accept.
55
+ # @param [Fixnum, String] county the county ANSI code or name to look up
56
+ # @return [County] the {County} associated with the query parameters
57
+ # @raise [ArgumentError] if the county parameter is not a Fixnum or String,
58
+ # or if the state parameter is not a {State}, Fixnum, or String.
59
+ # @raise [RuntimeError] if no associated {State} or {County} is found
60
+ def self.find(state, county)
61
+ state = state.is_a?(State) ? state : State.find(state)
62
+ case county
63
+ when Fixnum
64
+ county, selector = '%03d' % county, :ansi_code
65
+ when String
66
+ selector = county =~ /^[0-9]{3}$/ ? :ansi_code : :name
67
+ else raise(ArgumentError, 'Argument must be an integer or a string.')
68
+ end
69
+ @counties[state][selector][county.downcase].tap do |result|
70
+ raise(RuntimeError, "No county found for lookup '#{county}' in state #{state.name}") unless result
71
+ yield result if block_given?
72
+ end
73
+ end
74
+
75
+ @counties = Hash[State.all.map {|state| [state, { ansi_code: {}, name: {} }] }]
76
+ data_file = File.expand_path('../../../data/national_county.txt', __FILE__)
77
+ options = { headers: true, header_converters: :symbol }
78
+ CSV.foreach(data_file, options) do |row|
79
+ new row[:state_ansi], row[:county_ansi], row[:county_name]
80
+ end
81
+ @counties.values.flat_map(&:values).map(&:freeze)
82
+ @counties.values.map &:freeze
83
+ @counties.freeze
84
+ freeze
85
+ end
86
+ end
@@ -0,0 +1,87 @@
1
+ require 'csv'
2
+
3
+ module AnsiCodes
4
+ # A representation of US states and equivalent census areas.
5
+ # State instances are created at class load time and are immutable.
6
+ class State
7
+ # @return [String] the two-digit ANSI code string
8
+ # @api public
9
+ attr_reader :ansi_code
10
+ # @return [String] the state name in title case
11
+ # @api public
12
+ attr_reader :name
13
+ # @return [String] the two-letter state abbreviation in caps
14
+ # @api public
15
+ attr_reader :abbreviation
16
+
17
+ private
18
+
19
+ # Create a new State instance.
20
+ # @note This is only meant to be called internally during class loading.
21
+ # You cannot call #new directly.
22
+ # @param ansi_code [String] the two-digit state ANSI code
23
+ # @param name [String] the state name
24
+ # @param abbreviation [String] the two-letter state abbreviation
25
+ def initialize(ansi_code, name, abbreviation)
26
+ @ansi_code = ansi_code
27
+ @name = name
28
+ @abbreviation = abbreviation
29
+ freeze
30
+ self.class.instance_variable_get(:@states).tap do |states|
31
+ states[:ansi_code][@ansi_code.downcase] =
32
+ states[:name][@name.downcase] =
33
+ states[:abbreviation][@abbreviation.downcase] = self
34
+ end
35
+ end
36
+
37
+ public
38
+
39
+
40
+ # @return [Array<County>] all of this state's counties
41
+ # @api public
42
+ # @!attribute [r] counties
43
+ def counties
44
+ County.all(self)
45
+ end
46
+
47
+ # Look up a state by ANSI code, abbreviation, or name
48
+ # @param [Fixnum, String] value the lookup query
49
+ # @return [State] the {State} associated with the query parameter
50
+ # @raise [ArgumentError] if the argument is not a Fixnum or String
51
+ # @raise [RuntimeError] if no associated {State} is found
52
+ def self.find(value)
53
+ case value
54
+ when Fixnum
55
+ value = '%02d' % value
56
+ selector = :ansi_code
57
+ when String
58
+ begin
59
+ Integer(value, 10)
60
+ selector = :ansi_code
61
+ rescue ArgumentError
62
+ selector = value.size == 2 ? :abbreviation : :name
63
+ end
64
+ else raise(ArgumentError, 'Argument must be an integer or a string.')
65
+ end
66
+ @states[selector][value.downcase].tap do |result|
67
+ raise(RuntimeError, "No state found for lookup '#{value}'") unless result
68
+ yield result if block_given?
69
+ end
70
+ end
71
+
72
+ # @return [Array<State>] an array of all states
73
+ def self.all
74
+ @states[:ansi_code].values
75
+ end
76
+
77
+ @states = { ansi_code: {}, name: {}, abbreviation: {} }
78
+ data_file = File.expand_path('../../../data/state.txt', __FILE__)
79
+ options = { col_sep: '|', headers: true, header_converters: :symbol }
80
+ CSV.foreach(data_file, options) do |row|
81
+ new row[:state], row[:state_name], row[:stusab]
82
+ end
83
+ @states.values.map &:freeze
84
+ @states.freeze
85
+ freeze
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module AnsiCodes
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ describe AnsiCodes::County do
4
+
5
+ it 'should not be instantiable' do
6
+ expect{AnsiCodes::County.new('01', '001', 'Autauga County')}.to raise_error(RuntimeError)
7
+ end
8
+
9
+ it 'should not be modifiable' do
10
+ expect{AnsiCodes::County.extend Module.new}.to raise_error
11
+ end
12
+
13
+ describe '#ansi_code' do
14
+ it 'should return a three-digit string' do
15
+ AnsiCodes::County.all.each do |county|
16
+ county.ansi_code.should match(/^[0-9]{3}$/)
17
+ end
18
+ end
19
+
20
+ it 'should return the same county when looked up by result' do
21
+ AnsiCodes::County.all.each do |county|
22
+ AnsiCodes::County.find(county.state, county.ansi_code).should be(county)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '#name' do
28
+ before(:each) { @names = AnsiCodes::County.all.map(&:name) }
29
+
30
+ it 'should return a string' do
31
+ @names.each {|name| name.should be_a(String)}
32
+ end
33
+
34
+ it 'should be at least 4 characters long' do
35
+ @names.each {|name| name.should have_at_least(4).items}
36
+ end
37
+
38
+ it 'should return the same county when looked up by result' do
39
+ AnsiCodes::County.all.each do |county|
40
+ AnsiCodes::County.find(county.state, county.name).should be(county)
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#short_name' do
46
+ before(:each) { @short_names = AnsiCodes::County.all.map(&:short_name) }
47
+
48
+ it 'should return a string' do
49
+ @short_names.each {|short_name| short_name.should be_a(String)}
50
+ end
51
+
52
+ it 'should be no longer than #name' do
53
+ AnsiCodes::County.all.each do |county|
54
+ county.short_name.should have_at_most(county.name.size).characters
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '#designation' do
60
+ before(:each) { @designations = AnsiCodes::County.all.map(&:designation) }
61
+
62
+ it 'should return a string' do
63
+ @designations.each {|designation| designation.should be_a(String)}
64
+ end
65
+
66
+ it 'should be shorter than #name' do
67
+ AnsiCodes::County.all.each do |county|
68
+ county.designation.should have_at_most(county.name.size - 1).characters
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '#state' do
74
+ before(:each) { @states = AnsiCodes::County.all.map(&:state) }
75
+
76
+ it 'should return an AnsiCodes::State' do
77
+ @states.each {|state| state.should be_an(AnsiCodes::State)}
78
+ end
79
+ end
80
+
81
+ describe '.all' do
82
+ it 'should return 3235 elements' do
83
+ AnsiCodes::County.all.should have(3235).items
84
+ end
85
+
86
+ it 'should return an array of AnsiCodes::County' do
87
+ AnsiCodes::County.all.tap do |counties|
88
+ counties.should be_an(Array)
89
+ counties.each {|county| county.should be_an_instance_of(AnsiCodes::County)}
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '.find' do
95
+ it 'should accept a string as first param' do
96
+ expect{AnsiCodes::County.find '01', '001'}.not_to raise_error
97
+ end
98
+
99
+ it 'should accept an integer as first param' do
100
+ expect{AnsiCodes::County.find 1, '001'}.not_to raise_error
101
+ end
102
+
103
+ it 'should accept an AnsiCodes::State as first param' do
104
+ expect{AnsiCodes::County.find AnsiCodes::State.all.first, '001'}.not_to raise_error
105
+ end
106
+
107
+ it 'should accept a string as second param' do
108
+ expect{AnsiCodes::County.find 1, '001'}.not_to raise_error
109
+ end
110
+
111
+ it 'should accept an integer as second param' do
112
+ expect{AnsiCodes::County.find 1, 1}.not_to raise_error
113
+ end
114
+
115
+ it 'should raise an ArgumentError on any other type as second param' do
116
+ expect{AnsiCodes::County.find 1, nil}.to raise_error(ArgumentError)
117
+ expect{AnsiCodes::County.find 1, 1.0}.to raise_error(ArgumentError)
118
+ expect{AnsiCodes::County.find 1, /really?/}.to raise_error(ArgumentError)
119
+ expect{AnsiCodes::County.find 1, {}}.to raise_error(ArgumentError)
120
+ expect{AnsiCodes::County.find 1, []}.to raise_error(ArgumentError)
121
+ end
122
+
123
+ it 'should yield the result if a block is given' do
124
+ county = AnsiCodes::County.find 'Virginia', 'Norfolk city'
125
+ expect { |b| AnsiCodes::County.find('Virginia', 'Norfolk city', &b) }.to yield_with_args(county)
126
+ end
127
+
128
+ it 'should raise an exception if no county is found' do
129
+ expect{AnsiCodes::County.find 45, 1000}.to raise_error
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_filter '/spec/'
5
+ add_filter {|source| source.lines.count < 10}
6
+ end
7
+ rescue LoadError
8
+ STDERR.puts 'SimpleCov not installed. Not generating coverage report.'
9
+ end
10
+
11
+ require 'ansi_codes'
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.run_all_when_everything_filtered = true
16
+ config.filter_run :focus
17
+ config.order = 'random'
18
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe AnsiCodes::State do
4
+ it 'should not be instantiable' do
5
+ expect{AnsiCodes::State.new('01', 'AL', 'Alabame')}.to raise_error(RuntimeError)
6
+ end
7
+
8
+ it 'should not be modifiable' do
9
+ expect{AnsiCodes::State.extend Module.new}.to raise_error
10
+ end
11
+
12
+ describe '#ansi_code' do
13
+ it 'should return a two-digit string' do
14
+ AnsiCodes::State.all.each do |state|
15
+ state.ansi_code.should match(/^[0-9]{2}$/)
16
+ end
17
+ end
18
+
19
+ it 'should return the same state when result is looked up' do
20
+ AnsiCodes::State.all.each do |state|
21
+ AnsiCodes::State.find(state.ansi_code).should be(state)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#name' do
27
+ it 'should return a string longer than 2 characters' do
28
+ AnsiCodes::State.all.each do |state|
29
+ state.name.should have_at_least(3).items
30
+ end
31
+ end
32
+
33
+ it 'should return a proper name' do
34
+ AnsiCodes::State.all.each do |state|
35
+ state.name.should match(/^[A-Z].*[a-z]/)
36
+ end
37
+ end
38
+
39
+ it 'should return the same state when result is looked up' do
40
+ AnsiCodes::State.all.each do |state|
41
+ AnsiCodes::State.find(state.name).should be(state)
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '#abbreviation' do
47
+ it 'should return a two-character uppercase string' do
48
+ AnsiCodes::State.all.each do |state|
49
+ state.abbreviation.should match(/^[A-Z]{2}$/)
50
+ end
51
+ end
52
+
53
+ it 'should return the same state when result is looked up' do
54
+ AnsiCodes::State.all.each do |state|
55
+ AnsiCodes::State.find(state.abbreviation).should be(state)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#counties' do
61
+ it 'should return an array of AnsiCodes::County' do
62
+ counties = AnsiCodes::State.all.first.counties
63
+ counties.should be_an(Array)
64
+ counties.each do |county|
65
+ county.should be_a(AnsiCodes::County)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '.all' do
71
+ it 'should return 57 elements' do
72
+ AnsiCodes::State.all.should have(57).items
73
+ end
74
+
75
+ it 'should return an array' do
76
+ AnsiCodes::State.all.should be_an(Array)
77
+ end
78
+
79
+ it 'should contain AnsiCodes::State instances' do
80
+ AnsiCodes::State.all.each do |state|
81
+ state.should be_an_instance_of(AnsiCodes::State)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '.find' do
87
+ it 'should raise ArgumentError with no arguments' do
88
+ expect{AnsiCodes::State.find}.to raise_error(ArgumentError)
89
+ end
90
+
91
+ it 'should raise ArgumentError with more than one argument' do
92
+ expect{AnsiCodes::State.find 1, 2}.to raise_error(ArgumentError)
93
+ end
94
+
95
+ it 'should accept an integer argument' do
96
+ expect{AnsiCodes::State.find 12}.not_to raise_error
97
+ end
98
+
99
+ it 'should accept a string argument' do
100
+ expect{AnsiCodes::State.find '12'}.not_to raise_error
101
+ end
102
+
103
+ it 'should return Alabama with lookup = 1' do
104
+ AnsiCodes::State.find(1).name.should == 'Alabama'
105
+ end
106
+
107
+ it 'should return Alabama with lookup = "01"' do
108
+ AnsiCodes::State.find('01').name.should == 'Alabama'
109
+ end
110
+
111
+ it 'should be case-insensitive' do
112
+ AnsiCodes::State.find('wi').should == AnsiCodes::State.find('WI')
113
+ AnsiCodes::State.find('NEW JERSEY').should == AnsiCodes::State.find('nEw JeRsEy')
114
+ end
115
+
116
+ it 'should raise an ArgumentError on any other type as second param' do
117
+ expect{AnsiCodes::State.find nil}.to raise_error(ArgumentError)
118
+ expect{AnsiCodes::State.find 1.0}.to raise_error(ArgumentError)
119
+ expect{AnsiCodes::State.find /really?/}.to raise_error(ArgumentError)
120
+ expect{AnsiCodes::State.find {}}.to raise_error(ArgumentError)
121
+ expect{AnsiCodes::State.find []}.to raise_error(ArgumentError)
122
+ end
123
+
124
+ it 'should raise a RuntimeError if no match is found' do
125
+ expect{AnsiCodes::State.find 'chimichanga'}.to raise_error(RuntimeError)
126
+ end
127
+
128
+ it 'should yield the result if a block is given' do
129
+ state = AnsiCodes::State.find 'New York'
130
+ expect { |b| AnsiCodes::State.find('New York', &b) }.to yield_with_args(state)
131
+ end
132
+
133
+ it 'should return identical objects with the same parameters' do
134
+ AnsiCodes::State.find('01').should eql(AnsiCodes::State.find('01'))
135
+ end
136
+
137
+ it 'should return identical objects when lookup result is same' do
138
+ AnsiCodes::State.find('AL').should eql(AnsiCodes::State.find('01'))
139
+ end
140
+ end
141
+
142
+ end