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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96c91603e5956e6918476045972a183754d150c8bfef8a8f7d33de5159ca4c60
4
- data.tar.gz: 72ade76ae58f72cfcc07ff2f301bd3a7067fc5e1fb836ca91438b767ff1198ae
3
+ metadata.gz: bde662fd124b6754ac6c830f95e58cbcf63d8cce2168f04a410b5fe0df1c80d9
4
+ data.tar.gz: 77894785fb4edf1bcb2fe575fe6ed55932a7f0eadbd056988bd666742e7c2b06
5
5
  SHA512:
6
- metadata.gz: cbf18e6e8e9099d19c023e12ed688374335213dc04ec9c95217a2b4f3df085fa29be735d4de828ddea254306357ff7463d4e2346e344ccde9281ac0c593ca258
7
- data.tar.gz: 82a5a80e5d3880e65711f7ee09c0d9b3c50b866faf879360997bfec5b98830bd2fe4c49f467e3545800cd80f6e9acd6f3cc41c7735cbdf06c592ff7fdc0ee172
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/", "lib/gov_codes/version.rb"
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.0] - 2025-05-02
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 codes
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 # => "Aircrew operations"
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("11M4")
43
- puts code.name # => "Pilot"
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 # => "4"
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
@@ -13,4 +13,5 @@ require "reissue/gem"
13
13
 
14
14
  Reissue::Task.create :reissue do |task|
15
15
  task.version_file = "lib/gov_codes/version.rb"
16
+ task.fragment = :git # Enable git trailer extraction for changelog
16
17
  end
@@ -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 and combine with career field
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
- skill_level = scanner.scan(/[\dX]/)
48
- return result unless skill_level
49
- result[:skill_level] = skill_level.to_sym
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
- # Scan for specific AFSC digit and combine with previous components
52
- specific_digit = scanner.scan(/\d/)
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
- result[:subcategory] = result[:specific_afsc][2..4].to_sym
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
- return nil if result.reject { |_, v| v.nil? }.empty?
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