Soft launch: Soldier biographies and research notes vary in completeness. Prefer class-roster, NARA, newspapers, and other cited sources for contested facts — treat Research notes as leads, not settled history. Anyone can correct a page; contact us with questions.

Module:Soldier: Difference between revisions

From Ritchie Boys Wiki
Jump to navigation Jump to search
Import soldier lead module
Import soldier lead module
Line 159: Line 159:
end
end
return out
return out
end
local function assignmentLink(raw)
raw = trim(raw)
if raw == '' then
return ''
end
raw = mw.ustring.gsub(raw, '[#<>%[%]|{}]', '')
raw = trim(raw)
if raw == '' then
return ''
end
return string.format('[[Assignment %s|%s]]', raw, raw)
end
end


Line 179: Line 192:
end
end
td:wikitext(value)
td:wikitext(value)
end
local function yearOnly(s)
s = trim(s)
if s == '' or s == '?' then
return ''
end
local year = mw.ustring.match(s, '(%d%d%d%d)')
if year then
return year
end
return ''
end
local RANK_MAP = {
['PVT'] = 'Private',
['PVTE'] = 'Private',
['PRIVATE'] = 'Private',
['PFC'] = 'Private First Class',
['P F C'] = 'Private First Class',
['PRIVATE FIRST CLASS'] = 'Private First Class',
['CPL'] = 'Corporal',
['CORPORAL'] = 'Corporal',
['T/5'] = 'Technician Fifth Grade',
['TEC5'] = 'Technician Fifth Grade',
['TEC 5'] = 'Technician Fifth Grade',
['T5'] = 'Technician Fifth Grade',
['TECH 5'] = 'Technician Fifth Grade',
['SGT'] = 'Sergeant',
['SERGEANT'] = 'Sergeant',
['T/4'] = 'Technician Fourth Grade',
['TEC4'] = 'Technician Fourth Grade',
['TEC 4'] = 'Technician Fourth Grade',
['T4'] = 'Technician Fourth Grade',
['TECH 4'] = 'Technician Fourth Grade',
['S/SGT'] = 'Staff Sergeant',
['SSGT'] = 'Staff Sergeant',
['S SGT'] = 'Staff Sergeant',
['STAFF SERGEANT'] = 'Staff Sergeant',
['T/3'] = 'Technician Third Grade',
['TEC3'] = 'Technician Third Grade',
['TEC 3'] = 'Technician Third Grade',
['T3'] = 'Technician Third Grade',
['TECH 3'] = 'Technician Third Grade',
['T/SGT'] = 'Technical Sergeant',
['TSGT'] = 'Technical Sergeant',
['T SGT'] = 'Technical Sergeant',
['TECHNICAL SERGEANT'] = 'Technical Sergeant',
['1ST SGT'] = 'First Sergeant',
['1SGT'] = 'First Sergeant',
['FIRST SERGEANT'] = 'First Sergeant',
['M/SGT'] = 'Master Sergeant',
['MSGT'] = 'Master Sergeant',
['MASTER SERGEANT'] = 'Master Sergeant',
['WO'] = 'Warrant Officer',
['WOJG'] = 'Warrant Officer',
['WARRANT OFFICER'] = 'Warrant Officer',
['CWO'] = 'Chief Warrant Officer',
['2ND LT'] = 'Second Lieutenant',
['2D LT'] = 'Second Lieutenant',
['2LT'] = 'Second Lieutenant',
['SECOND LIEUTENANT'] = 'Second Lieutenant',
['1ST LT'] = 'First Lieutenant',
['1LT'] = 'First Lieutenant',
['FIRST LIEUTENANT'] = 'First Lieutenant',
['LT'] = 'Lieutenant',
['LIEUTENANT'] = 'Lieutenant',
['CAPT'] = 'Captain',
['CPT'] = 'Captain',
['CAPTAIN'] = 'Captain',
['MAJ'] = 'Major',
['MAJOR'] = 'Major',
['LT COL'] = 'Lieutenant Colonel',
['LTCOL'] = 'Lieutenant Colonel',
['LTC'] = 'Lieutenant Colonel',
['LIEUTENANT COLONEL'] = 'Lieutenant Colonel',
['COL'] = 'Colonel',
['COLONEL'] = 'Colonel',
['BRIG GEN'] = 'Brigadier General',
['BG'] = 'Brigadier General',
['BRIGADIER GENERAL'] = 'Brigadier General',
['MAJ GEN'] = 'Major General',
['MG'] = 'Major General',
['MAJOR GENERAL'] = 'Major General',
['LT GEN'] = 'Lieutenant General',
['LIEUTENANT GENERAL'] = 'Lieutenant General',
['GEN'] = 'General',
['GENERAL'] = 'General',
['TECH'] = 'Technician',
['TEC'] = 'Technician',
}
local function expandRank(raw)
raw = trim(raw)
if raw == '' then
return ''
end
local key = mw.ustring.upper(raw)
key = mw.ustring.gsub(key, '%.', '')
key = mw.ustring.gsub(key, '%s+', ' ')
key = trim(key)
return RANK_MAP[key] or raw
end
local function lifespan(birth, death)
birth = yearOnly(birth)
death = yearOnly(death)
if birth ~= '' and death ~= '' then
return ' (' .. birth .. '&ndash;' .. death .. ')'
end
if birth ~= '' then
return ' (born ' .. birth .. ')'
end
if death ~= '' then
return ' (died ' .. death .. ')'
end
return ''
end
local function preferredRank(frame)
local rank = expandRank(parentArg(frame, 'rank_final'))
if rank == '' then
rank = expandRank(parentArg(frame, 'class_rank'))
end
return rank
end
end


Line 220: Line 358:


local alias = parentArg(frame, 'alias')
local alias = parentArg(frame, 'alias')
local birth = parentArg(frame, 'birth_year')
local birth = yearOnly(parentArg(frame, 'birth_year'))
local death = parentArg(frame, 'death_year')
local death = yearOnly(parentArg(frame, 'death_year'))
local nativity = parentArg(frame, 'nativity')
local nativity = parentArg(frame, 'nativity')
local religion = parentArg(frame, 'religion')
local religion = parentArg(frame, 'religion')
Line 256: Line 394:
addRow(tbl, 'ASN', parentArg(frame, 'asn'), 'serial')
addRow(tbl, 'ASN', parentArg(frame, 'asn'), 'serial')


local rankFinal = parentArg(frame, 'rank_final')
local rankFinal = expandRank(parentArg(frame, 'rank_final'))
local classRank = parentArg(frame, 'class_rank')
local classRank = expandRank(parentArg(frame, 'class_rank'))
if rankFinal ~= '' and classRank ~= '' and classRank ~= rankFinal then
if rankFinal ~= '' and classRank ~= '' and classRank ~= rankFinal then
addRow(tbl, 'Rank', rankFinal)
addRow(tbl, 'Rank', rankFinal)
Line 281: Line 419:
addRow(tbl, 'Enlisted', titleCase(enlist))
addRow(tbl, 'Enlisted', titleCase(enlist))
end
end
addRow(tbl, 'Assignment', parentArg(frame, 'assignment'))
addRow(tbl, 'Assignment', assignmentLink(parentArg(frame, 'assignment')))
addRow(tbl, 'Unit / division', parentArg(frame, 'division'))
addRow(tbl, 'Unit / division', parentArg(frame, 'division'))
addRow(tbl, 'IPW (German)', parentArg(frame, 'ipw_ge'))
addRow(tbl, 'IPW (German)', parentArg(frame, 'ipw_ge'))
addRow(tbl, 'IPW (Italian)', parentArg(frame, 'ipw_it'))
addRow(tbl, 'IPW (Italian)', parentArg(frame, 'ipw_it'))
addRow(tbl, 'Overseas', parentArg(frame, 'overseas'))
addRow(tbl, 'Special', parentArg(frame, 'special'))
addRow(tbl, 'Special', parentArg(frame, 'special'))
addRow(tbl, 'LOC', parentArg(frame, 'loc'))
addRow(tbl, 'LOC', parentArg(frame, 'loc'))
Line 297: Line 434:
function p.lead(frame)
function p.lead(frame)
local name = displayName(frame)
local name = displayName(frame)
local surname = parentArg(frame, 'surname')
local rank = preferredRank(frame)
if surname == '' then
local role = 'United States Army soldier'
surname = name
if rank ~= '' then
end
role = 'United States Army ' .. rank
local birth = parentArg(frame, 'birth_year')
local death = parentArg(frame, 'death_year')
local dates = ''
if birth ~= '' and death ~= '' then
dates = string.format(' (%s – %s)', birth, death)
elseif birth ~= '' then
dates = string.format(' (born %s)', birth)
elseif death ~= '' then
dates = string.format(' (died %s)', death)
end
end


Line 316: Line 444:
if #classLinks == 1 then
if #classLinks == 1 then
trained = ' who trained at [[Camp Ritchie]], Maryland, in ' .. classLinks[1]
trained = ' who trained at [[Camp Ritchie]], Maryland, in ' .. classLinks[1]
elseif #classLinks > 1 then
elseif #classLinks == 2 then
trained = ' who trained at [[Camp Ritchie]], Maryland, in classes ' .. table.concat(classLinks, ', ')
trained = ' who trained at [[Camp Ritchie]], Maryland, in ' .. classLinks[1] .. ' and ' .. classLinks[2]
elseif #classLinks > 2 then
trained = ' who trained at [[Camp Ritchie]], Maryland, in '
.. table.concat(classLinks, ', ', 1, #classLinks - 1)
.. ', and ' .. classLinks[#classLinks]
else
else
trained = ' associated with Camp Ritchie, Maryland'
trained = ' associated with Camp Ritchie, Maryland'
end
end


local first = string.format(
return string.format(
"'''%s'''%s was a United States Army soldier%s.",
"'''%s'''%s was a %s%s.",
name,
name,
dates,
lifespan(parentArg(frame, 'birth_year'), parentArg(frame, 'death_year')),
role,
trained
trained
)
)
local more = {}
local enlist = parentArg(frame, 'enlistment_place')
local state = parentArg(frame, 'state')
if enlist ~= '' then
local place = titleCase(enlist)
if state ~= '' then
local stateTitle = titleCase(state)
if not containsIgnoreCase(place, stateTitle) then
place = place .. ', ' .. stateTitle
end
end
table.insert(more, surname .. ' enlisted at ' .. place .. '.')
elseif state ~= '' then
table.insert(more, 'Records list ' .. titleCase(state) .. ' as the state of enlistment or residence.')
end
local rank = parentArg(frame, 'rank_final')
if rank == '' then
rank = parentArg(frame, 'class_rank')
end
if rank ~= '' then
table.insert(more, 'Records list the rank as ' .. rank .. '.')
end
local assignment = parentArg(frame, 'assignment')
if assignment ~= '' then
table.insert(more, surname .. ' was assigned to ' .. assignment .. '.')
end
local ipw = {}
local ge = parentArg(frame, 'ipw_ge')
local it = parentArg(frame, 'ipw_it')
if ge ~= '' then
table.insert(ipw, 'German-language IPW team ' .. ge)
end
if it ~= '' then
table.insert(ipw, 'Italian-language IPW team ' .. it)
end
if #ipw > 0 then
table.insert(more, 'Interrogation duty included ' .. table.concat(ipw, ' and ') .. '.')
end
if #more == 0 then
return first
end
return first .. '\n\n' .. table.concat(more, ' ')
end
end


return p
return p



Revision as of 02:00, 18 August 2026

Documentation for this module may be created at Module:Soldier/doc

local p = {}

local function trim(s)
	return mw.text.trim(tostring(s or ''))
end

local function countChar(s, ch)
	local _, n = mw.ustring.gsub(s, ch, '')
	return n
end

local function tidyDisplay(s)
	s = trim(s)
	if s == '' then
		return ''
	end
	s = mw.text.decode(s, true)
	s = mw.ustring.gsub(s, mw.ustring.char(160), ' ')
	s = mw.ustring.gsub(s, mw.ustring.char(0xFFFD), '')
	s = mw.ustring.gsub(s, mw.ustring.char(0x200B), '')
	s = trim(s)
	local lower = mw.ustring.lower(s)
	if lower == '#n/a' or lower == 'n/a' or lower == 'none' or lower == 'null' or lower == '-' then
		return ''
	end
	if s == '.' or s == '?' then
		return ''
	end
	local prev
	repeat
		prev = s
		s = mw.ustring.gsub(s, '""', '"')
	until s == prev
	s = trim(s)

	local changed = true
	while changed and mw.ustring.len(s) >= 2 do
		changed = false
		local first = mw.ustring.sub(s, 1, 1)
		local last = mw.ustring.sub(s, -1)
		if first == '"' and last == '"' then
			local inner = mw.ustring.sub(s, 2, -2)
			if countChar(inner, '"') == 0 then
				s = trim(inner)
				changed = true
			elseif countChar(s, '"') % 2 == 1 then
				local second = mw.ustring.find(s, '"', 2, true)
				local slen = mw.ustring.len(s)
				if second and second + 1 < slen then
					local nxt = mw.ustring.sub(s, second + 1, second + 1)
					if mw.ustring.find(nxt, '%w') then
						s = mw.ustring.sub(s, 2)
					else
						s = mw.ustring.sub(s, 1, -2)
					end
				else
					s = mw.ustring.sub(s, 1, -2)
				end
				s = trim(s)
				changed = true
			end
		elseif first == "'" and last == "'" then
			local inner = mw.ustring.sub(s, 2, -2)
			if not mw.ustring.find(inner, "'", 1, true) then
				s = trim(inner)
				changed = true
			end
		end
	end

	local q = countChar(s, '"')
	local first = mw.ustring.sub(s, 1, 1)
	local last = mw.ustring.sub(s, -1)
	if first == '"' and q == 1 and last ~= '"' then
		s = trim(mw.ustring.sub(s, 2))
	end
	last = mw.ustring.sub(s, -1)
	first = mw.ustring.sub(s, 1, 1)
	q = countChar(s, '"')
	if last == '"' and q == 1 and first ~= '"' then
		s = trim(mw.ustring.sub(s, 1, -2))
	end
	s = mw.ustring.gsub(s, '^\\+', '')
	s = trim(s)
	if s == '"' or s == "'" or s == '.' or s == '?' or s == '\\' then
		return ''
	end
	s = mw.ustring.gsub(s, '%s+', ' ')
	return trim(s)
end

local function parentArg(frame, name)
	local parent = frame:getParent()
	local raw
	if parent and parent.args[name] then
		raw = parent.args[name]
	else
		raw = frame.args[name]
	end
	return tidyDisplay(raw)
end

local function titleCase(s)
	s = trim(s)
	if s == '' then
		return s
	end
	s = mw.ustring.lower(s)
	s = mw.ustring.gsub(s, "(%a)([%w']*)", function(first, rest)
		return mw.ustring.upper(first) .. rest
	end)
	return s
end

local function containsIgnoreCase(haystack, needle)
	return mw.ustring.find(mw.ustring.lower(haystack), mw.ustring.lower(needle), 1, true) ~= nil
end

local function displayName(frame)
	local first = parentArg(frame, 'first')
	local middle = parentArg(frame, 'middle')
	local surname = parentArg(frame, 'surname')
	local suffix = parentArg(frame, 'suffix')
	local parts = {}
	if first ~= '' then
		table.insert(parts, first)
	end
	if middle ~= '' then
		table.insert(parts, middle)
	end
	if surname ~= '' then
		table.insert(parts, surname)
	end
	local name = table.concat(parts, ' ')
	if suffix ~= '' then
		if name ~= '' then
			name = name .. ', ' .. suffix
		else
			name = suffix
		end
	end
	if name == '' then
		name = 'This soldier'
	end
	return name
end

local function classLinkList(raw, withPrefix)
	local out = {}
	for part in mw.text.gsplit(raw, ';', true) do
		part = trim(part)
		if part ~= '' then
			if withPrefix then
				table.insert(out, string.format('[[Class %s]]', part))
			else
				table.insert(out, string.format('[[Class %s|%s]]', part, part))
			end
		end
	end
	return out
end

local function assignmentLink(raw)
	raw = trim(raw)
	if raw == '' then
		return ''
	end
	raw = mw.ustring.gsub(raw, '[#<>%[%]|{}]', '')
	raw = trim(raw)
	if raw == '' then
		return ''
	end
	return string.format('[[Assignment %s|%s]]', raw, raw)
end

local function addHeader(tbl, text)
	tbl:tag('tr'):tag('th')
		:attr('colspan', '2')
		:addClass('infobox-header')
		:wikitext(text)
end

local function addRow(tbl, label, value, dataClass)
	if trim(value) == '' then
		return
	end
	local tr = tbl:tag('tr')
	tr:tag('th'):addClass('infobox-label'):wikitext(label)
	local td = tr:tag('td'):addClass('infobox-data')
	if dataClass and dataClass ~= '' then
		td:addClass(dataClass)
	end
	td:wikitext(value)
end

local function yearOnly(s)
	s = trim(s)
	if s == '' or s == '?' then
		return ''
	end
	local year = mw.ustring.match(s, '(%d%d%d%d)')
	if year then
		return year
	end
	return ''
end

local RANK_MAP = {
	['PVT'] = 'Private',
	['PVTE'] = 'Private',
	['PRIVATE'] = 'Private',
	['PFC'] = 'Private First Class',
	['P F C'] = 'Private First Class',
	['PRIVATE FIRST CLASS'] = 'Private First Class',
	['CPL'] = 'Corporal',
	['CORPORAL'] = 'Corporal',
	['T/5'] = 'Technician Fifth Grade',
	['TEC5'] = 'Technician Fifth Grade',
	['TEC 5'] = 'Technician Fifth Grade',
	['T5'] = 'Technician Fifth Grade',
	['TECH 5'] = 'Technician Fifth Grade',
	['SGT'] = 'Sergeant',
	['SERGEANT'] = 'Sergeant',
	['T/4'] = 'Technician Fourth Grade',
	['TEC4'] = 'Technician Fourth Grade',
	['TEC 4'] = 'Technician Fourth Grade',
	['T4'] = 'Technician Fourth Grade',
	['TECH 4'] = 'Technician Fourth Grade',
	['S/SGT'] = 'Staff Sergeant',
	['SSGT'] = 'Staff Sergeant',
	['S SGT'] = 'Staff Sergeant',
	['STAFF SERGEANT'] = 'Staff Sergeant',
	['T/3'] = 'Technician Third Grade',
	['TEC3'] = 'Technician Third Grade',
	['TEC 3'] = 'Technician Third Grade',
	['T3'] = 'Technician Third Grade',
	['TECH 3'] = 'Technician Third Grade',
	['T/SGT'] = 'Technical Sergeant',
	['TSGT'] = 'Technical Sergeant',
	['T SGT'] = 'Technical Sergeant',
	['TECHNICAL SERGEANT'] = 'Technical Sergeant',
	['1ST SGT'] = 'First Sergeant',
	['1SGT'] = 'First Sergeant',
	['FIRST SERGEANT'] = 'First Sergeant',
	['M/SGT'] = 'Master Sergeant',
	['MSGT'] = 'Master Sergeant',
	['MASTER SERGEANT'] = 'Master Sergeant',
	['WO'] = 'Warrant Officer',
	['WOJG'] = 'Warrant Officer',
	['WARRANT OFFICER'] = 'Warrant Officer',
	['CWO'] = 'Chief Warrant Officer',
	['2ND LT'] = 'Second Lieutenant',
	['2D LT'] = 'Second Lieutenant',
	['2LT'] = 'Second Lieutenant',
	['SECOND LIEUTENANT'] = 'Second Lieutenant',
	['1ST LT'] = 'First Lieutenant',
	['1LT'] = 'First Lieutenant',
	['FIRST LIEUTENANT'] = 'First Lieutenant',
	['LT'] = 'Lieutenant',
	['LIEUTENANT'] = 'Lieutenant',
	['CAPT'] = 'Captain',
	['CPT'] = 'Captain',
	['CAPTAIN'] = 'Captain',
	['MAJ'] = 'Major',
	['MAJOR'] = 'Major',
	['LT COL'] = 'Lieutenant Colonel',
	['LTCOL'] = 'Lieutenant Colonel',
	['LTC'] = 'Lieutenant Colonel',
	['LIEUTENANT COLONEL'] = 'Lieutenant Colonel',
	['COL'] = 'Colonel',
	['COLONEL'] = 'Colonel',
	['BRIG GEN'] = 'Brigadier General',
	['BG'] = 'Brigadier General',
	['BRIGADIER GENERAL'] = 'Brigadier General',
	['MAJ GEN'] = 'Major General',
	['MG'] = 'Major General',
	['MAJOR GENERAL'] = 'Major General',
	['LT GEN'] = 'Lieutenant General',
	['LIEUTENANT GENERAL'] = 'Lieutenant General',
	['GEN'] = 'General',
	['GENERAL'] = 'General',
	['TECH'] = 'Technician',
	['TEC'] = 'Technician',
}

local function expandRank(raw)
	raw = trim(raw)
	if raw == '' then
		return ''
	end
	local key = mw.ustring.upper(raw)
	key = mw.ustring.gsub(key, '%.', '')
	key = mw.ustring.gsub(key, '%s+', ' ')
	key = trim(key)
	return RANK_MAP[key] or raw
end

local function lifespan(birth, death)
	birth = yearOnly(birth)
	death = yearOnly(death)
	if birth ~= '' and death ~= '' then
		return ' (' .. birth .. '&ndash;' .. death .. ')'
	end
	if birth ~= '' then
		return ' (born ' .. birth .. ')'
	end
	if death ~= '' then
		return ' (died ' .. death .. ')'
	end
	return ''
end

local function preferredRank(frame)
	local rank = expandRank(parentArg(frame, 'rank_final'))
	if rank == '' then
		rank = expandRank(parentArg(frame, 'class_rank'))
	end
	return rank
end

function p.displayName(frame)
	return displayName(frame)
end

function p.classLinks(frame)
	return table.concat(classLinkList(parentArg(frame, 'class_number'), false), ', ')
end

function p.infobox(frame)
	local name = displayName(frame)
	local tbl = mw.html.create('table')
	tbl:addClass('infobox'):addClass('vcard'):addClass('soldier-infobox')

	tbl:tag('tr'):tag('th')
		:attr('colspan', '2')
		:addClass('infobox-above fn')
		:wikitext(name)

	local photo = parentArg(frame, 'photo')
	if photo ~= '' then
		local size = parentArg(frame, 'image_size')
		if size == '' then
			size = '220px'
		end
		local alt = parentArg(frame, 'alt')
		if alt == '' then
			alt = name
		end
		local td = tbl:tag('tr'):tag('td')
			:attr('colspan', '2')
			:addClass('infobox-image')
		td:wikitext(string.format('[[File:%s|%s|alt=%s]]', photo, size, alt))
		local caption = parentArg(frame, 'caption')
		if caption ~= '' then
			td:tag('div'):addClass('infobox-caption'):wikitext(caption)
		end
	end

	local alias = parentArg(frame, 'alias')
	local birth = yearOnly(parentArg(frame, 'birth_year'))
	local death = yearOnly(parentArg(frame, 'death_year'))
	local nativity = parentArg(frame, 'nativity')
	local religion = parentArg(frame, 'religion')
	local marital = parentArg(frame, 'marital')
	local education = parentArg(frame, 'education')
	local occupation = parentArg(frame, 'occupation')
	local state = parentArg(frame, 'state')

	if alias ~= '' or birth ~= '' or death ~= '' or nativity ~= ''
		or religion ~= '' or marital ~= '' or education ~= ''
		or occupation ~= '' or state ~= '' then
		addHeader(tbl, 'Personal details')
	end

	addRow(tbl, 'Nickname', alias, 'nickname')
	addRow(tbl, 'Born', birth)
	addRow(tbl, 'Nativity', nativity)
	addRow(tbl, 'Died', death)
	addRow(tbl, 'Religion', religion)
	addRow(tbl, 'Spouse / marital', marital)
	addRow(tbl, 'Education', education)
	addRow(tbl, 'Occupation', occupation)
	if state ~= '' then
		addRow(tbl, 'State', titleCase(state))
	end

	addHeader(tbl, 'Military service')
	addRow(tbl, 'Allegiance', 'United States')
	local branch = parentArg(frame, 'class_branch')
	if branch == '' then
		branch = 'United States Army'
	end
	addRow(tbl, 'Branch', branch)
	addRow(tbl, 'ASN', parentArg(frame, 'asn'), 'serial')

	local rankFinal = expandRank(parentArg(frame, 'rank_final'))
	local classRank = expandRank(parentArg(frame, 'class_rank'))
	if rankFinal ~= '' and classRank ~= '' and classRank ~= rankFinal then
		addRow(tbl, 'Rank', rankFinal)
		addRow(tbl, 'Rank at Ritchie', classRank)
	else
		local rank = rankFinal
		if rank == '' then
			rank = classRank
		end
		addRow(tbl, 'Rank', rank)
	end

	local classRaw = parentArg(frame, 'class_number')
	local classBits = classLinkList(classRaw, false)
	if #classBits > 0 then
		addRow(tbl, 'Camp Ritchie', 'Class ' .. table.concat(classBits, ', '))
	end
	addRow(tbl, 'Section', parentArg(frame, 'class_section'))
	addRow(tbl, 'Other class', parentArg(frame, 'other_class'))

	local enlist = parentArg(frame, 'enlistment_place')
	if enlist ~= '' then
		addRow(tbl, 'Enlisted', titleCase(enlist))
	end
	addRow(tbl, 'Assignment', assignmentLink(parentArg(frame, 'assignment')))
	addRow(tbl, 'Unit / division', parentArg(frame, 'division'))
	addRow(tbl, 'IPW (German)', parentArg(frame, 'ipw_ge'))
	addRow(tbl, 'IPW (Italian)', parentArg(frame, 'ipw_it'))
	addRow(tbl, 'Special', parentArg(frame, 'special'))
	addRow(tbl, 'LOC', parentArg(frame, 'loc'))
	addRow(tbl, 'Arrived', parentArg(frame, 'date_arrive'))
	addRow(tbl, 'Departed', parentArg(frame, 'date_depart'))
	addRow(tbl, 'Certificate', parentArg(frame, 'certificate'))

	return tostring(tbl)
end

function p.lead(frame)
	local name = displayName(frame)
	local rank = preferredRank(frame)
	local role = 'United States Army soldier'
	if rank ~= '' then
		role = 'United States Army ' .. rank
	end

	local classLinks = classLinkList(parentArg(frame, 'class_number'), true)
	local trained
	if #classLinks == 1 then
		trained = ' who trained at [[Camp Ritchie]], Maryland, in ' .. classLinks[1]
	elseif #classLinks == 2 then
		trained = ' who trained at [[Camp Ritchie]], Maryland, in ' .. classLinks[1] .. ' and ' .. classLinks[2]
	elseif #classLinks > 2 then
		trained = ' who trained at [[Camp Ritchie]], Maryland, in '
			.. table.concat(classLinks, ', ', 1, #classLinks - 1)
			.. ', and ' .. classLinks[#classLinks]
	else
		trained = ' associated with Camp Ritchie, Maryland'
	end

	return string.format(
		"'''%s'''%s was a %s%s.",
		name,
		lifespan(parentArg(frame, 'birth_year'), parentArg(frame, 'death_year')),
		role,
		trained
	)
end

return p