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()
--[[
@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