gov_codes 0.1.0 → 0.1.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 +4 -4
- data/.simplecov +3 -1
- data/CHANGELOG.md +52 -2
- data/README.md +50 -4
- data/Rakefile +1 -0
- data/checksums/gov_codes-0.1.0.gem.sha512 +1 -0
- data/lib/gov_codes/afsc/enlisted.rb +92 -13
- data/lib/gov_codes/afsc/enlisted.yml +532 -725
- data/lib/gov_codes/afsc/officer.rb +73 -13
- data/lib/gov_codes/afsc/officer.yml +1072 -0
- data/lib/gov_codes/afsc/ri.rb +172 -0
- data/lib/gov_codes/afsc/ri.yml +237 -0
- data/lib/gov_codes/afsc.rb +12 -1
- data/lib/gov_codes/data_loader.rb +30 -36
- data/lib/gov_codes/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bde662fd124b6754ac6c830f95e58cbcf63d8cce2168f04a410b5fe0df1c80d9
|
|
4
|
+
data.tar.gz: 77894785fb4edf1bcb2fe575fe6ed55932a7f0eadbd056988bd666742e7c2b06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c8d2bcb57eb86164c8484b9db9b2c00bf9853a0fa0ef33178a8f02267d4558a88d3ca1834458de46c120a0bf68a0f3f566f65c65c865dda0c51c4ab7352ca05
|
|
7
|
+
data.tar.gz: 335e26092052f94f0b949cf28cc6c1d3d4194621f641dfa78b9098b05b71ddc6fb71c8a3e1c911d029064ff891b4114ea719a2faee4ab7d3c3d99a57959ed7ca
|
data/.simplecov
CHANGED
|
@@ -2,7 +2,8 @@ SimpleCov.start do
|
|
|
2
2
|
# enable_coverage :branch
|
|
3
3
|
|
|
4
4
|
# Add any files or directories you want to exclude from coverage
|
|
5
|
-
add_filter "/test/"
|
|
5
|
+
add_filter "/test/"
|
|
6
|
+
add_filter "lib/gov_codes/version.rb"
|
|
6
7
|
|
|
7
8
|
# Set minimum coverage requirements
|
|
8
9
|
# minimum_coverage 80
|
|
@@ -11,5 +12,6 @@ SimpleCov.start do
|
|
|
11
12
|
track_files "lib/**/*.rb"
|
|
12
13
|
|
|
13
14
|
# Group files by module
|
|
15
|
+
|
|
14
16
|
add_group "AFSC", "lib/gov_codes/afsc"
|
|
15
17
|
end
|
data/CHANGELOG.md
CHANGED
|
@@ -5,8 +5,58 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
-
## [0.1.
|
|
8
|
+
## [0.1.1] - 2025-11-17
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Test against Ruby 3.3.8 and 3.4.3
|
|
13
|
+
- Officer YAML structure to match Wikipedia (11BX, 11MX, etc.) (4ca6aef)
|
|
14
|
+
- Officer parser to accept letter qualification levels (X, Y, Z) (4ca6aef)
|
|
15
|
+
- Officer lookup to include qualification level in key (4ca6aef)
|
|
16
|
+
- Enlisted/Officer lookups to handle String leaf values (4ca6aef)
|
|
17
|
+
- Use git trailers to track changelog changes. (c83929a)
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- AFSC Officer support (bb96aaa)
|
|
22
|
+
- Data extractor script for Wikipedia (c8ec55b)
|
|
23
|
+
- 1Z Special Warfare career field (Pararescue, Combat Control, TACP) (4ca6aef)
|
|
24
|
+
- Nokogiri gem for HTML parsing (4ca6aef)
|
|
25
|
+
- Reporting identifiers with GovCodes::AFSC::RI (6102656)
|
|
26
|
+
- AFSC.search to return an array of matching codes for the given prefix. (5c5bdf1)
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- 824 hallucinated codes not found in Wikipedia (4ca6aef)
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Tests updated to use real Wikipedia codes (4ca6aef)
|
|
35
|
+
|
|
36
|
+
## [0.1.1] - 2025-11-17
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Test against Ruby 3.3.8 and 3.4.3
|
|
41
|
+
- Officer YAML structure to match Wikipedia (11BX, 11MX, etc.) (4ca6aef)
|
|
42
|
+
- Officer parser to accept letter qualification levels (X, Y, Z) (4ca6aef)
|
|
43
|
+
- Officer lookup to include qualification level in key (4ca6aef)
|
|
44
|
+
- Enlisted/Officer lookups to handle String leaf values (4ca6aef)
|
|
45
|
+
- Use git trailers to track changelog changes. (c83929a)
|
|
9
46
|
|
|
10
47
|
### Added
|
|
11
48
|
|
|
12
|
-
- AFSC
|
|
49
|
+
- AFSC Officer support (bb96aaa)
|
|
50
|
+
- Data extractor script for Wikipedia (c8ec55b)
|
|
51
|
+
- 1Z Special Warfare career field (Pararescue, Combat Control, TACP) (4ca6aef)
|
|
52
|
+
- Nokogiri gem for HTML parsing (4ca6aef)
|
|
53
|
+
- Reporting identifiers with GovCodes::AFSC::RI (6102656)
|
|
54
|
+
- AFSC.search to return an array of matching codes for the given prefix. (5c5bdf1)
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
|
|
58
|
+
- 824 hallucinated codes not found in Wikipedia (4ca6aef)
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- Tests updated to use real Wikipedia codes (4ca6aef)
|
data/README.md
CHANGED
|
@@ -31,7 +31,7 @@ require 'gov_codes/afsc'
|
|
|
31
31
|
|
|
32
32
|
# Find an enlisted AFSC code
|
|
33
33
|
code = GovCodes::AFSC.find("1A1X2")
|
|
34
|
-
puts code.name # => "
|
|
34
|
+
puts code.name # => "Mobility force aviator"
|
|
35
35
|
puts code.career_field # => "1A"
|
|
36
36
|
puts code.career_field_subdivision # => "1A1"
|
|
37
37
|
puts code.skill_level # => "X"
|
|
@@ -39,12 +39,58 @@ puts code.specific_afsc # => "1A1X2"
|
|
|
39
39
|
puts code.shredout # => nil
|
|
40
40
|
|
|
41
41
|
# Find an officer AFSC code
|
|
42
|
-
code = GovCodes::AFSC.find("
|
|
43
|
-
puts code.name # => "
|
|
42
|
+
code = GovCodes::AFSC.find("11MX")
|
|
43
|
+
puts code.name # => "Mobility pilot"
|
|
44
44
|
puts code.career_group # => "11"
|
|
45
45
|
puts code.functional_area # => "M"
|
|
46
|
-
puts code.qualification_level # => "
|
|
46
|
+
puts code.qualification_level # => "X"
|
|
47
47
|
puts code.shredout # => nil
|
|
48
|
+
|
|
49
|
+
# Find a Reporting Identifier (RI) or Special Duty Identifier (SDI)
|
|
50
|
+
code = GovCodes::AFSC.find("8A400")
|
|
51
|
+
puts code.name # => "Talent management consultant"
|
|
52
|
+
puts code.career_field # => "8A"
|
|
53
|
+
puts code.identifier # => "400"
|
|
54
|
+
puts code.suffix # => nil
|
|
55
|
+
|
|
56
|
+
# Find a code with a shredout/suffix
|
|
57
|
+
code = GovCodes::AFSC.find("11BXA")
|
|
58
|
+
puts code.name # => "B-1"
|
|
59
|
+
puts code.specific_afsc # => "11BX"
|
|
60
|
+
puts code.shredout # => "A"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Searching for Codes
|
|
64
|
+
|
|
65
|
+
You can search for all codes matching a prefix:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Search for all Special Warfare codes
|
|
69
|
+
results = GovCodes::AFSC.search("1Z")
|
|
70
|
+
results.each do |code|
|
|
71
|
+
puts "#{code.specific_afsc}: #{code.name}"
|
|
72
|
+
end
|
|
73
|
+
# Output:
|
|
74
|
+
# 1Z1X1: Pararescue
|
|
75
|
+
# 1Z2X1: Combat control
|
|
76
|
+
# 1Z3X1: Tactical air control party (TACP)
|
|
77
|
+
# 1Z4X1: Special reconnaissance
|
|
78
|
+
|
|
79
|
+
# Search for Bomber Pilot shredouts
|
|
80
|
+
results = GovCodes::AFSC.search("11BX")
|
|
81
|
+
results.each do |code|
|
|
82
|
+
shredout = code.shredout ? code.shredout.to_s : ""
|
|
83
|
+
puts "#{code.specific_afsc}#{shredout}: #{code.name}"
|
|
84
|
+
end
|
|
85
|
+
# Output:
|
|
86
|
+
# 11BX: Bomber pilot
|
|
87
|
+
# 11BXA: B-1
|
|
88
|
+
# 11BXB: B-2
|
|
89
|
+
# 11BXC: B-52
|
|
90
|
+
# ...
|
|
91
|
+
|
|
92
|
+
# Search is case-insensitive
|
|
93
|
+
GovCodes::AFSC.search("1z1") # Same as search("1Z1")
|
|
48
94
|
```
|
|
49
95
|
|
|
50
96
|
### Extending with Custom AFSC Codes
|
data/Rakefile
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
a47544403935737f6dc23d24d03620744582275e6f585ca3dd7610b01efde9afbafe3b54949b5fc16c67d92b56a5175ec4109237d7eda96df86821eab3c2c8ef
|
|
@@ -2,8 +2,6 @@ require "strscan"
|
|
|
2
2
|
require "yaml"
|
|
3
3
|
require_relative "../data_loader"
|
|
4
4
|
|
|
5
|
-
puts "Loading enlisted.rb"
|
|
6
|
-
|
|
7
5
|
module GovCodes
|
|
8
6
|
module AFSC
|
|
9
7
|
module Enlisted
|
|
@@ -38,22 +36,25 @@ module GovCodes
|
|
|
38
36
|
return result unless career_field_letter
|
|
39
37
|
result[:career_field] = :"#{career_group}#{career_field_letter}"
|
|
40
38
|
|
|
41
|
-
# Scan for subdivision digit
|
|
39
|
+
# Scan for subdivision digit
|
|
42
40
|
subdivision_digit = scanner.scan(/\d/)
|
|
43
41
|
return result unless subdivision_digit
|
|
44
42
|
result[:career_field_subdivision] = :"#{result[:career_field]}#{subdivision_digit}"
|
|
45
43
|
|
|
46
|
-
# Scan for skill level
|
|
47
|
-
|
|
48
|
-
return result unless
|
|
49
|
-
result[:skill_level] =
|
|
44
|
+
# Scan for skill level letter (usually X)
|
|
45
|
+
skill_level_letter = scanner.scan(/[A-Z]/)
|
|
46
|
+
return result unless skill_level_letter
|
|
47
|
+
result[:skill_level] = skill_level_letter.to_sym
|
|
48
|
+
|
|
49
|
+
# Scan for skill level digit
|
|
50
|
+
skill_level_digit = scanner.scan(/\d/)
|
|
51
|
+
return result unless skill_level_digit
|
|
50
52
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
return result unless specific_digit
|
|
54
|
-
result[:specific_afsc] = :"#{result[:career_field_subdivision]}#{result[:skill_level]}#{specific_digit}"
|
|
53
|
+
# Subcategory is subdivision digit + letter + digit (e.g. '1X2')
|
|
54
|
+
result[:subcategory] = :"#{subdivision_digit}#{skill_level_letter}#{skill_level_digit}"
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# Build specific AFSC
|
|
57
|
+
result[:specific_afsc] = :"#{result[:career_field_subdivision]}#{skill_level_letter}#{skill_level_digit}"
|
|
57
58
|
|
|
58
59
|
# Scan for shredout (optional)
|
|
59
60
|
result[:shredout] = scanner.scan(/[A-Z]/)&.to_sym
|
|
@@ -66,6 +67,7 @@ module GovCodes
|
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
extend GovCodes::DataLoader
|
|
70
|
+
|
|
69
71
|
DATA = data
|
|
70
72
|
|
|
71
73
|
Code = Data.define(
|
|
@@ -86,11 +88,25 @@ module GovCodes
|
|
|
86
88
|
parser = Parser.new(code)
|
|
87
89
|
result = parser.parse
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
# Return nil if parsing failed or required fields are missing
|
|
92
|
+
return nil if result.reject { |_, v| v.nil? }.empty? ||
|
|
93
|
+
result[:career_group].nil? ||
|
|
94
|
+
result[:career_field].nil? ||
|
|
95
|
+
result[:career_field_subdivision].nil? ||
|
|
96
|
+
result[:skill_level].nil? ||
|
|
97
|
+
result[:specific_afsc].nil? ||
|
|
98
|
+
result[:subcategory].nil?
|
|
99
|
+
|
|
100
|
+
# Additional validation: check for invalid characters or too long codes
|
|
101
|
+
return nil if code.length > 7 ||
|
|
102
|
+
code.match?(/[^A-Z0-9]/)
|
|
90
103
|
|
|
91
104
|
# Find the name by recursively searching the codes hash
|
|
92
105
|
name = find_name_recursive(result)
|
|
93
106
|
|
|
107
|
+
# Return nil if name is 'Unknown'
|
|
108
|
+
return nil if name == "Unknown"
|
|
109
|
+
|
|
94
110
|
# Add the name to the result
|
|
95
111
|
result[:name] = name
|
|
96
112
|
|
|
@@ -98,6 +114,69 @@ module GovCodes
|
|
|
98
114
|
Code.new(**result)
|
|
99
115
|
end
|
|
100
116
|
end
|
|
117
|
+
|
|
118
|
+
def self.find_name_recursive(result)
|
|
119
|
+
data = DATA
|
|
120
|
+
|
|
121
|
+
# Career field (e.g., "9Z")
|
|
122
|
+
cf = result[:career_field]&.to_sym
|
|
123
|
+
return "Unknown" unless cf && data[cf]
|
|
124
|
+
name = data[cf][:name]
|
|
125
|
+
data = data[cf][:subcategories]
|
|
126
|
+
|
|
127
|
+
# Subcategory (e.g., "0X1" from "9Z0X1")
|
|
128
|
+
if data && result[:subcategory]
|
|
129
|
+
sub = result[:subcategory].to_sym
|
|
130
|
+
lookup_value = data[sub]
|
|
131
|
+
if lookup_value
|
|
132
|
+
if lookup_value.is_a?(Hash)
|
|
133
|
+
name = lookup_value[:name] || name
|
|
134
|
+
data = lookup_value[:subcategories]
|
|
135
|
+
else
|
|
136
|
+
# String value (leaf node)
|
|
137
|
+
name = lookup_value
|
|
138
|
+
data = nil
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Shredout (optional, e.g., :A)
|
|
144
|
+
if data && result[:shredout]
|
|
145
|
+
lookup_value = data[result[:shredout]]
|
|
146
|
+
if lookup_value
|
|
147
|
+
name = lookup_value.is_a?(Hash) ? (lookup_value[:name] || name) : (lookup_value || name)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
name || "Unknown"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.search(prefix)
|
|
155
|
+
results = []
|
|
156
|
+
prefix = prefix.to_s.upcase
|
|
157
|
+
collect_codes_recursive(DATA, "", prefix, results)
|
|
158
|
+
results.map { |code| find(code) }.compact
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.collect_codes_recursive(data, current_code, prefix, results)
|
|
162
|
+
return unless data.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
data.each do |key, value|
|
|
165
|
+
code = "#{current_code}#{key}"
|
|
166
|
+
|
|
167
|
+
if value.is_a?(Hash) && value[:name]
|
|
168
|
+
# This is a node with a name and possibly subcategories
|
|
169
|
+
results << code if code.start_with?(prefix)
|
|
170
|
+
collect_codes_recursive(value[:subcategories], code, prefix, results) if value[:subcategories]
|
|
171
|
+
elsif value.is_a?(String)
|
|
172
|
+
# This is a leaf node (simple string value)
|
|
173
|
+
results << code if code.start_with?(prefix)
|
|
174
|
+
elsif value.is_a?(Hash)
|
|
175
|
+
# Nested subcategories without a name at this level
|
|
176
|
+
collect_codes_recursive(value, current_code, prefix, results)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
101
180
|
end
|
|
102
181
|
end
|
|
103
182
|
end
|