Find Duplicate Facts.fh_lua--[[
@Title: Find Duplicate Facts
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.4
@Keywords:
@LastUpdated: 13 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: Create a Result Set of any duplicate Facts where chosen sub-fields also match.
@V1.4: Centre windows on FH window; Check for Updates button; Ch-ViS 1.4; Added Media OBJE subtags;
@V1.3: Disregard white-space and control characters when comparing fields; Handle long text fields; Add Citation Template Fields (_FIELD);
@V1.2: Disregard Fact order; Fix Place field; Add Sort Date (_SDATE), Fact Flags (_FLGS), Fact Fax (FAX), (RELI) & (RESN), Place Phonetic (FONE), Roman (ROMN) and (MAP), Address hierarchy, and 5.5.1 Object (OBJE) format;
@V1.1: FH V7 Lua 3.5 IUP 3.28 compatible; Always produce Result Set;
@V1.0: First published version;
]]
require "iuplua"
local strVersion = "1.4"
local strTitle = "Find Duplicate Facts "..strVersion -- V1.1
-- fhSetStringEncoding("UTF-8") -- Are accents, etc handled correctly???
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local intTicks = 1 -- Current tick setting may be 0 or 1 -- V1.4 Used in getParamEmulator
local noToggle = true -- Toggle ticks button used -- V1.4 Used in getParamEmulator
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.4
@LastUpdated: 20 Jan 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 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 getParamEmulator() -- Prototype for iup.GetParam(...) -- V1.4
if fhGetAppVersion() > 6 then unpack = table.unpack end
local iupDialog = nil
local iupParams = nil
local arrValues = {}
local isSuccess = false
local strPlugin = fhGetContextInfo("CI_PLUGIN_NAME"):gsub(" %- .*","")
local function handleButton(iupDialog,intIndex,strTitle) -- Handle the dialog buttons
if intIndex == (iup.GETPARAM_OK or -1) then
-- strTitle sometimes needed to determine the function -- 1st button action -- FH V5 needs -1
isSuccess = true
elseif intIndex == (iup.GETPARAM_CANCEL or -3) then -- 2nd button action -- FH V5 needs -3
intTicks = 1 - intTicks
noToggle = false
elseif intIndex == (iup.GETPARAM_HELP or -4) then -- 3rd 'Later Version?' button -- FH V5 needs -4
iupDialog.Active = "NO"
CheckVersionInStore(strPlugin,strVersion)
iupDialog.Active = "YES"
iupDialog.BringFront = "YES"
return 0
end
return 1
end -- function handleButton
local function makeDialog(strTitle,strFormat) -- Make emulated iup.GetParam(...) dialog
local arrFormat = {}
for strForm in strFormat:gmatch(".-\n") do -- Construct parameters from format
local iupParam = iup.param{ format=strForm; }
table.insert(arrFormat,iupParam)
end
iupParams = iup.parambox{ unpack(arrFormat) }
local iupButton = iup.button{ Title="Help && Advice"; Padding="12x8"; } -- Example of extra button
-- iupDialog = iup.dialog{ Title=strTitle; iup.vbox{ iupParams; iupButton; ALIGNMENT="ACENTER"; MARGIN="10x10"; }; close_cb=function() isSuccess = false return iup.CLOSE end; }
iupDialog = iup.dialog{ Title=strTitle; iupParams; close_cb=function() isSuccess = false return iup.CLOSE end; }
if fhGetAppVersion() > 6 then -- Window centres on FH parent
iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
for intParam = 1, iupParams.ParamCount do -- Set all parameter values
local iupParam = iupParams:GetParamParam(intParam-1)
local iupCntrl = iupParam.Control
local anyValue = arrValues[intParam]
if iupParam.Type == "LIST" then anyValue = anyValue + 1 end -- Droplists need an adjustment
iupCntrl.Value = anyValue
end
function iupParams:param_cb(intIndex) -- Parameter call back actions
if intIndex >= 0 then
local iupParam = iupParams:GetParamParam(intIndex) -- Save any parameter value
arrValues[intIndex+1] = tonumber(iupParam.Value) or iupParam.Value
return 1
else
return handleButton(iupDialog,intIndex,strTitle) -- Handle buttons
end
end -- function iupParams:param_cb
function iupButton:action(intButton) -- Display Help Page
local strPlugin = strPlugin:gsub(" ","-"):lower()
fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/"..strPlugin,"","","open")
fhSleep(3000,500)
iupDialog.BringFront = "YES"
return 1
end -- function iupButton:action
end -- local function makeDialog
local function getParam(strTitle,strSize,strFormat,...) -- Emulate iup.GetParam(...)
arrValues = {...}
if fhGetAppVersion() > 6 then
makeDialog(strTitle,strFormat)
if strSize then iupDialog.Size = strSize end
iupDialog:map()
iupDialog.MinSize = iupDialog.NaturalSize
iupDialog:showxy(iup.CENTERPARENT,iup.CENTERPARENT)
if iup.MainLoopLevel()==0 then iup.MainLoop() end
iup.Destroy(iupDialog)
iupDialog = nil
else
local function fncAction(iupDialog,intIndex)
return handleButton(iupDialog,intIndex,strTitle) -- Handle buttons
end -- local function fncAction
arrValues = { iup.GetParam(strTitle,fncAction,strFormat,unpack(arrValues)) }
isSuccess = arrValues[1]
table.remove(arrValues,1)
end
return isSuccess, unpack(arrValues)
end -- local function getParam
return getParam
end -- function getParamEmulator
local getParam = getParamEmulator()
local arrTags = { }
local arrRefs = { -- Level 3 and lower sub-fields
PLAC = { "FORM"; "NOTE2"; "NOTE"; "SOUR2"; "SOUR"; "FONE"; "ROMN"; "MAP"; }; -- V1.2
FONE = { "TYPE"; }; -- V1.2
ROMN = { "TYPE"; }; -- V1.2
MAP = { "LATI"; "LONG"; }; -- V1.2
ADDR = { "ADR1"; "ADR2"; "ADR3"; "CITY"; "STAE"; "POST"; "CTRY"; }; -- V1.2
_SHAR = { "ROLE"; "NOTE2"; "SOUR"; "SOUR2"; "_SENT"; };
_SHAN = { "ROLE"; "NOTE2"; "SOUR"; "SOUR2"; };
SOUR2 = { "TEXT"; "NOTE2"; "NOTE"; };
SOUR = { "DATA"; "NOTE2"; "NOTE"; "OBJE2"; "OBJE"; "QUAY"; "PAGE"; "EVEN"; "_FIELD"; }; -- V1.3
DATA = { "DATE"; "TEXT"; };
EVEN = { "ROLE"; };
OBJE2 = { "TITL"; "FORM"; "FILE"; "NOTE2"; "NOTE"; "_NOTE"; "_DATE"; };
OBJE = { "_SEQ"; "_CAPT"; "_EXCL"; "_AREA"; "_NOTA"; "SOUR"; "SOUR2"; }; -- V1.4
FILE = { "FORM"; }; -- V1.2
FORM = { "MEDI"; }; -- V1.2
NOTE2 = { "SOUR"; "SOUR2"; };
NOTE = { "SOUR"; "SOUR2"; };
HUSB = { "AGE"; };
WIFE = { "AGE"; };
}
function Pick() -- Pick which level 2 Fields to include
local strFactMail = "Fact Email (_EMAIL)"
local strFactWeb = "Fact Web Site (_WEB)"
if fhGetAppVersion() > 6 then -- Adjust FH V6 GEDCOM 5.5 & FH V7 GEDCOM 5.5.1 tags -- V1.1
strFactMail = "Fact Email (EMAIL)"
strFactWeb = "Fact Web Site (WWW)"
end
local arrField = {
"Fact Age (AGE)"; "Fact Place (PLAC)"; "Fact Address (ADDR)"; "Fact Cause (CAUS)";
"Fact Phone (PHON)"; strFactMail; strFactWeb; "Fact Descriptor (TYPE)"; -- V1.1
"Witness Individual (_SHAR)"; "Witness Name Only (_SHAN)";
"Narrative Sentence (_SENT)"; "Parents Family (FAMC)"; "Responsible Agency (AGNC)";
"Local Note (NOTE2)"; "Note Record (NOTE)";
"Media Record (OBJE)"; "Local Media (OBJE2)";
"Source Citation (SOUR)"; "Source Note (SOUR2)";
}
if fhGetAppVersion() > 6 then -- Add FH V7 Sort Date, Flags, Fax, etc -- V1.2
table.insert(arrField, 1,"Sort Date (_SDATE)")
table.insert(arrField, 2,"Fact Flags (_FLGS)")
table.insert(arrField, 9,"Fact Fax (FAX)")
table.insert(arrField,17,"Religious Affiliation (RELI)")
table.insert(arrField,18,"Restriction Notice (RESN)")
end
local arrReply = { } -- GetParam reply tick values
local strParam = "" -- Format of paramaters
local arrTicks = { " Enable Ticks "; " Remove Ticks "; } -- Toggle ticks button captions
local function setTicks(intParam) -- Return same number of 0's or 1's as Field params
intParam = intParam - 1
if intParam == 0 then return intTicks end -- End recursion
return intTicks,setTicks(intParam)
end -- local function setTicks
local function GetParam(intParam) -- GetParam user dialogue
strParam = "Tick fields that must match in duplicate Facts: %t\n"..strParam
strParam = strParam.."%u[ Find Facts , Enable Ticks , Later Version? ]\nPlugin provides a Result Set of the duplicates. %t\n" -- V1.4
local arrParam = { }
repeat
noToggle = true -- Repeat until toggle button not used, and swap button caption each time
strParam = strParam:gsub(arrTicks[2-intTicks],arrTicks[intTicks+1])
arrParam = { getParam(strTitle,nil,strParam,setTicks(intParam)) } -- V1.4
until noToggle
for intParam = 2, #arrParam do
table.insert(arrReply,arrParam[intParam]) -- Append parameter tick replies
end
return arrParam[1] -- Find Facts = true, X Close = false
end -- local function GetParam
for _, strName in ipairs (arrField) do -- Loop through every Field to set user parameters
strParam = strParam..strName.." %b\n"
end
if GetParam(#arrField) then -- Get user parameter tick replies
for intReply, isReply in ipairs (arrReply) do
if isReply == 1 then
local strTags = arrField[intReply]:match(" %((.+)%)$") -- Compose list of Field Tags
table.insert(arrTags,strTags)
if strTags == "AGE" then -- Cater for Family Fact Ages
table.insert(arrTags,"HUSB")
table.insert(arrTags,"WIFE")
end
end
end
return true -- Continue to Find Facts
end
return false -- X Close Plugin
end -- function Pick
function Check(ptrThis,ptrThat,strRef) -- Check any subfield matches
ptrThis = fhGetItemPtr(ptrThis,"~."..strRef)
ptrThat = fhGetItemPtr(ptrThat,"~."..strRef)
while ptrThis:IsNotNull() or ptrThat:IsNotNull() do -- Check all instances but disregard control and white-space characters
if fhGetValueType(ptrThis):match("text") then -- Text fields need fhGetValueAsText for full text string -- V1.3
if fhGetValueAsText(ptrThis):gsub("[%c%s]","") ~= fhGetValueAsText(ptrThat):gsub("[%c%s]","") then return false end
else -- Age, Date, Integer, etc, fields need fhGetDisplayText -- V1.3
if fhGetDisplayText(ptrThis):gsub("[%c%s]","") ~= fhGetDisplayText(ptrThat):gsub("[%c%s]","") then return false end
end
for _, strRef in ipairs(arrRefs[strRef] or {}) do -- Check any subsidiary fields
if not Check(ptrThis,ptrThat,strRef) then return false end
end
ptrThis:MoveNext("SAME_TAG")
ptrThat:MoveNext("SAME_TAG")
end
return true
end -- function Check
function Main()
local tblRecd = {} -- Result Set tables
local tblThis = {}
local tblThat = {}
local intRecs = 0
if not Pick() then return end -- Pick the Field Tags to check
for intTag, strTag in ipairs({"INDI","FAM"}) do -- Check all Individual & Family Records
local ptrRecd = fhNewItemPtr()
ptrRecd:MoveToFirstRecord(strTag)
while ptrRecd:IsNotNull() do -- Scan all Records
local tblList = {}
local tblDate = {}
local tblFact = {}
local tblDupl = {}
local ptrThis = fhNewItemPtr() -- This Fact pointer
ptrThis:MoveToFirstChildItem(ptrRecd)
while ptrThis:IsNotNull() do -- Scan all Facts -- V1.2 -- Rewrite
if fhIsFact(ptrThis) then
local strDate = fhGetDisplayText(ptrThis,"~.DATE","min")
local strFact = fhGetTag(ptrThis)..fhGetValueAsText(ptrThis)
if not tblList[strDate] then tblList[strDate] = {} end
tblDate = tblList[strDate] -- Add Date to List
if not tblDate[strFact] then tblDate[strFact] = {} end
tblFact = tblDate[strFact] -- Add Fact to Date
table.insert(tblFact,ptrThis:Clone())
if #tblFact > 1 then -- Found a potential duplicate with same Fact Date & Tag & Value
tblDupl[strDate..strFact] = tblFact
end
end
ptrThis:MoveNext()
end
for _, tblFact in pairs( tblDupl ) do -- Review potential duplicates -- V1.2 -- Rewrite
for intThis = 1, #tblFact do
local ptrThis = tblFact[intThis]
for intThat = intThis+1, #tblFact do -- Cater for multiple duplications
local ptrThat = tblFact[intThat]
local flgDuplicate = true
for _, strRef in ipairs(arrTags) do
if not Check(ptrThis,ptrThat,strRef) then -- Check if this Fact and that Fact have matching fields
flgDuplicate = false
break
end
end
if flgDuplicate then -- Duplicate found so save Record and Fact pointers
table.insert(tblRecd,ptrRecd:Clone())
table.insert(tblThis,ptrThis:Clone())
table.insert(tblThat,ptrThat:Clone())
end
end
end
end
ptrRecd:MoveNext()
end
end
local strMessage = (tostring(#tblRecd).." duplicate Facts found."):gsub("1 duplicate Facts","1 duplicate Fact")
local strMode = "item"
if #tblRecd == 0 then -- Report no duplicates found -- V1.1
strMessage = strMessage:gsub("^0","No")
strMode = "text"
table.insert(tblRecd,fhNewItemPtr())
table.insert(tblThis,strMessage)
table.insert(tblThat,fhNewItemPtr())
end
fhOutputResultSetTitles(strTitle) -- Output the Result Set -- V1.1
fhOutputResultSetColumn("Owner's Record", "item", tblRecd, #tblRecd, 200, "align_left", 1)
fhOutputResultSetColumn("Original Fact" ,strMode, tblThis, #tblRecd, 250, "align_left", 2)
fhOutputResultSetColumn("Duplicate Fact", "item", tblThat, #tblRecd, 250, "align_left", 2)
fhMessageBox(strMessage,"MB_OK","MB_ICONINFORMATION")
end -- function Main()
fhInitialise(6,0,0,"save_recommended")
Main()
--[[
@Title: Find Duplicate Facts
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.4
@Keywords:
@LastUpdated: 13 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: Create a Result Set of any duplicate Facts where chosen sub-fields also match.
@V1.4: Centre windows on FH window; Check for Updates button; Ch-ViS 1.4; Added Media OBJE subtags;
@V1.3: Disregard white-space and control characters when comparing fields; Handle long text fields; Add Citation Template Fields (_FIELD);
@V1.2: Disregard Fact order; Fix Place field; Add Sort Date (_SDATE), Fact Flags (_FLGS), Fact Fax (FAX), (RELI) & (RESN), Place Phonetic (FONE), Roman (ROMN) and (MAP), Address hierarchy, and 5.5.1 Object (OBJE) format;
@V1.1: FH V7 Lua 3.5 IUP 3.28 compatible; Always produce Result Set;
@V1.0: First published version;
]]
require "iuplua"
local strVersion = "1.4"
local strTitle = "Find Duplicate Facts "..strVersion -- V1.1
-- fhSetStringEncoding("UTF-8") -- Are accents, etc handled correctly???
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local intTicks = 1 -- Current tick setting may be 0 or 1 -- V1.4 Used in getParamEmulator
local noToggle = true -- Toggle ticks button used -- V1.4 Used in getParamEmulator
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.4
@LastUpdated: 20 Jan 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 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 getParamEmulator() -- Prototype for iup.GetParam(...) -- V1.4
if fhGetAppVersion() > 6 then unpack = table.unpack end
local iupDialog = nil
local iupParams = nil
local arrValues = {}
local isSuccess = false
local strPlugin = fhGetContextInfo("CI_PLUGIN_NAME"):gsub(" %- .*","")
local function handleButton(iupDialog,intIndex,strTitle) -- Handle the dialog buttons
if intIndex == (iup.GETPARAM_OK or -1) then
-- strTitle sometimes needed to determine the function -- 1st button action -- FH V5 needs -1
isSuccess = true
elseif intIndex == (iup.GETPARAM_CANCEL or -3) then -- 2nd button action -- FH V5 needs -3
intTicks = 1 - intTicks
noToggle = false
elseif intIndex == (iup.GETPARAM_HELP or -4) then -- 3rd 'Later Version?' button -- FH V5 needs -4
iupDialog.Active = "NO"
CheckVersionInStore(strPlugin,strVersion)
iupDialog.Active = "YES"
iupDialog.BringFront = "YES"
return 0
end
return 1
end -- function handleButton
local function makeDialog(strTitle,strFormat) -- Make emulated iup.GetParam(...) dialog
local arrFormat = {}
for strForm in strFormat:gmatch(".-\n") do -- Construct parameters from format
local iupParam = iup.param{ format=strForm; }
table.insert(arrFormat,iupParam)
end
iupParams = iup.parambox{ unpack(arrFormat) }
local iupButton = iup.button{ Title="Help && Advice"; Padding="12x8"; } -- Example of extra button
-- iupDialog = iup.dialog{ Title=strTitle; iup.vbox{ iupParams; iupButton; ALIGNMENT="ACENTER"; MARGIN="10x10"; }; close_cb=function() isSuccess = false return iup.CLOSE end; }
iupDialog = iup.dialog{ Title=strTitle; iupParams; close_cb=function() isSuccess = false return iup.CLOSE end; }
if fhGetAppVersion() > 6 then -- Window centres on FH parent
iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
for intParam = 1, iupParams.ParamCount do -- Set all parameter values
local iupParam = iupParams:GetParamParam(intParam-1)
local iupCntrl = iupParam.Control
local anyValue = arrValues[intParam]
if iupParam.Type == "LIST" then anyValue = anyValue + 1 end -- Droplists need an adjustment
iupCntrl.Value = anyValue
end
function iupParams:param_cb(intIndex) -- Parameter call back actions
if intIndex >= 0 then
local iupParam = iupParams:GetParamParam(intIndex) -- Save any parameter value
arrValues[intIndex+1] = tonumber(iupParam.Value) or iupParam.Value
return 1
else
return handleButton(iupDialog,intIndex,strTitle) -- Handle buttons
end
end -- function iupParams:param_cb
function iupButton:action(intButton) -- Display Help Page
local strPlugin = strPlugin:gsub(" ","-"):lower()
fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/"..strPlugin,"","","open")
fhSleep(3000,500)
iupDialog.BringFront = "YES"
return 1
end -- function iupButton:action
end -- local function makeDialog
local function getParam(strTitle,strSize,strFormat,...) -- Emulate iup.GetParam(...)
arrValues = {...}
if fhGetAppVersion() > 6 then
makeDialog(strTitle,strFormat)
if strSize then iupDialog.Size = strSize end
iupDialog:map()
iupDialog.MinSize = iupDialog.NaturalSize
iupDialog:showxy(iup.CENTERPARENT,iup.CENTERPARENT)
if iup.MainLoopLevel()==0 then iup.MainLoop() end
iup.Destroy(iupDialog)
iupDialog = nil
else
local function fncAction(iupDialog,intIndex)
return handleButton(iupDialog,intIndex,strTitle) -- Handle buttons
end -- local function fncAction
arrValues = { iup.GetParam(strTitle,fncAction,strFormat,unpack(arrValues)) }
isSuccess = arrValues[1]
table.remove(arrValues,1)
end
return isSuccess, unpack(arrValues)
end -- local function getParam
return getParam
end -- function getParamEmulator
local getParam = getParamEmulator()
local arrTags = { }
local arrRefs = { -- Level 3 and lower sub-fields
PLAC = { "FORM"; "NOTE2"; "NOTE"; "SOUR2"; "SOUR"; "FONE"; "ROMN"; "MAP"; }; -- V1.2
FONE = { "TYPE"; }; -- V1.2
ROMN = { "TYPE"; }; -- V1.2
MAP = { "LATI"; "LONG"; }; -- V1.2
ADDR = { "ADR1"; "ADR2"; "ADR3"; "CITY"; "STAE"; "POST"; "CTRY"; }; -- V1.2
_SHAR = { "ROLE"; "NOTE2"; "SOUR"; "SOUR2"; "_SENT"; };
_SHAN = { "ROLE"; "NOTE2"; "SOUR"; "SOUR2"; };
SOUR2 = { "TEXT"; "NOTE2"; "NOTE"; };
SOUR = { "DATA"; "NOTE2"; "NOTE"; "OBJE2"; "OBJE"; "QUAY"; "PAGE"; "EVEN"; "_FIELD"; }; -- V1.3
DATA = { "DATE"; "TEXT"; };
EVEN = { "ROLE"; };
OBJE2 = { "TITL"; "FORM"; "FILE"; "NOTE2"; "NOTE"; "_NOTE"; "_DATE"; };
OBJE = { "_SEQ"; "_CAPT"; "_EXCL"; "_AREA"; "_NOTA"; "SOUR"; "SOUR2"; }; -- V1.4
FILE = { "FORM"; }; -- V1.2
FORM = { "MEDI"; }; -- V1.2
NOTE2 = { "SOUR"; "SOUR2"; };
NOTE = { "SOUR"; "SOUR2"; };
HUSB = { "AGE"; };
WIFE = { "AGE"; };
}
function Pick() -- Pick which level 2 Fields to include
local strFactMail = "Fact Email (_EMAIL)"
local strFactWeb = "Fact Web Site (_WEB)"
if fhGetAppVersion() > 6 then -- Adjust FH V6 GEDCOM 5.5 & FH V7 GEDCOM 5.5.1 tags -- V1.1
strFactMail = "Fact Email (EMAIL)"
strFactWeb = "Fact Web Site (WWW)"
end
local arrField = {
"Fact Age (AGE)"; "Fact Place (PLAC)"; "Fact Address (ADDR)"; "Fact Cause (CAUS)";
"Fact Phone (PHON)"; strFactMail; strFactWeb; "Fact Descriptor (TYPE)"; -- V1.1
"Witness Individual (_SHAR)"; "Witness Name Only (_SHAN)";
"Narrative Sentence (_SENT)"; "Parents Family (FAMC)"; "Responsible Agency (AGNC)";
"Local Note (NOTE2)"; "Note Record (NOTE)";
"Media Record (OBJE)"; "Local Media (OBJE2)";
"Source Citation (SOUR)"; "Source Note (SOUR2)";
}
if fhGetAppVersion() > 6 then -- Add FH V7 Sort Date, Flags, Fax, etc -- V1.2
table.insert(arrField, 1,"Sort Date (_SDATE)")
table.insert(arrField, 2,"Fact Flags (_FLGS)")
table.insert(arrField, 9,"Fact Fax (FAX)")
table.insert(arrField,17,"Religious Affiliation (RELI)")
table.insert(arrField,18,"Restriction Notice (RESN)")
end
local arrReply = { } -- GetParam reply tick values
local strParam = "" -- Format of paramaters
local arrTicks = { " Enable Ticks "; " Remove Ticks "; } -- Toggle ticks button captions
local function setTicks(intParam) -- Return same number of 0's or 1's as Field params
intParam = intParam - 1
if intParam == 0 then return intTicks end -- End recursion
return intTicks,setTicks(intParam)
end -- local function setTicks
local function GetParam(intParam) -- GetParam user dialogue
strParam = "Tick fields that must match in duplicate Facts: %t\n"..strParam
strParam = strParam.."%u[ Find Facts , Enable Ticks , Later Version? ]\nPlugin provides a Result Set of the duplicates. %t\n" -- V1.4
local arrParam = { }
repeat
noToggle = true -- Repeat until toggle button not used, and swap button caption each time
strParam = strParam:gsub(arrTicks[2-intTicks],arrTicks[intTicks+1])
arrParam = { getParam(strTitle,nil,strParam,setTicks(intParam)) } -- V1.4
until noToggle
for intParam = 2, #arrParam do
table.insert(arrReply,arrParam[intParam]) -- Append parameter tick replies
end
return arrParam[1] -- Find Facts = true, X Close = false
end -- local function GetParam
for _, strName in ipairs (arrField) do -- Loop through every Field to set user parameters
strParam = strParam..strName.." %b\n"
end
if GetParam(#arrField) then -- Get user parameter tick replies
for intReply, isReply in ipairs (arrReply) do
if isReply == 1 then
local strTags = arrField[intReply]:match(" %((.+)%)$") -- Compose list of Field Tags
table.insert(arrTags,strTags)
if strTags == "AGE" then -- Cater for Family Fact Ages
table.insert(arrTags,"HUSB")
table.insert(arrTags,"WIFE")
end
end
end
return true -- Continue to Find Facts
end
return false -- X Close Plugin
end -- function Pick
function Check(ptrThis,ptrThat,strRef) -- Check any subfield matches
ptrThis = fhGetItemPtr(ptrThis,"~."..strRef)
ptrThat = fhGetItemPtr(ptrThat,"~."..strRef)
while ptrThis:IsNotNull() or ptrThat:IsNotNull() do -- Check all instances but disregard control and white-space characters
if fhGetValueType(ptrThis):match("text") then -- Text fields need fhGetValueAsText for full text string -- V1.3
if fhGetValueAsText(ptrThis):gsub("[%c%s]","") ~= fhGetValueAsText(ptrThat):gsub("[%c%s]","") then return false end
else -- Age, Date, Integer, etc, fields need fhGetDisplayText -- V1.3
if fhGetDisplayText(ptrThis):gsub("[%c%s]","") ~= fhGetDisplayText(ptrThat):gsub("[%c%s]","") then return false end
end
for _, strRef in ipairs(arrRefs[strRef] or {}) do -- Check any subsidiary fields
if not Check(ptrThis,ptrThat,strRef) then return false end
end
ptrThis:MoveNext("SAME_TAG")
ptrThat:MoveNext("SAME_TAG")
end
return true
end -- function Check
function Main()
local tblRecd = {} -- Result Set tables
local tblThis = {}
local tblThat = {}
local intRecs = 0
if not Pick() then return end -- Pick the Field Tags to check
for intTag, strTag in ipairs({"INDI","FAM"}) do -- Check all Individual & Family Records
local ptrRecd = fhNewItemPtr()
ptrRecd:MoveToFirstRecord(strTag)
while ptrRecd:IsNotNull() do -- Scan all Records
local tblList = {}
local tblDate = {}
local tblFact = {}
local tblDupl = {}
local ptrThis = fhNewItemPtr() -- This Fact pointer
ptrThis:MoveToFirstChildItem(ptrRecd)
while ptrThis:IsNotNull() do -- Scan all Facts -- V1.2 -- Rewrite
if fhIsFact(ptrThis) then
local strDate = fhGetDisplayText(ptrThis,"~.DATE","min")
local strFact = fhGetTag(ptrThis)..fhGetValueAsText(ptrThis)
if not tblList[strDate] then tblList[strDate] = {} end
tblDate = tblList[strDate] -- Add Date to List
if not tblDate[strFact] then tblDate[strFact] = {} end
tblFact = tblDate[strFact] -- Add Fact to Date
table.insert(tblFact,ptrThis:Clone())
if #tblFact > 1 then -- Found a potential duplicate with same Fact Date & Tag & Value
tblDupl[strDate..strFact] = tblFact
end
end
ptrThis:MoveNext()
end
for _, tblFact in pairs( tblDupl ) do -- Review potential duplicates -- V1.2 -- Rewrite
for intThis = 1, #tblFact do
local ptrThis = tblFact[intThis]
for intThat = intThis+1, #tblFact do -- Cater for multiple duplications
local ptrThat = tblFact[intThat]
local flgDuplicate = true
for _, strRef in ipairs(arrTags) do
if not Check(ptrThis,ptrThat,strRef) then -- Check if this Fact and that Fact have matching fields
flgDuplicate = false
break
end
end
if flgDuplicate then -- Duplicate found so save Record and Fact pointers
table.insert(tblRecd,ptrRecd:Clone())
table.insert(tblThis,ptrThis:Clone())
table.insert(tblThat,ptrThat:Clone())
end
end
end
end
ptrRecd:MoveNext()
end
end
local strMessage = (tostring(#tblRecd).." duplicate Facts found."):gsub("1 duplicate Facts","1 duplicate Fact")
local strMode = "item"
if #tblRecd == 0 then -- Report no duplicates found -- V1.1
strMessage = strMessage:gsub("^0","No")
strMode = "text"
table.insert(tblRecd,fhNewItemPtr())
table.insert(tblThis,strMessage)
table.insert(tblThat,fhNewItemPtr())
end
fhOutputResultSetTitles(strTitle) -- Output the Result Set -- V1.1
fhOutputResultSetColumn("Owner's Record", "item", tblRecd, #tblRecd, 200, "align_left", 1)
fhOutputResultSetColumn("Original Fact" ,strMode, tblThis, #tblRecd, 250, "align_left", 2)
fhOutputResultSetColumn("Duplicate Fact", "item", tblThat, #tblRecd, 250, "align_left", 2)
fhMessageBox(strMessage,"MB_OK","MB_ICONINFORMATION")
end -- function Main()
fhInitialise(6,0,0,"save_recommended")
Main()
Source:Find-Duplicate-Facts-5.fh_lua