Clean Living Persons.fh_lua

--[[
@Title:			Clean Living Persons
@Type:				Standard
@Author:			Mike Tate
@Contributors:	Jane Taubman
@Version:			2.0
@Keywords:		
@LastUpdated:		16 Feb 2026
@Licence:			This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description:		Privatise Living People and Private People.
					When sharing files on line, it is a good idea to remove any detailed information.
					This plugin offers several options to reduce the amount of information in a GEDCOM file for living people.
					It is designed to be used in conjunction with the Export GEDCOM File and Split Tree Helper commands,
					and will prompt for confirmation if you try to use it on a GEDCOM that is open in Project mode.
					It will take note of the Living Flag on a person, but can be additionally set to assume anyone
					with an estimated birth date after a selected date and with no death date could be living.
					Options for living people include:
					1. Change Name to either just initials or initials and Surname, or change the Name to Private.
					2. Remove all Facts except optionally BMD Years.
					3. Remove all Dates and Sources from Facts.
					4. Remove associated Notes or Media or Sources.
					5. Clean "orphaned" Notes, Sources, and Media from the file.

@V2.0:				Centre windows on FH window; Check for Updates button; Ch-ViS 1.4; Better GetDayNumber() error reports;
@V1.9:				Support FSO, multi-monitors, replace iup.GetParam(), etc...;
@V1.8:				Update in preparation for iup.dialog version;
@V1.7:				FH V7 Lua 3.5 IUP 3.28 compatible;
@V1.6:				Fix the Day Number for invalid dates, and cater for non-alphabetic names and [unnamed person], and correctly use same .dat file for all Plugin versions.
@V1.5:				Allows 'Do Not Clean Living Persons' Named List to override the 'Living' status, cater for blank Date changes, use same .dat file for all Plugin versions. 
@V1.4:				Now remembers its screen position.
@V1.3:				Published in Plugin Store to supersede Clean Living People.
@V1.2.9:			Minor updates prior to publishing.
@V1.2.8:			Revise options wording.
@V1.2.7:			Add new options and features, revised estimated Birth/Death dates fix.
@V1.2.6:			Lookup table of functions for each level 1 tag to improve efficiency and simplify changes.
@V1.2.5:			Handle LDS Ordinances as if facts.
@V1.2.4:			When changing Names also remove subsidiary Name fields, Alternate Names, Aliases, Titles, and INDI/FAMS whole record Sources.
@V1.2.3:			Fix problem with V1.2.2 not removing facts correctly.
@V1.2.2:			Add selection of estimated date function option i.e EARLIEST, MID, or LATEST, default is MID.
@V1.2.1:			Add a check to use the Spouses birth date for people whose estimated birth could not be computed.
					Add option to select living for people whose birth date can not be computed. 
@V1.1:				Fixed Problem where Notes were removed when media was removed.
]]

require("iuplua")
iup.SetGlobal("CUSTOMQUITMESSAGE","YES")										-- Needed for IUP 3.28

require("luacom")																	-- To create File System Object	-- V1.8
FSO = luacom.CreateObject("Scripting.FileSystemObject")

if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end				-- Needed for Unicode
if fhGetAppVersion() > 6 then unpack = table.unpack end						-- Needed for Lua 5.3

local strVersion = "2.0"															-- Update version and title here	-- V2.0
local strPlugin  = "Clean Living Persons"
local strPluginName = strPlugin.."  "..strVersion								
local tblRecordList = {}															-- List of records to process, with Record Id of records in list
local tblDeleteList = {}															-- List of pointers to delete, with Record Id of records in list
local tblChangeList = {}															-- List of entries to change, with ptr, value & type fields
local isOkForDelete = true															-- Is subsidiary item Ok to delete, or has parent been deleted
local isPrimaryName = true															-- Is this the Primary Name, or an Alternate, Alias, or Title

-- Split a string using "," or chosen separator --

function split(strTxt,strSep)
	local tblFields = {}
	local strPattern = string.format("([^%s]+)", strSep or ",")
	strTxt = tostring(strTxt or "")
	strTxt:gsub(strPattern, function(strField) tblFields[#tblFields+1] = strField end)
	return tblFields
end -- function split

-- Report error message --
local function doError(strMessage,errFunction)
	-- strMessage		~ error message text
	-- errFunction		~ optional error reporting function
	if type(errFunction) == "function" then
		errFunction(strMessage)
	else
		error(strMessage)
	end
end -- local function doError

-- Convert filename to ANSI alternative and indicate success --
function FileNameToANSI(strFileName,strAnsiName)
	-- strFileName		~ full file path
	-- strAnsiFile		~ ANSI file name & type
	-- return values	~ ANSI file path, true if original path was ANSI compatible
	if stringx.encoding() == "ANSI" then return strFileName, true end
	local isFlag = fhIsConversionLossFlagSet()
	fhSetConversionLossFlag(false)
	local strAnsi = fhConvertUTF8toANSI(strFileName)
	local wasAnsi = true
	if fhIsConversionLossFlagSet() then
		strAnsiName = strAnsiName or "ANSI.ANSI"
		strAnsi = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"..strAnsiName
		wasAnsi = false
	end
	fhSetConversionLossFlag(isFlag)
	return strAnsi, wasAnsi
end -- local function FileNameToANSI

-- Get parent folder --
function GetParentFolder(strFileName)
	-- strFileName		~ full file path
	-- return value		~ parent folder path
	local strParent = FSO:GetParentFolderName(strFileName)	--! Faulty in FH v6 with Unicode chars in path
	if fhGetAppVersion() == 6 then
		local _, wasAnsi = FileNameToANSI(strFileName)
		if not wasAnsi then
			strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
		end
	end
	return strParent
end -- function GetParentFolder

-- Check if file exists --
function FlgFileExists(strFileName)
	-- strFileName		~ full file path
	-- return value		~ true if it exists
	return FSO:FileExists(strFileName)
end -- function FlgFileExists

-- Delete a file if it exists --
function DeleteFile(strFileName,errFunction)
	-- strFileName		~ full file path
	-- errFunction		~ optional error reporting function
	-- return value		~ true if file does not exist or is deleted else false
	if FSO:FileExists(strFileName) then
		FSO:DeleteFile(strFileName,true)
		if FSO:FileExists(strFileName) then
			doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
			return false
		end
	end
	return true
end -- function DeleteFile

-- Copy a file if it exists and destination is not a folder --
function CopyFile(strFileName,strDestination)
	-- strFileName		~ full source file path
	-- strDestination	~ full target file path
	-- return value		~ true if file exists and is copied else false
	if MakeFolder(GetParentFolder(strDestination)) and FSO:FileExists(strFileName) and not FSO:FolderExists(strDestination) then
		FSO:CopyFile(strFileName,strDestination)
		if FSO:FileExists(strDestination) then
			return true
		end
	end
	return false
end -- function CopyFile

-- Move a file if it exists and destination is not a folder --
function MoveFile(strFileName,strDestination)
	-- strFileName		~ full source file path
	-- strDestination	~ full target file path
	-- return value		~ true if file exists and is moved else false
	if MakeFolder(GetParentFolder(strDestination)) and FSO:FileExists(strFileName) and not FSO:FolderExists(strDestination) then
		if DeleteFile(strDestination) then
			FSO:MoveFile(strFileName,strDestination)
			if FSO:FileExists(strDestination) then
				return true
			end
		end
	end
	return false
end -- function MoveFile

local function CreateFolder(strFolderName)				-- V3.3
	FSO:CreateFolder(strFolderName)
end -- local function CreateFolder(strFolderName)

-- Make subfolder recursively if does not exist --
function MakeFolder(strFolderName,errFunction)
	-- strFolderName	~ full source folder path
	-- errFunction		~ optional error reporting function
	-- return value		~ true if folder exists or created else false
	if not FSO:FolderExists(strFolderName) then
		if #strFolderName > 4									-- V3.3
		and not MakeFolder(GetParentFolder(strFolderName),errFunction) then
			return false
		end
		if not pcall(CreateFolder,strFolderName)			-- V3.3
		or not FSO:FolderExists(strFolderName) then
			doError("Cannot Make Folder Path:                 \n"..strFolderName.." \n",errFunction)
			return false
		end
	end
	return true
end -- function MakeFolder

-- Open File with ANSI path and return Handle --
function OpenFile(strFileName,strMode)
	-- strFileName		~ full file path
	-- strMode			~ "r", "w", "a" optionally suffixed with "+" &/or "b"
	-- return value		~ file handle
	local fileHandle, strError = io.open(strFileName,strMode)
	if fileHandle == nil then
		error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
	end
	return fileHandle
end -- function OpenFile

-- Save string to file --
function SaveStringToFile(strContents,strFileName,strFormat)
	-- strContents		~ text string
	-- strFileName		~ full file path
	-- strFormat			~ optional "UTF-8" or "UTF-16LE"
	-- return value		~ true if successful else false
	strFormat = strFormat or "UTF-8"
	if fhGetAppVersion() > 6 then
		return fhSaveTextFile(strFileName,strContents,strFormat)
	end
	local strAnsi, wasAnsi = FileNameToANSI(strFileName)
	local fileHandle = OpenFile(strAnsi,"w")
	fileHandle:write(strContents)
	assert(fileHandle:close())
	if not wasAnsi then
		MoveFile(strAnsi,strFileName)
	end
	return true
end -- function SaveStringToFile

-- Load string from file --
function StrLoadFromFile(strFileName,strFormat)
	-- strFileName		~ full file path
	-- strFormat			~ optional "UTF-8" or "UTF-16LE"
	-- return value		~ file contents
	strFormat = strFormat or "UTF-8"
	if fhGetAppVersion() > 6 then
		return fhLoadTextFile(strFileName,strFormat)
	end
	local strAnsi, wasAnsi = FileNameToANSI(strFileName)
	if not wasAnsi then
		CopyFile(strFileName,strAnsi)
	end
	local fileHandle = OpenFile(strAnsi,"r")
	local strContents = fileHandle:read("*all")
	assert(fileHandle:close())
	return strContents
end -- function StrLoadFromFile

--[[
@Module:			+fh+progbar_v3
@Author:			Mike Tate
@Version:			3.1
@LastUpdated:		23 Jan 2026
@Description:		Progress Bar library module.
@V3.1:				Use NATIVEPARENT amd CENTERPARENT.
@V3.0:				Function Prototype Closure version.
@V1.0:				Initial version.
]]

local function progbar_v3()

	local fh = {}														-- Local environment table

	require "iuplua"													-- To access GUI window builder

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")					-- Needed for IUP 3.28

	local tblBars = {}												-- Table for optional external attributes
	local strBack = "255 255 255"									-- Background colour default is white
	local strBody = "0 0 0"											-- Body text colour default is black
	local strFont = nil												-- Font dialogue default is current font
	local strStop = "255 0 0"										-- Stop button colour default is red
	local intPosX = iup.CENTERPARENT								-- Show window default position is central	-- V3.1
	local intPosY = iup.CENTERPARENT
	local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
	local lblText, barGauge, lblDelta, btnStop, dlgGauge

	local function doFocus()										-- Bring the Progress Bar window into Focus
		dlgGauge.BringFront="YES"									-- If used too often, inhibits other windows scroll bars, etc
	end -- local function doFocus

	local function doUpdate()										-- Update the Progress Gauge and the Delta % with clock
		barGauge.Value = intVal
		lblDelta.Title = string.format("%4d %%      %s ",math.floor(intPercent),strClock)
	end -- local function doUpdate

	local function doReset()										-- Reset all dialogue variables and Update display
		intVal		= 0													-- Current value of Progress Bar
		intPercent= 0.01												-- Percentage of progress
		intStart	= os.time()										-- Start time of progress
		intDelta	= 0													-- Delta time of progress
		intScale	= math.ceil( intMax / 1000 )					-- Scale of percentage per second of progress (initial guess is corrected in Step function)
		strClock	= "00 : 00 : 00"									-- Clock delta time display
		isBarStop	= false											-- Stop button pressed signal
		doUpdate()
		doFocus()
	end -- local function doReset

	function fh.Start(strTitle,intMaximum)						-- Create & start Progress Bar window
		if not dlgGauge then
			strTitle	= strTitle or ""								-- Dialogue and button title
			intMax		= intMaximum or 100							-- Maximun range of Progress Bar, default is 100
			local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30"			-- Adjust Stop button size to Title
			lblText	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
			barGauge	= iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
			lblDelta	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
			btnStop	= iup.button	{ Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; }	-- Signal Stop button pressed	return iup.CLOSE -- Often caused main GUI to close !!!
			dlgGauge	= iup.dialog	{ Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES";	-- Remove Windows minimize/maximize menu
								iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
									lblText;
									barGauge;
									lblDelta;
									btnStop;
								};
								move_cb	= function(self,x,y) tblBars.X = x tblBars.Y = y end;
								close_cb	= btnStop.action;		-- Windows Close button = Stop button
							}
			if type(tblBars.GUI) == "table"
			and type(tblBars.GUI.ShowDialogue) == "function" then
				dlgGauge.move_cb = nil								-- Use GUI library to show & move window
				tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
			else
				if fhGetAppVersion() > 6 then 					-- Window centres on FH parent	-- V3.1
					iup.SetAttribute(dlgGauge,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
				dlgGauge:showxy(intPosX,intPosY)					-- Show the Progress Bar window
			end
			doReset()													-- Reset the Progress Bar display
		end
	end -- function Start

	function fh.Message(strText)									-- Show the Progress Bar message
		if dlgGauge then lblText.Title = strText end
	end -- function Message

	function fh.Step(intStep)										-- Step the Progress Bar forward
		if dlgGauge then
			intVal = intVal + ( intStep or 1 )					-- Default step is 1
			local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
			if intPercent ~= intNew then							-- Update progress once per percent or per second, whichever is smaller
				intPercent = math.max( 0.1, intNew )				-- Ensure percentage is greater than zero
				if intVal > intMax then intVal = intMax intPercent = 100 end		-- Ensure values do not exceed maximum
				intNew = os.difftime(os.time(),intStart)
				if intDelta < intNew then							-- Update clock of elapsed time
					intDelta = intNew
					intScale = math.ceil( intDelta / intPercent )	-- Scale of seconds per percentage step
					local intHour = math.floor( intDelta / 3600 )
					local intMins = math.floor( intDelta / 60 - intHour * 60 )
					local intSecs = intDelta - intMins * 60 - intHour * 3600
					strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
				end
				doUpdate()											-- Update the Progress Bar display
			end
			iup.LoopStep()
		end
	end -- function Step

	function fh.Focus()												-- Bring the Progress Bar window to front
		if dlgGauge then doFocus() end
	end -- function Focus

	function fh.Reset()												-- Reset the Progress Bar display
		if dlgGauge then doReset() end
	end -- function Reset

	function fh.Stop()												-- Check if Stop button pressed
		iup.LoopStep()
		return isBarStop
	end -- function Stop

	function fh.Close()												-- Close the Progress Bar window
		isBarStop = false
		if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
	end -- function Close

	function fh.Setup(tblSetup)									-- Setup optional table of external attributes
		if tblSetup then
			tblBars = tblSetup
			strBack = tblBars.Back or strBack						-- Background colour
			strBody = tblBars.Body or strBody						-- Body text colour
			strFont = tblBars.Font or strFont						-- Font dialogue
			strStop = tblBars.Stop or strStop						-- Stop button colour
			intPosX = tblBars.X or intPosX						-- Window position
			intPosY = tblBars.Y or intPosY
		end
	end -- function Setup

	return fh

end -- local function progbar_v3

local progbar = progbar_v3()										-- To access FH progress bars module

--[[
@Function:		CheckVersionInStore
@Author:			Mike Tate
@Version:			1.4
@LastUpdated:		15 Feb 2026
@Description:		Check plugin version against version in Plugin Store
@Parameter:		Plugin name and version
@Returns:			None
@Requires:		luacom
@V1.4:				Dispense with files and assume called via IUP button;
@V1.3:				Save and retrieve latest version in file;
@V1.2:				Ensure the Plugin Data folder exists;
@V1.1:				Monthly interval between checks; Report if Internet is inaccessible;
@V1.0:				Initial version;
]]

function CheckVersionInStore(strPlugin,strVersion)							-- Check if later Version available in Plugin Store

	require("luacom")
	local FSO = luacom.CreateObject("Scripting.FileSystemObject")
	local strFile = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\VersionInStore "..strPlugin..".dat"
	if FSO:FileExists(strFile) then FSO:DeleteFile(strFile,true) end		-- Delete obsolete file

	local function httpRequest(strRequest)										-- Luacom http request protected by pcall() below
		local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
		http:Open("GET",strRequest,false)
		http:Send()
		return http.Responsebody
	end -- local function httpRequest

	local function intVersion(strVersion)										-- Convert version string to comparable integer
		local intVersion = 0
		local arrNumbers = {}
		strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)
		for i = 1, 5 do
			intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
		end
		return intVersion
	end -- local function intVersion

	local strLatest = "0"
	if strPlugin then
		local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..tostring(strPlugin)
		local isOK, strReturn = pcall(httpRequest,strRequest)
		if not isOK then																-- Problem with Internet access
			fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
		elseif strReturn then
			strLatest = strReturn:match("([%d%.]*),%d*")						-- Version digits & dots then comma and Id digits 
		end
	end
	local strMessage = "No later Version"
	if intVersion(strLatest) > intVersion(strVersion or "0") then
		strMessage = "Later Version "..strLatest
	end
	fhMessageBox(strMessage.." of this Plugin is available from the 'Plugin Store'.")
end -- function CheckVersionInStore

function doResetDefaults(dicOption)
	dicOption = {																		-- Default options
		["RemovePrivate"]	=	2	;
		["CleanLiving"]		=	2	;
		["AssumeLiving"]		=	2	;
		["EstimateDates"]	=	3	;
		["LivingBirthDate"]	= 1910	;
		["NoBirthDate"]		= "ON"	;
		["RelativeAlive"]	=	1	;
		["ClnName"]			=	1	;
		["ClnFacts"]			=	1	;
		["ClnNotes"]			=	1	;
		["ClnMedia"]			=	1	;
		["ClnSources"]		=	1	;
		["ClnOther"]			=	1	;
		["ClnNoteRecords"]	= "ON"	;
		["ClnMediaRecords"]	= "ON"	;
		["ClnSourceRecords"]= "ON"	;
	}
	return dicOption
end -- function doResetDefaults

function doUpdateValues(dicOption)
	local arrValue = { "OFF"; "ON"; }
	dicOption["RemovePrivate"]		=	dicOption["RemovePrivate"]	+ 1
	dicOption["CleanLiving"]		=	dicOption["CleanLiving"]	+ 1
	dicOption["AssumeLiving"]		=	dicOption["AssumeLiving"]	+ 1
	dicOption["EstimateDates"]		=	dicOption["EstimateDates"]	+ 1
	dicOption["LivingBirthDate"]	=	dicOption["LivingBirthDate"]
	dicOption["NoBirthDate"]		=	arrValue[ dicOption["NoBirthDate"] + 1 ]
	dicOption["RelativeAlive"]		=	dicOption["RelativeAlive"]	+ 1
	dicOption["ClnName"]			=	dicOption["ClnName"]		+ 1
	dicOption["ClnFacts"]			=	dicOption["ClnFacts"]		+ 1
	dicOption["ClnNotes"]			=	dicOption["ClnNotes"]		+ 1
	dicOption["ClnMedia"]			=	dicOption["ClnMedia"]		+ 1
	dicOption["ClnSources"]			=	dicOption["ClnSources"]		+ 1
	dicOption["ClnOther"]			=	dicOption["ClnOther"]		+ 1
	dicOption["ClnNoteRecords"]	=	arrValue[ dicOption["ClnNoteRecords"]	+ 1 ]
	dicOption["ClnMediaRecords"]	=	arrValue[ dicOption["ClnMediaRecords"]	+ 1 ]
	dicOption["ClnSourceRecords"]	=	arrValue[ dicOption["ClnSourceRecords"]	+ 1 ]
	return dicOption
end -- function doUpdateValues

-- Load Options

function loadOptions(strFileName)
	local dicOption = {}
	strFileName = strFileName or "?"
	dicOption = doResetDefaults(dicOption)
	if FlgFileExists(strFileName) then											-- Read the file in table lines
		local tblField = {}
		local strClip = StrLoadFromFile(strFileName)
		for strLine in strClip:gmatch("[^\r\n]+") do
			local arrFields = split(strLine,"=")
			dicOption[arrFields[1]] = tonumber(arrFields[2]) or arrFields[2]
		end
		if dicOption["ScreenPosX"] and dicOption["ScreenPosY"] then
			if tonumber( dicOption["NoBirthDate"] ) then
				dicOption = doUpdateValues(dicOption)
			end
			dicOption["ScreenPosX"] = nil
			dicOption["ScreenPosY"] = nil
		end
	end
	return dicOption
end -- function loadOptions

-- Save Options

function saveOptions(dicOption,strFileName)
	strFileName = strFileName or "?"
	local tblClip = {}
	for strField, strValue in pairs(dicOption) do								-- Write the file in table lines
		table.insert(tblClip,strField.."="..strValue)
	end
	local strClip = table.concat(tblClip,"\n").."\n"
	if not SaveStringToFile(strClip,strFileName) then
		error("\nSettings file not saved successfully.\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.")
	end	
end -- function saveOptions

-- Obtain the Day Number for any Date Point --									-- V1.6 -- Fix problems with invalid dates in DayNumber function

local dicGetDayNumberError = {}													-- User selection of errors to report	-- V2.0

function GetDayNumber(datDate)
	if datDate:IsNull() then return 0 end
	local intDay = fhCallBuiltInFunction("DayNumber",datDate) 				-- Only works for Gregorian dates that were not skipped nor BC dates
	if not intDay then
		local strError = "because "												-- Error message reason	-- V3.0
		local calendar = datDate:GetCalendar()
		local oldMonth = datDate:GetMonth()
		local oldDayNo = datDate:GetDay()
		local intMonth = math.min( oldMonth, 12 ) 								-- Limit month to 12, and day to last of each month
		local intDayNo = math.min( oldDayNo, ({0;31;28;31;30;31;30;31;31;30;31;30;31;})[intMonth+1] )
		local intYear  = datDate:GetYear()
		if oldDayNo > intDayNo then strError = strError.."day "..oldDayNo.." too big "   end
		if oldMonth > intMonth then strError = strError.."month "..oldMonth.." too big " end
		if calendar == "Hebrew" and intYear > 3761 then
			intYear = intYear - 3761
			strError = strError.."Hebrew year > 3761 "
		elseif calendar ~= "Gregorian" then
			strError = strError..calendar.." disallowed "
		end
		if     intYear == 1752 and intMonth ==  9 and intDayNo <= 13 then	-- Use 2 Sep 1752 for 3 - 13 Sep 1752 dates skipped
			intDayNo = 2
			strError = strError.."3 - 13 Sep 1752 skipped "
		elseif intYear == 1582 and intMonth == 10 and intDayNo <= 14 then	-- Use 4 Oct 1582 for 5 - 14 Oct 1582 dates skipped
			intDayNo = 4
			strError = strError.."5 - 14 Oct 1582 skipped "
		end	
		local setDate = fhNewDatePt(intYear,intMonth,intDayNo,datDate:GetYearDD())
		intDay = fhCallBuiltInFunction("DayNumber",setDate) 					-- Remove BC and Julian, Hebrew, French calendars
		if not intDay then intDay = 0 end
		local oldDate = fhNewDate()		oldDate:SetSimpleDate(datDate)	-- Report problem to user
		local newDate = fhNewDate()		newDate:SetSimpleDate(setDate)
		local strIsBC = ""
		if datDate:GetBC() then
			strError = strError.." B.C. disallowed "
			intDay = -intDay
			strIsBC = "and Day Number negated"
		end
		if dicGetDayNumberError[strError] ~= "No" then							-- V2.0
			local anyAns = fhMessageBox("\n Get Day Number issue for date \n "..oldDate:GetDisplayText().." \n "..strError.." \n So replaced it with date \n "..newDate:GetDisplayText().." \n "..strIsBC.." \n \n SHOULD ALL SIMILAR ERRORS STILL BE REPORTED? ","MB_YESNO","MB_ICONQUESTION")
			dicGetDayNumberError[strError] = anyAns
		end
	end
	return intDay
end -- function GetDayNumber

-- Make EstimatedBirthDate EARLIEST <= LATEST <= 1st Fact Date -- Fix errors in EstimatedBirthDate function same as Lookup Missing BMD/Census

function EstimatedBirthDates(ptrIndi,intGens)
	intGens = intGens or 2
	local dateMin = fhCallBuiltInFunction("EstimatedBirthDate",ptrIndi,"EARLIEST",intGens)
	local dateMax = fhCallBuiltInFunction("EstimatedBirthDate",ptrIndi,"LATEST",intGens)
	local dateMid = fhNewDatePt()
	if not ( dateMin:IsNull() or dateMax:IsNull() ) then
		local ptrFact = fhNewItemPtr()
		ptrFact:MoveToFirstChildItem(ptrIndi)
		while ptrFact:IsNotNull() do												-- Find 1st Fact with a Date (ideally Time Frame not None nor Pre-Birth which can set premature Date)
			if fhIsFact(ptrFact) then
				local datFact = fhGetValueAsDate(fhGetItemPtr(ptrFact,"~.DATE"))
				if not datFact:IsNull() then										-- Do not skip Birth/Baptism Event as Period/Range upsets estimates
					local datLast = datFact:GetDatePt1()							-- Last date = DatePt1 for Simple, Range, and Before
					local strType = datFact:GetSubtype()							-- Between = DatePt2 and After = DatePt1 + 100yrs
					if   strType == "Between" then datLast = datFact:GetDatePt2()
					elseif strType == "After" then datLast:SetValue(datLast:GetYear()+100,datLast:GetMonth(),datLast:GetDay(),datLast:GetYearDD()) end -- FH V6.2 says 'Invalid number of parameters' if all 6 supplied so have removed last two: ,datLast:GetBC(),datLast:GetCalendar()
					if dateMax:Compare(datLast) > 0 then dateMax = datLast end
					if dateMin:Compare(dateMax) > 0 then dateMin = dateMax end
					if strType ~= "After" then break end							-- Now EARLIEST <= LATEST <= Last date
				end
			end
			ptrFact:MoveNext("ANY")
		end																				-- Need approximate MID year & month
		local intDays = ( GetDayNumber(dateMax) - GetDayNumber(dateMin) ) / 2
		local intYear,remYear = math.modf( intDays / 365.2422 )				-- Offset year @ 365.2422 days per year, and remainder fraction
		local intMnth = math.floor( remYear * 12 )								-- Offset month is remainder fraction of year * 12
		dateMid = fhCallBuiltInFunction("CalcDate",dateMin,intYear,intMnth)
	end
	return { Min=dateMin; Mid=dateMid; Max=dateMax; }							-- Return EARLIEST, MID, LATEST dates
end -- function EstimatedBirthDates

-- Make EstimatedDeathDate EARLIEST <= LATEST <= DEAT/BURI/CREM Date -- Fix errors in EstimatedDeathDate function same as Lookup Missing BMD/Census

function EstimatedDeathDates(ptrIndi,intGens)
	intGens = intGens or 2
	local dateMin = fhCallBuiltInFunction("EstimatedDeathDate",ptrIndi,"EARLIEST",intGens)
	local dateMax = fhCallBuiltInFunction("EstimatedDeathDate",ptrIndi,"LATEST",intGens)
	local dateMid = fhNewDatePt()
	if dateMin:IsNull() or dateMax:IsNull() then
		dateMin:SetNull()
		dateMax:SetNull()
	else
		local anyDate = false
		for intFact, strFact in ipairs ({"~.DEAT.DATE";"~.BURI.DATE";"~.CREM.DATE";}) do
			local datFact = fhGetValueAsDate(fhGetItemPtr(ptrIndi,strFact))
			if not datFact:IsNull() then											-- Find 1st Death/Burial/Cremation Date
				anyDate = true
				local datLast = datFact:GetDatePt1()								-- Last date = DatePt1 for Simple, Range, and Before
				local strType = datFact:GetSubtype()								-- Between = DatePt2 and After = DatePt1 + 100yrs
				if   strType == "Between" then datLast = datFact:GetDatePt2()
				elseif strType == "After" then datLast:SetValue(datLast:GetYear()+100,datLast:GetMonth(),datLast:GetDay(),datLast:GetYearDD()) end -- FH V6.2 says 'Invalid number of parameters' if all 6 supplied so have removed last two: ,datLast:GetBC(),datLast:GetCalendar()
				if dateMax:Compare(datLast) > 0 then dateMax = datLast end
				if dateMin:Compare(dateMax) > 0 then dateMin = dateMax end
				if strType ~= "After" then break end								-- Now EARLIEST <= LATEST <= Last date
			end
		end	
		if anyDate then																-- Need approximate MID year & month
			local intDays = ( GetDayNumber(dateMax) - GetDayNumber(dateMin) ) / 2
			local intYear,remYear = math.modf( intDays / 365.2422 )			-- Offset year @ 365.2422 days per year, and remainder fraction
			local intMnth = math.floor( remYear * 12 )							-- Offset month is remainder fraction of year * 12
			dateMid = fhCallBuiltInFunction("CalcDate",dateMin,intYear,intMnth)
		else
			dateMin:SetNull()														-- No Death/Burial/Cremation Date exists
			dateMax:SetNull()
		end
	end
	return { Min=dateMin; Mid=dateMid; Max=dateMax; }							-- Return EARLIEST, MID, LATEST dates
end -- function EstimatedDeathDates

-- Save Record of Relative

function saveRecord(ptrField,dicOption)
	if fhGetTag(ptrField) == "FAMS"
	or dicOption["RelativeAlive"] == 2 then										-- Treat any HUSBand, WIFE, or CHILd as Living
		local ptrItem = fhGetValueAsLink(ptrField)
		if ptrItem:IsNotNull() then
			local intItem = fhGetRecordId(ptrItem)
			local strItem = intItem..fhGetTag(ptrItem)							-- Ensure same record is not saved again
			if intItem == 0
			or not tblRecordList[strItem] then
				table.insert(tblRecordList,ptrItem)
				tblRecordList[strItem] = true
			end
		end
	end
end -- function saveRecord

-- Delete Item unless it is already listed

function deleteItem(ptrItem)
	if ptrItem:IsNotNull() then
		local intItem = fhGetRecordId(ptrItem)
		local strItem = intItem..fhGetTag(ptrItem)
		if intItem == 0 then
			if isOkForDelete then													-- Ensure subsidiary of already deleted field is not saved
				table.insert(tblDeleteList,ptrItem:Clone())
				isOkForDelete = false
			end
		elseif not tblDeleteList[strItem] then									-- Ensure same record is not saved again
			table.insert(tblDeleteList,ptrItem:Clone())
			tblDeleteList[strItem] = ptrItem:Clone()  
		end
	end
end -- function deleteItem

-- Clean Item Field and Linked Records

function cleanItem(ptrField,intSetting)
	if intSetting > 1 then															-- Remove all local Items & Links
		deleteItem(ptrField)
		if intSetting == 3 then														-- Remove all local Items & Records
			deleteItem(fhGetValueAsLink(ptrField))
		end
	end
end -- function cleanItem

-- Extract Initials from Name

function getInitials(strName)
	strName = strName:gsub("([^ ])([^ ]*)","%1")								-- V1.6 simplified and caters for non-alpha names
	return strName:upper()
end -- function getInitials

-- Clean Name, Alias & Title Fields

function cleanName(ptrField,dicOption)
	if dicOption["ClnName"] > 1 then
		if fhGetTag(ptrField) == "NAME"
		and isPrimaryName then														-- Only clean Primary Name field MBT V1.2.7
			isPrimaryName = false
			local strSurname = string.match(fhGetValueAsText(ptrField),"/(.-)/") or ""	-- V1.6 caters for [unnamed person] without / /
			local strGiven = fhGetItemText(ptrField,"~:GIVEN_ALL")
			local newName = ""
			if dicOption["ClnName"] == 2 then
				-- Change Given to Initials
				newName = getInitials(strGiven).." /"..strSurname.."/"
			elseif dicOption["ClnName"] == 3 then
				-- Change All to Initials
				newName = getInitials(strGiven).." /"..getInitials(strSurname).."/"
			elseif dicOption["ClnName"] == 4 then
				newName = "/Private/"
			end
			table.insert(tblChangeList,{ptr=ptrField:Clone(), value=newName, type="text"})
			local ptrClear = fhNewItemPtr()
			ptrClear:MoveToFirstChildItem(ptrField)								-- Remove subsidiary Name fields MBT V1.2.4+
			while ptrClear:IsNotNull() do
				deleteItem(ptrClear)												-- Should Note & Media & Source depend on their Options to also delete linked record?
				ptrClear:MoveNext()
			end
		else
			deleteItem(ptrField)													-- Remove Alternate Name, Alias, Title MBT V1.2.7
		end
	end
end -- function cleanName

-- Clean Events, Attributes & LDS Ordinances 

function cleanFact(ptrField,dicOption)
	if dicOption["ClnFacts"] == 1 then											-- No Change
	elseif dicOption["ClnFacts"] == 2 then										-- Remove all Fact Dates
		deleteItem(fhGetItemPtr(ptrField,"~.DATE"))
	elseif dicOption["ClnFacts"] == 3 then										-- Remove all Fact Dates and Source Links
		deleteItem(fhGetItemPtr(ptrField,"~.DATE"))
		local ptrClear = fhGetItemPtr(ptrField,"~.SOUR")
		while ptrClear:IsNotNull() do
			deleteItem(ptrClear)													-- Should this depend on Source Option to also delete linked record?
			ptrClear:MoveNext("SAME_TAG")
		end
	elseif dicOption["ClnFacts"] == 4 then										-- Remove all Facts completely
		deleteItem(ptrField)
	elseif dicOption["ClnFacts"] == 5 then										-- Remove all Facts except BMD Years
		local strFact = fhGetTag(ptrField)
		if strFact == "BIRT"
		or strFact == "MARR"
		or strFact == "DEAT" then
			local ptrClear = fhNewItemPtr()
			ptrClear:MoveToFirstChildItem(ptrField)
			while ptrClear:IsNotNull() do
				if fhGetTag(ptrClear) == "DATE" then
					local datDate = fhGetValueAsDate(ptrClear)
					local strDate = datDate:GetValueAsText()
					local strYear = strDate:match("(%d%d%d%d)") or ""	-- V1.5
					table.insert(tblChangeList,{ptr=ptrClear:Clone(), value=strYear, type="date"})
				else
					deleteItem(ptrClear)
				end
				ptrClear:MoveNext()
			end
		else
			deleteItem(ptrField)
		end
	end
end -- function cleanFact

-- Clean Note Fields

function cleanNotes(ptrField,dicOption)
	cleanItem(ptrField,dicOption["ClnNotes"])
end -- function cleanNotes

-- Clean Media Fields

function cleanMedia(ptrField,dicOption)
	cleanItem(ptrField,dicOption["ClnMedia"])
end -- function cleanMedia

-- Clean Source Fields

function cleanSources(ptrField,dicOption)
	cleanItem(ptrField,dicOption["ClnSources"])
end -- function cleanSources

-- Do Nothing

function doNothing(ptrField,dicOption)
	-- Keep this unconditionally
end -- function doNothing

-- Dictionary of Tag Actions for cleanFields/Record

local dicField = {
	FAMC=doNothing;  SEX=doNothing;
	FAMS=saveRecord; HUSB=saveRecord; WIFE=saveRecord; CHIL=saveRecord;
	NAME=cleanName;  ALIA=cleanName;  TITL=cleanName;
	BAPL=cleanFact;  CONL=cleanFact;  ENDL=cleanFact;  SLGC=cleanFact;  SLGS=cleanFact;
	NOTE=cleanNotes; NOTE2=cleanNotes;
	OBJE=cleanMedia; OBJE2=cleanMedia;
	SOUR=cleanSources; SOUR2=cleanSources;
}

-- Clean Individual or Family Fields Recursively
 
function cleanFields(ptrField,dicOption)
	local isDelete = isOkForDelete
	ptrField:MoveToFirstChildItem(ptrField)
	while ptrField:IsNotNull() do
		local strField = fhGetTag(ptrField)
		if dicField[strField] then													-- Use dicField action defined above
			dicField[strField](ptrField,dicOption)
			cleanFields(ptrField:Clone(),dicOption)								-- Call cleanFields recursively
			isOkForDelete = isDelete
		end
		ptrField:MoveNext()
	end
end -- function cleanFields

-- Clean Individual or Family Record

function cleanRecord(ptrRecord,dicOption)
	isPrimaryName = true
	local isDelete = isOkForDelete
	local ptrField = fhNewItemPtr()
	ptrField:MoveToFirstChildItem(ptrRecord)
	while ptrField:IsNotNull() do
		if fhIsFact(ptrField) then													-- Use the cleanFact action
			cleanFact(ptrField,dicOption)
			cleanFields(ptrField:Clone(),dicOption)								-- Call cleanFields recursively
		else
			local strField = fhGetTag(ptrField)
			if dicField[strField] then												-- Use dicField action defined above
				dicField[strField](ptrField,dicOption)
				cleanFields(ptrField:Clone(),dicOption)							-- Call cleanFields recursively
			elseif dicOption["ClnOther"] == 2 then
				deleteItem(ptrField)
				cleanFields(ptrField:Clone(),dicOption)							-- Call cleanFields recursively
			end
		end
		isOkForDelete = isDelete
		ptrField:MoveNext()
	end
end -- function cleanRecord

-- Delete Unused Records

function deleteUnused(strType)
	local intDel = 0
	local ptrRec = fhNewItemPtr()
	ptrRec:MoveToFirstRecord(strType)
	while ptrRec:IsNotNull() do
		local ptrDel = ptrRec:Clone()
		ptrRec:MoveNext()
		if fhCallBuiltInFunction("LinksTo",ptrDel) == 0 then
			fhDeleteItem(ptrDel)
			intDel = intDel + 1
		end
	end
	return intDel
end -- function deleteUnused

-- Check for Project and Warn if in Project Mode

function checkMode()
	local strName = ""																-- V1.5
	if fhGetContextInfo("CI_APP_MODE") == "Project Mode" then
		local strAns = fhMessageBox(
	[[
	Plugin Warning:
	This is designed to delete data from the current file.
	You have a Project open.
	Please confirm you want to clean the data from 
	]]..fhGetContextInfo("CI_PROJECT_NAME"),
		"MB_OKCANCEL",
		"MB_ICONEXCLAMATION")
		if strAns ~= "OK" then
			return false
		end
		strName = fhGetPluginDataFileName()										-- Use Project sticky options
	else
		strName = fhGetPluginDataFileName("LOCAL_MACHINE")					-- Use global sticky options needs V5.0.8
	end
	return strName:gsub(" %- V%d.*%.dat$",".dat")								-- Use same .dat file for all Plugin versions	-- V1.5 -- V1.6
end -- function checkMode

-- Prompt User to Select Options

function getOptions(dicOption)													-- Rewritten to use custom dialog insteda of iup.GetParam()	-- V1.8

	local dicValue = {}
	local doApplyTheRules = true
	local strMargin = "18,2"

	local function setLabel( strTitle, strTip, strFont )
		local strMargin = strMargin
		if strTitle:match("^ %d") then strMargin = "0,2" end
		if strFont == "bold" then strFont = "Helvetica, Bold 10" end
		local iupLabel	= iup.label	{ Tip = strTip; Title = strTitle; Expand = "Yes"; Font = strFont; }
		local iupHbox 	= iup.hbox	{ Homogeneous = "Yes"; Margin = strMargin; iupLabel; }
		return iupHbox
	end -- local function setLabel

	local function setChoice( strTitle, strOption, strTip, arrChoice )
		local iupLabel	= iup.label	{ Tip = strTip; Title = strTitle; Expand = "Yes"; }
		local iupList 	= iup.list	{ Tip = strTip; DropDown = "Yes"; Expand = "Yes"; Value = dicOption[strOption]; action = function(self,strText,intState) dicOption[strOption] = intState end; }
		local iupHbox 	= iup.hbox	{ Homogeneous = "Yes"; Margin = strMargin; iupLabel; iupList; }
		for j, k in ipairs( arrChoice ) do iupList[j] = k end
		dicValue[strOption] = iupList
		return iupHbox
	end -- local function setChoice

	local function setNumber( strTitle, strOption, strTip )
		local iupLabel	= iup.label	{ Tip = strTip; Title = strTitle; Expand = "Yes"; }
		local iupText 	= iup.text	{ Tip = strTip; Spin = "Yes"; Expand = "Yes"; SpinValue = dicOption[strOption]; SpinInc = 1; SpinMin = 1000; SpinMax = 2000; valuechanged_cb = function(self) dicOption[strOption] = tonumber(self.SpinValue) end; }
		local iupHbox 	= iup.hbox	{ Homogeneous = "Yes"; Margin = strMargin; iupLabel; iupText; }
		dicValue[strOption] = iupText
		return iupHbox
	end -- local function setNumber

	local function setToggle( strTitle, strOption, strTip )
		local iupLabel	= iup.label	{ Tip = strTip; Title = strTitle; Expand = "Yes"; }
		local iupToggle	= iup.toggle	{ Tip = strTip; Title = ""; Value = dicOption[strOption]; RightButton = "Yes"; action = function(self,intState) dicOption[strOption] = self.Value end; }
		local iupHbox 	= iup.hbox	{ Homogeneous = "Yes"; Margin = strMargin; iupLabel; iupToggle; }
		dicValue[strOption] = iupToggle
		return iupHbox
	end -- local function setToggle

	local function setButton( strTitle, strTip )
		local iupButton	= iup.button	{ Tip = strTip; Title = strTitle; Expand = "Yes"; }
		return iupButton
	end -- local function setButton

	-- Create each GUI label and button with title and tooltip, etc
	local boxApplyTheRules	= setLabel	( "Apply these rules to each Individual record in turn. \n ", "", "bold" )
	local boxPrivacyRules	= setLabel	( " 1) Individual Privacy Rules : ", "'Private' flag rules", "bold" )
	local boxRemovePrivate	= setChoice	( " If the 'Private' record flag is set then ",		"RemovePrivate",	"Only applies if 'Private' flag is set",
													{ " ignore the flag and do nothing "; " remove the Individual record "; " treat as if the 'Living' flag were set "; } )
	local boxLivingRules	= setLabel	( " 2) Individual Living Rules : " , "'Living' flag rules" , "bold" )
	local boxSkipNamedList	= setLabel	( " Skip if Individual is in the 'Do Not Clean Living Persons' Named List", "Omit Individuals in Named List", "bold" )
	local boxCleanLiving	= setChoice	( " If the 'Living' record flag is set then ",			"CleanLiving",		"Only applies if 'Living' flag is set",
													{ " treat as if 'Living' flag is NOT set "; " apply 3) Clean 'Living' rules below "; } )
	local boxAssumeLiving	= setChoice	( " If the 'Living' record flag is NOT set then ",	"AssumeLiving",		"Only applies if 'Living' flag is NOT set",
													{ " ignore death and birth rules below "; " apply death and birth rules below "; } )
	local boxEstimateDates	= setChoice	( "     For death dates and birth dates use the ",	"EstimateDates",	"Using actual dates makes more use of no death/birth dates rule\rUsing earliest estimated dates includes fewest people\rUsing mid-point estimated dates lies in between\rUsing latest estimated dates includes the most people\rThe 'Tools > Preferences > Estimates' may play a part",
													{ " actual close family dates "; " earliest estimated dates "; " mid-point estimated dates "; " latest estimated dates "; } )
	local boxLiveBirthRule	= setLabel	( "     Apply 3) Clean 'Living' Person Rules if no death date ", "" )
	local boxLiveBirthDate	= setNumber	( "                    and the birth date is after ",	"LivingBirthDate",	"Select cut off birth year" )
	local boxIsNoBirthDate	= setToggle	( "                    or there is no birth date ? ",	"NoBirthDate",		"Applies if no actual or estimated death and birth date" )
	local boxRelativeRule	= setLabel	( " If 3) Clean 'Living' Person Rules apply to spouse or parent ", "" )
	local boxRelativeAlive	= setChoice	( " but they do NOT already apply to current person ","RelativeAlive",	"Applies primarily where current person has died",
													{ " ignore relatives and do nothing "; " apply 3) Clean 'Living' rules below "; } )
	local boxPersonRules	= setLabel	( " 3) Clean 'Living' Person Rules : ", "Clean 'Living' Individual subfields", "bold" )
	local boxCleanAllNames	= setChoice	( " For any Name, Alias && Title fields ",				"ClnName",			"Applies to Primary Name, and its subfields are removed\rAny Alternate Name, Alias, or Title fields are removed",
													{ " keep them all "; " use given Initials and full Surname "; " convert the whole Name to Initials "; " convert the whole Name to 'Private' "; } )
	local boxCleanAllFacts	= setChoice	( " For any Facts and LDS Ordinances ",					"ClnFacts",			"Applies to all Events, Attributes, and LDS Ordinances",
													{ " keep them all "; " remove all Date fields "; " remove all Date and Source fields "; " remove them all completely "; " remove them all except BMD Years "; } )
	local boxCleanAllNotes	= setChoice	( " For any Note fields and linked records ",			"ClnNotes",			"Applies to all local Note fields and linked Note Records",
													{ " keep them all "; " remove all fields and unlink records "; " remove all fields and delete records "; } )
	local boxCleanAllMedia	= setChoice	( " For any Media fields and linked records ",			"ClnMedia",			"Applies to all local Media fields and linked Media Records",
													{ " keep them all "; " remove all fields and unlink records "; " remove all fields and delete records "; } )
	local boxCleanSources	= setChoice	( " For any Source fields and linked records ",		"ClnSources",			"Applies to all local Source fields and linked Source Records",
													{ " keep them all "; " remove all fields and unlink records "; " remove all fields and delete records "; } )
	local boxCleanAllOther	= setChoice	( " For any other data fields ",							"ClnOther",			"Applies to all other data fields, but keeps Sex field and Family record links",
													{ " keep them all "; " remove all data fields, except for Sex "; } )
	local boxRecordRules	= setLabel	( " 4) Unused Linked Record Rules : ", "Remove unused records", "bold" )
	local boxCleanNotesRec	= setToggle	( " Remove unused Note Records ? ",						"ClnNoteRecords",	"Delete all Note Records with 0 Links?" )
	local boxCleanMediaRec	= setToggle	( " Remove unused Media Records ? ",						"ClnMediaRecords",	"Delete all Media Records with 0 Links?" )
	local boxCleanSourceRec	= setToggle	( " Remove unused Source Records ? ",					"ClnSourceRecords",	"Delete all Source Records with 0 Citations?" )

	local btnCheckUpdates	= setButton	( " Check for Updates"	, "Check for later version in Plugin Store" )	-- V2.0
	local btnCancelPlugin	= setButton	( " Cancel Plugin "		, "Cancel the Plugin but save Options" )
	local btnResetDefaults	= setButton	( " Restore Defaults "	, "Restore original default settings" )
	local btnHelpAndAdvice	= setButton	( " Help && Advice "	, "Obtain the Plugin Store Help page" )
	local btnApplyTheRules	= setButton	( " Apply Rules "		, "Apply the Rules to all Individuals" )
	local boxButtonItems	= iup.hbox 	{ Homogeneous = "Yes"; Margin = strMargin; Gap = "4"; btnCheckUpdates; btnCancelPlugin; btnResetDefaults; btnHelpAndAdvice; btnApplyTheRules; }	-- V2.0

	local boxApplyTheRules	= iup.vbox	{ boxApplyTheRules; boxPrivacyRules; boxRemovePrivate; boxLivingRules; boxSkipNamedList; boxCleanLiving; boxAssumeLiving; boxEstimateDates; boxLiveBirthRule; boxLiveBirthDate; boxIsNoBirthDate; boxRelativeRule; boxRelativeAlive; boxPersonRules; boxCleanAllNames; boxCleanAllFacts; boxCleanAllNotes; boxCleanAllMedia; boxCleanSources; boxCleanAllOther; boxRecordRules; boxCleanNotesRec; boxCleanMediaRec; boxCleanSourceRec; }
	local frmApplyTheRules	= iup.frame	{ boxApplyTheRules; }
	local iupDialog			= iup.dialog	{ Title = strPluginName; iup.vbox { Margin = "4x4"; frmApplyTheRules; boxButtonItems; }; }

	function iupDialog:close_cb()
		doApplyTheRules = false
	end -- function iupDialog:close_cb

	function btnCheckUpdates:action()												-- Action for Check for Updates button	-- V2.0
		iupDialog.Active = "NO"
		CheckVersionInStore(strPlugin,strVersion)
		iupDialog.Active = "YES"
		iupDialog.BringFront = "YES"
	end -- function CheckUpdates:action

	function btnCancelPlugin:action()
		doApplyTheRules = false
		return iup.CLOSE
	end -- function btnCancelPlugin:action

	function btnResetDefaults:action()
		dicOption = doResetDefaults(dicOption)
		for j, k in pairs(dicOption) do
			dicValue[j].Value = k
		end
	end -- function btnResetDefaults:action

	function btnHelpAndAdvice:action()
		fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/clean-living-persons","","","open")
		fhSleep(3000,500)
		iupDialog.BringFront="YES"
	end -- function btnHelpAndAdvice:action

	function btnApplyTheRules:action()
		doApplyTheRules = true
		return iup.CLOSE
	end -- function btnApplyTheRules:action

	if fhGetAppVersion() > 6 then 												-- Window centres on FH parent	-- V2.0
		iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
	end

	iupDialog:showxy()						-- V2.0iup.CENTERPARENT,iup.CENTERPARENT
	iupDialog.MinSize = iupDialog.RasterSize
	iup.MainLoop()
	iup.Destroy(iupDialog)
--	if not doApplyTheRules then return false, dicOption end

	return doApplyTheRules, dicOption
end -- function getOptions

-- Main Script

function Main()

	local arrLiving = {}															-- Result Set tables for assumed living (disabled)
	local arrBirth = {}
	local fileOptions = checkMode()												-- Check Project/Gedcom mode
	if not fileOptions then return end
	local dicOption = loadOptions(fileOptions)									-- Load options from file
	local isApply, dicOption = getOptions(dicOption)							-- Get options from user
	saveOptions(dicOption,fileOptions)											-- Save options to file
	if not isApply then return end

	local intRec = 0
	local ptrRecord = fhNewItemPtr()												-- Count records	-- V2.0
	ptrRecord:MoveToFirstRecord("INDI")
	while ptrRecord:IsNotNull() do
		intRec = intRec + 1
		ptrRecord:MoveNext()
	end
	if intRec > 10000 then progbar.Start("Checking People",intRec) end

	local intRec = 0
	local ptrRecord = fhNewItemPtr()												-- Process database
	ptrRecord:MoveToFirstRecord("INDI")
	while ptrRecord:IsNotNull() do
		intRec = intRec + 1
		if intRec > 10000 then														-- V2.0
			progbar.Step(10000)
			if progbar.Stop() then break end
			intRec = 0
		end
		local isLiving  = ( fhGetItemText(ptrRecord,"~._FLGS.__LIVING")  == "Y" )
		local isPrivate = ( fhGetItemText(ptrRecord,"~._FLGS.__PRIVATE") == "Y" )
		isOkForDelete = true
		if isPrivate then															-- Process Private Flag
			if dicOption["RemovePrivate"] == 2 then
				deleteItem(ptrRecord:Clone())
				isOkForDelete = false
			elseif dicOption["RemovePrivate"] == 3 then
				isLiving = true
			end
		end
		if isLiving then																-- Process Living Flag
			if dicOption["CleanLiving"] == 1 then
				isLiving = false
			end
		end
		if not isLiving then														-- Process Living Criteria
			if dicOption["AssumeLiving"] == 2 then
				local arrDateValue = { "Mid"; "Min"; "Mid"; "Max"; }	-- V1.6
				local strDateValue = arrDateValue[dicOption["EstimateDates"]]
				local intGen = 9
				if dicOption["EstimateDates"] == 1 then intGen = 0 end
				local arrBirthDate = EstimatedBirthDates(ptrRecord,intGen)	-- Fix erroneous EstimatedBirthDate function
				local arrDeathDate = EstimatedDeathDates(ptrRecord,intGen)	-- Fix erroneous EstimatedDeathDate function
				local intBirthYear = arrBirthDate[strDateValue]:GetYear()
				local intDeathYear = arrDeathDate[strDateValue]:GetYear()
				if intBirthYear == 0 and dicOption["NoBirthDate"] == "ON" then
					intBirthYear = dicOption["LivingBirthDate"]					-- Assume recent
				end
				if intBirthYear >= dicOption["LivingBirthDate"] and intDeathYear == 0 then
					isLiving = true
				end
--[=[
				if isLiving then														-- Update assumed living debug Result Set (disabled)
					table.insert(arrLiving,ptrRecord:Clone())
					table.insert(arrBirth ,intBirthYear)
				end
--]=]
			end
		end
		if isLiving and
		not fhCallBuiltInFunction("IsInList",ptrRecord,"Do Not Clean Living Persons") then	-- V1.5
			cleanRecord(ptrRecord,dicOption)										-- Process Living Rules
		end
		ptrRecord:MoveNext()
	end
	for _,ptrRecord in ipairs(tblRecordList) do									-- Process Family & Relations Records of Living Individuals
		cleanRecord(ptrRecord,dicOption)
	end
	progbar.Close()
	local strAns = fhMessageBox("Please Confirm Changes:\n"..
	#tblDeleteList.." items will be deleted and "..
	#tblChangeList.." items changed.\nThen unused records may be removed.","MB_OKCANCEL")
	if strAns == "OK" then
		local intInc = 100
		intRec = #tblDeleteList + #tblChangeList
		if intRec > intInc then progbar.Start("Cleaning People",intRec) end
		intRec = 0
		progbar.Message("Deleting Items")
		for _,ptrItem in ipairs(tblDeleteList) do								-- Delete listed items
			intRec = intRec + 1
			if intRec > intInc then													-- V2.0
				progbar.Step(intInc)
				if progbar.Stop() then break end
				intRec = 0
			end
			fhDeleteItem(ptrItem)
		end
		progbar.Message("Changing Items")
		intInc = 400
		for _,tblItem in ipairs(tblChangeList) do								-- Change listed items
			intRec = intRec + 1
			if intRec > intInc then													-- V2.0
				progbar.Step(intInc)
				if progbar.Stop() then break end
				intRec = 0
			end
			if tblItem.type == "text" then
				fhSetValueAsText(tblItem.ptr,tblItem.value)
			elseif tblItem.type == "date" then
				local datDate = fhNewDate()
				datDate:SetValueAsText(tblItem.value)
				fhSetValueAsDate(tblItem.ptr,datDate)
			end
		end
		repeat
			local intDel = 0
			if dicOption["ClnSourceRecords"] == "ON" then						-- Delete unused Source records and break links to Media & Note records
				intDel = intDel + deleteUnused("SOUR")
			end
			if dicOption["ClnMediaRecords"] == "ON" then						-- Delete unused Media records and break links to Note & Source records
				intDel = intDel + deleteUnused("OBJE")
			end
			if dicOption["ClnNoteRecords"] == "ON" then							-- Delete unused Note records and break links to Source records
				intDel = intDel + deleteUnused("NOTE")
			end
		until intDel == 0															-- Repeat until no unused records need deleting
	end
	progbar.Close()
	if #arrLiving > 0 then
		fhOutputResultSetTitles("Clean Living Persons")
		fhOutputResultSetColumn("Forced Living Individual", "item", arrLiving, #arrLiving, 200, "align_left")
		fhOutputResultSetColumn("Birth Year",            "integer", arrBirth , #arrLiving,  40, "align_mid" )
	end
	fhMessageBox("Clean Living Persons Finished")
end -- function Main()

fhInitialise(5,0,8,"save_recommended")											-- V5.0.8 for sticky settings with scope

Main()

Source:Clean-Living-Persons-4.fh_lua