Class: GovCodes::Dafecd::Publication

Inherits:
Object
  • Object
show all
Defined in:
lib/gov_codes/dafecd/publication.rb

Overview

Per-publication configuration for the classification-directory extractor.

The DAFECD (enlisted) and DAFOCD (officer) directories share the same extraction pipeline (RecordSplitter -> SpecialtyParser -> ShredoutParser -> IndexBuilder) but differ in a handful of concrete details: the running header, the shape of a skill/qualification ladder line, whether standalone single-code records and CEM lines exist, how the X-form specialty key is derived, and where the release artifact is written.

A Publication is an immutable value object capturing exactly those differences. It is injected into every pipeline stage. The default everywhere is Publication.dafecd, so the enlisted extraction is byte-for-byte unchanged by the parameterization.

Constant Summary collapse

DAFECD_HEADER =

--- Enlisted (DAFECD) patterns ------------------------------------------ Running page header, e.g. "DAFECD, 31 Oct 25".

/^\s*DAFECD,\s+\d/
DAFECD_LADDER =

Skill-ladder line (group 1 = concrete 5-char AFSC, group 2 = skill-level word). Tolerates the DAFECD's pdf-reader quirks (see Patterns::LADDER).

/
  ^\s*(?:AFSC\s+)?(\d[A-Z]\d\d\d)\*?
  (?:\s+or\s+\d[A-Z0-9]+)?
  ,?\s*
  (?:[A-Za-z][A-Za-z\/]*\s+){0,3}
  (Helper|Apprentice|Journeyman|Craftsman|
   Superintendent|Senior\s+Enlisted(?:\s+Leader)?|Entry)
  (?:\s*\([A-Z]{2,6}\))?
  (?:\s*AFSC)?
  \s*$
/x
DAFECD_CEM =

CEM (Chief Enlisted Manager) code line, e.g. "CEM Code 1A100*".

/^\s*CEM Code\s+(\d[A-Z]\d00)\*?/
DAFECD_GLUED_AFSC =

Inserts a boundary before an "AFSC " glued to a word.

/(?<=[A-Za-z])(?=AFSC \d[A-Z]\d\d\d)/
DAFECD_CHANGE_DATE =

The DAFECD's per-record change-date annotation.

/\((?:Changed|Established|Effective)\s+(\d{1,2}\s+\w{3,9}\s+\d{2,4})\)/
DAFECD_SHREDOUT_HEADER =

The DAFECD shredout table header ("Suffix ... Primary Aircraft").

/Suffix\s+\w/
DAFOCD_HEADER =

--- Officer (DAFOCD) patterns ------------------------------------------- Running page header, e.g. "DAFOCD, 31 Oct 25".

/^\s*DAFOCD,\s+\d/
DAFOCD_LADDER =

Qualification-ladder line (group 1 = concrete 4-char AFSC, group 2 = free-form qualification title). The officer title vocabulary is open, so the anchor relies on the CODE SHAPE (exactly 4 chars: two digits, a letter, and a level digit 1-4) plus a SHORT title that ENDS the line AND STARTS WITH AN UPPERCASE LETTER. Every real DAFOCD qualification title is a proper label (Entry, Qualified, Aircraft Commander, Staff, ...), so the uppercase-initial requirement rejects wrapped source PROSE that happens to begin "AFSC <4-char-code>, " (e.g. the real lines "AFSC 14F3, but applied to developing" and "AFSC 42G3, current and continuous"), which otherwise satisfied the code+short-title shape. It also still rejects the 5-char shred-code / long-sentence prose mentions. An optional leading/trailing "AFSC" absorbs the same pdf-reader glue the enlisted ladder handles (e.g. "11G4, StaffAFSC").

The directory is INCONSISTENT about three things, all tolerated here:

* COMMA (group A): most cards print "AFSC 11B4, Staff", but several live
medical cards omit the comma and separate with a space ("AFSC 44B4
Staff", "AFSC 45A4* Staff"). Hence the separator is comma-or-space,
NOT a bare optional comma: requiring at least one of comma/whitespace
is what still rejects glued-shred prose such as "AFSC 42B3Z requires
successful completion of" and "AFSC 43E3Aand complete the education",
where an uppercase shred letter is glued to the code with no
separator before the lowercase prose. A bare ",?" would let those in.
* SHRED LETTER (group B): a few cards print the qualification code with
a glued shredout letter, e.g. "AFSC 17D4W*, Staff". The optional
"[A-Z]" absorbs it so it is NOT baked into the captured concrete code
(group 1 stays "17D4"); the letter is documented as a shredout via the
card's shredout table instead (see ShredoutParser).
* LEADING GLYPH: pdf-reader prepends a Private Use Area / bullet glyph
to some ladder lines (e.g. U+F0EA on the 17D card). The leading class
mirrors Patterns::DECORATIVE so the anchor tolerates it.
%r{
  ^[\s\u{E000}-\u{F8FF}★☆•●▪■⁃∙]*(?:AFSC\s+)?(\d\d[A-Z][1-4])[A-Z]?\*?
  (?:,\s*|\s+)
  ([A-Z][A-Za-z/\ ]{0,44}?)
  (?:\s*AFSC)?
  \s*$
}x
DAFOCD_BARE_CODE =

A standalone single-code officer record, e.g. "AFSC 10C0". These carry a title/date/sections but no ladder; each is a full record keyed by the literal code.

/^\s*AFSC\s+(\d\d[A-Z][0-9X])\s*$/
DAFOCD_GLUED_AFSC =

Inserts a boundary before an "AFSC " glued to a word.

/(?<=[A-Za-z])(?=AFSC \d\d[A-Z][1-4])/
DAFOCD_CHANGE_DATE =

The DAFOCD's per-record change-date annotation. Tolerant of the officer directory's glued day/month ("30Apr 23") and of a trailing second date ("30Apr 14, Effective 25 Oct 13"): the first annotation is captured.

/\((?:Changed|Established|Effective)\s+(\d{1,2}\s*\w{3,9}\s+\d{2,4})/
DAFOCD_SHREDOUT_HEADER =

The DAFOCD shredout table header ("Suffix ... Portion of AFS to Which Related").

/Suffix\s+\w/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id:, directory_name:, header:, ladder:, bare_code:, cem:, glued_afsc:, change_date:, shredout_header:, levels_key:, code_has_specific:, captures_shredout_acronyms:, acronym_exclusions:, release_dir:, index_filename:, title_overrides_filename:, shredout_overrides_filename:) ⇒ Publication

Returns a new instance of Publication.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/gov_codes/dafecd/publication.rb', line 177

def initialize(id:, directory_name:, header:, ladder:, bare_code:, cem:,
  glued_afsc:, change_date:, shredout_header:, levels_key:,
  code_has_specific:, captures_shredout_acronyms:, acronym_exclusions:,
  release_dir:, index_filename:, title_overrides_filename:,
  shredout_overrides_filename:)
  @id = id
  @directory_name = directory_name
  @header = header
  @ladder = ladder
  @bare_code = bare_code
  @cem = cem
  @glued_afsc = glued_afsc
  @change_date = change_date
  @shredout_header = shredout_header
  @levels_key = levels_key
  @code_has_specific = code_has_specific
  @captures_shredout_acronyms = captures_shredout_acronyms
  @acronym_exclusions = acronym_exclusions
  @release_dir = release_dir
  @index_filename = index_filename
  @title_overrides_filename = title_overrides_filename
  @shredout_overrides_filename = shredout_overrides_filename
  freeze
end

Instance Attribute Details

#acronym_exclusionsObject (readonly)

Returns the value of attribute acronym_exclusions.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def acronym_exclusions
  @acronym_exclusions
end

#bare_codeObject (readonly)

Returns the value of attribute bare_code.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def bare_code
  @bare_code
end

#cemObject (readonly)

Returns the value of attribute cem.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def cem
  @cem
end

#change_dateObject (readonly)

Returns the value of attribute change_date.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def change_date
  @change_date
end

#directory_nameObject (readonly)

Returns the value of attribute directory_name.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def directory_name
  @directory_name
end

#glued_afscObject (readonly)

Returns the value of attribute glued_afsc.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def glued_afsc
  @glued_afsc
end

#headerObject (readonly)

Returns the value of attribute header.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def header
  @header
end

#idObject (readonly)

Returns the value of attribute id.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def id
  @id
end

#index_filenameObject (readonly)

Returns the value of attribute index_filename.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def index_filename
  @index_filename
end

#ladderObject (readonly)

Returns the value of attribute ladder.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def ladder
  @ladder
end

#levels_keyObject (readonly)

Returns the value of attribute levels_key.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def levels_key
  @levels_key
end

#release_dirObject (readonly)

Returns the value of attribute release_dir.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def release_dir
  @release_dir
end

#shredout_headerObject (readonly)

Returns the value of attribute shredout_header.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def shredout_header
  @shredout_header
end

#shredout_overrides_filenameObject (readonly)

Returns the value of attribute shredout_overrides_filename.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def shredout_overrides_filename
  @shredout_overrides_filename
end

#title_overrides_filenameObject (readonly)

Returns the value of attribute title_overrides_filename.



172
173
174
# File 'lib/gov_codes/dafecd/publication.rb', line 172

def title_overrides_filename
  @title_overrides_filename
end

Class Method Details

.dafecdPublication

Returns the enlisted directory configuration.

Returns:

  • (Publication)

    the enlisted directory configuration



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/gov_codes/dafecd/publication.rb', line 111

def dafecd
  @dafecd ||= new(
    id: :dafecd,
    directory_name: "Department of the Air Force Enlisted Classification Directory",
    header: DAFECD_HEADER,
    ladder: DAFECD_LADDER,
    bare_code: nil,
    cem: DAFECD_CEM,
    glued_afsc: DAFECD_GLUED_AFSC,
    change_date: DAFECD_CHANGE_DATE,
    shredout_header: DAFECD_SHREDOUT_HEADER,
    levels_key: :skill_levels,
    code_has_specific: true,
    captures_shredout_acronyms: false,
    acronym_exclusions: %i[1D7X2 1A8X0],
    release_dir: "dafecd",
    index_filename: "enlisted.yml",
    title_overrides_filename: "title_overrides.yml",
    shredout_overrides_filename: "shredout_overrides/dafecd.yml"
  )
end

.dafocdPublication

Returns the officer directory configuration.

Returns:

  • (Publication)

    the officer directory configuration



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/gov_codes/dafecd/publication.rb', line 134

def dafocd
  @dafocd ||= new(
    id: :dafocd,
    directory_name: "Department of the Air Force Officer Classification Directory",
    header: DAFOCD_HEADER,
    ladder: DAFOCD_LADDER,
    bare_code: DAFOCD_BARE_CODE,
    cem: nil,
    glued_afsc: DAFOCD_GLUED_AFSC,
    change_date: DAFOCD_CHANGE_DATE,
    shredout_header: DAFOCD_SHREDOUT_HEADER,
    levels_key: :qual_levels,
    code_has_specific: false,
    captures_shredout_acronyms: true,
    acronym_exclusions: [],
    release_dir: "dafocd",
    index_filename: "officer.yml",
    title_overrides_filename: "title_overrides/dafocd.yml",
    shredout_overrides_filename: "shredout_overrides/dafocd.yml"
  )
end

.detect(text) ⇒ Object

Select the publication that matches +text+'s running header, defaulting to enlisted.



158
159
160
# File 'lib/gov_codes/dafecd/publication.rb', line 158

def detect(text)
  [dafocd, dafecd].find { |pub| pub.header.match?(text) } || dafecd
end

.for(id) ⇒ Object

Look a publication up by its id symbol (:dafecd / :dafocd).



163
164
165
166
167
168
169
# File 'lib/gov_codes/dafecd/publication.rb', line 163

def for(id)
  by_id = {dafecd: dafecd, dafocd: dafocd}
  by_id.fetch(id.to_sym) do
    raise ArgumentError,
      "unknown publication #{id.inspect}; valid options are #{by_id.keys.map(&:inspect).join(", ")}"
  end
end

Instance Method Details

#captures_shredout_acronyms?Boolean

Whether this publication extracts shredout-level acronyms (officer).

Returns:

  • (Boolean)


203
204
205
# File 'lib/gov_codes/dafecd/publication.rb', line 203

def captures_shredout_acronyms?
  @captures_shredout_acronyms
end

#career_field(codes) ⇒ Object

The career-field key (first two chars of the basis code).



219
220
221
222
# File 'lib/gov_codes/dafecd/publication.rb', line 219

def career_field(codes)
  return nil if codes.empty?
  :"#{specialty_basis(codes).first[0, 2]}"
end

#shredout_overrides_pathObject

The absolute path to this publication's shredout-overrides file.



230
231
232
# File 'lib/gov_codes/dafecd/publication.rb', line 230

def shredout_overrides_path
  File.expand_path(@shredout_overrides_filename, __dir__)
end

#specialty_key(codes) ⇒ Object

The X-form specialty key for a ladder group's concrete codes. enlisted: 1A172,1A152,... -> :1A1X2 (X at the level digit, specific kept) officer: 11B4,11B3,... -> :11BX (X at the level digit, no specific) Returns nil when no codes are given.



211
212
213
214
215
216
# File 'lib/gov_codes/dafecd/publication.rb', line 211

def specialty_key(codes)
  return nil if codes.empty?
  basis = specialty_basis(codes)
  prefix = basis.first[0, 3]
  @code_has_specific ? :"#{prefix}X#{most_common_specific(basis)}" : :"#{prefix}X"
end

#title_overrides_pathObject

The absolute path to this publication's title-overrides file.



225
226
227
# File 'lib/gov_codes/dafecd/publication.rb', line 225

def title_overrides_path
  File.expand_path(@title_overrides_filename, __dir__)
end