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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
require "strscan"
|
|
2
|
+
require "yaml"
|
|
3
|
+
require_relative "../data_loader"
|
|
4
|
+
|
|
5
|
+
module GovCodes
|
|
6
|
+
module AFSC
|
|
7
|
+
# Reporting Identifiers (RI) and Special Duty Identifiers (SDI)
|
|
8
|
+
# These codes follow a different format than standard AFSCs:
|
|
9
|
+
# - Career field: digit + letter (e.g., "9Z", "8A")
|
|
10
|
+
# - Identifier: 3 digits (e.g., "200", "400")
|
|
11
|
+
# - Optional suffix: letter (e.g., "A", "B")
|
|
12
|
+
#
|
|
13
|
+
# Examples: 9Z200, 8A400, 8G000B, 8R300A
|
|
14
|
+
module RI
|
|
15
|
+
class Parser
|
|
16
|
+
def initialize(code)
|
|
17
|
+
@code = code
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
scanner = StringScanner.new(@code.to_s)
|
|
22
|
+
result = {
|
|
23
|
+
career_group: nil,
|
|
24
|
+
career_field: nil,
|
|
25
|
+
identifier: nil,
|
|
26
|
+
suffix: nil,
|
|
27
|
+
specific_ri: nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Scan for career group (single digit)
|
|
31
|
+
career_group = scanner.scan(/\d/)
|
|
32
|
+
return result unless career_group
|
|
33
|
+
result[:career_group] = career_group.to_sym
|
|
34
|
+
|
|
35
|
+
# Scan for career field letter
|
|
36
|
+
career_field_letter = scanner.scan(/[A-Z]/)
|
|
37
|
+
return result unless career_field_letter
|
|
38
|
+
result[:career_field] = :"#{career_group}#{career_field_letter}"
|
|
39
|
+
|
|
40
|
+
# Scan for identifier (3 digits)
|
|
41
|
+
identifier = scanner.scan(/\d{3}/)
|
|
42
|
+
return result unless identifier
|
|
43
|
+
result[:identifier] = identifier.to_sym
|
|
44
|
+
|
|
45
|
+
# Build specific RI code
|
|
46
|
+
result[:specific_ri] = :"#{result[:career_field]}#{identifier}"
|
|
47
|
+
|
|
48
|
+
# Scan for optional suffix (letter)
|
|
49
|
+
suffix = scanner.scan(/[A-Z]/)
|
|
50
|
+
result[:suffix] = suffix&.to_sym
|
|
51
|
+
|
|
52
|
+
# Check if we've reached the end of the string
|
|
53
|
+
return result unless scanner.eos?
|
|
54
|
+
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
extend GovCodes::DataLoader
|
|
60
|
+
|
|
61
|
+
DATA = data
|
|
62
|
+
|
|
63
|
+
Code = Data.define(
|
|
64
|
+
:career_group,
|
|
65
|
+
:career_field,
|
|
66
|
+
:identifier,
|
|
67
|
+
:suffix,
|
|
68
|
+
:specific_ri,
|
|
69
|
+
:name
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def self.find_name_recursive(result)
|
|
73
|
+
name = nil
|
|
74
|
+
data = DATA
|
|
75
|
+
|
|
76
|
+
career_field = result[:career_field]
|
|
77
|
+
identifier = result[:identifier]
|
|
78
|
+
suffix = result[:suffix]
|
|
79
|
+
|
|
80
|
+
# Look for the career field (e.g., "9Z", "8A")
|
|
81
|
+
if data[career_field]
|
|
82
|
+
field_data = data[career_field]
|
|
83
|
+
|
|
84
|
+
# Career field has name and subcategories
|
|
85
|
+
if field_data.is_a?(Hash) && field_data[:subcategories]
|
|
86
|
+
subcats = field_data[:subcategories]
|
|
87
|
+
|
|
88
|
+
# Look for the identifier (e.g., "200", "400")
|
|
89
|
+
if subcats[identifier]
|
|
90
|
+
identifier_data = subcats[identifier]
|
|
91
|
+
|
|
92
|
+
if identifier_data.is_a?(Hash)
|
|
93
|
+
name = identifier_data[:name]
|
|
94
|
+
|
|
95
|
+
# Look for suffix if present
|
|
96
|
+
if suffix && identifier_data[:subcategories]
|
|
97
|
+
suffix_data = identifier_data[:subcategories][suffix]
|
|
98
|
+
name = suffix_data if suffix_data.is_a?(String)
|
|
99
|
+
name = suffix_data[:name] if suffix_data.is_a?(Hash)
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
# Simple string value
|
|
103
|
+
name = identifier_data
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
name || "Unknown"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.find(code)
|
|
113
|
+
code = code.to_s
|
|
114
|
+
CODES[code] ||= begin
|
|
115
|
+
parser = Parser.new(code)
|
|
116
|
+
result = parser.parse
|
|
117
|
+
|
|
118
|
+
# Return nil if parsing failed or required fields are missing
|
|
119
|
+
return nil if result.reject { |_, v| v.nil? }.empty? ||
|
|
120
|
+
result[:career_group].nil? ||
|
|
121
|
+
result[:career_field].nil? ||
|
|
122
|
+
result[:identifier].nil? ||
|
|
123
|
+
result[:specific_ri].nil?
|
|
124
|
+
|
|
125
|
+
# Find the name by recursively searching the codes hash
|
|
126
|
+
name = find_name_recursive(result)
|
|
127
|
+
|
|
128
|
+
# Return nil if name is "Unknown" (code not in data)
|
|
129
|
+
return nil if name == "Unknown"
|
|
130
|
+
|
|
131
|
+
# Add the name to the result
|
|
132
|
+
result[:name] = name
|
|
133
|
+
|
|
134
|
+
Code.new(**result)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.reset_data(lookup: $LOAD_PATH)
|
|
139
|
+
remove_const(:DATA) if const_defined?(:DATA)
|
|
140
|
+
const_set(:DATA, data(lookup:))
|
|
141
|
+
CODES.clear
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.search(prefix)
|
|
145
|
+
results = []
|
|
146
|
+
prefix = prefix.to_s.upcase
|
|
147
|
+
collect_codes_recursive(DATA, "", prefix, results)
|
|
148
|
+
results.map { |code| find(code) }.compact
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.collect_codes_recursive(data, current_code, prefix, results)
|
|
152
|
+
return unless data.is_a?(Hash)
|
|
153
|
+
|
|
154
|
+
data.each do |key, value|
|
|
155
|
+
code = "#{current_code}#{key}"
|
|
156
|
+
|
|
157
|
+
if value.is_a?(Hash) && value[:name]
|
|
158
|
+
# This is a node with a name and possibly subcategories
|
|
159
|
+
results << code if code.start_with?(prefix)
|
|
160
|
+
collect_codes_recursive(value[:subcategories], code, prefix, results) if value[:subcategories]
|
|
161
|
+
elsif value.is_a?(String)
|
|
162
|
+
# This is a leaf node (simple string value)
|
|
163
|
+
results << code if code.start_with?(prefix)
|
|
164
|
+
elsif value.is_a?(Hash)
|
|
165
|
+
# Nested subcategories without a name at this level
|
|
166
|
+
collect_codes_recursive(value, current_code, prefix, results)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Reporting Identifiers (RI) and Special Duty Identifiers (SDI)
|
|
2
|
+
# Source: https://en.wikipedia.org/wiki/Air_Force_Specialty_Code
|
|
3
|
+
# Wikipedia Revision: 1318430021
|
|
4
|
+
# Extracted: 2025-11-17
|
|
5
|
+
#
|
|
6
|
+
# IMPORTANT: This file contains ONLY codes explicitly listed on Wikipedia.
|
|
7
|
+
# No predictions, no assumptions, no hallucinations.
|
|
8
|
+
# See .agent-os/product/decisions.md (DEC-003) for anti-hallucination policy.
|
|
9
|
+
#
|
|
10
|
+
# Format: Career field (digit + letter) + identifier (3 digits) + optional suffix
|
|
11
|
+
# Examples: 8A400, 9Z200, 8G000B, 8R300A
|
|
12
|
+
|
|
13
|
+
:8A:
|
|
14
|
+
:name: Enlisted aide and protocol
|
|
15
|
+
:subcategories:
|
|
16
|
+
:200: Enlisted aide
|
|
17
|
+
:300: Protocol
|
|
18
|
+
:400: Talent management consultant
|
|
19
|
+
:8B:
|
|
20
|
+
:name: Military training
|
|
21
|
+
:subcategories:
|
|
22
|
+
:000: Military training instructor
|
|
23
|
+
:100: Military Training leader
|
|
24
|
+
:200: Academy military training NCO
|
|
25
|
+
:300: Officer accessions instructor
|
|
26
|
+
:8C:
|
|
27
|
+
:name: Military and family readiness
|
|
28
|
+
:subcategories:
|
|
29
|
+
:000: Military and family readiness non-commissioned officer (RNCO)
|
|
30
|
+
:8D:
|
|
31
|
+
:name: Language and culture
|
|
32
|
+
:subcategories:
|
|
33
|
+
:100: Language & culture advisor
|
|
34
|
+
:8F:
|
|
35
|
+
:name: First sergeant
|
|
36
|
+
:subcategories:
|
|
37
|
+
:000: First sergeant
|
|
38
|
+
:8G:
|
|
39
|
+
:name: Honor guard
|
|
40
|
+
:subcategories:
|
|
41
|
+
:000:
|
|
42
|
+
:name: Premier honor guard
|
|
43
|
+
:subcategories:
|
|
44
|
+
:B: Pallbearer
|
|
45
|
+
:C: Color guard
|
|
46
|
+
:D: Drill team
|
|
47
|
+
:100: Base honor guard program manager
|
|
48
|
+
:8H:
|
|
49
|
+
:name: Airmen dorm leader
|
|
50
|
+
:subcategories:
|
|
51
|
+
:000: Airmen dorm leader
|
|
52
|
+
:8I:
|
|
53
|
+
:name: Inspector general
|
|
54
|
+
:subcategories:
|
|
55
|
+
:000: Superintendent, inspector general
|
|
56
|
+
:100: Inspections coordinator
|
|
57
|
+
:200: Complaints & resolution coordinator
|
|
58
|
+
:8K:
|
|
59
|
+
:name: Software development
|
|
60
|
+
:subcategories:
|
|
61
|
+
:000: Software development specialist
|
|
62
|
+
:8L:
|
|
63
|
+
:name: Air advisor
|
|
64
|
+
:subcategories:
|
|
65
|
+
:100: Air advisor basic
|
|
66
|
+
:200: Air advisor basic, team sergeant
|
|
67
|
+
:300: Air advisor basic, team leader
|
|
68
|
+
:400: Air advisor advanced
|
|
69
|
+
:500: Air advisor advanced, team sergeant
|
|
70
|
+
:600: Air advisor advanced, team leader
|
|
71
|
+
:700: Combat aviation advisor
|
|
72
|
+
:800: Combat aviation advisor team sergeant
|
|
73
|
+
:900: Combat aviation advisor team leader
|
|
74
|
+
:8P:
|
|
75
|
+
:name: Courier and defense attaché
|
|
76
|
+
:subcategories:
|
|
77
|
+
:000: Courier
|
|
78
|
+
:100: Defense attaché
|
|
79
|
+
:8R:
|
|
80
|
+
:name: Recruiting
|
|
81
|
+
:subcategories:
|
|
82
|
+
:000: Enlisted accessions recruiter
|
|
83
|
+
:200: Second-tier recruiter
|
|
84
|
+
:300:
|
|
85
|
+
:name: Third-tier recruiter
|
|
86
|
+
:subcategories:
|
|
87
|
+
:A: Flight chief
|
|
88
|
+
:B: Graduated, flight chief
|
|
89
|
+
:C: Production superintendent
|
|
90
|
+
:E: Senior enlisted leader
|
|
91
|
+
:8S:
|
|
92
|
+
:name: Missile facility
|
|
93
|
+
:subcategories:
|
|
94
|
+
:000: Missile facility manager
|
|
95
|
+
:200: Combat crew communications
|
|
96
|
+
:8T:
|
|
97
|
+
:name: Professional military education
|
|
98
|
+
:subcategories:
|
|
99
|
+
:000: Professional military education instructor
|
|
100
|
+
:100: Enlisted professional military education instructional system designer
|
|
101
|
+
:200: Development advisor
|
|
102
|
+
:8U:
|
|
103
|
+
:name: Unit deployment
|
|
104
|
+
:subcategories:
|
|
105
|
+
:000: Unit deployment manager
|
|
106
|
+
:100: Weapons of mass destruction civil support team (WMD-CST)
|
|
107
|
+
:8W:
|
|
108
|
+
:name: Weapons safety
|
|
109
|
+
:subcategories:
|
|
110
|
+
:000: Weapons safety manager – wing level
|
|
111
|
+
:8Y:
|
|
112
|
+
:name: Pathfinder
|
|
113
|
+
:subcategories:
|
|
114
|
+
:000: Pathfinder
|
|
115
|
+
:9A:
|
|
116
|
+
:name: Disqualified/awaiting separation
|
|
117
|
+
:subcategories:
|
|
118
|
+
:000: Enlisted airman/guardian – disqualified for reasons beyond control
|
|
119
|
+
:100: Enlisted airman/guardian – disqualified for reasons within control
|
|
120
|
+
:200: Enlisted airman/guardian awaiting discharge, separation, or retirement for reasons within their control
|
|
121
|
+
:300: Enlisted airman/guardian awaiting discharge, separation, or retirement for reasons beyond their control
|
|
122
|
+
:400: Disqualified airman/guardian, return to duty program
|
|
123
|
+
:500: Enlisted airman/guardian temporarily ineligible for retraining – disqualified for reasons beyond control
|
|
124
|
+
:9B:
|
|
125
|
+
:name: Senior enlisted advisor
|
|
126
|
+
:subcategories:
|
|
127
|
+
:000: Senior enlisted advisor to the chairman of the joint chiefs of staff
|
|
128
|
+
:100: Senior enlisted advisor to the chief of the National Guard Bureau
|
|
129
|
+
:9C:
|
|
130
|
+
:name: Chief Master Sergeant of the Air Force
|
|
131
|
+
:subcategories:
|
|
132
|
+
:000: Chief Master Sergeant of the Air Force
|
|
133
|
+
:100: Executive assistant to the Chief Master Sergeant of the Air Force
|
|
134
|
+
:9D:
|
|
135
|
+
:name: Developmental positions
|
|
136
|
+
:subcategories:
|
|
137
|
+
:100: AF developmental senior enlisted positions
|
|
138
|
+
:200: Key developmental senior enlisted positions
|
|
139
|
+
:9E:
|
|
140
|
+
:name: Command Chief Master Sergeant
|
|
141
|
+
:subcategories:
|
|
142
|
+
:000: Command Chief Master Sergeant
|
|
143
|
+
:100: Command chief executive assistant
|
|
144
|
+
:200: Individual Mobilization Augmentee to Command Chief Master Sergeant
|
|
145
|
+
:9F:
|
|
146
|
+
:name: First term airmen center
|
|
147
|
+
:subcategories:
|
|
148
|
+
:000: First term airmen center (FTAC) NCOIC
|
|
149
|
+
:9G:
|
|
150
|
+
:name: Group senior enlisted leader
|
|
151
|
+
:subcategories:
|
|
152
|
+
:100: Group senior enlisted leader
|
|
153
|
+
:9H:
|
|
154
|
+
:name: Academic and WHCA
|
|
155
|
+
:subcategories:
|
|
156
|
+
:000: Academic faculty instructor
|
|
157
|
+
:100: White House communications agency technician (WHCA)
|
|
158
|
+
:9I:
|
|
159
|
+
:name: Futures airmen
|
|
160
|
+
:subcategories:
|
|
161
|
+
:000: Futures airmen
|
|
162
|
+
:9J:
|
|
163
|
+
:name: Prisoner
|
|
164
|
+
:subcategories:
|
|
165
|
+
:000: Prisoner
|
|
166
|
+
:9L:
|
|
167
|
+
:name: Interpreter/translator
|
|
168
|
+
:subcategories:
|
|
169
|
+
:000: Interpreter/translator
|
|
170
|
+
:100: Enlisted international affairs manager
|
|
171
|
+
:9M:
|
|
172
|
+
:name: Military entrance and health
|
|
173
|
+
:subcategories:
|
|
174
|
+
:000: Military entrance processing command (MEPCOM) senior enlisted advisor
|
|
175
|
+
:200: International health specialists (IHS)
|
|
176
|
+
:400: Chief, medical enlisted force (CMEF)
|
|
177
|
+
:9N:
|
|
178
|
+
:name: Legislative fellows
|
|
179
|
+
:subcategories:
|
|
180
|
+
:000: Secretary of the Air Force enlisted legislative fellows
|
|
181
|
+
:9P:
|
|
182
|
+
:name: Patient
|
|
183
|
+
:subcategories:
|
|
184
|
+
:000: Patient
|
|
185
|
+
:9Q:
|
|
186
|
+
:name: Reserve force
|
|
187
|
+
:subcategories:
|
|
188
|
+
:000: Reserve force generation and oversight NCO
|
|
189
|
+
:9R:
|
|
190
|
+
:name: Civil Air Patrol
|
|
191
|
+
:subcategories:
|
|
192
|
+
:000: Civil Air Patrol (CAP) – USAF reserve assistance NCO
|
|
193
|
+
:9S:
|
|
194
|
+
:name: Scientific applications
|
|
195
|
+
:subcategories:
|
|
196
|
+
:100: Scientific applications specialist
|
|
197
|
+
:9T:
|
|
198
|
+
:name: Trainee
|
|
199
|
+
:subcategories:
|
|
200
|
+
:000: Basic enlisted airman
|
|
201
|
+
:100: Officer trainee
|
|
202
|
+
:200: Precadet assignee
|
|
203
|
+
:400: AFIT/EWI enlisted student
|
|
204
|
+
:500: Basic special warfare enlisted airman
|
|
205
|
+
:9U:
|
|
206
|
+
:name: Ineligible/unallotted
|
|
207
|
+
:subcategories:
|
|
208
|
+
:000: Enlisted airman/guardian ineligible for local utilization
|
|
209
|
+
:100: Unallotted enlisted authorization
|
|
210
|
+
:9V:
|
|
211
|
+
:name: Joint senior enlisted
|
|
212
|
+
:subcategories:
|
|
213
|
+
:000: Key developmental joint senior enlisted position
|
|
214
|
+
:100: Executive assistant to the senior enlisted advisor to the Chairman of the Joint Chiefs of Staff
|
|
215
|
+
:9W:
|
|
216
|
+
:name: Wounded warrior
|
|
217
|
+
:subcategories:
|
|
218
|
+
:000: Combat wounded warrior
|
|
219
|
+
:100: Reserved for future use
|
|
220
|
+
:200: Combat wounded warrior with exemptions
|
|
221
|
+
:300: Non-combat wounded warrior
|
|
222
|
+
:400: Wounded warrior – limited assignment status (LAS)
|
|
223
|
+
:500: Reserved for future use
|
|
224
|
+
:600: Reserved for future use
|
|
225
|
+
:700: Reserved for future use
|
|
226
|
+
:800: Wounded warrior – ambassador
|
|
227
|
+
:900: Wounded warrior – project planner/officer
|
|
228
|
+
:9Y:
|
|
229
|
+
:name: Air Force parachute team
|
|
230
|
+
:subcategories:
|
|
231
|
+
:000: Air force parachute team (AFPT) instructor
|
|
232
|
+
:9Z:
|
|
233
|
+
:name: Special warfare mission support
|
|
234
|
+
:subcategories:
|
|
235
|
+
:000: Special warfare mission support (SWMS) career field manager (CFM) on headquarters Air Force staff, Air Force Special Warfare Division
|
|
236
|
+
:100: Special warfare mission support (SWMS) senior enlisted leader, Air Force Special Warfare (AFSPECWAR)
|
|
237
|
+
:200: Special warfare mission support (SWMS) superintendent, Air Force Special Warfare (AFSPECWAR)
|
data/lib/gov_codes/afsc.rb
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
require_relative "afsc/enlisted"
|
|
2
2
|
require_relative "afsc/officer"
|
|
3
|
+
require_relative "afsc/ri"
|
|
3
4
|
|
|
4
5
|
module GovCodes
|
|
5
6
|
module AFSC
|
|
6
7
|
def self.find(code)
|
|
7
8
|
AFSC::Enlisted.find(code) ||
|
|
8
|
-
AFSC::Officer.find(code)
|
|
9
|
+
AFSC::Officer.find(code) ||
|
|
10
|
+
AFSC::RI.find(code)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.search(prefix)
|
|
14
|
+
results = []
|
|
15
|
+
results.concat(Enlisted.search(prefix))
|
|
16
|
+
results.concat(Officer.search(prefix))
|
|
17
|
+
results.concat(RI.search(prefix))
|
|
18
|
+
results
|
|
9
19
|
end
|
|
10
20
|
|
|
11
21
|
def self.reset_data(lookup: $LOAD_PATH)
|
|
12
22
|
Enlisted.reset_data(lookup:)
|
|
13
23
|
Officer.reset_data(lookup:)
|
|
24
|
+
RI.reset_data(lookup:)
|
|
14
25
|
end
|
|
15
26
|
end
|
|
16
27
|
end
|
|
@@ -11,65 +11,59 @@ module GovCodes
|
|
|
11
11
|
|
|
12
12
|
def data(lookup: $LOAD_PATH)
|
|
13
13
|
data = {}
|
|
14
|
+
lookup_array = Array(lookup)
|
|
15
|
+
return data if lookup_array.empty?
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
# Add the gem's lib directory to the lookup path
|
|
18
|
+
gem_lib_dir = File.expand_path("..", __dir__)
|
|
19
|
+
lookup_paths = [gem_lib_dir] + lookup_array
|
|
18
20
|
|
|
19
|
-
#
|
|
21
|
+
# Convert namespace to file path parts (e.g., "GovCodes::AFSC::Enlisted" -> ["gov_codes", "afsc", "enlisted.yml"])
|
|
22
|
+
namespace_parts = name.split("::")
|
|
23
|
+
.map { |part| part.gsub(/([A-Z])([a-z])/, '_\1\2').downcase.sub(/^_/, "") }
|
|
20
24
|
namespace_parts[-1] = "#{namespace_parts[-1]}.yml"
|
|
21
25
|
|
|
22
|
-
#
|
|
23
|
-
|
|
26
|
+
# Find all existing YAML files in lookup paths
|
|
27
|
+
lookup_paths.filter_map do |dir|
|
|
24
28
|
yaml_path = File.join(dir, *namespace_parts)
|
|
25
29
|
yaml_path if File.exist?(yaml_path)
|
|
26
|
-
end
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
end.uniq.each do |path|
|
|
31
|
+
yaml_data = YAML.load_file(path, symbolize_names: true)
|
|
32
|
+
data.merge!(yaml_data) if yaml_data.is_a?(Hash)
|
|
33
|
+
rescue Psych::SyntaxError, TypeError
|
|
34
|
+
# Handle invalid YAML gracefully
|
|
35
|
+
next
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
data
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
def reset_data(lookup: $LOAD_PATH)
|
|
37
|
-
remove_const(:DATA)
|
|
42
|
+
remove_const(:DATA) if const_defined?(:DATA, false)
|
|
38
43
|
const_set(:DATA, data(lookup:).freeze)
|
|
39
|
-
remove_const(:CODES)
|
|
44
|
+
remove_const(:CODES) if const_defined?(:CODES, false)
|
|
40
45
|
const_set(:CODES, {})
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def find_name_recursive(result)
|
|
44
|
-
# Start with the career field (e.g., "1N")
|
|
45
49
|
base_code = result[:career_field].to_sym
|
|
50
|
+
base_data = self::DATA[base_code]
|
|
51
|
+
return "Unknown" unless base_data
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
subdivision = loaded_data.dig(base_code, :subcategories, result[:subcategory])
|
|
56
|
-
# If we have a shredout, try to find an even more specific name
|
|
57
|
-
if result[:shredout] &&
|
|
58
|
-
subdivision.dig(:subcategories) &&
|
|
59
|
-
subdivision.dig(:subcategories, result[:shredout])
|
|
60
|
-
return subdivision.dig(:subcategories, result[:shredout], :name)
|
|
53
|
+
# Try subcategory lookup if present
|
|
54
|
+
if result[:subcategory]
|
|
55
|
+
subdivision = base_data.dig(:subcategories, result[:subcategory])
|
|
56
|
+
if subdivision
|
|
57
|
+
# Try shredout lookup if present
|
|
58
|
+
if result[:shredout]
|
|
59
|
+
shredout_name = subdivision.dig(:subcategories, result[:shredout], :name)
|
|
60
|
+
return shredout_name if shredout_name
|
|
61
61
|
end
|
|
62
|
-
|
|
63
|
-
# Return the subdivision name if no shredout match
|
|
64
|
-
return subdivision.dig(:name)
|
|
62
|
+
return subdivision[:name] if subdivision[:name]
|
|
65
63
|
end
|
|
66
|
-
|
|
67
|
-
# Return the base name if no subdivision match
|
|
68
|
-
return loaded_data.dig(base_code, :name)
|
|
69
64
|
end
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
"Unknown"
|
|
66
|
+
base_data[:name] || "Unknown"
|
|
73
67
|
end
|
|
74
68
|
end
|
|
75
69
|
end
|
data/lib/gov_codes/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gov_codes
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Gay
|
|
@@ -22,11 +22,15 @@ files:
|
|
|
22
22
|
- CHANGELOG.md
|
|
23
23
|
- README.md
|
|
24
24
|
- Rakefile
|
|
25
|
+
- checksums/gov_codes-0.1.0.gem.sha512
|
|
25
26
|
- lib/gov_codes.rb
|
|
26
27
|
- lib/gov_codes/afsc.rb
|
|
27
28
|
- lib/gov_codes/afsc/enlisted.rb
|
|
28
29
|
- lib/gov_codes/afsc/enlisted.yml
|
|
29
30
|
- lib/gov_codes/afsc/officer.rb
|
|
31
|
+
- lib/gov_codes/afsc/officer.yml
|
|
32
|
+
- lib/gov_codes/afsc/ri.rb
|
|
33
|
+
- lib/gov_codes/afsc/ri.yml
|
|
30
34
|
- lib/gov_codes/data_loader.rb
|
|
31
35
|
- lib/gov_codes/version.rb
|
|
32
36
|
- sig/gov_codes.rbs
|
|
@@ -50,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
50
54
|
- !ruby/object:Gem::Version
|
|
51
55
|
version: '0'
|
|
52
56
|
requirements: []
|
|
53
|
-
rubygems_version: 3.
|
|
57
|
+
rubygems_version: 3.7.2
|
|
54
58
|
specification_version: 4
|
|
55
59
|
summary: Handle codes used by the US government.
|
|
56
60
|
test_files: []
|