Module: GovCodes::AFSC::Officer

Defined in:
lib/gov_codes/afsc/officer.rb

Overview

Officer AFSCs resolved against the versioned DAFOCD release index. Mirrors Enlisted: find(code, as_of:) resolves the X-form specialty (e.g. :11BX) or a literal bare code (e.g. :10C0) in the release in effect on as_of, defaulting to today.

Defined Under Namespace

Classes: Code, Parser

Class Method Summary collapse

Class Method Details

.find(code, as_of: nil) ⇒ Object

Resolve an officer AFSC against the DAFOCD release in effect on as_of (default: today). Returns nil when the code does not parse, when neither the X-form specialty nor a literal bare code resolves in the release, or when as_of precedes the earliest shipped release (or no release has taken effect yet).



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/gov_codes/afsc/officer.rb', line 84

def self.find(code, as_of: nil)
  code = code.to_s
  # Key the memo on the RESOLVED release date so equivalent as_of values
  # (nil, the Date, its string form, any date in the same release window)
  # share one slot instead of growing unbounded for time-series callers.
  effective_date = Releases.effective_date_for(as_of: as_of, publication: Releases::OFFICER_PUBLICATION)
  CODES[[code, effective_date]] ||= begin
    result = Parser.new(code).parse

    # Return nil if parsing failed or required fields are missing
    return nil if result.reject { |_, v| v.nil? }.empty? ||
      result[:career_group].nil? ||
      result[:functional_area].nil? ||
      result[:qualification_level].nil? ||
      result[:specific_afsc].nil?

    index = Releases.officer_index(as_of: as_of)

    # Prefer the X-form specialty key (11B3 -> :11BX). Fall back to the
    # literal 4-char code for bare specialties keyed that way (:10C0, :62S0,
    # :63G0, :63S0). By design these bare-code specialties resolve ONLY via
    # their literal code -- there is no X-form ladder for them, so
    # find("10CX")/find("62SX")/find("63GX")/find("63SX") return nil while
    # find("10C0")/find("62S0")/find("63G0")/find("63S0") resolve.
    specialty = :"#{result[:career_group]}#{result[:functional_area]}X"
    if !index.key?(specialty) && index.key?(result[:specific_afsc])
      specialty = result[:specific_afsc]
    end

    entry = index[specialty]
    return nil unless entry

    result[:specialty] = specialty
    result[:specialty_name] = entry[:name]

    # Qualification-level title: numeric level (1-4) maps to the directory's
    # per-specialty title; a letter (X/Y/Z) or an absent level leaves it nil.
    result[:qualification_level_number] = nil
    result[:qualification_level_name] = nil
    qual = result[:qualification_level].to_s
    if qual.match?(/\d/)
      number = Integer(qual)
      result[:qualification_level_number] = number
      result[:qualification_level_name] = entry.dig(:qual_levels, number, :title)
    end

    # Shredout meaning, only when the directory documents this shredout.
    shredout = result[:shredout]
    shredout_name = shredout && entry.dig(:shredouts, shredout)
    result[:shredout_name] = shredout_name

    name = shredout_name || entry[:name]
    return nil if name.nil?
    result[:name] = name

    # Acronym: a documented shredout acronym wins for a shredded code;
    # otherwise the specialty acronym (which a consumer overlay may set).
    shredout_acronym = shredout && entry.dig(:shredout_acronyms, shredout)
    result[:acronym] = shredout_acronym || entry[:acronym]
    result[:effective_date] = effective_date

    Code.new(**result)
  end
end

.find_by_acronym(acronym, as_of: nil) ⇒ Object

Resolve the officer specialty/shredout whose acronym matches acronym (case-insensitive) in the release in effect on as_of. Searches both specialty acronyms and shredout acronyms; a shredout match returns the concrete shredded code (so "TACPO" resolves find("19ZXB")). First match wins: entries are scanned in index order and, within an entry, the specialty acronym is tried before its shredout acronyms. Returns nil when no acronym in the resolved release matches (no leakage across dates).



156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/gov_codes/afsc/officer.rb', line 156

def self.find_by_acronym(acronym, as_of: nil)
  acronym = acronym.to_s.upcase
  return nil if acronym.empty?

  index = Releases.officer_index(as_of: as_of)
  index.each do |specialty, entry|
    return find(specialty.to_s, as_of: as_of) if entry[:acronym].to_s.upcase == acronym

    (entry[:shredout_acronyms] || {}).each do |shredout, acr|
      return find("#{specialty}#{shredout}", as_of: as_of) if acr.to_s.upcase == acronym
    end
  end
  nil
end

.reset_data(lookup: $LOAD_PATH) ⇒ Object

Clears the memoized lookups and resets the versioned release loader. The lookup keyword is accepted for interface parity with Enlisted/RI; the versioned index is resolved from the load path at lookup time.



194
195
196
197
# File 'lib/gov_codes/afsc/officer.rb', line 194

def self.reset_data(lookup: $LOAD_PATH)
  Releases.reset!
  CODES.clear
end

.search(prefix, as_of: nil) ⇒ Object

Walk the resolved index emitting each specialty code and its specialty+shredout combinations, returning the Codes matching prefix.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/gov_codes/afsc/officer.rb', line 173

def self.search(prefix, as_of: nil)
  prefix = prefix.to_s.upcase
  index = Releases.officer_index(as_of: as_of)

  codes = []
  index.each do |specialty, entry|
    specialty_code = specialty.to_s
    codes << specialty_code
    (entry[:shredouts] || {}).each_key do |shredout|
      codes << "#{specialty_code}#{shredout}"
    end
  end

  codes.select { |code| code.start_with?(prefix) }
    .map { |code| find(code, as_of: as_of) }
    .compact
end