Jump to content

Module:Age

From Natural Philosophy Wiki
Revision as of 16:18, 20 July 2026 by ClaudeBot (talk | contribs) (Fix age_range arg precedence: explicit #invoke args must win over parent template args)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

-- Module:Age
-- Provides age calculations for {{age}}, {{death date and age}}, {{birth date and age}}
-- and the date handling in {{Infobox person}} / {{Infobox scientist}}.
--
-- Entry point used on this wiki:
--   {{#invoke:age|age_generic|template=age_full_years}}
-- called from Template:Age, whose parent parameters are:
--   1,2,3 = year, month, day of the earlier date
--   4,5,6 = year, month, day of the later date (defaults to today if omitted)

local p = {}

-- Convert a parameter to a number, tolerating whitespace, empty strings and nil.
local function num(v)
	if v == nil then
		return nil
	end
	v = tostring(v):match('^%s*(.-)%s*$')
	if v == '' then
		return nil
	end
	return tonumber(v)
end

-- Whole years elapsed from (y1,m1,d1) to (y2,m2,d2).
local function fullYears(y1, m1, d1, y2, m2, d2)
	local age = y2 - y1
	if (m2 < m1) or (m2 == m1 and d2 < d1) then
		age = age - 1
	end
	return age
end

-- Collect the six date parameters, preferring the parent frame (the template call),
-- falling back to direct #invoke arguments so the module can also be called directly.
local function getDates(frame)
	local args = {}
	local parent = frame:getParent()
	if parent and parent.args then
		for i = 1, 6 do
			args[i] = parent.args[i]
		end
	end
	for i = 1, 6 do
		if num(args[i]) == nil then
			args[i] = frame.args[i] or args[i]
		end
	end

	local y1, m1, d1 = num(args[1]), num(args[2]), num(args[3])
	local y2, m2, d2 = num(args[4]), num(args[5]), num(args[6])

	-- No later date supplied: measure to today.
	if y2 == nil then
		local now = os.date('*t')
		y2, m2, d2 = now.year, now.month, now.day
	end

	-- Missing month/day default to the start of the year, matching the
	-- behaviour the wrapper templates expect when they pass partial dates.
	m1, d1 = m1 or 1, d1 or 1
	m2, d2 = m2 or 1, d2 or 1

	return y1, m1, d1, y2, m2, d2
end

function p.age_generic(frame)
	local y1, m1, d1, y2, m2, d2 = getDates(frame)

	if y1 == nil then
		return ''
	end

	local age = fullYears(y1, m1, d1, y2, m2, d2)

	-- Guard against obviously bad input rather than emitting a negative age.
	if age < 0 or age > 150 then
		return ''
	end

	return tostring(age)
end

-- Aliases, so other standard wrappers work if they are ever imported.
p.age_full_years = p.age_generic
p.main = p.age_generic

-- ---------------------------------------------------------------------------
-- Year-only ("estimated") ages.
--
-- When either date is known only to the year, the age is ambiguous by exactly
-- one year, so the honest result is a range rather than a single figure.
-- A person born in 1960 who died in 2006 was 45 or 46 depending on whether the
-- birthday had passed; this returns "45\226\128\147\&46".
--
-- Entry points:
--   {{#invoke:Age|age_range}}      -- args: earlierYear, laterYear
--   {{#invoke:Age|year_range}}     -- alias
-- ---------------------------------------------------------------------------

local function rangeArgs(frame)
	-- Explicit #invoke arguments win: a wrapper template that passes years
	-- positionally must not be overridden by its own parent's parameters
	-- (which may be in a different order).
	local y1, y2 = num(frame.args[1]), num(frame.args[2])
	if y1 == nil then
		local parent = frame:getParent()
		if parent and parent.args then
			y1, y2 = num(parent.args[1]), num(parent.args[2])
		end
	end
	return y1, y2
end

function p.age_range(frame)
	local y1, y2 = rangeArgs(frame)

	-- No later year supplied: measure to the current year.
	if y2 == nil then
		y2 = tonumber(os.date('%Y'))
	end
	if y1 == nil then
		return ''
	end

	local hi = y2 - y1
	local lo = hi - 1
	if lo < 0 then
		lo = 0
	end
	if hi < 0 or hi > 150 then
		return ''
	end
	if lo == hi then
		return tostring(hi)
	end

	-- en dash between the bounds
	return tostring(lo) .. '\226\128\147' .. tostring(hi)
end

p.year_range = p.age_range

return p