Module:FamilyTree: Difference between revisions

From KB Lexicon
(Created page with "local p = {} local cargo = mw.ext.cargo local function esc(value) if not value then return '' end value = tostring(value) value = value:gsub('\\', '\\\\') value = value:gsub('"', '\\"') return value end local function cargoQuery(tables, fields, args) args = args or {} local ok, result = pcall(function() return cargo.query(tables, fields, args) end) if ok and result then return result end return {} end local function trim(s) if not s then return...")
 
No edit summary
Line 38: Line 38:
end
end


local function addToSet(set, value)
local function addUnique(list, seen, value)
value = trim(value)
value = trim(value)
if value then
if value and not seen[value] then
set[value] = true
seen[value] = true
table.insert(list, value)
end
end
end
local function sorted(list)
table.sort(list, function(a, b)
return a:lower() < b:lower()
end)
return list
end
end


Line 67: Line 75:
end
end


local function getNeighbors(person)
local function getCharacter(pageName)
local neighbors = {}
pageName = trim(pageName)
if not pageName then
return nil
end
 
local rows = cargoQuery(
'Characters',
'Page,DisplayName,Gender,BirthDate,DeathDate,Status',
{
where = 'Page="' .. esc(pageName) .. '"',
limit = 1
}
)
 
return rows[1]
end
 
local function getParents(person)
local rows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
where = 'Child="' .. esc(person) .. '"',
limit = 20
}
)


person = trim(person)
local parents = {}
if not person then
local seen = {}
return neighbors
 
for _, row in ipairs(rows) do
addUnique(parents, seen, row.Parent1)
addUnique(parents, seen, row.Parent2)
end
end


local unionRows = cargoQuery(
return sorted(parents), rows[1]
end
 
local function getChildren(person)
local rows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
where = 'Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
limit = 200
}
)
 
local children = {}
local seen = {}
 
table.sort(rows, function(a, b)
local aOrder = tonumber(a.BirthOrder) or 9999
local bOrder = tonumber(b.BirthOrder) or 9999
if aOrder == bOrder then
return tostring(a.Child):lower() < tostring(b.Child):lower()
end
return aOrder < bOrder
end)
 
for _, row in ipairs(rows) do
addUnique(children, seen, row.Child)
end
 
return children
end
 
local function getPartners(person)
local rows = cargoQuery(
'Unions',
'Unions',
'UnionID,Partner1,Partner2',
'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
{
{
where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
limit = 500
limit = 50
}
}
)
)


for _, row in ipairs(unionRows) do
local partners = {}
addToSet(neighbors, row.Partner1)
local seen = {}
addToSet(neighbors, row.Partner2)
 
for _, row in ipairs(rows) do
if trim(row.Partner1) == person then
addUnique(partners, seen, row.Partner2)
elseif trim(row.Partner2) == person then
addUnique(partners, seen, row.Partner1)
end
end
end


local parentChildRows = cargoQuery(
return sorted(partners), rows
end
 
local function getSiblings(person)
local rows = cargoQuery(
'ParentChild',
'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
{
where = 'Parent1 HOLDS ""',
limit = 1000
}
)
 
local targetRows = cargoQuery(
'ParentChild',
'ParentChild',
'Child,Parent1,Parent2,UnionID',
'Child,Parent1,Parent2,UnionID',
{
{
where = 'Child="' .. esc(person) .. '" OR Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
where = 'Child="' .. esc(person) .. '"',
limit = 1000
limit = 10
}
}
)
)


for _, row in ipairs(parentChildRows) do
if not targetRows[1] then
addToSet(neighbors, row.Child)
return {}
addToSet(neighbors, row.Parent1)
end
addToSet(neighbors, row.Parent2)
 
local targetUnion = trim(targetRows[1].UnionID)
local targetP1 = trim(targetRows[1].Parent1)
local targetP2 = trim(targetRows[1].Parent2)
 
local siblings = {}
local seen = {}
 
for _, row in ipairs(rows) do
local sameUnion = targetUnion and trim(row.UnionID) == targetUnion
local sameParents = trim(row.Parent1) == targetP1 and trim(row.Parent2) == targetP2
 
if (sameUnion or sameParents) and trim(row.Child) ~= person then
addUnique(siblings, seen, row.Child)
end
end
end


neighbors[person] = nil
return sorted(siblings)
end


return neighbors
local function makeLinkedName(pageName)
local displayName = getDisplayName(pageName)
return '[[' .. pageName .. '|' .. displayName .. ']]'
end
end


local function collectConnectedComponent(root)
function p.connected(frame)
local visited = {}
local args = frame.args
local queue = {}
local parentArgs = frame:getParent() and frame:getParent().args or {}
local head = 1
local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])


root = trim(root)
if not root then
if not root then
return visited
return 'Error: no root provided. Use root=Character Name'
end
end


local visited = {}
local queue = { root }
local head = 1
visited[root] = true
visited[root] = true
table.insert(queue, root)


while head <= #queue do
while head <= #queue do
Line 126: Line 232:
head = head + 1
head = head + 1


local neighbors = getNeighbors(current)
local parents = getParents(current)
local children = getChildren(current)
local partners = getPartners(current)


for neighbor, _ in pairs(neighbors) do
for _, person in ipairs(parents) do
if not visited[neighbor] then
if not visited[person] then
visited[neighbor] = true
visited[person] = true
table.insert(queue, neighbor)
table.insert(queue, person)
end
end
end
end
end


return visited
for _, person in ipairs(children) do
end
if not visited[person] then
visited[person] = true
table.insert(queue, person)
end
end


local function sortedKeys(set)
for _, person in ipairs(partners) do
local arr = {}
if not visited[person] then
visited[person] = true
table.insert(queue, person)
end
end
end


for key, _ in pairs(set) do
local people = {}
table.insert(arr, key)
for name, _ in pairs(visited) do
table.insert(people, name)
end
end
sorted(people)


table.sort(arr, function(a, b)
local lines = {}
return a:lower() < b:lower()
table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
end)
table.insert(lines, '* Total people found: ' .. tostring(#people))
for _, person in ipairs(people) do
table.insert(lines, '* ' .. makeLinkedName(person))
end


return arr
return table.concat(lines, '\n')
end
end


function p.connected(frame)
function p.profile(frame)
local args = frame.args
local args = frame.args
local parentArgs = frame:getParent() and frame:getParent().args or {}
local parentArgs = frame:getParent() and frame:getParent().args or {}
local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])


Line 163: Line 283:
end
end


local component = collectConnectedComponent(root)
local parents = getParents(root)
local people = sortedKeys(component)
local siblings = getSiblings(root)
local partners = getPartners(root)
local children = getChildren(root)
 
local lines = {}
table.insert(lines, '{| class="wikitable" style="width:100%; max-width:900px;"')
table.insert(lines, '|-')
table.insert(lines, '! colspan="2" | Family profile for ' .. getDisplayName(root))
table.insert(lines, '|-')
table.insert(lines, '! style="width:20%;" | Person')
table.insert(lines, '| ' .. makeLinkedName(root))


if #people == 0 then
table.insert(lines, '|-')
return 'No connected people found for ' .. root
table.insert(lines, '! Parents')
if #parents > 0 then
table.insert(lines, '| ' .. table.concat(mw.text.map(parents, makeLinkedName), '<br>'))
else
table.insert(lines, '| —')
end
end


local lines = {}
table.insert(lines, '|-')
table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
table.insert(lines, '! Siblings')
table.insert(lines, '* Total people found: ' .. tostring(#people))
if #siblings > 0 then
table.insert(lines, '| ' .. table.concat(mw.text.map(siblings, makeLinkedName), '<br>'))
else
table.insert(lines, '| —')
end
 
table.insert(lines, '|-')
table.insert(lines, '! Partners')
if #partners > 0 then
table.insert(lines, '| ' .. table.concat(mw.text.map(partners, makeLinkedName), '<br>'))
else
table.insert(lines, '| —')
end


for _, pageName in ipairs(people) do
table.insert(lines, '|-')
local displayName = getDisplayName(pageName)
table.insert(lines, '! Children')
table.insert(lines, '* [[' .. pageName .. '|' .. displayName .. ']]')
if #children > 0 then
table.insert(lines, '| ' .. table.concat(mw.text.map(children, makeLinkedName), '<br>'))
else
table.insert(lines, '| —')
end
end
table.insert(lines, '|}')


return table.concat(lines, '\n')
return table.concat(lines, '\n')

Revision as of 21:02, 27 March 2026

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

local p = {}

local cargo = mw.ext.cargo

local function esc(value)
	if not value then
		return ''
	end
	value = tostring(value)
	value = value:gsub('\\', '\\\\')
	value = value:gsub('"', '\\"')
	return value
end

local function cargoQuery(tables, fields, args)
	args = args or {}
	local ok, result = pcall(function()
		return cargo.query(tables, fields, args)
	end)

	if ok and result then
		return result
	end

	return {}
end

local function trim(s)
	if not s then
		return nil
	end
	s = tostring(s)
	s = mw.text.trim(s)
	if s == '' then
		return nil
	end
	return s
end

local function addUnique(list, seen, value)
	value = trim(value)
	if value and not seen[value] then
		seen[value] = true
		table.insert(list, value)
	end
end

local function sorted(list)
	table.sort(list, function(a, b)
		return a:lower() < b:lower()
	end)
	return list
end

local function getDisplayName(pageName)
	pageName = trim(pageName)
	if not pageName then
		return nil
	end

	local rows = cargoQuery(
		'Characters',
		'Page,DisplayName',
		{
			where = 'Page="' .. esc(pageName) .. '"',
			limit = 1
		}
	)

	if rows[1] and trim(rows[1].DisplayName) then
		return trim(rows[1].DisplayName)
	end

	return pageName
end

local function getCharacter(pageName)
	pageName = trim(pageName)
	if not pageName then
		return nil
	end

	local rows = cargoQuery(
		'Characters',
		'Page,DisplayName,Gender,BirthDate,DeathDate,Status',
		{
			where = 'Page="' .. esc(pageName) .. '"',
			limit = 1
		}
	)

	return rows[1]
end

local function getParents(person)
	local rows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{
			where = 'Child="' .. esc(person) .. '"',
			limit = 20
		}
	)

	local parents = {}
	local seen = {}

	for _, row in ipairs(rows) do
		addUnique(parents, seen, row.Parent1)
		addUnique(parents, seen, row.Parent2)
	end

	return sorted(parents), rows[1]
end

local function getChildren(person)
	local rows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{
			where = 'Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
			limit = 200
		}
	)

	local children = {}
	local seen = {}

	table.sort(rows, function(a, b)
		local aOrder = tonumber(a.BirthOrder) or 9999
		local bOrder = tonumber(b.BirthOrder) or 9999
		if aOrder == bOrder then
			return tostring(a.Child):lower() < tostring(b.Child):lower()
		end
		return aOrder < bOrder
	end)

	for _, row in ipairs(rows) do
		addUnique(children, seen, row.Child)
	end

	return children
end

local function getPartners(person)
	local rows = cargoQuery(
		'Unions',
		'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
		{
			where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
			limit = 50
		}
	)

	local partners = {}
	local seen = {}

	for _, row in ipairs(rows) do
		if trim(row.Partner1) == person then
			addUnique(partners, seen, row.Partner2)
		elseif trim(row.Partner2) == person then
			addUnique(partners, seen, row.Partner1)
		end
	end

	return sorted(partners), rows
end

local function getSiblings(person)
	local rows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
		{
			where = 'Parent1 HOLDS ""',
			limit = 1000
		}
	)

	local targetRows = cargoQuery(
		'ParentChild',
		'Child,Parent1,Parent2,UnionID',
		{
			where = 'Child="' .. esc(person) .. '"',
			limit = 10
		}
	)

	if not targetRows[1] then
		return {}
	end

	local targetUnion = trim(targetRows[1].UnionID)
	local targetP1 = trim(targetRows[1].Parent1)
	local targetP2 = trim(targetRows[1].Parent2)

	local siblings = {}
	local seen = {}

	for _, row in ipairs(rows) do
		local sameUnion = targetUnion and trim(row.UnionID) == targetUnion
		local sameParents = trim(row.Parent1) == targetP1 and trim(row.Parent2) == targetP2

		if (sameUnion or sameParents) and trim(row.Child) ~= person then
			addUnique(siblings, seen, row.Child)
		end
	end

	return sorted(siblings)
end

local function makeLinkedName(pageName)
	local displayName = getDisplayName(pageName)
	return '[[' .. pageName .. '|' .. displayName .. ']]'
end

function p.connected(frame)
	local args = frame.args
	local parentArgs = frame:getParent() and frame:getParent().args or {}
	local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])

	if not root then
		return 'Error: no root provided. Use root=Character Name'
	end

	local visited = {}
	local queue = { root }
	local head = 1
	visited[root] = true

	while head <= #queue do
		local current = queue[head]
		head = head + 1

		local parents = getParents(current)
		local children = getChildren(current)
		local partners = getPartners(current)

		for _, person in ipairs(parents) do
			if not visited[person] then
				visited[person] = true
				table.insert(queue, person)
			end
		end

		for _, person in ipairs(children) do
			if not visited[person] then
				visited[person] = true
				table.insert(queue, person)
			end
		end

		for _, person in ipairs(partners) do
			if not visited[person] then
				visited[person] = true
				table.insert(queue, person)
			end
		end
	end

	local people = {}
	for name, _ in pairs(visited) do
		table.insert(people, name)
	end
	sorted(people)

	local lines = {}
	table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
	table.insert(lines, '* Total people found: ' .. tostring(#people))
	for _, person in ipairs(people) do
		table.insert(lines, '* ' .. makeLinkedName(person))
	end

	return table.concat(lines, '\n')
end

function p.profile(frame)
	local args = frame.args
	local parentArgs = frame:getParent() and frame:getParent().args or {}
	local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])

	if not root then
		return 'Error: no root provided. Use root=Character Name'
	end

	local parents = getParents(root)
	local siblings = getSiblings(root)
	local partners = getPartners(root)
	local children = getChildren(root)

	local lines = {}
	table.insert(lines, '{| class="wikitable" style="width:100%; max-width:900px;"')
	table.insert(lines, '|-')
	table.insert(lines, '! colspan="2" | Family profile for ' .. getDisplayName(root))
	table.insert(lines, '|-')
	table.insert(lines, '! style="width:20%;" | Person')
	table.insert(lines, '| ' .. makeLinkedName(root))

	table.insert(lines, '|-')
	table.insert(lines, '! Parents')
	if #parents > 0 then
		table.insert(lines, '| ' .. table.concat(mw.text.map(parents, makeLinkedName), '<br>'))
	else
		table.insert(lines, '| —')
	end

	table.insert(lines, '|-')
	table.insert(lines, '! Siblings')
	if #siblings > 0 then
		table.insert(lines, '| ' .. table.concat(mw.text.map(siblings, makeLinkedName), '<br>'))
	else
		table.insert(lines, '| —')
	end

	table.insert(lines, '|-')
	table.insert(lines, '! Partners')
	if #partners > 0 then
		table.insert(lines, '| ' .. table.concat(mw.text.map(partners, makeLinkedName), '<br>'))
	else
		table.insert(lines, '| —')
	end

	table.insert(lines, '|-')
	table.insert(lines, '! Children')
	if #children > 0 then
		table.insert(lines, '| ' .. table.concat(mw.text.map(children, makeLinkedName), '<br>'))
	else
		table.insert(lines, '| —')
	end

	table.insert(lines, '|}')

	return table.concat(lines, '\n')
end

return p