Този модул позволява добавянето на години на раждане и смърт в статии за личности. Заедно с това се добавя и информация за възрастта на личността. Съответните статии, в които се използва този модул, се причисляват към категориите „Родени/Починали през X г.“ и „Родени/Починали на X“.

Модулът се използва по следния начин:

Дата на раждане: {{#invoke:Person|birth_date|ИДЕНТИФИКАТОР}} или {{#invoke:Person|birth_date}}

Дата на смърт: {{#invoke:Person|death_date|ИДЕНТИФИКАТОР}} или {{#invoke:Person|death_date}}

С параметъра ИДЕНТИФИКАТОР може да се укаже идентификатор в Уикиданни на страница за личност. Той е незадължителен - по подразбиране се използва идентификаторът на текущата страница.

Например:

Дата на раждане: {{#invoke:Person|birth_date}} или {{#invoke:Person|birth_date|Q347118}}

Дата на смърт: {{#invoke:Person|death_date}} или {{#invoke:Person|death_date|Q347118}}



local p = {}
local wd = require('Модул:Wd')

function isEmpty(var)
	return var == nil or var == ""
end

Date = {
	-- _ = nil, -- save here the original date string, e.g. "31 май 2000"
	-- day = nil,
	-- month = nil,
	-- monthName = nil,
	-- year = nil,
	-- bce = nil, -- is it in the BCE epoch
	-- julian = nil, -- is the date in Julian calendar
}

function Date.currentDate()
	return os.date("*t")
end

function Date:isEmpty()
	return isEmpty(self._)
end
function Date:monthNameToNumber(monthName)
	local map = {
		["януари"] = 1,
		["февруари"] = 2,
		["март"]  = 3,
		["април"] = 4,
		["май"] = 5,
		["юни"] = 6,
		["юли"] = 7,
		["август"] = 8,
		["септември"] = 9,
		["октомври"] = 10,
		["ноември"] = 11,
		["декември"] = 12
	}
	return map[monthName] or nil
end
function Date:set(year, monthName, day)
	self.year = year
	self.monthName = monthName
	self.month = self:monthNameToNumber(monthName)
	self.day = day
	return self
end

function Date:fromWikidata(eid, property)
	local dateString = wd._property({eid, property})
	dateString = mw.ustring.gsub(dateString, '<!%-%-%s*', '')
	dateString = mw.ustring.gsub(dateString, '%-%->', '')

	d = { _ = dateString, q = eid, p = property }
	setmetatable(d, self)
	self.__index = self
	if isEmpty(dateString) or (dateString == 'няма') then
		return d
	end
	if (dateString == 'неизвестна') then
		d.unknown = true
		return d
	end

	if string.match(dateString, 'пр.н.е.') then
		d.bce = true
	end
	if string.match(dateString, 'стар стил') then
		d.julian = true
	end

	local day, monthName, year = mw.ustring.match(dateString, "^(%d+)%s+(%a+)%s+(%d+)")
	if day and monthName and year then
		return d:set(year, monthName, day)
	end
	monthName, year = mw.ustring.match(dateString, "^(%a+)%s+(%d+)")
	if monthName and year then
		return d:set(year, monthName)
	end

	millennium = mw.ustring.match(dateString, "^(%d+)%s+хилядолетие")
	if millennium then
		d.millennium = millennium
		return d
	end
	century = mw.ustring.match(dateString, "^(%d+)%s+век")
	if century then
		d.century = century
		return d
	end
	decade = mw.ustring.match(dateString, "^(%d+)%-те")
	if decade then
		d.decade = decade
		return d
	end
	year = mw.ustring.match(dateString, "^(%d+)")
	if year then
		return d:set(year)
	end

	return d
end

function age(dateOfBirth, dateOfDeath)
	if not dateOfBirth.year then
		return nil
	end
	if isEmpty(dateOfDeath) or dateOfDeath:isEmpty() then
		dateOfDeath = Date.currentDate()
	elseif dateOfDeath.unknown then
		return nil
	end
	local startDate = dateForCalc(dateOfBirth)
	local endDate = dateForCalc(dateOfDeath)
	if dateOfBirth.bce and not dateOfDeath.bce then
		-- reverse the sign and subtract one year
		startDate = startDate * -1 + 10000
	end
	local age = math.abs(math.floor((endDate - startDate) / 10000))
	if age > 125 then -- put a max age
		return nil
	end
	return age
end

function dateForCalc(date)
	return string.format("%d%02d%02d", date.year or 0, date.month or 0, date.day or 0)
end

function bceSuffix(isBce)
	if isBce then return " пр.н.е." else return "" end
end

function birthCategories(date)
	local cats = {}
	if date.millennium then
		table.insert(cats, "Родени през " .. date.millennium .. " хилядолетие" .. bceSuffix(date.bce))
	end
	if date.century then
		table.insert(cats, "Родени през " .. date.century .. " век" .. bceSuffix(date.bce))
	end
	if date.decade then
		table.insert(cats, "Родени през " .. date.decade .. "-те години" .. bceSuffix(date.bce))
	end
	if date.year then
		table.insert(cats, "Родени през " .. date.year .. " година" .. bceSuffix(date.bce))
	end
	if date.day and date.monthName then
		table.insert(cats, "Родени на " ..  date.day .. " " .. date.monthName)
	end
	if date.unknown then
		table.insert(cats, "Статии за личности с неизвестна година на раждане")
	end
	if date.julian and isAfterGregorianIntroduced(date) then
		table.insert(cats, "Статии с дати на раждане или смърт по стар стил")
	end
	return cats
end

function deathCategories(date)
	local cats = {}
	if date.millennium then
		table.insert(cats, "Починали през " ..  date.millennium .. " хилядолетие" .. bceSuffix(date.bce))
	end
	if date.century then
		table.insert(cats, "Починали през " ..  date.century .. " век" .. bceSuffix(date.bce))
	end
	if date.decade then
		table.insert(cats, "Починали през " ..  date.decade .. "-те години" .. bceSuffix(date.bce))
	end
	if date.year then
		table.insert(cats, "Починали през " ..  date.year .. " година" .. bceSuffix(date.bce))
	end
	if date.day and date.monthName then
		table.insert(cats, "Починали на " ..  date.day .. " " .. date.monthName)
	end
	if date.unknown then
		table.insert(cats, "Статии за личности с неизвестна година на смърт")
	end
	if date.julian and isAfterGregorianIntroduced(date) then
		table.insert(cats, "Статии с дати на раждане или смърт по стар стил")
	end
	return cats
end

function prepareBirthDateVarsWikidata(eid)
	local vars = {}
	vars.date = Date:fromWikidata(eid, 'P569')

	if isEmpty(wd._property({eid, 'P570'})) then 
		vars.age = age(vars.date)
	end
	vars.cats = birthCategories(vars.date)

	return vars
end

function prepareDeathDateVarsWikidata(eid)
	local vars = {}
	vars.date = Date:fromWikidata(eid, 'P570')
	if isEmpty(vars.date._) then
		return nil
	end

	local birthDate = Date:fromWikidata(eid, 'P569')
	if not isEmpty(birthDate._) then
		vars.age = age(birthDate, vars.date)
	end
	vars.cats = deathCategories(vars.date)

	return vars
end

function formatAgeSuffix(age)
	return '<span class="noprint"> <small>('.. age .. ' г.)</small></span>'
end

function isJulian(calendar)
	-- The "Q" things are calendarmodels as returned by Wikidata queries.
	-- For more information, see [[:wikidata:Help:Dates]].
	local julian_items = {
		["Q11184"] = true,
		["Q1985786"] = true,
		["юлиански"] = true,
	}
	-- Equivalent to Python's "if calendar in julian_items".
	return julian_items[calendar]
end

function isAfterGregorianIntroduced(date)
	-- Shouldn't be possible if calendarmodel is defined, but best be safe.
	if date.unknown then
		return false
	end
	-- All dates BC are definitely before Gregorian has been introduced.
	if date.bce then
		return false
	end
	-- Not sure what comparison with nil would return so check if defined first.
	-- Feel free to simplify if you know it's an overkill.
	if date.year and tonumber(date.year) < 1583 then
		return false
	end
	if date.decade and tonumber(date.decade) < 1580 then
		return false
	end
	if date.century and tonumber(date.century) < 16 then
		return false
	end
	-- The calendar makes very little sense with millennium level accuracy.
	if date.millennium then
		return false
	end
	
	-- After or in 1583, the 1580s, or the 16th century.
	return true
end

function formatDate(vars, calendar)
	if vars == nil then return '' end

	local output = ''
	local earliest = ''
	local latest = ''
	local sourcing = ''
	local str = wd._property({'linked', 'qualifier', 'qualifier', 'qualifier', vars.date.q, vars.date.p, 'P1319', 'P1326', 'P1480', format='o=%p[\ne=%q1][\nl=%q2][\ns=%q3]'})
	for m in mw.ustring.gmatch(str, '[^\n]+') do
		m = mw.text.trim(m)
		output = mw.ustring.match(m, '^o=(.+)$') or output
		earliest = mw.ustring.match(m, '^e=(.+)$') or earliest
		latest = mw.ustring.match(m, '^l=(.+)$') or latest
		sourcing = mw.ustring.match(m, '^s=(.+)$') or sourcing
	end
	if output == '' then return '' end

	if output == 'неизв.' then
		if (earliest ~= '') and (latest ~= '') then
			output = 'между ' .. earliest .. ' и ' .. latest
		elseif (earliest ~= '') then
			output = 'не по-рано от ' .. earliest
		elseif (latest ~= '') then
			output = 'не по-късно от ' .. latest
		else
			output = 'неизв.'
		end
	else
		if sourcing ~= '' then
			output = sourcing .. ' ' .. output
		end
	end

	if vars.age then
		output = output .. formatAgeSuffix(vars.age)
	end
	output = '<span class="oneline">' .. output .. '</span>'
	for k, category in pairs(vars.cats) do
		output = output .. '[[Категория:' .. category .. ']]'
	end
	return output
end

function inArray (array, value)
    for index, v in ipairs(array) do
        if v == value then
            return true
        end
    end

    return false
end

function isCountry(qid)
        if isSettlement(qid, 0) then
                return false
        end

        local countries = {'Q6256', 'Q3624078', 'Q1151405', 'Q161243', 'Q15239622', 'Q2577883', 'Q3024240', 'Q6726158', 'Q15634554'}
        local s = wd._properties ({ 'raw', qid, 'P31', sep='', ["sep%s"]='\t' })

        for token in string.gmatch(s, "[^\t]+") do
                if inArray(countries, token) then
                        return true
                end
        end
        return false
end

function isSettlement(qid, iterate)
        local notsettlements = {'Q123705'}
        local settlements = {'Q486972', 'Q3957', 'Q7930989', 'Q10354598', 'Q498162', 'Q17343829', 'Q22674925', 'Q1529096', 'Q515', 'Q484170', 'Q15630849', 'Q89487741', 'Q562061', 'Q667509', 'Q2039348', 'Q5084', 'Q1802801', 'Q494721', 'Q493522', 'Q51049922'}
        local s = wd._properties ({ 'raw', qid, 'P31', sep='', ["sep%s"]='\t' })
        if s == '' then
                s = wd._properties ({ 'raw', qid, 'P279', sep='', ["sep%s"]='\t' })
        end
        local t = false

        for token in string.gmatch(s, "[^\t]+") do
                if inArray(notsettlements, token) then
                        return false
                end
                if inArray(settlements, token) then
                        return true
                end
                if iterate > 0 then
                        t = isSettlement(token, iterate - 1)
                        if t then
                                return true
                        end
                end
        end
        return false
end

function findSettlement(qid, date, iterate)
        local s = wd._properties ({ 'raw', qid, 'P131', sep='', ["sep%s"]='\t' , ["date"]=date })
        local t = ''
        for token in string.gmatch(s, "[^\t]+") do
                if isSettlement(token, 1) then
                        return token
                end
                if iterate > 0 then
                        t = findSettlement(token, date, iterate - 1)
                        if t ~= '' then
                                return t
                        end
                end
        end
        return ''
end

function joinStrings(strings, separator)
        local res = ''
        for i, s in ipairs(strings) do
                if (s ~= '') and (s ~= nil) then
                        res = res .. s .. separator
                end
        end
        if res ~= '' then
                res = string.sub (res, 1, string.len(res) - string.len(separator))
        end
        return res
end

function relabel(link, newlabel)
	if newlabel ~= '' then
		if link:find("]]") ~= nil then
			if link:find("|") ~= nil then
				link = link:gsub("|.*]]", "|" .. newlabel .. "]]")
			else
				link = link:gsub("]]", "|" .. newlabel .. "]]")
			end
		else
			link = newlabel
		end
	end
	return link
end

function lsc(location, defcountry, date, earliestdate, latestdate)
	local s = ''
	local d = ''

	if location == '' or location == ' ' then
		return ''
	end

	local settlement = ''
	local lstr = wd._label({ 'linked', location })
	local sstr = ''
	local cstr = defcountry or ''

	if isCountry(location) then
		return lstr
	end

    d = mw.text.trim(date or '')

    if d == '' then
      d = mw.text.trim(latestdate or '')   -- tries with latest date
    end
    if d == '' then
      d = mw.text.trim(earliestdate or '')   -- tries with earliest date
    end
	if isSettlement(location, 1) then
		if cstr == '' then
			country = wd._property({'raw', 'deprecated+', location, 'P17', date=d})
			if country ~= '' then
				cstr = wd._label({ 'linked', country})
				cstr = relabel(cstr, wd._property({'linked', 'deprecated+', country, 'P1448', date=d}))
			end
		end
		lstr = relabel(lstr, wd._property({'linked', 'deprecated+', location, 'P1448', date=d}))
	else
		settlement = findSettlement(location, date, 3)
		if settlement ~= '' then
			if cstr == '' then
				country = wd._property({'raw', 'deprecated+', settlement, 'P17', date=d})
				if country ~= '' then
					cstr = wd._label({ 'linked', country})
					cstr = relabel(cstr, wd._property({'linked', 'deprecated+', country, 'P1448', date=d}))
				end
			end
			sstr = wd._label({ 'linked', settlement})
			sstr = relabel(sstr, wd._property({'linked', 'deprecated+', settlement, 'P1448', date=d}))
		else
			if cstr == '' then
				country = wd._property({'raw', 'deprecated+', location, 'P17', date=d})
				if country ~= '' then
					cstr = wd._label({ 'linked', country})
					cstr = relabel(cstr, wd._property({'linked', 'deprecated+', country, 'P1448', date=d}))
				end
			end
		end
	end
	if lstr == sstr then sstr = '' end
	if lstr == cstr then cstr = '' end
	if sstr == cstr then cstr = '' end
	return joinStrings({lstr, sstr, cstr}, ', ')
end

function p.lsc(frame)
    return lsc(frame.args[1], frame.args[3], frame.args[2], frame.args[4], frame.args[5])
end

function p.birth_date(frame)
	local eid = frame.args[1] or ''
	return formatDate(prepareBirthDateVarsWikidata(eid))
end

function p.death_date(frame)
	local eid = frame.args[1] or ''
	return formatDate(prepareDeathDateVarsWikidata(eid))
end

function p.service_years(frame)
	local eid = frame.args[1] or ''
	local occupation = 'P106'
	local militaryPersonnel = 'Q47064'
	local startTime = 'P580'
	local endTime = 'P582'
	local serviceStart = wd._qualifier({'raw', eid, occupation, militaryPersonnel, startTime}) or ''
	local serviceEnd = wd._qualifier({'raw', eid, occupation, militaryPersonnel,  endTime}) or ''

	if serviceStart == '' and serviceEnd == '' then
		return ''
	end

	local yearStart = string.match(serviceStart, "%d+") or ''
	local yearEnd = string.match(serviceEnd, "%d+") or ''

	if yearStart == '' then
		serviceYears = 'до ' .. yearEnd
	elseif yearEnd == '' then
		serviceYears = 'от ' .. yearStart
	else
		serviceYears = yearStart .. ' – ' .. yearEnd
	end

	return serviceYears .. ' г.'
end

return p