Find Duplicate Individuals.fh_lua--[[
@Title: Find Duplicate Individuals
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 3.9
@Keywords:
@LastUpdated: 25 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: Find duplicated Individual Records
@Version Log: See end of file.
@TBD: www.behindthename.com/api for Related Names as per http://www.fhug.org.uk/forum/viewtopic.php?f=42&t=6089&p=26187#p26187 et seq ...
@TBD: SynthesiseDates(tblInd) to use datBirthDate:SetSimpleDate(fhCallBuiltInFunction('EstimatedBirthDate',ptrIndi,'EARLIEST')) and datDeathDate:SetSimpleDate(fhCallBuiltInFunction('EstimatedDeathDate',ptrIndi,'LATEST'))
@TBD: but only once those functions estimate the Spouse dates when needed to estimate Individual dates as per FH support log EstimatedBirth/DeathDate Functions [#622325].
]]
if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end
--[[
@Title: aa Library Functions Preamble
@Author: Mike Tate
@Version: 4.1
@LastUpdated: 18 Feb 2026
@Description: All the library functions prototype closures for Plugins.
]]
--[[
@Module: +fh+stringx_v3
@Author: Mike Tate
@Version: 3.0
@LastUpdated: 19 Sep 2020
@Description: Extended string functions to supplement LUA string library.
@V3.0: Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility; Added inert(strTxt) function;
@V2.5: Support FH V6 Encoding = UTF-8;
@V2.4: Tolerant of integer & nil parameters just link match & gsub;
@V1.0: Initial version.
]]
local function stringx_v3()
local fh = {} -- Local environment table
-- Supply current file encoding format --
function fh.encoding()
if fhGetAppVersion() > 5 then return fhGetStringEncoding() end
return "ANSI"
end -- function encoding
-- Split a string using "," or chosen separator --
function fh.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
-- Split a string into numbers using " " or "," or "x" separators -- Any non-number remains as a string
function fh.splitnumbers(strTxt)
local tblNum = {}
strTxt = tostring(strTxt or "")
strTxt:gsub("([^ ,x]+)", function(strNum) tblNum[#tblNum+1] = tonumber(strNum) or strNum end)
return tblNum
end -- function splitnumbers
local strMagic = "([%^%$%(%)%%%.%[%]%*%+%-%?])" -- UTF-8 replacement for "(%W)"
-- Hide magic pattern symbols ^ $ ( ) % . [ ] * + - ?
function fh.plain(strTxt)
-- Prefix every magic pattern character with a % escape character,
-- where %% is the % escape, and %1 is the original character capture.
strTxt = tostring(strTxt or ""):gsub(strMagic,"%%%1")
return strTxt
end -- function plain
-- matches is plain text version of string.match()
function fh.matches(strTxt,strFind,intInit)
strFind = tostring(strFind or ""):gsub(strMagic,"%%%1") -- Hide magic pattern symbols
return tostring(strTxt or ""):match(strFind,tonumber(intInit))
end -- function matches
-- replace is plain text version of string.gsub()
function fh.replace(strTxt,strOld,strNew,intNum)
strOld = tostring(strOld or ""):gsub(strMagic,"%%%1") -- Hide magic pattern symbols
return tostring(strTxt or ""):gsub(strOld,function() return strNew end,tonumber(intNum)) -- Hide % capture symbols
end -- function replace
-- Hide % escape/capture symbols in replacement so they are inert
function fh.inert(strTxt)
strTxt = tostring(strTxt or ""):gsub("%%","%%%%") -- Hide all % symbols
return strTxt
end -- function inert
-- convert is pattern without captures version of string.gsub()
function fh.convert(strTxt,strOld,strNew,intNum)
return tostring(strTxt or ""):gsub(tostring(strOld or ""),function() return strNew end,tonumber(intNum)) -- Hide % capture symbols
end -- function convert
local dicUpper = { }
local dicLower = { }
local dicCaseX = { }
-- ASCII unaccented letter translations for Upper, Lower, and Case Insensitive
for intUpper = string.byte("A"), string.byte("Z") do
local strUpper = string.char(intUpper)
local strLower = string.char(intUpper - string.byte("A") + string.byte("a"))
dicUpper[strLower] = strUpper
dicLower[strUpper] = strLower
local strCaseX = "["..strUpper..strLower.."]"
dicCaseX[strLower] = strCaseX
dicCaseX[strUpper] = strCaseX
end
-- Supply character length of ANSI text --
function fh.length(strTxt)
return string.len(strTxt or "")
end -- function length
-- Supply character substring of ANSI text --
function fh.substring(strTxt,i,j)
return string.sub(strTxt or "",i,j)
end -- function substring
-- Translate upper/lower case ANSI letters to pattern that matches both --
function fh.caseless(strTxt)
strTxt = tostring(strTxt or ""):gsub("[A-Za-z]",dicCaseX)
return strTxt
end -- function caseless
if fh.encoding() == "UTF-8" then
-- Supply character length of UTF-8 text --
function fh.length(strTxt)
isFlag = fhIsConversionLossFlagSet()
strTxt = fhConvertUTF8toANSI(strTxt or "")
fhSetConversionLossFlag(isFlag)
return string.len(strTxt)
end -- function length
local strUTF8 = "([%z\1-\127\194-\244][\128-\191]*)" -- Cater for Lua 5.1 %z or Lua 5.3 \0
if fhGetAppVersion() > 6 then
strUTF8 = "([\0-\127\194-\244][\128-\191]*)"
end
-- Supply character substring of UTF-8 text --
function fh.substring(strTxt,i,j)
local strSub = ""
j = j or -1
if j < 0 then j = j + length(strTxt) + 1 end
if i < 0 then i = i + length(strTxt) + 1 end
for strChr in string.gmatch(strTxt or "",strUTF8) do
if j <= 0 then break end
j = j - 1
i = i - 1
if i <= 0 then strSub = strSub..strChr end
end
return strSub
end -- function substring
-- Translate lower case to upper case UTF-8 letters --
function fh.upper(strTxt)
strTxt = tostring(strTxt or ""):gsub("([a-z\194-\244][\128-\191]*)",dicUpper)
return strTxt
end -- function upper
-- Translate upper case to lower case UTF-8 letters --
function fh.lower(strTxt)
strTxt = tostring(strTxt or ""):gsub("([A-Z\194-\244][\128-\191]*)",dicLower)
return strTxt
end -- function lower
-- Translate upper/lower case UTF-8 letters to pattern that matches both --
function fh.caseless(strTxt)
strTxt = tostring(strTxt or ""):gsub("([A-Za-z\194-\244][\128-\191]*)",dicCaseX)
return strTxt
end -- function caseless
-- Following tables use ASCII numeric coding to be immune from ANSI/UTF-8 encoding --
local arrPairs = -- Upper & Lower case groups of UTF-8 letters with same prefix --
{-- { Prefix; Beg ; End ; Inc; Offset Upper > Lower }; -- These include all ANSI letters and many more
{ "\195"; 0x80; 0x96; 1 ; 32 }; -- 195=0xC3 À U+00C0 to Ö U+00D6 and à U+00E0 to ö U+00F6
{ "\195"; 0x98; 0x9E; 1 ; 32 }; -- 195=0xC3 Ø U+00D8 to Þ U+00DE and ø U+00F8 to þ U+00FE
{ "\196"; 0x80; 0xB6; 2 ; 1 }; -- 196=0xC4 A U+0100 to k U+0137 in pairs
{ "\196"; 0xB9; 0xBD; 2 ; 1 }; -- 196=0xC4 L U+0139 to l U+013E in pairs
{ "\197"; 0x81; 0x87; 2 ; 1 }; -- 197=0xC5 L U+0141 to n U+0148 in pairs
{ "\197"; 0x8A; 0xB6; 2 ; 1 }; -- 197=0xC5 ? U+014A to y U+0177 in pairs
{ "\197"; 0xB9; 0xBD; 2 ; 1 }; -- 197=0xC5 Z U+0179 to ž U+017E in pairs
{ "\198"; 0x82; 0x84; 2 ; 1 }; -- 198=0xC6 ? U+0182 to ? U+0185 in pairs
-- Add more Unicode groups here as usage increases --
}
local dicPairs = -- Upper v Lower case UTF-8 letters that don't fit groups above --
{ [string.char(0xC4,0xBF)] = string.char(0xC5,0x80); -- ? U+013F and ? U+0140
[string.char(0xC5,0xB8)] = string.char(0xC3,0xBF); -- Ÿ U+0178 and ÿ U+00FF
}
local intBeg1 = string.byte(string.sub("À",1))
local intBeg2 = string.byte(string.sub("À",2))
local intEnd1 = string.byte(string.sub("Z",1))
local intEnd2 = string.byte(string.sub("Z",2))
-- print(string.format("%#x %#x %#x %#x",intBeg1,intBeg2,intEnd1,intEnd2)) -- Useful to work out numeric coding
-- Populate the UTF-8 letter translation dictionaries --
for intGroup, tblGroup in ipairs ( arrPairs ) do -- UTF-8 accented letter groups
local strPrefix = tblGroup[1]
for intUpper = tblGroup[2], tblGroup[3], tblGroup[4] do
local strUpper = string.char(intUpper)
local strLower = string.char(intUpper + tblGroup[5])
local strCaseX = strPrefix.."["..strUpper..strLower.."]"
strUpper = strPrefix..strUpper
strLower = strPrefix..strLower
dicUpper[strLower] = strUpper
dicLower[strUpper] = strLower
dicCaseX[strLower] = strCaseX
dicCaseX[strUpper] = strCaseX
end
end
for strUpper, strLower in pairs ( dicPairs ) do -- UTF-8 accented letters where upper & lower have different prefix
dicUpper[strLower] = strUpper
dicLower[strUpper] = strLower
local strCaseX = ""
for intByte = 1, #strUpper do -- Matches more than just the two letters, but can't do any better
strCaseX = strCaseX.."["..strUpper:sub(intByte,intByte)..strLower:sub(intByte,intByte).."]"
end
dicCaseX[strLower] = strCaseX
dicCaseX[strUpper] = strCaseX
end
end
-- overload fh functions into string table
for strIndex, anyValue in pairs(fh) do
if type(anyValue) == "function" then
string[strIndex] = anyValue
end
end
return fh
end -- local function stringx_v3
local stringx = stringx_v3() -- To access FH string extension module
--[[
@Module: +fh+iterate_v3
@Author: Mike Tate
@Version: 3.0
@LastUpdated: 25 Aug 2020
@Description: An iterater functions module to supplement LUA functions.
@V3.0: Function Prototype Closure version.
@V1.2: RecordTypes() includes HEAD tag.
@V1.1: ?
@V1.0: Initial version.
]]
local function iterate_v3()
local fh = {} -- Local environment table
-- Iterator for all records of one chosen type --
function fh.Records(strType)
local ptrAll = fhNewItemPtr() -- Pointer to all records in turn
local ptrRec = fhNewItemPtr() -- Pointer to record returned to user
ptrAll:MoveToFirstRecord(strType)
return function ()
ptrRec:MoveTo(ptrAll)
ptrAll:MoveNext()
if ptrRec:IsNotNull() then return ptrRec end
end
end -- function Records
-- Iterator for all the record types --
function fh.RecordTypes()
local intNext = -1 -- Next record type number
local intLast = fhGetRecordTypeCount() -- Last record type number
return function()
intNext = intNext + 1
if intNext == 0 then -- Includes HEAD tag -- V1.2
return "HEAD"
elseif intNext <= intLast then
return fhGetRecordTypeTag(intNext) -- Return record type tag
end
end
end -- function RecordTypes
-- Iterator for all items in all records of chosen types --
function fh.Items(...)
local arg = {...}
local intType = 1 -- Integer record type number
local tblType = {} -- Table of record type tags
local ptrNext = fhNewItemPtr() -- Pointer to next item in turn
local ptrItem = fhNewItemPtr() -- Pointer to item returned to user
if #arg == 0 then
for intType = 1, fhGetRecordTypeCount() do -- No parameters so use all record types
tblType[intType] = fhGetRecordTypeTag(intType)
end
else
tblType = arg -- Got parameters so use them instead
end
-- print(tblType[intType],intType)
ptrNext:MoveToFirstRecord(tblType[intType]) -- Get first record of first type
return function()
repeat
while ptrNext:IsNotNull() do -- Loop through all items
ptrItem:MoveTo(ptrNext)
ptrNext:MoveNextSpecial()
if ptrItem:IsNotNull() then return ptrItem end
end
intType = intType + 1 -- Loop through each record type
if intType <= #tblType then
ptrNext:MoveToFirstRecord(tblType[intType])
end
until intType > #tblType
end
end -- function Items
-- Iterator for all facts of an individual --
function fh.Facts(ptrIndi)
local ptrItem = fhNewItemPtr() -- Pointer to each item at level 1
local ptrFact = fhNewItemPtr() -- Pointer to each fact returned to user
ptrItem:MoveToFirstChildItem(ptrIndi)
return function ()
while ptrItem:IsNotNull() do
ptrFact:MoveTo(ptrItem)
ptrItem:MoveNext()
if fhIsFact(ptrFact) then return ptrFact end
end
end
end -- function Facts
return fh
end -- local function iterate_v3
local iterate = iterate_v3() -- To access FH iterate items module
--[[
@Module: +fh+general_v3
@Author: Mike Tate
@Version: 3.5
@LastUpdated: 12 Dec 2024
@Description: A general functions module to supplement LUA functions, where filenames use UTF-8 but for a few exceptions.
@V3.5: Further fix for Crossover/WINE file attributes;
@V3.4: Further fix for Unix/WINE file attributes;
@V3.3: Fix problem in fh.MakeFolder(...) when folder path is invalid; Remove Type, ShortName & ShortPath from attributes(...) as unsupported in WINE;
@V3.2: Added function DetectOldModules(); Updated functions RenameFile(), RenameFolder() & GetFolderContents();
@V3.1: Functions derived from FH V7 fhFileUtils library using File System Objects, plus additional features;
@V3.0: Function Prototype Closure version; GetDayNumber() error message reasons;
@V1.5: Revised SplitFilename(strFilename) for missing extension.
@V1.4: Revised EstimatedBirthDates() & EstimatedDeathDates() to fix null Dates.
@V1.3: Add GetDayNumber(), EstimatedBirthDates(), EstimatedDeathDates().
@V1.2: SplitFilename() updated for directory only paths, and MakeFolder() added.
@V1.1: pl.path experiment revoked. New DirTree with omit branch option. Avoid using stringx_v2.
@V1.0: Initial version.
]]
local function general_v3()
local fh = {} -- Local environment table
require("luacom") -- To create File System Object
fh.FSO = luacom.CreateObject("Scripting.FileSystemObject")
-- 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 fh.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 fh.GetParentFolder(strFileName)
-- strFileName ~ full file path
-- return value ~ parent folder path
local strParent = fh.FSO:GetParentFolderName(strFileName) --! Faulty in FH v6 with Unicode chars in path
if fhGetAppVersion() == 6 then
local _, wasAnsi = fh.FileNameToANSI(strFileName)
if not wasAnsi then
strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
end
end
return strParent
end -- function GetParentFolder
-- Check if file exists --
function fh.FlgFileExists(strFileName)
-- strFileName ~ full file path
-- return value ~ true if it exists
return fh.FSO:FileExists(strFileName)
end -- function FlgFileExists
-- Check if folder exists --
function fh.FlgFolderExists(strFolderName)
-- strFolderName ~ full file path
-- return value ~ true if it exists
return fh.FSO:FolderExists(strFolderName)
end -- function FlgFolderExists
-- Delete a file if it exists --
function fh.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 fh.FSO:FileExists(strFileName) then
fh.FSO:DeleteFile(strFileName,true)
if fh.FSO:FileExists(strFileName) then
doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
return false
end
end
return true
end -- function DeleteFile
-- Delete a folder if it exists including contents --
function fh.DeleteFolder(strFolderName,errFunction)
-- strFolderName ~ full folder path
-- errFunction ~ optional error reporting function
-- return value ~ true if folder does not exist or is deleted else false
if fh.FSO:FolderExists(strFolderName) then
fh.FSO:DeleteFolder(strFolderName,true)
if fh.FSO:FolderExists(strFolderName) then
doError("Folder Not Deleted:\n"..strFolderName.."\n",errFunction)
return false
end
end
return true
end -- function DeleteFolder
-- Rename a file if it exists --
function fh.RenameFile(strFileName,strNewName)
-- strFileName ~ full file path
-- strNewName ~ new file name & type
-- return value ~ true if file exists but new name does not and rename is OK else false
local strNewFile = fh.GetParentFolder(strFileName).."\\"..strNewName
if fh.FSO:FileExists(strFileName) and not fh.FSO:FileExists(strNewFile) then
local fileObject = fh.FSO:GetFile(strFileName)
fileObject.Name = strNewName
if fh.FSO:FileExists(strNewFile) then
return true
end
end
return false
end -- function RenameFile
-- Rename a folder if it exists --
function fh.RenameFolder(strFolderName,strNewName)
-- strFolderName ~ full folder path
-- strNewName ~ new folder name
-- return value ~ true if folder exists but new name does not and rename is OK else false
local strNewFolder = fh.GetParentFolder(strFolderName).."\\"..strNewName
if fh.FSO:FolderExists(strFolderName) and not fh.FSO:FolderExists(strNewFolder) then
local folderObject = fh.FSO:GetFolder(strFolderName)
folderObject.Name = strNewName
if fh.FSO:FolderExists(strNewFolder) then
return true
end
end
return false
end -- function RenameFolder
-- Copy a file if it exists and destination is not a folder --
function fh.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 fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
fh.FSO:CopyFile(strFileName,strDestination)
if fh.FSO:FileExists(strDestination) then
return true
end
end
return false
end -- function CopyFile
-- Copy a folder if it exists and destination is not a file --
function fh.CopyFolder(strFolderName,strDestination)
-- strFolderName ~ full source folder path
-- strDestination ~ full target folder path
-- return value ~ true if folder exists and is copied else false
if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
fh.FSO:CopyFolder(strFolderName,strDestination)
if fh.FSO:FolderExists(strDestination) then
return true
end
end
return false
end -- function CopyFolder
-- Move a file if it exists and destination is not a folder --
function fh.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 fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
if fh.DeleteFile(strDestination) then
fh.FSO:MoveFile(strFileName,strDestination)
if fh.FSO:FileExists(strDestination) then
return true
end
end
end
return false
end -- function MoveFile
-- Move a folder if it exists and destination is not a file --
function fh.MoveFolder(strFolderName,strDestination)
-- strFolderName ~ full source folder path
-- strDestination ~ full target folder path
-- return value ~ true if folder exists and is moved else false
if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
if fh.DeleteFolder(strDestination) then
fh.FSO:MoveFolder(strFolderName,strDestination)
if fh.FSO:FolderExists(strDestination) then
return true
end
end
end
return false
end -- function MoveFolder
local function CreateFolder(strFolderName) -- V3.3
fh.FSO:CreateFolder(strFolderName)
end -- local function CreateFolder(strFolderName)
-- Make subfolder recursively if does not exist --
function fh.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 fh.FSO:FolderExists(strFolderName) then
if #strFolderName > 4 -- V3.3
and not fh.MakeFolder(fh.GetParentFolder(strFolderName),errFunction) then
return false
end
if not pcall(CreateFolder,strFolderName) -- V3.3
or not fh.FSO:FolderExists(strFolderName) then
doError("Cannot Make Folder Path: \n"..strFolderName.." \n",errFunction)
return false
end
end
return true
end -- function MakeFolder
-- Check if folder writable --
function fh.FlgFolderWrite(strFolderName)
-- strFolderName ~ full source folder path
-- return value ~ true if folder writable else false
if fh.FlgFolderExists(strFolderName) then
if fh.MakeFolder(strFolderName.."\\vwxyz") then
fh.FSO:DeleteFolder(strFolderName.."\\vwxyz",true)
return true
end
end
return false
end -- function FlgFolderWrite
-- Open File with ANSI path and return Handle --
function fh.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 fh.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 = fh.FileNameToANSI(strFileName)
local fileHandle = fh.OpenFile(strAnsi,"w")
fileHandle:write(strContents)
assert(fileHandle:close())
if not wasAnsi then
fh.MoveFile(strAnsi,strFileName)
end
return true
end -- function SaveStringToFile
-- Load string from file --
function fh.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 = fh.FileNameToANSI(strFileName)
if not wasAnsi then
fh.CopyFile(strFileName,strAnsi)
end
local fileHandle = fh.OpenFile(strAnsi,"r")
local strContents = fileHandle:read("*all")
assert(fileHandle:close())
return strContents
end -- function StrLoadFromFile
-- Returns the Path, Filename, and Extension as 3 values --
function fh.SplitFilename(strFileName)
-- strFileName ~ full file path
-- return values ~ path, name.type, type
if fh.FSO:FolderExists(strFileName) then
local strPath = strFileName:gsub("[\\/]$","")
return strPath.."\\","",""
end
strFileName = strFileName.."."
return strFileName:match("^(.-)([^\\/]-%.([^\\/%.]-))%.?$")
end -- function SplitFilename
-- Convert dd/mm/yyyy hh:mm:ss format to integer seconds -- (DateTime format is used in attributes returned by GetFolderContents and DirTree below)
function fh.IntTime(strDateTime)
-- strDateTime ~ date time string
-- return value ~ integer seconds since 01/01/1970 00:00:00
local strDay,strMonth,strYear,strHour,strMin,strSec = strDateTime:match("^(%d%d)/(%d%d)/(%d+) (%d%d):(%d%d):(%d%d)")
if tonumber(strYear) < 1970 then return 0 end
local isDST = false
if tonumber(strMonth) > 4 and tonumber(strMonth) < 11 then isDST = true end -- Approximation is sometimes wrong
local intTime = os.time( { year=strYear; month=strMonth; day=strDay; hour=strHour; min=strMin; sec=strSec; isdst=isDST; } )
local tblDat = os.date("*t",intTime)
if tblDat.isdst then
intTime = intTime + 3600
isDST = true
end
return intTime
end -- function IntTime
-- Return table of attributes for folder --
local function attribFolder(tblAttr)
-- tblAttr ~ folder attributes table
-- return value ~ attributes table like LFS except datetimes
-- WINE only supports the tblAttr.name & tblAttr.path
return { mode="directory"; name=tblAttr.name; path=tblAttr.path; }
end -- local function attribFolder
-- Return table of attributes for file --
local function attribFile(tblAttr)
-- tblAttr ~ file attributes table
-- return value ~ attributes table like LFS except datetimes
-- WINE does not support the tblAttr.type, tblAttr.shortname, tblAttr.shortpath & sometimes tblAttr.datecreated
return { mode="file"; name=tblAttr.name; path=tblAttr.path; size=tblAttr.size; modified=tblAttr.datelastmodified; attributes=tblAttr.attributes; }
end -- local function attribFile
-- Return attributes table of all files and folders in a specified folder --
function fh.GetFolderContents(strFolder,doRecurse)
-- strFolder ~ full folder path
-- doRecurse ~ true for recursion
-- return value ~ attributes table
local arrList = {}
if fh.FSO:FolderExists(strFolder) then
local function getFileList(strFolder)
local tblList = fh.FSO:GetFolder(strFolder)
local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
local tblAttr = tblEnum:Next()
while tblAttr do
table.insert(arrList,attribFolder(tblAttr))
if doRecurse then getFileList(tblAttr.path) end
tblAttr = tblEnum:Next()
end
local tblEnum = luacom.GetEnumerator(tblList.Files)
local tblAttr = tblEnum:Next()
while tblAttr do
table.insert(arrList,attribFile(tblAttr))
tblAttr = tblEnum:Next()
end
end
getFileList(strFolder)
end
return arrList
end -- function GetFolderContents
-- Return a Directory Tree entry & attributes on each iteration --
function fh.DirTree(strDir,...)
-- strDir ~ full folder path
-- ... ~ list of folders to omit
-- return value ~ full path, attributes table
local arg = {...}
assert( fh.FSO:FolderExists(strDir), "directory parameter is missing or empty" )
local function yieldtree(strDir)
local tblList = fh.FSO:GetFolder(strDir)
local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
local tblAttr = tblEnum:Next()
while tblAttr do -- for _,tblAttr in luacom.pairs(tblList.SubFolders) do -- pairs not working in FH v6 so use tblEnum code
coroutine.yield(tblAttr.path,attributes(tblAttr,"directory"))
local isOK = true
for _,strOmit in ipairs (arg) do
if tblAttr.path:match(strOmit) then -- Omit tree branch
isOK = false
break
end
end
if isOK then yieldtree(tblAttr.path) end
tblAttr = tblEnum:Next()
end
local tblEnum = luacom.GetEnumerator(tblList.Files)
local tblAttr = tblEnum:Next()
while tblAttr do -- for _,tblAttr in luacom.pairs(tblList.Files) do -- pairs not working in FH v6 so use tblEnum code
coroutine.yield(tblAttr.path,attributes(tblAttr,"file"))
tblAttr = tblEnum:Next()
end
end
return coroutine.wrap(function() yieldtree(strDir) end)
end -- function DirTree
-- Detect FH V5/6 old library modules and advise removal --
function fh.DetectOldModules()
if fhGetAppVersion() > 6 then
local strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugins\\"
local arrFile = { "compat53.lua"; "ltn12.lua"; "luasql\\sqlite3.dll"; "md5.lua"; "pl\\init.lua"; "socket.lua"; "utf8.lua"; "zip.dll"; }
for _, strFile in ipairs (arrFile) do
if fh.FSO:FileExists(strPath..strFile) then
fhMessageBox("\n Detected some old FH V6 library modules. \n\nPlease remove them by running the plugin: \n\n 'Delete old FH6 Plugin Module Files' \n","MB_OK","MB_ICONEXCLAMATION")
break
end
end
end
end -- function DetectOldModules
if fhGetAppVersion() > 6 then unpack = table.unpack end
-- Invoke FH Shell Execute API --
function fh.DoExecute(strExecutable,...)
-- strExecutable ~ full path of executable
-- ... ~ parameter list and optional error reporting function
-- return value ~ true if successful else false
local arg = {...}
local errFunction = fhMessageBox
if type(arg[#arg]) == 'function' then
errFunction = arg[#arg]
table.remove(arg)
end
local isOK, intErrorCode, strErrorText = fhShellExecute(strExecutable,unpack(arg))
if not isOK then
errFunction(tostring(strErrorText).." ("..tostring(intErrorCode)..")")
end
return isOK
end -- function DoExecute
-- Obtain the Day Number for any Date Point -- -- Fix problems with invalid dates in DayNumber function
function fh.GetDayNumber(datDate)
-- datDate ~ date point
-- return value ~ day number
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
fhMessageBox("\n Get Day Number issue for date \n "..oldDate:GetDisplayText().." \n "..strError.." \n So replaced it with date \n "..newDate:GetDisplayText().." \n "..strIsBC,"MB_OK","MB_ICONEXCLAMATION")
end
return intDay
end -- function GetDayNumber
local dtpYearMin = fhNewDatePt(1000) -- Minimum year to use when earliest estimate is null
local dtpYearMax = fhNewDatePt(2000) -- Maximum year to use when latest estimate is null
function fh.GetYearToday() -- Get the Year for Today
-- return value ~ integer year today
local intYearToday = fhCallBuiltInFunction("Year",fhCallBuiltInFunction("Today"))
dtpYearMax = fhNewDatePt(intYearToday) -- Set maximum year date point
return intYearToday
end -- function GetYearToday()
local function getDeathFacts(ptrIndi) -- Iterate Death, Burial, Cremation facts
-- ptrIndi ~ pointer to individual
-- return value ~ pointer to fact
local arrFact = { "~.DEAT"; "~.BURI"; "~.CREM"; }
local intFact = 0
local ptrFact = fhNewItemPtr() -- Pointer to each fact returned to user
return function ()
while intFact < #arrFact do
intFact = intFact + 1
ptrFact = fhGetItemPtr(ptrIndi,arrFact[intFact])
if ptrFact:IsNotNull() then return ptrFact end
end
end
end -- local function getDeathFacts
-- Ensure Estimated Date EARLIEST <= LATEST <= Fact Date -- -- Fix errors in EstimatedBirth/DeathDate function
local function estimatedDates(strFunc,ptrIndi,intGens,getFact,intYrs)
-- strFunc ~ "EstimatedBirthDate" or "EstimatedDeathDate"
-- ptrIndi ~ Individual of interest
-- intGens ~ Number of generations (may be nil)
-- getFact ~ Iterator function for facts
-- intYrs ~ Years to add to After dates
-- return values ~ EARLIEST, MID, LATEST dates
intGens = intGens or 2
local dtpMin = fhCallBuiltInFunction(strFunc,ptrIndi,"EARLIEST",intGens)
local dtpMax = fhCallBuiltInFunction(strFunc,ptrIndi,"LATEST",intGens)
local dtpMid = fhNewDatePt()
if not ( dtpMin:IsNull() and dtpMax:IsNull() ) then -- Skip if both null
if dtpMax:IsNull() then dtpMax = dtpYearMax elseif dtpMin:IsNull() then dtpMin = dtpYearMin end
for ptrFact in getFact(ptrIndi) do
local datFact = fhGetValueAsDate(fhGetItemPtr(ptrFact,"~.DATE"))
if not datFact:IsNull() then -- Find 1st Fact Date
local dtpLast = datFact:GetDatePt1() -- Last date = DatePt1 for Simple, Range, and Before
local strType = datFact:GetSubtype() -- Between = DatePt2 and After = DatePt1 + intYrs
if strType == "Between" then dtpLast = datFact:GetDatePt2()
elseif strType == "After" then dtpLast = fhNewDatePt(dtpLast:GetYear()+intYrs,dtpLast:GetMonth(),dtpLast:GetDay()) end -- Compare only uses Year, Month, Day so omitted ,dtpLast:GetYearDD(),dtpLast:GetBC(),dtpLast:GetCalendar()
if dtpMax:Compare(dtpLast) > 0 then dtpMax = dtpLast end
if dtpMin:Compare(dtpMax) > 0 then dtpMin = dtpMax end
if strType ~= "After" then break end -- Now EARLIEST <= LATEST <= Last date
end
end
local intDays = ( fh.GetDayNumber(dtpMax) - fh.GetDayNumber(dtpMin) ) / 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 ) + 0.1 ) -- Offset month is remainder fraction of year * 12
dtpMid = fhCallBuiltInFunction("CalcDate",dtpMin,intYear,intMnth) -- Need approximate MID year & month
end
return { Min=dtpMin; Mid=dtpMid; Max=dtpMax; } -- Return EARLIEST, MID, LATEST dates
end -- local function estimatedDates
-- Make EstimatedBirthDate EARLIEST <= LATEST <= 1st Fact Date -- -- Fix errors in EstimatedBirthDate function
function fh.EstimatedBirthDates(ptrIndi,intGens)
-- ptrInd ~ pointer to individual
-- intGens ~ generations to include
-- return values ~ EARLIEST, MID, LATEST dates
return estimatedDates("EstimatedBirthDate",ptrIndi,intGens,iterate.Facts,10)
end -- function EstimatedBirthDates
-- Make EstimatedDeathDate EARLIEST <= LATEST <= DEAT/BURI/CREM Date -- -- Fix errors in EstimatedDeathDate function
function fh.EstimatedDeathDates(ptrIndi,intGens)
-- ptrInd ~ pointer to individual
-- intGens ~ generations to include
-- return values ~ EARLIEST, MID, LATEST dates
return estimatedDates("EstimatedDeathDate",ptrIndi,intGens,getDeathFacts,100)
end -- function EstimatedDeathDates
--[[
@function: BuildDataRef
@description: Get Full Data Reference for Pointer
@parameters: Item Pointer
@returns: Data Reference String, Record Id Integer, Record Type Tag String
@requires: None
]]
function fh.BuildDataRef(ptrRef)
local strDataRef = "" -- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
local intRecId = 0 -- Record Id for associated Record
local strRecTag = "" -- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc
-- getDataRef() is called recursively per level of the Data Ref
-- ptrRef points to the upper Data Ref levels yet to be analysed
-- strRef compiles the lower Data Ref levels including instances
local function getDataRef(ptrRef,strRef)
local ptrTag = ptrRef:Clone()
local strTag = fhGetTag(ptrTag) -- Current level Tag
ptrTag:MoveToParentItem(ptrTag)
if ptrTag:IsNotNull() then -- Parent level exists
local intSib = 1
local ptrSib = ptrRef:Clone() -- Pointer to siblings with same Tag
ptrSib:MovePrev("SAME_TAG")
while ptrSib:IsNotNull() do -- Count previous siblings with same Tag
intSib = intSib + 1
ptrSib:MovePrev("SAME_TAG")
end
if intSib > 1 then strTag = strTag.."["..intSib.."]" end
getDataRef(ptrTag,"."..strTag..strRef) -- Now analyse the parent level
else
strDataRef = strTag..strRef -- Record level reached, so set return values
intRecId = fhGetRecordId(ptrRef)
strRecTag = strTag
if not fhIsValidDataRef(strDataRef) then print("BuildDataRef: "..strDataRef.." is Invalid") end
end
end -- local function getDataRef
if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end
return strDataRef, intRecId, strRecTag
end -- function BuildDataRef
--[[
@function: GetDataRefPtr
@description: Get Pointer for Full Data Reference
@parameters: Data Reference String, Record Id Integer, Record Type Tag String (optional)
@returns: Item Pointer which IsNull() if any parameters are invalid
@requires: None
]]
function fh.GetDataRefPtr(strDataRef,intRecId,strRecTag)
strDataRef = strDataRef or ""
if not strRecTag then
strRecTag = strDataRef:gsub("^(%u+).*$","%1") -- Extract Record Tag from Data Ref
end
local ptrRef = fhNewItemPtr()
ptrRef:MoveToRecordById(strRecTag,intRecId or 0) -- Lookup the Record by Id
ptrRef:MoveTo(ptrRef,strDataRef) -- Move to the Data Ref
return ptrRef
end -- function GetDataRefPtr
function fh.TblDataRef(ptrRef)
local tblRef = {}
tblRef.DataRef, tblRef.RecId, tblRef.RecTag = BuildDataRef(ptrRef)
return tblRef
end -- function TblDataRef
function fh.PtrDataRef(tblRef)
local tblRef = tblRef or {} -- Ensure table and its fields exist
return GetDataRefPtr(tblRef.DataRef or "",tblRef.RecId or 0,tblRef.RecTag or "")
end -- function PtrDataRef
return fh
end -- local function general_v3
local general = general_v3() -- To access FH general tools module
--[[
@Module: +fh+tablex_v3
@Author: Mike Tate
@Version: 3.1
@LastUpdated: 08 Jan 2022
@Description: A Table Load Save Module.
@V3.1: Cater for full UTF-8 filenames.
@V3.0: Function Prototype Closure version.
@V1.2: Added local definitions of _ to ensure nil gets returned on error.
@V1.1: ?
@V1.0: Initial version 0.94 is Lua 5.1 compatible.
]]
local function tablex_v3()
local fh = {} -- Local environment table
------------------------------------------------------ Start Table Load Save
-- require "_tableloadsave"
--[[
Save Table to File/Stringtable
Load Table from File/Stringtable
v 0.94
Lua 5.1 compatible
Userdata and indices of these are not saved
Functions are saved via string.dump, so make sure it has no upvalues
References are saved
----------------------------------------------------
table.save( table [, filename] )
Saves a table so it can be called via the table.load function again
table must a object of type 'table'
filename is optional, and may be a string representing a filename or true/1
table.save( table )
on success: returns a string representing the table (stringtable)
(uses a string as buffer, ideal for smaller tables)
table.save( table, true or 1 )
on success: returns a string representing the table (stringtable)
(uses io.tmpfile() as buffer, ideal for bigger tables)
table.save( table, "filename" )
on success: returns 1
(saves the table to file "filename")
on failure: returns as second argument an error msg
----------------------------------------------------
table.load( filename or stringtable )
Loads a table that has been saved via the table.save function
on success: returns a previously saved table
on failure: returns as second argument an error msg
----------------------------------------------------
chillcode, http://lua-users.org/wiki/SaveTableToFile
Licensed under the same terms as Lua itself.
]]--
-- declare local variables
--// exportstring( string )
--// returns a "Lua" portable version of the string
local function exportstring( s )
s = string.format( "%q",s )
-- to replace
s = string.gsub( s,"\\\n","\\n" )
s = string.gsub( s,"\r","\\r" )
s = string.gsub( s,string.char(26),"\"..string.char(26)..\"" )
return s
end
--// The Save Function
function fh.save( tbl,filename )
local charS,charE = " ","\n"
local file,err,_,stransi,wasansi -- V1.2 -- V3.1 -- Added _,stransi,wasansi --!
-- create a pseudo file that writes to a string and return the string
if not filename then
file = { write = function( self,newstr ) self.str = self.str..newstr end, str = "" }
charS,charE = "",""
-- write table to tmpfile
elseif filename == true or filename == 1 then
charS,charE,file = "","",io.tmpfile()
-- write table to file
-- use io.open here rather than io.output, since in windows when clicking on a file opened with io.output will create an error
else
stransi,wasansi = general.FileNameToANSI(filename) -- V3.1 -- Cater for non-ANSI filename --!
file,err = io.open( stransi, "w" )
if err then return _,err end
end
-- initiate variables for save procedure
local tables,lookup = { tbl },{ [tbl] = 1 }
file:write( "return {"..charE )
for idx,t in ipairs( tables ) do
if filename and filename ~= true and filename ~= 1 then
file:write( "-- Table: {"..idx.."}"..charE )
end
file:write( "{"..charE )
local thandled = {}
for i,v in ipairs( t ) do
thandled[i] = true
-- escape functions and userdata
if type( v ) ~= "userdata" then
-- only handle value
if type( v ) == "table" then
if not lookup[v] then
table.insert( tables, v )
lookup[v] = #tables
end
file:write( charS.."{"..lookup[v].."},"..charE )
elseif type( v ) == "function" then
file:write( charS.."loadstring("..exportstring(string.dump( v )).."),"..charE )
else
local value = ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
file:write( charS..value..","..charE )
end
end
end
for i,v in pairs( t ) do
-- escape functions and userdata
if (not thandled[i]) and type( v ) ~= "userdata" then
-- handle index
if type( i ) == "table" then
if not lookup[i] then
table.insert( tables,i )
lookup[i] = #tables
end
file:write( charS.."[{"..lookup[i].."}]=" )
else
local index = ( type( i ) == "string" and "["..exportstring( i ).."]" ) or string.format( "[%d]",i )
file:write( charS..index.."=" )
end
-- handle value
if type( v ) == "table" then
if not lookup[v] then
table.insert( tables,v )
lookup[v] = #tables
end
file:write( "{"..lookup[v].."},"..charE )
elseif type( v ) == "function" then
file:write( "loadstring("..exportstring(string.dump( v )).."),"..charE )
else
local value = ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
file:write( value..","..charE )
end
end
end
file:write( "},"..charE )
end
file:write( "}" )
-- Return Values
-- return stringtable from string
if not filename then
-- set marker for stringtable
return file.str.."--|"
-- return stringttable from file
elseif filename == true or filename == 1 then
file:seek ( "set" )
-- no need to close file, it gets closed and removed automatically
-- set marker for stringtable
return file:read( "*a" ).."--|"
-- close file and return 1
else
file:close()
if not ( wasansi ) then -- V3.1 -- Cater for non-ANSI filename --!
general.MoveFile(stransi,filename)
end
return 1
end
end
--// The Load Function
function fh.load( sfile )
local tables,err,_ -- V1.2 -- Added _
-- catch marker for stringtable
if string.sub( sfile,-3,-1 ) == "--|" then
tables,err = loadstring( sfile )
else
local stransi,wasansi = general.FileNameToANSI(sfile) -- V3.1 -- Cater for non-ANSI filename --!
if not ( wasansi ) then
general.CopyFile(sfile,stransi)
end
tables,err = loadfile( stransi )
if not ( wasansi ) then
general.DeleteFile(stransi) -- V3.1 -- Cater for non-ANSI filename --!
end
end
if err then return _,err
end
tables = tables()
for idx = 1,#tables do
local tolinkv,tolinki = {},{}
for i,v in pairs( tables[idx] ) do
if type( v ) == "table" and tables[v[1]] then
table.insert( tolinkv,{ i,tables[v[1]] } )
end
if type( i ) == "table" and tables[i[1]] then
table.insert( tolinki,{ i,tables[i[1]] } )
end
end
-- link values, first due to possible changes of indices
for _,v in ipairs( tolinkv ) do
tables[idx][v[1]] = v[2]
end
-- link indices
for _,v in ipairs( tolinki ) do
tables[idx][v[2]],tables[idx][v[1]] = tables[idx][v[1]],nil
end
end
return tables[1]
end
------------------------------------------------------ End Table Load Save
-- overload fh functions into table
for strIndex, anyValue in pairs(fh) do
if type(anyValue) == "function" then
table[strIndex] = anyValue
end
end
return fh
end -- local function tablex_v3
local tablex = tablex_v3 () -- To access FH table extension module
--[[
@Module: +fh+encoder_v3
@Author: Mike Tate
@Version: 3.6
@LastUpdated: 27 Aug 2024
@Description: Text encoder module for HTML XHTML XML URI UTF8 UTF16 ISO CP1252/ANSI character codings.
@V3.6: In fh.FileLines(...) cater for empty file;
@V3.5: Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility.
@V3.4: Ensure expressions involving gsub return just text parameter.
@V3.3: Adds UNICODE U+10000 to U+10FFFF UTF-16 Supplementary Planes.
@V3.2: Update for ANSI & Unicode to ASCII for sorting, Soundex, etc.
@V3.1: Update for Unicode UTF-16 & UTF-8 and fhConvertANSItoUTF8 & fhConvertUTF8toANSI, name change UTF to UTF8 & CP to ANSI.
@V2.0: StrUTF8_Encode() replaced by StrUTF_CP1252() for entire UTF-8 range, plus new StrCP1252_ISO().
@V1.0: Initial version.
]]
local function encoder_v3()
local fh = {} -- Local environment table
local fhVersion = fhGetAppVersion()
local br_Tag = "
" -- Markup language break tag default
local br_Lua = "
" -- Lua pattern for break tag recognition
local tblCodePage = {} -- Code Page to XML/XHTML/HTML/URI/UTF8 encodings: http://en.wikipedia.org/wiki/Windows-1252 & 1250 & etc
-- Control characters "\000" to "\031" for URI & Markup "[%c]" encodings are disallowed except for "\t" to "\r"
tblCodePage["\000"] = "" -- NUL
tblCodePage["\001"] = "" -- SOH
tblCodePage["\002"] = "" -- STX
tblCodePage["\003"] = "" -- ETX
tblCodePage["\004"] = "" -- EOT
tblCodePage["\005"] = "" -- ENQ
tblCodePage["\006"] = "" -- ACK
tblCodePage["\a"] = "" -- BEL
tblCodePage["\b"] = "" -- BS
tblCodePage["\t"] = "+" -- HT space in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["\n"] = "%0A" -- LF br_Tag in Markup
tblCodePage["\v"] = "%0A" -- VT br_Tag in Markup
tblCodePage["\f"] = "%0A" -- FF br_Tag in Markup
tblCodePage["\r"] = "%0D" -- CR br_Tag in Markup
tblCodePage["\014"] = "" -- SO
tblCodePage["\015"] = "" -- SI
tblCodePage["\016"] = "" -- DLE
tblCodePage["\017"] = "" -- DC1
tblCodePage["\018"] = "" -- DC2
tblCodePage["\019"] = "" -- DC3
tblCodePage["\020"] = "" -- DC4
tblCodePage["\021"] = "" -- NAK
tblCodePage["\022"] = "" -- SYN
tblCodePage["\023"] = "" -- ETB
tblCodePage["\024"] = "" -- CAN
tblCodePage["\025"] = "" -- EM
tblCodePage["\026"] = "" -- SUB
tblCodePage["\027"] = "" -- ESC
tblCodePage["\028"] = "" -- FS
tblCodePage["\029"] = "" -- GS
tblCodePage["\030"] = "" -- RS
tblCodePage["\031"] = "" -- US
-- ASCII characters "\032" to "\127" for URI "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding
tblCodePage[" "] = "+" -- or "%20" Space
tblCodePage["!"] = "%21" -- Reserved character
tblCodePage['"'] = "%22" -- """ in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["#"] = "%23" -- Reserved character
tblCodePage["$"] = "%24" -- Reserved character
tblCodePage["%"] = "%25" -- Must be encoded
tblCodePage["&"] = "%26" -- Reserved character -- "&" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["'"] = "%27" -- Reserved character -- "'" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["("] = "%28" -- Reserved character
tblCodePage[")"] = "%29" -- Reserved character
tblCodePage["*"] = "%2A" -- Reserved character
tblCodePage["+"] = "%2B" -- Reserved character
tblCodePage[","] = "%2C" -- Reserved character
-- tblCodePage["-"] = "%2D" -- Unreserved character not encoded
-- tblCodePage["."] = "%2E" -- Unreserved character not encoded
tblCodePage["/"] = "%2F" -- Reserved character
-- Digits 0 to 9 -- Unreserved characters not encoded
tblCodePage[":"] = "%3A" -- Reserved character
tblCodePage[";"] = "%3B" -- Reserved character
tblCodePage["<"] = "%3C" -- "<" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["="] = "%3D" -- Reserved character
tblCodePage[">"] = "%3E" -- ">" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["?"] = "%3F" -- Reserved character
tblCodePage["@"] = "%40" -- Reserved character
-- Letters A to Z -- Unreserved characters not encoded
tblCodePage["["] = "%5B" -- Reserved character
tblCodePage["\\"]= "%5C"
tblCodePage["]"] = "%5D" -- Reserved character
tblCodePage["^"] = "%5E"
-- tblCodePage["_"] = "%5F" -- Unreserved character not encoded
tblCodePage["`"] = "%60"
-- Letters a to z -- Unreserved characters not encoded
tblCodePage["{"] = "%7B"
tblCodePage["|"] = "%7C"
tblCodePage["}"] = "%7D"
-- tblCodePage["~"] = "%7E" -- Unreserved character not encoded
tblCodePage["\127"] = "" -- DEL
-- Code Page 1252 Unicode characters "\128" to "\255" for UTF-8 scheme "[€-ÿ]" encodings: http://en.wikipedia.org/wiki/UTF-8
tblCodePage["€"] = string.char(0xE2,0x82,0xAC) -- "€"
tblCodePage["\129"] = "" -- Undefined
tblCodePage["‚"] = string.char(0xE2,0x80,0x9A)
tblCodePage["ƒ"] = string.char(0xC6,0x92)
tblCodePage["„"] = string.char(0xE2,0x80,0x9E)
tblCodePage["…"] = string.char(0xE2,0x80,0xA6)
tblCodePage["†"] = string.char(0xE2,0x80,0xA0)
tblCodePage["‡"] = string.char(0xE2,0x80,0xA1)
tblCodePage["ˆ"] = string.char(0xCB,0x86)
tblCodePage["‰"] = string.char(0xE2,0x80,0xB0)
tblCodePage["Š"] = string.char(0xC5,0xA0)
tblCodePage["‹"] = string.char(0xE2,0x80,0xB9)
tblCodePage["Œ"] = string.char(0xC5,0x92)
tblCodePage["\141"] = "" -- Undefined
tblCodePage["Ž"] = string.char(0xC5,0xBD)
tblCodePage["\143"] = "" -- Undefined
tblCodePage["\144"] = "" -- Undefined
tblCodePage["‘"] = string.char(0xE2,0x80,0x98)
tblCodePage["’"] = string.char(0xE2,0x80,0x99)
tblCodePage["“"] = string.char(0xE2,0x80,0x9C)
tblCodePage["”"] = string.char(0xE2,0x80,0x9D)
tblCodePage["•"] = string.char(0xE2,0x80,0xA2)
tblCodePage["–"] = string.char(0xE2,0x80,0x93)
tblCodePage["—"] = string.char(0xE2,0x80,0x94)
tblCodePage["\152"] = string.char(0xCB,0x9C) -- Small Tilde
tblCodePage["™"] = string.char(0xE2,0x84,0xA2)
tblCodePage["š"] = string.char(0xC5,0xA1)
tblCodePage["›"] = string.char(0xE2,0x80,0xBA)
tblCodePage["œ"] = string.char(0xC5,0x93)
tblCodePage["\157"] = "" -- Undefined
tblCodePage["ž"] = string.char(0xC5,0xBE)
tblCodePage["Ÿ"] = string.char(0xC5,0xB8)
tblCodePage["\160"] = string.char(0xC2,0xA0) -- " " No Break Space
tblCodePage["¡"] = string.char(0xC2,0xA1) -- "¡"
tblCodePage["¢"] = string.char(0xC2,0xA2) -- "¢"
tblCodePage["£"] = string.char(0xC2,0xA3) -- "£"
tblCodePage["¤"] = string.char(0xC2,0xA4) -- "¤"
tblCodePage["¥"] = string.char(0xC2,0xA5) -- "¥"
tblCodePage["¦"] = string.char(0xC2,0xA6)
tblCodePage["§"] = string.char(0xC2,0xA7)
tblCodePage["¨"] = string.char(0xC2,0xA8)
tblCodePage["©"] = string.char(0xC2,0xA9)
tblCodePage["ª"] = string.char(0xC2,0xAA)
tblCodePage["«"] = string.char(0xC2,0xAB)
tblCodePage["¬"] = string.char(0xC2,0xAC)
tblCodePage[""] = string.char(0xC2,0xAD) -- "" Soft Hyphen
tblCodePage["®"] = string.char(0xC2,0xAE)
tblCodePage["¯"] = string.char(0xC2,0xAF)
tblCodePage["°"] = string.char(0xC2,0xB0)
tblCodePage["±"] = string.char(0xC2,0xB1)
tblCodePage["²"] = string.char(0xC2,0xB2)
tblCodePage["³"] = string.char(0xC2,0xB3)
tblCodePage["´"] = string.char(0xC2,0xB4)
tblCodePage["µ"] = string.char(0xC2,0xB5)
tblCodePage["¶"] = string.char(0xC2,0xB6)
tblCodePage["·"] = string.char(0xC2,0xB7)
tblCodePage["¸"] = string.char(0xC2,0xB8)
tblCodePage["¹"] = string.char(0xC2,0xB9)
tblCodePage["º"] = string.char(0xC2,0xBA)
tblCodePage["»"] = string.char(0xC2,0xBB)
tblCodePage["¼"] = string.char(0xC2,0xBC)
tblCodePage["½"] = string.char(0xC2,0xBD)
tblCodePage["¾"] = string.char(0xC2,0xBE)
tblCodePage["¿"] = string.char(0xC2,0xBF)
tblCodePage["À"] = string.char(0xC3,0x80)
tblCodePage["Á"] = string.char(0xC3,0x81)
tblCodePage["Â"] = string.char(0xC3,0x82)
tblCodePage["Ã"] = string.char(0xC3,0x83)
tblCodePage["Ä"] = string.char(0xC3,0x84)
tblCodePage["Å"] = string.char(0xC3,0x85)
tblCodePage["Æ"] = string.char(0xC3,0x86)
tblCodePage["Ç"] = string.char(0xC3,0x87)
tblCodePage["È"] = string.char(0xC3,0x88)
tblCodePage["É"] = string.char(0xC3,0x89)
tblCodePage["Ê"] = string.char(0xC3,0x8A)
tblCodePage["Ë"] = string.char(0xC3,0x8B)
tblCodePage["Ì"] = string.char(0xC3,0x8C)
tblCodePage["Í"] = string.char(0xC3,0x8D)
tblCodePage["Î"] = string.char(0xC3,0x8E)
tblCodePage["Ï"] = string.char(0xC3,0x8F)
tblCodePage["Ð"] = string.char(0xC3,0x90)
tblCodePage["Ñ"] = string.char(0xC3,0x91)
tblCodePage["Ò"] = string.char(0xC3,0x92)
tblCodePage["Ó"] = string.char(0xC3,0x93)
tblCodePage["Ô"] = string.char(0xC3,0x94)
tblCodePage["Õ"] = string.char(0xC3,0x95)
tblCodePage["Ö"] = string.char(0xC3,0x96)
tblCodePage["×"] = string.char(0xC3,0x97)
tblCodePage["Ø"] = string.char(0xC3,0x98)
tblCodePage["Ù"] = string.char(0xC3,0x99)
tblCodePage["Ú"] = string.char(0xC3,0x9A)
tblCodePage["Û"] = string.char(0xC3,0x9B)
tblCodePage["Ü"] = string.char(0xC3,0x9C)
tblCodePage["Ý"] = string.char(0xC3,0x9D)
tblCodePage["Þ"] = string.char(0xC3,0x9E)
tblCodePage["ß"] = string.char(0xC3,0x9F)
tblCodePage["à"] = string.char(0xC3,0xA0)
tblCodePage["á"] = string.char(0xC3,0xA1)
tblCodePage["â"] = string.char(0xC3,0xA2)
tblCodePage["ã"] = string.char(0xC3,0xA3)
tblCodePage["ä"] = string.char(0xC3,0xA4)
tblCodePage["å"] = string.char(0xC3,0xA5)
tblCodePage["æ"] = string.char(0xC3,0xA6)
tblCodePage["ç"] = string.char(0xC3,0xA7)
tblCodePage["è"] = string.char(0xC3,0xA8)
tblCodePage["é"] = string.char(0xC3,0xA9)
tblCodePage["ê"] = string.char(0xC3,0xAA)
tblCodePage["ë"] = string.char(0xC3,0xAB)
tblCodePage["ì"] = string.char(0xC3,0xAC)
tblCodePage["í"] = string.char(0xC3,0xAD)
tblCodePage["î"] = string.char(0xC3,0xAE)
tblCodePage["ï"] = string.char(0xC3,0xAF)
tblCodePage["ð"] = string.char(0xC3,0xB0)
tblCodePage["ñ"] = string.char(0xC3,0xB1)
tblCodePage["ò"] = string.char(0xC3,0xB2)
tblCodePage["ó"] = string.char(0xC3,0xB3)
tblCodePage["ô"] = string.char(0xC3,0xB4)
tblCodePage["õ"] = string.char(0xC3,0xB5)
tblCodePage["ö"] = string.char(0xC3,0xB6)
tblCodePage["÷"] = string.char(0xC3,0xB7)
tblCodePage["ø"] = string.char(0xC3,0xB8)
tblCodePage["ù"] = string.char(0xC3,0xB9)
tblCodePage["ú"] = string.char(0xC3,0xBA)
tblCodePage["û"] = string.char(0xC3,0xBB)
tblCodePage["ü"] = string.char(0xC3,0xBC)
tblCodePage["ý"] = string.char(0xC3,0xBD)
tblCodePage["þ"] = string.char(0xC3,0xBE)
tblCodePage["ÿ"] = string.char(0xC3,0xBF)
-- Set XML/XHTML/HTML "[%c\"&'<>]" Markup encodings: http://en.wikipedia.org/wiki/XML and http://en.wikipedia.org/wiki/HTML
local function setMarkupEncodings()
tblCodePage["\t"] = " " -- HT "\t" to "\r" are treated as white space in Markup Languages by default
tblCodePage["\n"] = br_Tag -- LF
tblCodePage["\v"] = br_Tag -- VT line break tag "
" or "
" or "
" or "
" is better
tblCodePage["\f"] = br_Tag -- FF
tblCodePage["\r"] = br_Tag -- CR
tblCodePage['"'] = """
tblCodePage["&"] = "&"
tblCodePage["'"] = "'"
tblCodePage["<"] = "<"
tblCodePage[">"] = ">"
end -- local function setMarkupEncodings
-- Set URI/URL/URN "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding
local function setURIEncodings()
tblCodePage["\t"] = "+" -- HT space
tblCodePage["\n"] = "%0A" -- LF newline
tblCodePage["\v"] = "%0A" -- VT newline
tblCodePage["\f"] = "%0A" -- FF newline
tblCodePage["\r"] = "%0D" -- CR return
tblCodePage['"'] = "%22"
tblCodePage["&"] = "%26"
tblCodePage["'"] = "%27"
tblCodePage["<"] = "%3C"
tblCodePage[">"] = "%3E"
end -- local function setURIEncodings
-- Encode characters according to gsub pattern & lookup table --
local function strEncode(strText,strPattern,tblPattern)
return ( (strText or ""):gsub(strPattern,tblPattern) ) -- V3.4
end -- local function strEncode
-- Encode CP1252/ANSI characters into UTF-8 codes --
function fh.StrANSI_UTF8(strText)
if fhVersion > 5 then
strText = fhConvertANSItoUTF8(strText)
else
strText = strEncode(strText,"[\127-ÿ]",tblCodePage)
end
return strText
end -- function StrANSI_UTF8
function fh.StrCP_UTF(strText) -- Legacy
return fh.StrANSI_UTF8(strText)
end -- function StrCP1252_UTF8
function fh.StrCP1252_UTF(strText) -- Legacy
return fh.StrANSI_UTF8(strText)
end -- function StrCP1252_UTF
-- Encode CP1252/ANSI or UTF-8 characters into UTF-8 --
function fh.StrEncode_UTF8(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_UTF8(strText)
else
return strText
end
end -- function StrEncode_UTF8
-- Encode CP1252/ANSI characters into XML/XHTML/HTML/UTF8 codes --
local strANSI_XML = "[%z\001-\031\"&'<>\127-ÿ]"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strANSI_XML = "[\000-\031\"&'<>\127-ÿ]"
end
function fh.StrANSI_XML(strText)
setMarkupEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes br_Tag
strText = strEncode(strText,strANSI_XML,tblCodePage)
return strText
end -- function StrANSI_XML
function StrCP_XML(strText) -- Legacy
return fh.StrANSI_XML(strText)
end -- function StrCP_XML
function StrCP1252_XML(strText) -- Legacy
return fh.StrANSI_XML(strText)
end -- function StrCP1252_XML
-- Encode UTF-8 ASCII characters into XML/XHTML/HTML codes --
local strUTF8_XML = "[%z\001-\031\"&'<>\127]"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strUTF8_XML = "[\000-\031\"&'<>\127]"
end
function fh.StrUTF8_XML(strText)
setMarkupEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes br_Tag
strText = strEncode(strText,strUTF8_XML,tblCodePage)
return strText
end -- function StrUTF8_XML
-- Encode CP1252/ANSI or UTF-8 ASCII characters into XML/XHTML/HTML codes --
function fh.StrEncode_XML(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_XML(strText)
else
return fh.StrUTF8_XML(strText)
end
end -- function StrEncode_XML
-- Encode Item Text characters into XML/HTML/UTF-8 codes --
function fh.StrGetItem_XML(ptrItem,strTags)
return fh.StrEncode_XML(fhGetItemText(ptrItem,strTags))
end -- function StrGetItem_XML
-- Encode CP1252/ANSI characters into URI codes --
function fh.StrANSI_URI(strText)
setURIEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes %0A
strText = strEncode(strText,"[^0-9A-Za-z]",tblCodePage)
return strText
end -- function StrANSI_URI
function fh.StrCP_URI(strText)
return fh.StrANSI_URI(strText)
end -- function StrCP_URI
function fh.StrCP1252_URI(strText)
return fh.StrANSI_URI(strText)
end -- function StrCP1252_URI
-- Encode UTF-8 ASCII characters into URI codes --
local strUTF8_URI = "[%z\001-\127]"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strUTF8_URI = "[\000-\127]"
end
function fh.StrUTF8_URI(strText)
setURIEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes br_Tag
strText = strEncode(strText,strUTF8_URI,tblCodePage)
return strText
end -- function StrUTF8_URI
-- Encode CP1252/ANSI or UTF-8 ASCII characters into URI codes --
function fh.StrEncode_URI(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_URI(strText)
else
return fh.StrUTF8_URI(strText)
end
end -- function StrEncode_URI
function fh.StrUTF8_Encode(strText) -- Legacy from V1.0
return fh.StrUTF8_ANSI(strText)
end -- function StrUTF8_Encode
-- Encode UTF-8 bytes into single CP1252/ANSI character V2.0 upvalues --
local strByteRange = "["..string.char(0xC0).."-"..string.char(0xFF).."]"
local tblBytePoint = {0xC0;0xE0;0xF0;0xF8;0xFC;} -- Byte codes for 2-byte, 3-byte, 4-byte, 5-byte, 6-byte UTF-8
local tblUTF8 = {}
for strByte = string.byte("€"), string.byte("ÿ") do
local strChar = string.char(strByte) -- Use CodePage to UTF-8 table to populate UTF-8 to CodePage table
local strCode = tblCodePage[strChar]
tblUTF8[strCode] = strChar
end
-- Encode UTF-8 bytes into single CP1252/ANSI character --
function fh.StrUTF8_ANSI(strText)
strText = strText or ""
if fhVersion > 5 then return fhConvertUTF8toANSI(strText) end
if strText:match(strByteRange) then -- If text contains characters that need translating then
local intChar = 0 -- Input character index
local strChar = "" -- Current character
local strCode = "" -- UTF-8 multi-byte code
local tblLine = {} -- Translated output line
repeat
intChar = intChar + 1 -- Step through each character in text
strChar = strText:sub(intChar,intChar)
if strChar:match(strByteRange) then -- Convert UTF-8 bytes into CP character
strCode = strChar -- First UTF-8 byte code, whose top bits say how many bytes to append
for intByte, strByte in ipairs(tblBytePoint) do
if string.byte(strChar) >= strByte then
intChar = intChar + 1 -- Append next UTF-8 byte code character
strCode = strCode..strText:sub(intChar,intChar)
else
break
end
end
strChar = tblUTF8[strCode] or "¿" -- Translate UTF-8 code into CP character
end
table.insert(tblLine,strChar) -- Accumulate output char by char
until intChar >= #strText
strText = table.concat(tblLine)
end
return strText
end -- function StrUTF8_ANSI
function fh.StrUTF_CP(strText) -- Legacy
return fh.StrUTF8_ANSI(strText)
end -- function StrUTF_CP
function fh.StrUTF_CP1252(strText) -- Legacy
return fh.StrUTF8_ANSI(strText)
end -- function StrUTF_CP1252
-- Encode CP1252/ANSI or UTF-8 characters into ANSI --
function fh.StrEncode_ANSI(strText)
if stringx.encoding() == "ANSI" then
return strText or ""
else
return fh.StrUTF8_ANSI(strText)
end
end -- function StrEncode_ANSI
-- Set ISO-8859-1 "[\127-Ÿ]" encodings: http://en.wikipedia.org/wiki/ISO/IEC_8859-1
local tblISO8859 = { }
tblISO8859["\127"]="" -- DEL
tblISO8859["€"] = "EUR"
tblISO8859["\129"]="" -- Undefined
tblISO8859["‚"] = "¸"
tblISO8859["ƒ"] = "f"
tblISO8859["„"] = "¸¸"
tblISO8859["…"] = "..."
tblISO8859["†"] = "+"
tblISO8859["‡"] = "±"
tblISO8859["ˆ"] = "^"
tblISO8859["‰"] = "%"
tblISO8859["Š"] = "S"
tblISO8859["‹"] = "<"
tblISO8859["Œ"] = "OE"
tblISO8859["\141"]="" -- Undefined
tblISO8859["Ž"] = "Z"
tblISO8859["\143"]="" -- Undefined
tblISO8859["\144"]="" -- Undefined
tblISO8859["‘"] = "'"
tblISO8859["’"] = "'"
tblISO8859["“"] = '"'
tblISO8859["”"] = '"'
tblISO8859["•"] = "º"
tblISO8859["–"] = "-"
tblISO8859["—"] = "-"
tblISO8859["\152"]="~" -- Small Tilde
tblISO8859["™"] = "TM"
tblISO8859["š"] = "s"
tblISO8859["›"] = ">"
tblISO8859["œ"] = "oe"
tblISO8859["\157"]="" -- Undefined
tblISO8859["ž"] = "z"
tblISO8859["Ÿ"] = "Y"
-- Encode CP1252/ANSI characters into ISO-8859-1 codes --
function fh.StrANSI_ISO(strText)
return strEncode(strText,"[\127-Ÿ]",tblISO8859)
end -- function StrANSI_ISO
function fh.StrCP_ISO(strText) -- Legacy
return fh.StrANSI_ISO(strText)
end -- function StrCP_ISO
function fh.StrCP1252_ISO(strText) -- Legacy
return fh.StrANSI_ISO(strText)
end -- function StrCP1252_ISO
function fh.StrUTF8_ISO(strText)
return fh.StrANSI_ISO(fh.StrUTF8_ANSI(strText))
end -- function StrUTF8_ISO
-- Encode CP1252/ANSI or UTF-8 ASCII characters into ISO-8859-1 codes --
function fh.StrEncode_ISO(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_ISO(strText)
else
return fh.StrUTF8_ISO(strText)
end
end -- function StrEncode_ISO
-- Convert UTF-8 bytes to a UTF-16 word or pair --
local tblByte = {}
local tblLead = { 0x80; 0xC0; 0xE0; 0xF0; 0xF8; 0xFC; }
function fh.StrUtf8toUtf16(strChar)
-- Convert any UTF-8 multibytes to UTF-16 --
local function strUtf8()
if #tblByte > 0 then
local intUtf16 = 0
for intIndex, intByte in ipairs (tblByte) do -- Convert UTF-8 bytes to UNICODE U+0080 to U+10FFFF
if intIndex == 1 then
intUtf16 = intByte - tblLead[#tblByte]
else
intUtf16 = intUtf16 * 0x40 + intByte - 0x80
end
end
if intUtf16 > 0xFFFF then -- U+10000 to U+10FFFF Supplementary Planes -- V2.6
tblByte = {}
intUtf16 = intUtf16 - 0x10000
local intLow10 = 0xDC00 + ( intUtf16 % 0x400 ) -- Low 16-bit Surrogate
local intTop10 = 0xD800 + math.floor( intUtf16 / 0x400 ) -- High 16-bit Surrogate
local intChar1 = intTop10 % 0x100
local intChar2 = math.floor( intTop10 / 0x100 )
local intChar3 = intLow10 % 0x100
local intChar4 = math.floor( intLow10 / 0x100 )
return string.char(intChar1,intChar2,intChar3,intChar4) -- Surrogate 16-bit Pair
end
if intUtf16 < 0xD800 -- U+0080 to U+FFFF (except U+D800 to U+DFFF) -- V2.6
or intUtf16 > 0xDFFF then -- Basic Multilingual Plane
tblByte = {}
local intChar1 = intUtf16 % 0x100
local intChar2 = math.floor( intUtf16 / 0x100 )
return string.char(intChar1,intChar2) -- BPL 16-bit
end
local strUtf8 = "" -- U+D800 to U+DFFF Reserved Code Points -- V2.6
for intIndex, intByte in ipairs (tblByte) do
strUtf8 = strUtf8..string.format("%.2X ",intByte)
end
local strUtf16 = string.format("%.4X ",intUtf16)
fhMessageBox("\n UTF-16 Reserved Code Point U+D800 to U+DFFF \n UTF-16 = "..strUtf16.." UTF-8 = "..strUtf8.."\n Character will be replaced by a question mark. \n","MB_OK","MB_ICONEXCLAMATION")
tblByte = {}
return "?\0"
end
return ""
end -- local function strUtf8
local intUtf8 = string.byte(strChar)
if intUtf8 < 0x80 then -- U+0000 to U+007F (ASCII)
return strUtf8()..strChar.."\0" -- Previous UTF-8 multibytes + current ASCII char
end
if intUtf8 >= 0xC0 then -- Next UTF-8 multibyte start
local strUtf16 = strUtf8()
table.insert(tblByte,intUtf8)
return strUtf16 -- Previous UTF-8 multibytes
end
table.insert(tblByte,intUtf8)
return ""
end -- function StrUtf8toUtf16
-- Encode UTF-8 bytes into UTF-16 words --
function fh.StrUTF8_UTF16(strText)
tblByte = {} -- (0xFF) flushes last UTF-8 character
return ( ((strText or "")..string.char(0xFF)):gsub("(.)",fh.StrUtf8toUtf16) ) -- V3.4
end -- function StrUTF8_UTF16
-- Encode CP1252/ANSI or UTF-8 characters into UTF-16 words --
function fh.StrEncode_UTF16(strText)
if stringx.encoding() == "ANSI" then
strText = fh.StrANSI_UTF8(strText)
end
return fh.StrUTF8_UTF16(strText)
end -- function StrEncode_UTF16
local intTop10 = 0
-- Convert a UTF-16 word or pair to UTF-8 bytes --
function fh.StrUtf16toUtf8(strChar1,strChar2)
local intUtf16 = string.byte(strChar2) * 0x100 + string.byte(strChar1)
if intUtf16 < 0x80 then -- U+0000 to U+007F (ASCII)
return string.char(intUtf16)
end
if intUtf16 < 0x800 then -- U+0080 to U+07FF
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16
return string.char( intByte2 + 0xC0, intByte1 + 0x80 )
end
if intUtf16 < 0xD800 -- U+0800 to U+FFFF
or intUtf16 > 0xDFFF then
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16
return string.char( intByte3 + 0xE0, intByte2 + 0x80, intByte1 + 0x80 )
end
if intUtf16 < 0xDC00 then -- U+10000 to U+10FFFF High 16-bit Surrogate Supplementary Planes -- V2.6
intTop10 = ( intUtf16 - 0xD800 ) * 0x400 + 0x10000
return ""
end
intUtf16 = intUtf16 - 0xDC00 + intTop10 -- U+10000 to U+10FFFF Low 16-bit Surrogate Supplementary Planes -- V2.6
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte4 = intUtf16
return string.char( intByte4 + 0xF0, intByte3 + 0x80, intByte2 + 0x80, intByte1 + 0x80 )
end -- function StrUtf16toUtf8
-- Encode UTF-16 words into UTF-8 bytes --
function fh.StrUTF16_UTF8(strText)
return ( (strText or ""):gsub("(.)(.)",fh.StrUtf16toUtf8) ) -- V3.4
end -- function StrUTF16_UTF8
-- Encode UTF-16 words into ANSI characters --
function fh.StrUTF16_ANSI(strText)
return fh.StrUTF8_ANSI(fh.StrUTF16_UTF8(strText))
end -- function StrUTF16_ANSI
-- Read UTF-16/UTF-8/ANSI file converted to chosen encoding via line iterator --
local strUtf16 = "^.%z"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strUtf16 = "^.\0"
end
function fh.FileLines(strFileName,strEncoding) -- Derived from http://lua-users.org/wiki/EnhancedFileLines
local bomUtf16= "^"..string.char(0xFF,0xFE) -- "ÿþ"
local bomUtf8 = "^"..string.char(0xEF,0xBB,0xBF) -- ""
local fncConv = tostring -- Function to convert input to current encoding
local intHead = 1 -- Index to start of current text line
local intLump = 1024
local fHandle = general.OpenFile(strFileName,"rb")
local strText = fHandle:read(1024) or "" -- Read first lump from file and cater for empty file
local intBOM = 0
strEncoding = strEncoding or string.encoding()
if strText:match(bomUtf16)
or strText:match(strUtf16) then
strText,intBOM = strText:gsub(bomUtf16,"") -- Strip UTF-16 BOM
if strEncoding == "ANSI" then -- Define UTF-16 conversion to current encoding
fncConv = fh.StrUTF16_ANSI
else
fncConv = fh.StrUTF16_UTF8
end
elseif strText:match(bomUtf8) then
strText,intBOM = strText:gsub(bomUtf8,"") -- Strip UTF-8 BOM
if strEncoding == "ANSI" then -- Define UTF-8 conversion to current encoding
fncConv = fh.StrUTF8_ANSI
end
else
if strEncoding == "UTF-8" and #strText > 0 then -- Define ANSI conversion to current encoding and cater for empty file
fncConv = fh.StrANSI_UTF8
end
end
strText = fncConv(strText) -- Convert first lump of text
return function() -- Iterator function
local intTail,strTail -- Index to end of current text line, and terminating characters
while true do
intTail, strTail = strText:match("()([\r\n].)",intHead)
if intTail or not fHandle then
if intHead > 1 then intLump = 0 end
break -- End of line or end of file
elseif fHandle then
local strLump = fHandle:read(1024) -- Read next lump from file
if strLump then -- Strip old text and add converted lump
strText = strText:sub(intHead)..fncConv(strLump)
intHead = 1
intLump = 1024
else
assert(fHandle:close()) -- End of file
fHandle = nil
end
end
end
if not intTail then
intTail = #strText -- Last fragment of file
elseif strTail == "\r\n" then
intTail = intTail + 1 -- Adjust tail for both \r & \n
end
local strLine = strText:sub(intHead,intTail) -- Extract line from text
intHead = intTail + 1
if #strLine > 0 then -- Return pruned line, tail chars, lump bytes read
local strBody, strTail = strLine:match("^(.-)([\r\n]+)$")
return strBody, strTail, intLump
end
end
end -- function FileLines
-- Set "[€-ÿ]" ASCII encodings same as Unidecode below
local tblASCII = { }
tblASCII["€"] = "=E"
tblASCII["\129"]="" -- Undefined
tblASCII["‚"] = ","
tblASCII["ƒ"] = "f"
tblASCII["„"] = ",,"
tblASCII["…"] = "..."
tblASCII["†"] = "|+"
tblASCII["‡"] = "|++"
tblASCII["ˆ"] = "^"
tblASCII["‰"] = "%0"
tblASCII["Š"] = "S"
tblASCII["‹"] = "<"
tblASCII["Œ"] = "OE"
tblASCII["\141"]="" -- Undefined
tblASCII["Ž"] = "Z"
tblASCII["\143"]="" -- Undefined
tblASCII["\144"]="" -- Undefined
tblASCII["‘"] = "'"
tblASCII["’"] = "'"
tblASCII["“"] = "\""
tblASCII["”"] = "\""
tblASCII["•"] = "*"
tblASCII["–"] = "-"
tblASCII["—"] = "--"
tblASCII["\152"]="~" -- Small Tilde
tblASCII["™"] = "TM"
tblASCII["š"] = "s"
tblASCII["›"] = ">"
tblASCII["œ"] = "oe"
tblASCII["\157"]="" -- Undefined
tblASCII["ž"] = "z"
tblASCII["Ÿ"] = "Y"
tblASCII["\160"]=" " -- " " No Break Space
tblASCII["¡"] = "!" -- "¡"
tblASCII["¢"] = "=c" -- "¢"
tblASCII["£"] = "=L" -- "£"
tblASCII["¤"] = "=$" -- "¤"
tblASCII["¥"] = "=Y" -- "¥"
tblASCII["¦"] = "|"
tblASCII["§"] = "=SS"
tblASCII["¨"] = "\""
tblASCII["©"] = "(C)"
tblASCII["ª"] = "a"
tblASCII["«"] = "<<"
tblASCII["¬"] = "-"
tblASCII[""] = "-" -- "" Soft Hyphen
tblASCII["®"] = "(R)"
tblASCII["¯"] = "-"
tblASCII["°"] = "=o"
tblASCII["±"] = "+-"
tblASCII["²"] = "2"
tblASCII["³"] = "3"
tblASCII["´"] = "'"
tblASCII["µ"] = "=u"
tblASCII["¶"] = "=p"
tblASCII["·"] = "*"
tblASCII["¸"] = ","
tblASCII["¹"] = "1"
tblASCII["º"] = "o"
tblASCII["»"] = ">>"
tblASCII["¼"] = "1/4"
tblASCII["½"] = "1/2"
tblASCII["¾"] = "3/4"
tblASCII["¿"] = "?"
tblASCII["À"] = "A"
tblASCII["Á"] = "A"
tblASCII["Â"] = "A"
tblASCII["Ã"] = "A"
tblASCII["Ä"] = "A"
tblASCII["Å"] = "A"
tblASCII["Æ"] = "AE"
tblASCII["Ç"] = "C"
tblASCII["È"] = "E"
tblASCII["É"] = "E"
tblASCII["Ê"] = "E"
tblASCII["Ë"] = "E"
tblASCII["Ì"] = "I"
tblASCII["Í"] = "I"
tblASCII["Î"] = "I"
tblASCII["Ï"] = "I"
tblASCII["Ð"] = "D"
tblASCII["Ñ"] = "N"
tblASCII["Ò"] = "O"
tblASCII["Ó"] = "O"
tblASCII["Ô"] = "O"
tblASCII["Õ"] = "O"
tblASCII["Ö"] = "O"
tblASCII["×"] = "*"
tblASCII["Ø"] = "O"
tblASCII["Ù"] = "U"
tblASCII["Ú"] = "U"
tblASCII["Û"] = "U"
tblASCII["Ü"] = "U"
tblASCII["Ý"] = "Y"
tblASCII["Þ"] = "TH"
tblASCII["ß"] = "ss"
tblASCII["à"] = "a"
tblASCII["á"] = "a"
tblASCII["â"] = "a"
tblASCII["ã"] = "a"
tblASCII["ä"] = "a"
tblASCII["å"] = "a"
tblASCII["æ"] = "ae"
tblASCII["ç"] = "c"
tblASCII["è"] = "e"
tblASCII["é"] = "e"
tblASCII["ê"] = "e"
tblASCII["ë"] = "e"
tblASCII["ì"] = "i"
tblASCII["í"] = "i"
tblASCII["î"] = "i"
tblASCII["ï"] = "i"
tblASCII["ð"] = "d"
tblASCII["ñ"] = "n"
tblASCII["ò"] = "o"
tblASCII["ó"] = "o"
tblASCII["ô"] = "o"
tblASCII["õ"] = "o"
tblASCII["ö"] = "o"
tblASCII["÷"] = "/"
tblASCII["ø"] = "o"
tblASCII["ù"] = "u"
tblASCII["ú"] = "u"
tblASCII["û"] = "u"
tblASCII["ü"] = "u"
tblASCII["ý"] = "y"
tblASCII["þ"] = "th"
tblASCII["ÿ"] = "y"
-- Encode CP1252/ANSI characters into ASCII codes [\000-\127] --
function fh.StrANSI_ASCII(strText)
return strEncode(strText,"[€-ÿ]",tblASCII)
end -- function StrANSI_ASCII
--[=[
Unidecode converts each codepoint into a few ASCII characters.
Lookup table indexed by codepoint [0x0000]-[0xFFFF] gives an ASCII string.
i.e. strASCII = Unidecode[intByte2][intByte1] or "=?" allowing for partially populated table.
See http://search.cpan.org/dist/Text-Unidecode/ and follow Browse to:
See http://cpansearch.perl.org/src/SBURKE/Text-Unidecode-1.22/lib/Text/Unidecode/
where each x??.pm gives 256 ASCII conversions.
Start with the first few European accented characters, and add the others later.
--]=]
local Unidecode = { }
function fh.StrUnidecode(strChar1,strChar2) -- Decode UTF-16 byte pair into ASCII characters
return Unidecode[string.byte(strChar2)][string.byte(strChar1)] or "=?"
end -- function StrUnidecode
-- Encode UTF-8 characters into ASCII codes [\000-\126] --
function fh.StrUTF8_ASCII(strText)
strText = fh.StrUTF8_UTF16(strText) -- Convert to UTF-16 Unicode and then to ASCII
return ( strText:gsub("(.)(.)",fh.StrUnidecode) )
end -- function StrUTF8_ASCII
-- Encode CP1252/ANSI or UTF-8 into ASCII codes [\000-\126] --
function fh.StrEncode_ASCII(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_ASCII(strText)
else
return fh.StrUTF8_ASCII(strText)
end
end -- function StrEncode_ASCII
-- Set markup language break tag --
function fh.SetBreakTag(br_New)
if not (br_New or ""):match(br_Lua) then -- Ensure new break tag is "
" or "
" or "
" or "
"
br_New = "
"
end
br_Tag = br_New
end -- function SetBreakTag
for intByte = 0x00, 0xFF do Unidecode[intByte] = { } end
Unidecode[0x00] =
{[0]="\00";"\01";"\02";"\03";"\04";"\05";"\06";"\a";"\b";"\t";"\n";"\v";"\f";"\r";"\14";"\15";"\16";"\17";"\18";"\19";"\20";"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\30";"\31";
" ";"!";'"';"#";"$";"%";"&";"'";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"?"; -- 0x20 to 0x3F
"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_"; -- 0x40 to 0x5F
"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~";"\127"; -- 0x60 to 0x7F
""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; -- 0x80 to 0x9F
" ";"!";"=c";"=L";"=$";"=Y";"|";"=SS";'"';"(C)";"a";"<<";"-";"-";"(R)";"-";"=o";"+-";"2";"3";"'";"=u";"=P";"*";",";"1";"o";">>";"1/4";"1/2";"3/4";"?"; -- 0xA0 to 0xBF
"A";"A";"A";"A";"A";"A";"AE";"C";"E";"E";"E";"E";"I";"I";"I";"I";"D";"N";"O";"O";"O";"O";"O";"*";"O";"U";"U";"U";"U";"Y";"TH";"ss"; -- 0xC0 to 0xDF
"a";"a";"a";"a";"a";"a";"ae";"c";"e";"e";"e";"e";"i";"i";"i";"i";"d";"n";"o";"o";"o";"o";"o";"/";"o";"u";"u";"u";"u";"y";"th";"y"; -- 0xE0 to 0xFF
}
Unidecode[0x01] =
{[0]="A";"a";"A";"a";"A";"a";"C";"c";"C";"c";"C";"c";"C";"c";"D";"d";"D";"d";"E";"e";"E";"e";"E";"e";"E";"e";"E";"e";"G";"g";"G";"g"; -- 0x00 to 0x1F
"G";"g";"G";"g";"H";"h";"H";"h";"I";"i";"I";"i";"I";"i";"I";"i";"I";"i";"IJ";"ij";"J";"j";"K";"k";"k";"L";"l";"L";"l";"L";"l";"L"; -- 0x20 to 0x3F
"l";"L";"l";"N";"n";"N";"n";"N";"n";"'n";"ng";"NG";"O";"o";"O";"o";"O";"o";"OE";"oe";"R";"r";"R";"r";"R";"r";"S";"s";"S";"s";"S";"s"; -- 0x40 to 0x5F
"S";"s";"T";"t";"T";"t";"T";"t";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"W";"w";"Y";"y";"Y";"Z";"z";"Z";"z";"Z";"z";"s"; -- 0x60 to 0x7F
"b";"B";"B";"b";"6";"6";"O";"C";"c";"D";"D";"D";"d";"d";"3";"@";"E";"F";"f";"G";"G";"hv";"I";"I";"K";"k";"l";"l";"W";"N";"n";"O"; -- 0x80 to 0x9F
"O";"o";"OI";"oi";"P";"p";"YR";"2";"2";"SH";"sh";"t";"T";"t";"T";"U";"u";"Y";"V";"Y";"y";"Z";"z";"ZH";"ZH";"zh";"zh";"2";"5";"5";"ts";"w"; -- 0xA0 to 0xBF
"|";"||";"|=";"!";"DZ";"Dz";"dz";"LJ";"Lj";"lj";"NJ";"Nj";"nj";"A";"a";"I";"i";"O";"o";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"@";"A";"a"; -- 0xC0 to 0xDF
"A";"a";"AE";"ae";"G";"g";"G";"g";"K";"k";"O";"o";"O";"o";"ZH";"zh";"j";"DZ";"Dz";"dz";"G";"g";"HV";"W";"N";"n";"A";"a";"AE";"ae";"O";"o"; -- 0xE0 to 0xFF
}
Unidecode[0x02] =
{[0]="A";"a";"A";"a";"E";"e";"E";"e";"I";"i";"I";"i";"O";"o";"O";"o";"R";"r";"R";"r";"U";"u";"U";"u";"S";"s";"T";"t";"Y";"y";"H";"h"; -- 0x00 to 0x1F
"N";"d";"OU";"ou";"Z";"z";"A";"a";"E";"e";"O";"o";"O";"o";"O";"o";"O";"o";"Y";"y";"l";"n";"t";"j";"db";"qp";"A";"C";"c";"L";"T";"s"; -- 0x20 to 0x3F
"z";"[?]";"[?]";"B";"U";"^";"E";"e";"J";"j";"q";"q";"R";"r";"Y";"y";"a";"a";"a";"b";"o";"c";"d";"d";"e";"@";"@";"e";"e";"e";"e";"j"; -- 0x40 to 0x5F
"g";"g";"g";"g";"u";"Y";"h";"h";"i";"i";"I";"l";"l";"l";"lZ";"W";"W";"m";"n";"n";"n";"o";"OE";"O";"F";"r";"r";"r";"r";"r";"r";"r"; -- 0x60 to 0x7F
"R";"R";"s";"S";"j";"S";"S";"t";"t";"u";"U";"v";"^";"w";"y";"Y";"z";"z";"Z";"Z";"?";"?";"?";"C";"@";"B";"E";"G";"H";"j";"k";"L"; -- 0x80 to 0x9F
"q";"?";"?";"dz";"dZ";"dz";"ts";"tS";"tC";"fN";"ls";"lz";"WW";"]]";"h";"h";"h";"h";"j";"r";"r";"r";"r";"w";"y";"'";'"';"`";"'";"`";"`";"'"; -- 0xA0 to 0xBF
"?";"?";"<";">";"^";"V";"^";"V";"'";"-";"/";"\\";",";"_";"\\";"/";":";".";"`";"'";"^";"V";"+";"-";"V";".";"@";",";"~";'"';"R";"X"; -- 0xC0 to 0xDF
"G";"l";"s";"x";"?";"";"";"";"";"";"";"";"V";"=";'"';"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0xE0 to 0xFF
}
Unidecode[0x03] =
{
}
Unidecode[0x04] =
{
}
Unidecode[0x20] =
{[0]=" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";"";"";"";"";"-";"-";"-";"-";"--";"--";"||";"_";"'";"'";",";"'";'"';'"';",,";'"'; -- 0x00 to 0x1F
"|+";"|++";"*";"*>";".";"..";"...";".";"\n";"\n\n";"";"";"";"";"";" ";"%0";"%00";"'";"''";"'''";"`";"``";"```";"^";"<";">";"*";"!!";"!?";"-";"_"; -- 0x20 to 0x3F
"-";"^";"***";"--";"/";"-[";"]-";"[?]";"?!";"!?";"7";"PP";"(]";"[)";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0x40 to 0x5F
"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"0";"";"";"";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"n"; -- 0x60 to 0x7F
"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0x80 to 0x9F
"ECU";"CL";"Cr";"FF";"L";"mil";"N";"Pts";"Rs";"W";"NS";"D";"=E";"K";"T";"Dr";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0xA0 to 0xBF
"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";""; -- 0xC0 to 0xDF
"";"";"";"";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0xE0 to 0xFF
}
Unidecode[0x21] =
{[34]="TM";
}
return fh
end -- local function encoder_v3
local encoder = encoder_v3() -- To access FH encoder chars module
--[[
@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
--[[
@Module: +fh+iup_gui_v3
@Author: Mike Tate
@Version: 4.6
@LastUpdated: 18 Feb 2026
@Description: Graphical User Interface Library Module
@V4.6: Improve handling of centred window RasterSize in fh.ShowDialogue(...);
@V4.5: Adjust CheckVersionInStore() for dedicated button use;
@V4.4: Introduce use of NATIVEPARENT and CENTERPARENT to centre on parent window by default; Ensure not off screen; Monitors with -ve X;
@V4.3: Added memo options to CheckVersionInStore;
@V4.2: Skip if standalone GEDCOM in fh.SaveSettings() and getDataFiles();
@V4.1: CheckVersionInStore() save & retrieve latest version in file; Remove old wiki Help features;
@V4.0: Cater for full UTF-8 filenames;
@V3.9: ShowDialogue() popup closure fhSleep() added; CheckVersionInStore() at monthly intervals;
@V3.8: Function Prototype Closure version.
@V3.7: AssignAttributes(tblControls) now allows any string attribute to invoke a function.
@V3.6: anyMemoDialogue() sets TopMost attribute.
@V3.5: Replace IsNormalWindow(iupDialog) with SetWindowCoord(tblName) and update CheckWindowPosition(tblName) to prevent negative values freezing main dialog.
@V3.4: Use general.MakeFolder() to ensure key folders exist, add Get/PutRegKey(), check Registry IE Shell Version in HelpDialogue(), better error handling in LoadSettings().
@V3.3: LoadFolder() and SaveFolder() use global folder as default for local folder to improve synch across PC.
@V3.2: Load & Save settings now use a single clipboard so Local PC settings are preserved across synchronised PC.
@V3.1: IUP 3.11.2 iup.GetGlobal("VERSION") to top, HelpDialogue conditional ExpandChildren="YES/NO", RefreshDialogue uses NaturalSize, SetUtf8Mode(), Load/SaveFolder(), etc
@V3.0: ShowDialogue "dialog" mode for Memo, new DestroyDialogue, NewHelpDialogue tblAttr for Font, AssignAttributes intSkip, CustomDialogue iup.CENTERPARENT+, IUP Workaround, BalloonToggle, Initialise test Plugin file exists.
@V2.0: Support for Plugin Data scope, new FontDialogue, RefreshDialogue, AssignAttributes, httpRequest handler, keep "dialog" mode.
@V1.0: Initial version.
]]
local function iup_gui_v3()
local fh = {} -- Local environment table
require "iuplua" -- To access GUI window builder
require "iupluacontrols" -- To access GUI window controls
require "lfs" -- To access LUA filing system
require "iupluaole" -- To access OLE subsystem
require "luacom" -- To access COM subsystem
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local iupVersion = iup.GetGlobal("VERSION") -- Obtain IUP module version
-- "iuplua" Omitted Constants Workaround --
iup.TOP = iup.LEFT
iup.BOTTOM = iup.RIGHT
iup.RED = iup.RGB(1,0,0)
iup.GREEN = iup.RGB(0,1,0)
iup.BLUE = iup.RGB(0,0,1)
iup.BLACK = iup.RGB(0,0,0)
iup.WHITE = iup.RGB(1,1,1)
iup.YELLOW = iup.RGB(1,1,0)
-- Shared Interface Attributes & Functions --
fh.Version = " " -- Plugin Version
fh.History = fh.Version -- Version History
fh.Red = "255 0 0" -- Color attributes (must exclude leading zeros & spaces to allow value comparisons)
fh.Maroon = "128 0 0"
fh.Amber = "250 160 0"
fh.Orange = "255 165 0"
fh.Yellow = "255 255 0"
fh.Olive = "128 128 0"
fh.Lime = "0 255 0"
fh.Green = "0 128 0"
fh.Cyan = "0 255 255"
fh.Teal = "0 128 128"
fh.Blue = "0 0 255"
fh.Navy = "0 0 128"
fh.Magenta = "255 0 255"
fh.Purple = "128 0 128"
fh.Black = "0 0 0"
fh.Gray = "128 128 128"
fh.Silver = "192 192 192"
fh.Smoke = "240 240 240"
fh.White = "255 255 255"
fh.Risk = fh.Red -- Risk colour for hazardous controls such as Close/Delete buttons
fh.Warn = fh.Magenta -- Warn colour for caution controls and warnings
fh.Safe = fh.Green -- Safe colour for active controls such as most buttons
fh.Info = fh.Black -- Info colour for text controls such as labels/tabs
fh.Head = fh.Black -- Head colour for headings
fh.Body = fh.Black -- Body colour for body text
fh.Back = fh.White -- Background colour for all windows
fh.Gap = "8" -- Layout attributes Gap was "10"
fh.Border = "8x8" -- was BigMargin="10x10"
fh.Margin = "1x1" -- was MinMargin
fh.Balloon = "NO" -- Tooltip balloon mode
fh.FontSet = 0 -- Legacy GUI font set assigned by FontAssignment but used globally
fh.FontHead = ""
fh.FontBody = ""
local GUI = { } -- Sub-table for GUI Dialogue attributes to allow any "Name"
--[[
GUI.Name table of dialogue attributes, where Name is Font, Main, Memo, Bars, etc
GUI.Name.CoordX x co-ordinate ( Loaded & Saved by default )
GUI.Name.CoordY y co-ordinate ( Loaded & Saved by default )
GUI.Name.Dialog dialogue handle
GUI.Name.Focus focus button handle
GUI.Name.Frame dialogframe mode, "normal" = dialogframe="NO" else "YES", "showxy" = showxy(), "popup" or "keep" = popup(), default is "normal & showxy"
GUI.Name.Height height
GUI.Name.Raster rastersize ( Loaded & Saved by default )
GUI.Name.Width width
GUI.Name.Back ProgressBar background colour
GUI.Name.Body ProgressBar body text colour
GUI.Name.Font ProgressBar font style
GUI.Name.Stop ProgressBar Stop button colour
GUI.Name.GUI Module table usable by other modules e.g. progbar.Setup
--]]
-- tblScrn[1] = origin x, tblScrn[2] = origin y, tblScrn[3] = width, tblScrn[4] = height
local tblScrn = stringx.splitnumbers(iup.GetGlobal("VIRTUALSCREEN")) -- Used by CustomDialogue() and CheckWindowPosition() and ShowDialogue() below
local intMinX = tblScrn[1]
local intMinY = tblScrn[2] -- V4.4
local intMaxW = tblScrn[3]
local intMaxH = tblScrn[4]
function fh.BalloonToggle() -- Toggle tooltips Balloon mode
local tblToggle = { YES="NO"; NO="YES"; }
fh.Balloon = tblToggle[fh.Balloon]
fh.SaveSettings()
end -- function BalloonToggle
iup.SetGlobal("UTF8MODE","NO")
iup.SetGlobal("UTF8MODE_FILE","NO") -- V4.0
function fh.SetUtf8Mode() -- Set IUP into UTF-8 mode
if iupVersion == "3.5" or stringx.encoding() == "ANSI" then return false end
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("UTF8MODE_FILE","YES") -- V4.0
return true
end -- function SetUtf8Mode
local function tblOfNames(...) -- Get table of dialogue Names including "Font","Main" by default -- V4.4
local arg = {...}
local tblNames = {"Font";"Main";}
for intName, strName in ipairs(arg) do
if type(strName) == "string"
and strName ~= "Font"
and strName ~= "Main" then
table.insert(tblNames,strName)
end
end
return tblNames
end -- local function tblOfNames
local function tblNameFor(strName) -- Get table of parameters for chosen dialogue Name
strName = tostring(strName)
if not GUI[strName] then -- Need new table with default minimum & raster size, and X & Y co-ordinates
GUI[strName] = { }
local tblName = GUI[strName]
tblName.Raster = "x"
tblName.CoordX = iup.CENTERPARENT
tblName.CoordY = iup.CENTERPARENT
end
return GUI[strName]
end -- local function tblNameFor
local function intDimension(intMin,intVal,intMax) -- Return a number bounded by intMin and intMax
if not intVal then return 0 end -- Except if no value then return 0
intVal = tonumber(intVal) or (intMin+intMax)/2
return math.max(intMin,math.min(intVal,intMax))
end -- local function intDimension
function fh.CustomDialogue(strName,strRas,intX,intY) -- GUI custom window raster size, and X & Y co-ordinates
-- strRas nil = old size, "x" or "0x0" = min size, "999x999" = new size
-- intX/Y nil = central, "99" = co-ordinate position
local tblName = tblNameFor(strName)
local tblSize = {}
local intWide = 0
local intHigh = 0
strRas = strRas or tblName.Raster
if strRas then -- Ensure raster size is between minimum and screen size
tblSize = stringx.splitnumbers(strRas)
intWide = intDimension(intWide,tblSize[1],intMaxW)
intHigh = intDimension(intHigh,tblSize[2],intMaxH)
strRas = tostring(intWide.."x"..intHigh)
end
if intX and intX < iup.CENTERPARENT then
intX = intDimension(intMinX,intX,intMaxW-intWide) -- Ensure X co-ordinate positions window on screen -- V4.4
end
if intY and intY < iup.CENTERPARENT then
intY = intDimension(intMinY,intY,intMaxH-intHigh) -- Ensure Y co-ordinate positions window on screen -- V4.4
end
tblName.Raster = strRas or "x"
tblName.CoordX = tonumber(intX) or iup.CENTERPARENT -- V4.4
tblName.CoordY = tonumber(intY) or iup.CENTERPARENT -- V4.4
end -- function CustomDialogue
function fh.DefaultDialogue(...) -- GUI default window minimum & raster size, and X & Y co-ordinates
for intName, strName in ipairs(tblOfNames(...)) do
fh.CustomDialogue(strName)
end
end -- function DefaultDialogue
function fh.DialogueAttributes(strName) -- Provide named Dialogue Attributes
local tblName = tblNameFor(strName) -- tblName.Dialog = dialog handle, so any other attributes could be retrieved
local tblSize = stringx.splitnumbers(tblName.Raster or "x") -- Split Raster Size into width=tblSize[1] and height=tblSize[2]
tblName.Width = tblSize[1]
tblName.Height= tblSize[2]
tblName.Back = fh.Back -- Following only needed for NewProgressBar
tblName.Body = fh.Body
tblName.Font = fh.FontBody
tblName.Stop = fh.Risk
tblName.GUI = fh -- Module table
return tblName
end -- function DialogueAttributes
local strDefaultScope = "Project" -- Default scope for Load/Save data is per Project/User/Machine as set by PluginDataScope()
local tblClipProj = { }
local tblClipUser = { } -- Clipboards of sticky data for each Plugin Data scope -- V3.2
local tblClipMach = { }
local function doLoadData(strParam,strDefault,strScope) -- Load sticky data for Plugin Data scope
strScope = tostring(strScope or strDefaultScope):lower()
local tblClipData = tblClipProj
if strScope:match("user") then tblClipData = tblClipUser
elseif strScope:match("mach") then tblClipData = tblClipMach
end
return tblClipData[strParam] or strDefault
end -- local function doLoadData
function fh.LoadGlobal(strParam,strDefault,strScope) -- Load Global Parameter for all PC
return doLoadData(strParam,strDefault,strScope)
end -- function LoadGlobal
function fh.LoadLocal(strParam,strDefault,strScope) -- Load Local Parameter for this PC
return doLoadData(fh.ComputerName.."-"..strParam,strDefault,strScope)
end -- function LoadLocal
local function doLoadFolder(strFolder) -- Use relative paths to let Paths change -- V3.3
strFolder = strFolder:gsub("^FhDataPath",function() return fh.FhDataPath end) -- Full path to .fh_data folder
strFolder = strFolder:gsub("^PublicPath",function() return fh.PublicPath end) -- Full path to Public folder
strFolder = strFolder:gsub("^FhProjPath",function() return fh.FhProjPath end) -- Full path to project folder
return strFolder
end -- local function doLoadFolder
function fh.LoadFolder(strParam,strDefault,strScope) -- Load Folder Parameter for this PC -- V3.3
local strFolder = doLoadFolder(fh.LoadLocal(strParam,"",strScope))
if not general.FlgFolderExists(strFolder) then -- If no local folder try global folder
strFolder = doLoadFolder(fh.LoadGlobal(strParam,strDefault,strScope))
end
return strFolder
end -- function LoadFolder
function fh.LoadDialogue(...) -- Load Dialogue Parameters for "Font","Main" by default
for intName, strName in ipairs(tblOfNames(...)) do
local tblName = tblNameFor(strName)
tblName.Raster = tostring(fh.LoadLocal(strName.."R",tblName.Raster))
tblName.CoordX = tonumber(fh.LoadLocal(strName.."X",tblName.CoordX))
tblName.CoordY = tonumber(fh.LoadLocal(strName.."Y",tblName.CoordY))
fh.CheckWindowPosition(tblName)
end
end -- function LoadDialogue
function fh.LoadSettings(...) -- Load Sticky Settings from File
for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
strFileName = fh[strFileName]
if general.FlgFileExists(strFileName) then -- Load Settings File in table lines with key & val fields
local tblField = {}
local strClip = general.StrLoadFromFile(strFileName) -- V4.0
for strLine in strClip:gmatch("[^\r\n]+") do -- V4.0
if #tblField == 0
and strLine:match("^return {") -- Unless entire Sticky Data table was saved
and type(table.load) == "function" then
local tblClip, strErr = table.load(strFileName) -- Load Settings File table
if strErr then error(strErr.."\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.") end
for i,j in pairs (tblClip) do
tblClipData[i] = tblClip[i]
end
break
end
tblField = stringx.split(strLine,"=")
if tblField[1] then tblClipData[tblField[1]] = tblField[2] end
end
else
for i,j in pairs (tblClipData) do
tblClipData[i] = nil -- Restore defaults and clear any junk -- V4.0
end
end
end
fh.Safe = tostring(fh.LoadGlobal("SafeColor",fh.Safe))
fh.Warn = tostring(fh.LoadGlobal("WarnColor",fh.Warn))
fh.Risk = tostring(fh.LoadGlobal("RiskColor",fh.Risk))
fh.Head = tostring(fh.LoadGlobal("HeadColor",fh.Head))
fh.Body = tostring(fh.LoadGlobal("BodyColor",fh.Body))
fh.FontHead= tostring(fh.LoadGlobal("FontHead" ,fh.FontHead))
fh.FontBody= tostring(fh.LoadGlobal("FontBody" ,fh.FontBody))
fh.History = tostring(fh.LoadGlobal("History" ,fh.History))
fh.Balloon = tostring(fh.LoadGlobal("Balloon" ,fh.Balloon, "Machine"))
fh.LoadDialogue(...)
end -- function LoadSettings
local function doSaveData(strParam,anyValue,strScope) -- Save sticky data for Plugin Data scope
strScope = tostring(strScope or strDefaultScope):lower()
local tblClipData = tblClipProj
if strScope:match("user") then tblClipData = tblClipUser
elseif strScope:match("mach") then tblClipData = tblClipMach
end
tblClipData[strParam] = anyValue
end -- local function doSaveData
function fh.SaveGlobal(strParam,anyValue,strScope) -- Save Global Parameter for all PC
doSaveData(strParam,anyValue,strScope)
end -- function SaveGlobal
function fh.SaveLocal(strParam,anyValue,strScope) -- Save Local Parameter for this PC
doSaveData(fh.ComputerName.."-"..strParam,anyValue,strScope)
end -- function SaveLocal
function fh.SaveFolder(strParam,strFolder,strScope) -- Save Folder Parameter for this PC
strFolder = stringx.replace(strFolder,fh.FhDataPath,"FhDataPath") -- Full path to .fh_data folder
strFolder = stringx.replace(strFolder,fh.PublicPath,"PublicPath") -- Full path to Public folder
strFolder = stringx.replace(strFolder,fh.FhProjPath,"FhProjPath") -- Full path to project folder
fh.SaveGlobal(strParam,strFolder,strScope) -- V3.3
fh.SaveLocal(strParam,strFolder,strScope) -- Uses relative paths to let Paths change
end -- function SaveFolder
function fh.SaveDialogue(...) -- Save Dialogue Parameters for "Font","Main" by default
for intName, strName in ipairs(tblOfNames(...)) do
local tblName = tblNameFor(strName)
fh.SaveLocal(strName.."R",tblName.Raster)
fh.SaveLocal(strName.."X",tblName.CoordX)
fh.SaveLocal(strName.."Y",tblName.CoordY)
end
end -- function SaveDialogue
function fh.SaveSettings(...) -- Save Sticky Settings to File
fh.SaveDialogue(...)
fh.SaveGlobal("SafeColor",fh.Safe)
fh.SaveGlobal("WarnColor",fh.Warn)
fh.SaveGlobal("RiskColor",fh.Risk)
fh.SaveGlobal("HeadColor",fh.Head)
fh.SaveGlobal("BodyColor",fh.Body)
fh.SaveGlobal("FontHead" ,fh.FontHead)
fh.SaveGlobal("FontBody" ,fh.FontBody)
fh.SaveGlobal("History" ,fh.History)
fh.SaveGlobal("Balloon" ,fh.Balloon, "Machine")
for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
for i,j in pairs (tblClipData) do -- Check if table has any entries
strFileName = fh[strFileName]
if #strFileName > 0 then -- Skip for standalone GEDCOM -- V4.2
if type(table.save) == "function" then -- Save entire Settings File table per Project/User/Machine
table.save(tblClipData,strFileName)
else
local tblClip = {}
for strKey,strVal in pairs(tblClipData) do -- Else save Settings File lines with key & val fields -- V4.0
table.insert(tblClip,strKey.."="..strVal.."\n") -- V4.0
end
local strClip = table.concat(tblClip,"\n") -- V4.0
if not general.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
end
break
end
end
end -- function SaveSettings
function fh.CheckWindowPosition(tblName) -- Ensure dialogue window coordinates are on Screen
local tblSize = stringx.splitnumbers(tblName.Raster or "600x400","x") -- Get window dimensions from the previous use of plugin -- V4.4
local intWinW = tblSize[1]
local intWinH = tblSize[2]
if tonumber(tblName.CoordX) == nil
or tonumber(tblName.CoordY) == nil
or tonumber(tblName.CoordX) < intMinX -- V4.4 -- V3.5
or tonumber(tblName.CoordY) < intMinY -- V4.4 -- V3.5
or tonumber(tblName.CoordX) + intWinW > intMaxW -- V4.4
or tonumber(tblName.CoordY) + intWinH > intMaxH then -- V4.4
tblName.CoordX = iup.CENTERPARENT -- V4.4
tblName.CoordY = iup.CENTERPARENT -- V4.4
end
end -- function CheckWindowPosition
function fh.SetWindowCoord(tblName) -- Set the Window coordinates if not Maximised or Minimised -- V3.5
-- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height
local tblPosn = stringx.splitnumbers(tblName.Dialog.ScreenPosition)
local intPosX = tblPosn[1] or -1
local intPosY = tblPosn[2] or -1
if intPosX < 0 and intPosY < 0 then -- If origin is negative (-8, -8 = Maximised, -3200, -3200 = Minimised)
return false -- then is Maximised or Minimised
end
tblName.CoordX = intPosX -- Otherwise set the Window coordinates
tblName.CoordY = intPosY
return true
end -- function SetWindowCoord
function fh.ShowDialogue(strName,iupDialog,btnFocus,strFrame) -- Set standard frame attributes and display dialogue window
local tblName = tblNameFor(strName)
iupDialog = iupDialog or tblName.Dialog -- Retrieve previous parameters if needed
btnFocus = btnFocus or tblName.Focus
strFrame = strFrame or tblName.Frame
strFrame = strFrame or "show norm" -- Default frame mode is dialog:showxy(X,Y) with DialogFrame="NO" ("normal" to vary size, otherwise fixed size)
strFrame = strFrame:lower() -- Other modes are "show", "popup" & "keep" with DialogFrame="YES", or with "normal" for DialogFrame="NO" ("show" for active windows, "popup"/"keep" for modal windows)
if strFrame:gsub("%s-%a-map%a*[%s%p]*","") == "" then -- May be prefixed with "map" mode to just map dialogue initially, also may be suffixed with "dialog" to inhibit iup.MainLoop() to allow progress messages
strFrame = "map show norm" -- If only "map" mode then default to "map show norm"
end
if type(iupDialog) == "userdata" then
tblName.Dialog = iupDialog
tblName.Focus = btnFocus -- Preserve parameters
tblName.Frame = strFrame
iupDialog.Background = fh.Back -- Background colour
iupDialog.Shrink = "YES" -- Sometimes needed to shrink controls to raster size
if type(btnFocus) == "userdata" then -- Set button as focus for Esc and Enter keys
iupDialog.StartFocus = iupDialog.StartFocus or btnFocus
iupDialog.DefaultEsc = iupDialog.DefaultEsc or btnFocus
iupDialog.DefaultEnter = iupDialog.DefaultEnter or btnFocus
end
if tblName.CoordX == iup.CENTERPARENT
or tblName.CoordY == iup.CENTERPARENT then -- When centred on parent refresh this window size -- V4.4 -- V4.6
fh.RefreshDialogue(strName)
else
iupDialog.MinSize = "x" -- Minimum size (default "x" becomes nil)
iupDialog.RasterSize = tblName.Raster or "x" -- Raster size (default "x" becomes nil)
end
iupDialog.MaxSize = intMaxW.."x"..intMaxH -- Maximum size is virtual screen size
if strFrame:match("norm") then -- DialogFrame mode is "NO" by default for variable size window
if strFrame:match("pop") or strFrame:match("keep") then
iupDialog.MinBox = "NO" -- For "popup" and "keep" hide Minimize and Maximize icons
iupDialog.MaxBox = "NO"
else
strFrame = strFrame.." show" -- If not "popup" nor "keep" then use "showxy" mode
end
else
iupDialog.DialogFrame = "YES" -- Define DialogFrame mode for fixed size window
end
iupDialog.close_cb = iupDialog.close_cb or function() return iup.CLOSE end -- Define default window X close, move, and resize actions
iupDialog.move_cb = iupDialog.move_cb or function(self) if iup.MainLoopLevel() > 0 then fh.SetWindowCoord(tblName) end end -- V3.5 -- V4.4
iupDialog.resize_cb = iupDialog.resize_cb or function(self) if iup.MainLoopLevel() > 0 then tblName.Raster=self.RasterSize end end -- V3.5 -- V4.4 -- V4.6
fh.RefreshDialogue(strName) -- Refresh to set Natural Size as Minimum Size
if strName == "Main" then
if fhGetAppVersion() > 6 then -- Main window centres on FH parent -- V4.4
iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
else
local tblMain = tblNameFor("Main") -- Others popup centrally in Main -- V4.4
local iupMain = tblMain.Dialog
if iupMain then -- Centre based on size of windows -- V1.4
local arrName = stringx.splitnumbers(tblName.Raster or "0x0")
local arrMain = stringx.splitnumbers(tblMain.Raster or arrName[1].."x"..arrName[2])
tblName.CoordX = iupMain.X + math.floor( (arrMain[1] - arrName[1]) / 2 )
tblName.CoordY = iupMain.Y + math.floor( (arrMain[2] - arrName[2]) / 2 )
elseif fhGetAppVersion() > 6 then -- This window centres on FH parent -- V4.4
iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
end
if strFrame:match("map") then -- Only dialogue mapping is required
iupDialog:map()
tblName.Frame = strFrame:gsub("%s-%a-map%a*[%s%p]*","") -- Remove "map" from frame mode ready for subsequent call
return
end
if iup.MainLoopLevel() == 0 -- Called from outside Main GUI, so must use showxy() and not popup()
or strFrame:match("dialog")
or strFrame:match("sho") then -- Use showxy() to dispay dialogue window for "showxy" or "dialog" mode
iupDialog:showxy(tblName.CoordX,tblName.CoordY)
if not strFrame:match("dialog") -- Inhibit MainLoop if "dialog" mode -- V4.1
and iup.MainLoopLevel() == 0 then iup.MainLoop() end
else
iupDialog:popup(tblName.CoordX,tblName.CoordY) -- Use popup() to display dialogue window for "popup" or "keep" modes
fhSleep(200,150) -- Sometimes needed to prevent MainLoop() closure! -- V3.9
end
if not strFrame:match("dialog") and strFrame:match("pop") then
tblName.Dialog = nil -- When popup closed, clear key parameters, but not for "keep" mode
tblName.Raster = nil
tblName.CoordX = nil -- iup.CENTERPARENT
tblName.CoordY = nil -- iup.CENTERPARENT
elseif tblName.CoordX ~= iup.CENTERPARENT -- V4.4
and tblName.CoordY ~= iup.CENTERPARENT then
fh.SetWindowCoord(tblName) -- Set Window coordinate pixel values -- V3.5
end
end
end -- function ShowDialogue
function fh.DestroyDialogue(strName) -- Destroy existing dialogue
local tblName = tblNameFor(strName)
if tblName then
local iupDialog = tblName.Dialog
if type(iupDialog) == "userdata" then
iupDialog:destroy()
tblName.Dialog = nil -- Prevent future misuse of handle -- 22 Jul 2014
end
end
end -- function DestroyDialogue
local function strDialogueArgs(strArgA,strArgB,comp) -- Compare two argument pairs and return matching pair
local tblArgA = stringx.splitnumbers(strArgA)
local tblArgB = stringx.splitnumbers(strArgB)
local strArgX = tostring(comp(tblArgA[1] or 100,tblArgB[1] or 100))
local strArgY = tostring(comp(tblArgA[2] or 100,tblArgB[2] or 100))
return strArgX.."x"..strArgY
end -- local function strDialogueArgs
function fh.RefreshDialogue(strName) -- Refresh dialogue window size after Font change, etc
local tblName = tblNameFor(strName)
local iupDialog = tblName.Dialog -- Retrieve the dialogue handle
if type(iupDialog) == "userdata" then
iupDialog.Size = iup.NULL
iupDialog.MinSize = iup.NULL -- V3.1
iup.Refresh(iupDialog) -- Refresh window to Natural Size and set as Minimum Size
if not iupDialog.RasterSize then
iupDialog:map()
iup.Refresh(iupDialog)
end
local strSize = iupDialog.NaturalSize or iupDialog.RasterSize -- IUP 3.5 NaturalSize = nil, IUP 3.11 needs NaturalSize -- V3.1
iupDialog.MinSize = strDialogueArgs(iupDialog.MaxSize,strSize,math.min) -- Set Minimum Size to smaller of Maximm Size or Natural/Raster Size -- V3.1
iupDialog.RasterSize = strDialogueArgs(tblName.Raster,strSize,math.max) -- Set Current Size to larger of Current Size or Natural/Raster Size -- V3.1
iup.Refresh(iupDialog)
tblName.Raster = iupDialog.RasterSize
if iupDialog.Visible == "YES" then -- Ensure visible dialogue origin is on screen
tblName.CoordX = math.max(tblName.CoordX,10)
tblName.CoordY = math.max(tblName.CoordY,10) -- Set both coordinates to larger of current value or 10 pixels
if iupDialog.Modal then -- V3.8
if iupDialog.Modal == "NO" then
iupDialog.ZOrder = "BOTTOM" -- Ensure dialogue is subservient to any popup
iupDialog:showxy(tblName.CoordX,tblName.CoordY) -- Use showxy() to reposition main window
else
iupDialog:popup(tblName.CoordX,tblName.CoordY) -- Use popup() to reposition modal window
end
end
else
iupDialog.BringFront="YES"
end
end
end -- function RefreshDialogue
function fh.AssignAttributes(tblControls) -- Assign the attributes of all controls supplied
local anyFunction = nil
for iupName, tblAttr in pairs ( tblControls or {} ) do
if type(iupName) == "userdata" and type(tblAttr) == "table" then -- Loop through each iup control
local intSkip = 0 -- Skip counter for attributes same for all controls
for intAttr, anyName in ipairs ( tblControls[1] or {} ) do -- Loop through each iup attribute
local strName = nil
local strAttr = nil
local strType = type(anyName)
if strType == "string" then -- Attribute is different for each control in tblControls
strName = anyName
strAttr = tblAttr[intAttr-intSkip]
elseif strType == "table" then -- Attribute is same for all controls as per tblControls[1]
intSkip = intSkip + 1
strName = anyName[1]
strAttr = anyName[2]
elseif strType == "function" then
intSkip = intSkip + 1
anyFunction = anyName
break
end
if type(strName) == "string" and ( type(strAttr) == "string" or type(strAttr) == "function" ) then
local anyRawGet = rawget(fh,strAttr) -- Use rawget() to stop require("pl.strict") complaining
if type(anyRawGet) == "string" then
strAttr = anyRawGet -- Use internal module attribute such as Head or FontBody
elseif type(iupName[strName]) == "string"
and type(strAttr) == "function" then -- Allow string attributes to invoke a function -- V3.7
strAttr = strAttr()
end
iupName[strName] = strAttr -- Assign attribute to control
end
end
end
end
if anyFunction then anyFunction() end -- Perform any control assignment function
end -- function AssignAttributes
-- Font Dialogue Attributes and Functions --
fh.FontBody = iup.GetGlobal("DEFAULTFONT") -- Set default font for Body and Head text
fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold ")
function fh.FontDialogue(tblAttr,strName) -- GUI Font Face & Style Dialogue
tblAttr = tblAttr or {}
strName = strName or "Main"
local isFontChosen = false
local btnFontHead = iup.button { Title="Choose Headings Font and default Colour"; }
local btnFontBody = iup.button { Title="Choose Body text Font and default Colour"; }
local btnCol_Safe = iup.button { Title=" Safe Colour "; }
local btnCol_Warn = iup.button { Title=" Warning Colour "; }
local btnCol_Risk = iup.button { Title=" Risky Colour "; }
local btnDefault = iup.button { Title=" Default Fonts "; }
local btnMinimum = iup.button { Title=" Minimum Size "; }
local btnDestroy = iup.button { Title=" Close Dialogue "; }
local frmSetFonts = iup.frame { Title=" Set Window Fonts & Colours ";
iup.vbox { Alignment="ACENTER"; Margin=fh.Margin; Homogeneous="YES";
btnFontHead;
btnFontBody;
iup.hbox { btnCol_Safe; btnCol_Warn; btnCol_Risk; Homogeneous="YES"; };
iup.hbox { btnDefault ; btnMinimum ; btnDestroy ; Homogeneous="YES"; };
} -- iup.vbox end
} -- iup.frame end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local dialogFont = iup.dialog { Title=" Set Window Fonts & Colours "; Gap=fh.Gap; Margin=fh.Border; frmSetFonts; }
local tblButtons = { }
local function setDialogues() -- Refresh the Main dialogue -- V4.4
fh.AssignAttributes(tblAttr) -- Assign parent dialogue attributes
fh.RefreshDialogue(strName) -- Refresh parent window size & position and bring infront of other window
fh.RefreshDialogue("Font") -- Refresh Font window size & position and bring infront of parent window
end -- local function setDialogues
local function getFont(strColor) -- Set font button function
local strTitle = " Choose font style & default colour for "..strColor:gsub("Head","Heading").." text "
local strValue = "Font"..strColor -- The font codes below are not recognised by iupFontDlg and result in empty font face!
local strFont = rawget(fh,strValue):gsub(" Black,",","):gsub(" Light, Bold",","):gsub(" Extra Bold,",","):gsub(" Semibold,",",")
local iupFontDlg = iup.fontdlg { Title=strTitle; Color=rawget(fh,strColor); Value=strFont; }
iupFontDlg:popup() -- Popup predefined font dialogue
if iupFontDlg.Status == "1" then
if iupFontDlg.Value:match("^,") then -- Font face missing so revert to original font
iupFontDlg.Value = rawget(fh,strValue)
end
fh[strColor] = iupFontDlg.Color -- Set Head or Body color attribute
fh[strValue] = iupFontDlg.Value -- Set FontHead or FontBody font style
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
setDialogues()
isFontChosen = true
end
end -- local function getFont
local function getColor(strColor) -- Set colour button function
local strTitle = " Choose colour for "..strColor:gsub("Warn","Warning"):gsub("Risk","Risky").." button & message text "
local iupColorDlg = iup.colordlg { Title=strTitle; Value=rawget(fh,strColor); ShowColorTable="YES"; }
iupColorDlg.DialogFrame="YES"
iupColorDlg:popup() -- Popup predefined color dialogue fixed size window
if iupColorDlg.Status == "1" then
fh[strColor] = iupColorDlg.Value -- Set Safe or Warn or Risk color attribute
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
setDialogues()
isFontChosen = true
end
end -- local function getColor
local function setDefault() -- Action for Default Fonts button
fh.Safe = fh.Green
fh.Warn = fh.Magenta
fh.Risk = fh.Red -- Set default colours
fh.Body = fh.Black
fh.Head = fh.Black
fh.FontBody = iup.GetGlobal("DEFAULTFONT") -- Set default fonts for Body and Head text
fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold")
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
setDialogues()
isFontChosen = true
end -- local function setDefault
local function setMinimum() -- Action for Minimum Size button
local tblName = tblNameFor(strName)
local iupDialog = tblName.Dialog -- Retrieve the parent dialogue handle
if type(iupDialog) == "userdata" then
tblName.Raster = "10x10" -- Refresh parent window to Minimum Size & adjust position
fh.RefreshDialogue(strName)
end
local tblFont = tblNameFor("Font")
tblFont.Raster = "10x10" -- Refresh Font window to Minimum Size & adjust position
fh.RefreshDialogue("Font")
end -- local function setMinimum
tblButtons = { { "Font" ; "FgColor" ; "Tip" ; "action" ; {"TipBalloon";"Balloon";} ; {"Expand";"YES";} ; };
[btnFontHead] = { "FontHead"; "Head"; "Choose the Heading text Font Face, Style, Size, Effects, and default Colour"; function() getFont("Head") end; };
[btnFontBody] = { "FontBody"; "Body"; "Choose the Body text Font Face, Style, Size, Effects, and default Colour" ; function() getFont("Body") end; };
[btnCol_Safe] = { "FontBody"; "Safe"; "Choose the colour for Safe operations" ; function() getColor("Safe") end; };
[btnCol_Warn] = { "FontBody"; "Warn"; "Choose the colour for Warning operations"; function() getColor("Warn") end; };
[btnCol_Risk] = { "FontBody"; "Risk"; "Choose the colour for Risky operations" ; function() getColor("Risk") end; };
[btnDefault ] = { "FontBody"; "Safe"; "Restore default Fonts and Colours"; function() setDefault() end; };
[btnMinimum ] = { "FontBody"; "Safe"; "Reduce window to its minimum size"; function() setMinimum() end; };
[btnDestroy ] = { "FontBody"; "Risk"; "Close this dialogue "; function() return iup.CLOSE end; };
[frmSetFonts] = { "FontHead"; "Head"; };
}
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep normal") -- Popup the Set Window Fonts dialogue: "keep normal" : vary size & posn, and remember size & posn
-- fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup normal") -- Popup the Set Window Fonts dialogue: "popup normal" : vary size & posn, but redisplayed centred
-- fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep") -- Popup the Set Window Fonts dialogue: "keep" : fixed size, vary posn, and only remember posn
-- fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup") -- Popup the Set Window Fonts dialogue: "popup": fixed size, vary posn, but redisplayed centred
dialogFont:destroy()
return isFontChosen
end -- function FontDialogue
local function anyMemoControl(anyName,fgColor) -- Compose any control Title and FgColor
local strName = tostring(anyName) -- anyName may be a string, and fgColor is default FgColor
local tipText = nil
if type(anyName) == "table" then -- anyName may be a table = { Title string ; FgColor string ; ToolTip string (optional); }
strName = anyName[1]
fgColor = anyName[2]:match("%d* %d* %d*") or fgColor
tipText = anyName[3]
end
return strName, fgColor, tipText
end -- local function anyMemoControl
local function anyMemoDialogue(strHead,anyHead,strMemo,anyMemo,...) -- Display framed memo dialogue with buttons
local arg = {...} -- Fix for Lua 5.2+
local intButt = 0 -- Returned value if "X Close" button is used
local tblButt = { [0]="X Close"; } -- Button names lookup table
local strHead, fgcHead, tipHead = anyMemoControl(anyHead or "",strHead)
local strMemo, fgcMemo, tipMemo = anyMemoControl(anyMemo or "",strMemo)
-- Create the GUI labels and buttons
local lblMemo = iup.label { Title=strMemo; FgColor=fgcMemo; Tip=tipMemo; TipBalloon=fh.Balloon; Alignment="ACENTER"; Padding=fh.Margin; Expand="YES"; WordWrap="YES"; }
local lblLine = iup.label { Separator="HORIZONTAL"; }
local iupHbox = iup.hbox { Homogeneous="YES"; }
local btnButt = iup.button { }
local strTop = "YES" -- Make dialogue TopMost -- V3.6
local strMode = "popup"
if arg[1] == "Keep Dialogue" then -- Keep dialogue open for a progress message
strMode = "keep dialogue"
lblLine = iup.label { }
if not arg[2] then strTop = "NO" end -- User chooses TopMost -- V3.6
else
if #arg == 0 then arg[1] = "OK" end -- If no buttons listed then default to an "OK" button
for intArg, anyButt in ipairs(arg) do
local strButt, fgcButt, tipButt = anyMemoControl(anyButt,fh.Safe)
tblButt[intArg] = strButt
btnButt = iup.button { Title=strButt; FgColor=fgcButt; Tip=tipButt; TipBalloon=fh.Balloon; Expand="NO"; MinSize="80"; Padding=fh.Margin; action=function() intButt=intArg return iup.CLOSE end; }
iup.Append( iupHbox, btnButt )
end
end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local iupMemo = iup.dialog { Title=fh.Plugin..fh.Version..strHead; TopMost=strTop; -- TopMost added -- V3.6
iup.vbox { Alignment="ACENTER"; Gap=fh.Gap; Margin=fh.Margin;
iup.frame { Title=strHead; FgColor=fgcHead; Font=fh.FontHead;
iup.vbox { Alignment="ACENTER"; Font=fh.FontBody; lblMemo; lblLine; iupHbox; };
};
};
}
fh.ShowDialogue("Memo",iupMemo,btnButt,strMode) -- Show popup Memo dialogue window with righthand button in focus (if any)
if strMode == "keep dialogue" then return lblMemo, iupMemo end -- Return label & dialogue controls so message can be changed and dialogue destroyed
iupMemo:destroy()
return intButt, tblButt[intButt] -- Return button number & title that was pressed
end -- local function anyMemoDialogue
function fh.MemoDialogue(anyMemo,...) -- Multi-Button GUI like iup.Alarm and fhMessageBox, with "Memo" in frame
return anyMemoDialogue(fh.Head,"Memo",fh.Body,anyMemo,...)
end -- function MemoDialogue
function fh.WarnDialogue(anyHead,anyMemo,...) -- Multi-Button GUI like iup.Alarm and fhMessageBox, with heading in frame
return anyMemoDialogue(fh.Warn,anyHead,fh.Warn,anyMemo,...)
end -- function WarnDialogue
function fh.GetRegKey(strKey) -- Read Windows Registry Key Value
local luaShell = luacom.CreateObject("WScript.Shell")
local anyValue = nil
if pcall( function() anyValue = luaShell:RegRead(strKey) end ) then
return anyValue -- Return Key Value if found
end
return nil
end -- function GetRegKey
function fh.PutRegKey(strKey,anyValue,strType) -- Write Windows Registry Key Value
local luaShell = luacom.CreateObject("WScript.Shell")
local strAns = nil
if pcall( function() strAns = luaShell:RegWrite(strKey,anyValue,strType) end ) then
return true
end
return nil
end -- function PutRegKey
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
function fh.VersionInStore(strPlugin) -- Obtain the Version in Plugin Store by Name only -- V4.5
local strVersion = "0"
if strPlugin then
local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat"
general.DeleteFile(strFile)
local lblMemo, iupMemo = fh.MemoDialogue("Checking for updated version in the Family Historian 'Plugin Store'.","Keep Dialogue")
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin
local isOK, strReturn = pcall(httpRequest,strRequest)
iupMemo:destroy()
if not isOK then -- Problem with Internet access
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION")
elseif strReturn ~= nil then
strVersion = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
end
end
return strVersion or "0"
end -- function VersionInStore
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) -- V4.1
for i=1,5 do
intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
end
return intVersion
end -- local function intVersion
function fh.CheckVersionInStore() -- Check if later Version available in Plugin Store -- V4.5
local strNewVer = fh.VersionInStore(fh.Plugin:gsub(" %- .*",""))
local strOldVer = fh.Version
if intVersion(strNewVer) > intVersion(strOldVer:match("%D*([%d%.]*)")) then
fh.MemoDialogue("Later Version "..strNewVer.." of this Plugin is available from the 'Plugin Store'.")
else
fh.MemoDialogue("No later Version of this Plugin is available from the 'Plugin Store'.")
end
end -- function CheckVersionInStore
function fh.PluginDataScope(strScope) -- Set default Plugin Data scope to per-Project, or per-User, or per-Machine
strScope = tostring(strScope):lower()
if strScope:match("mach") then -- Per-Machine
strDefaultScope = "Machine"
elseif strScope:match("user") then -- Per-User
strDefaultScope = "User"
end -- Per-Project is default
end -- function PluginDataScope
local function getPluginDataFileName(strScope) -- Get plugin data filename for chosen scope
local isOK, strDataFile = pcall(fhGetPluginDataFileName,strScope)
if not isOK then strDataFile = fhGetPluginDataFileName() end -- Before V5.0.8 parameter is disallowed and default = CURRENT_PROJECT
return strDataFile
end -- local function getPluginDataFileName
local function getDataFiles(strScope) -- Compose the Plugin Data file & path & root names
local strPluginName = fh.Plugin
local strPluginPlain = stringx.plain(strPluginName)
local strDataRoot = "" -- Plugin data file root name -- V4.2
local strDataPath = "" -- Plugin data folder path name
local strDataFile = getPluginDataFileName(strScope) -- Allow plugins with variant filenames to use same plugin data files
strDataFile = strDataFile:gsub("\\"..strPluginPlain:gsub(" ","_"):lower(),"\\"..strPluginName)
strDataFile = strDataFile:gsub("\\"..strPluginPlain..".+%.[D,d][A,a][T,t]$","\\"..strPluginName..".dat")
if #strDataFile > 0 then -- Standalone GEDCOM path is ""
strDataPath = strDataFile:gsub("\\"..strPluginPlain.."%.[D,d][A,a][T,t]$","")
strDataRoot = strDataPath.."\\"..strPluginName
general.MakeFolder(strDataPath) -- V3.4
end
return strDataFile, strDataPath, strDataRoot
end -- local function getDataFiles
function fh.Initialise(strVersion,strPlugin) -- Initialise the GUI module with optional Version & Plugin name
local strAppData = fhGetContextInfo("CI_APP_DATA_FOLDER")
fh.Plugin = fhGetContextInfo("CI_PLUGIN_NAME") -- Plugin Name from file
fh.Version = strVersion or " " -- Plugin Version
if fh.Version == " " then
local strTitle = "\n@Title is missing"
local strAuthor = "\n@Author is missing"
local strVersion = "\n@Version is missing"
local strPlugin = strAppData.."\\Plugins\\"..fh.Plugin..".fh_lua"
if general.FlgFileExists(strPlugin) then
for strLine in io.lines(strPlugin) do -- Read each line from the Plugin file
strPlugin = strLine:match("^@Title:[\t-\r ]*(.*)")
if strPlugin then
strPlugin = strPlugin:gsub("&&","&")
--? if fh.Plugin:match("^"..strPlugin:gsub("(%W)","%%%1")) then
if fh.Plugin:match("^"..stringx.plain(strPlugin)) then
fh.Plugin = strPlugin -- Prefer Title to Filename if it matches
strTitle = nil
else
strTitle = "\n@Title differs from Filename" -- Report abnormality
end
end
if strLine:match("^@Author:%s*(.*)") then -- Check @Author exists
strAuthor = nil
end
fh.Version = strLine:gsub("^@Version:%D*([%d%.]*)%D*"," %1 ")
if fh.Version ~= strLine then -- Obtain the @Version from Plugin file
strVersion = nil
break
end
end
if strTitle or strAuthor or strVersion then -- Report any header abnormalities
fhMessageBox("\nScript Header: "..fh.Plugin..(strTitle or "")..(strAuthor or "")..(strVersion or ""),"MB_OK","MB_ICONEXCLAMATION")
end
else
fhMessageBox("\nPlugin has not been saved!","MB_OK","MB_ICONEXCLAMATION")
end
end
fh.History = fh.Version -- Version History
fh.Plugin = strPlugin or fh.Plugin -- Plugin Name from argument or default from file
fh.DefaultDialogue() -- Default "Font","Main" dialogues
fh.MachineFile,fh.MachinePath,fh.MachineRoot = getDataFiles("LOCAL_MACHINE") -- Plugin data names per machine
fh.PerUserFile,fh.PerUserPath,fh.PerUserRoot = getDataFiles("CURRENT_USER") -- Plugin data names per user
fh.ProjectFile,fh.ProjectPath,fh.ProjectRoot = getDataFiles("CURRENT_PROJECT") -- Plugin data names per project
fh.FhDataPath = fhGetContextInfo("CI_PROJECT_DATA_FOLDER") -- Paths used by Load/SaveFolder for relative folders -- V4.0
fh.PublicPath = fhGetContextInfo("CI_PROJECT_PUBLIC_FOLDER") -- Public data folder path name -- V4.0
if fh.FhDataPath == "" then
fh.FhDataPath = fh.ProjectPath:gsub("\\Plugin Data$","")
end
if fh.PublicPath == "" then
fh.PublicPath = fh.ProjectPath
fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Plugin Data$","%1")
else
general.MakeFolder(fh.PublicPath) -- V3.4
fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Public$","%1")
end
fh.CalicoPie = strAppData:gsub("\\Calico Pie\\.*","\\Calico Pie") -- Program Data Calico Pie path name
fh.ComputerName = os.getenv("COMPUTERNAME") -- Local PC Computer Name
end -- function Initialise
fh.Initialise() -- Initialise module with default values
return fh
end -- local function iup_gui_v3
local iup_gui = iup_gui_v3() -- To access FH IUP GUI build module
-- Preset Global Data Definitions --
function PresetGlobalData()
iup_gui.Gap = "2" -- GUI defaults
iup_gui.Balloon = "NO" -- Needed for PlayOnLinux/Mac -- V3.8
iup_gui.SetUtf8Mode()
IntFhVersion = fhGetAppVersion()
StrPlusMinus = "±"
if IntFhVersion > 5 then
StrPlusMinus = fhConvertANSItoUTF8(StrPlusMinus) -- Fix "±" -- V3.7
end
StrC = "[%z-\031\127]" -- LUA control chars pattern becasue "%c" mishandles UTF-8 codes \129 \141 \143 \144 \157 \173
StrP = "[!-/:-@%[\\%]^_`%]{-~]" -- LUA punction chars pattern because "%p" mishandles UTF-8 codes
-- i.e. = "[\033-\047\058-\064\091\092\093\094\095\096\123-\126]"
StrS = "[\t-\r ]" -- i.e. = "[\009-\013\032]" -- LUA space chars pattern because "%s" mishandles UTF-8 code (ANSI nbsp=\160)
StrSP = "[\t-\r !-/:-@%[\\%]^_`%]{-~]"
StrNonDupsFile = iup_gui.ProjectRoot..".nondups" -- File names for saved data always ANSI
StrResultsFile = iup_gui.ProjectRoot..".results"
StrSoundexFile = iup_gui.ProjectRoot..".soundex"
PresetGlobalDefaults() -- User preferences defaults
SetUserInterfaceDefaults()
SetNamesMatchDefaults()
SetEventMatchDefaults()
SetChronologyDefaults()
SetOtherMatchDefaults()
end -- function PresetGlobalData
-- Global User Defaults Definition --
function PresetGlobalDefaults()
TblData = {} -- Data table per Individual Record Id of key information
TblNonDups = {} -- Non-Duplicate pairs of Record Id to exclude
-- User Interface Defaults -- Default Value -- Description of Default
IntIndiScoreMinDef = 12 -- 12 Points -- Minimum score needed for Individual to assess Relations
IntLeastResultsDef = 1 -- 1 Point -- Result Set lowest score to display
IntLimitResultsDef = 100 -- 100 Rows -- Result Set limit of rows to display
IntPruneResultsDef = 200 -- 200 Entries -- Results table threshold to avoid exhausting memory about twice IntLimitResults
IntProgBarStartDef = 400000 -- 400000 Compares -- Threshold of Comparisons at which to start Progress Bar
-- Comparisons = R * (R-1) / 2 where R = Total Individual Records
-- Names Matching Defaults -- Default Value -- Description of Default
IntLastNameRightDef = 7 -- 7 Points -- Addition for Lastname perfect match
IntForeNameRightDef = 6 -- 6 Points -- Addition for Forename perfect match in the right position
IntForeNameOtherDef = 3 -- 3 Points -- Addition for Forename perfect match but in other position
IntNameSoundexDef = 2 -- 2 Points -- Addition for any Name Soundex match only
IntNameLastWrongDef = -0 -- -0 Points -- Deduction for Lastname total mismatch
IntNameMinimumDef = 1 -- 1 Point -- Minimum needed to avoid entire Names mismatch
IntNameDeductionDef = -0 -- -0 Points -- Deduction for Relation entire Names mismatch (not used for Individual)
IntNameMaximumDef = 20 -- 20 Points -- Maximum entire Names match to avoid overwhelming result
IntNameThresholdDef = 6 -- 6 Points -- Threshold needed to proceed with Event assessments, etc
IntIndivi = 1 -- Table index per Relation
IntFather = 2
IntMother = 3
IntSpouse = 4
IntChild = 5
TblLastNameRight = { } -- Table entry per Relation
TblForeNameRight = { }
TblForeNameOther = { }
TblNameLastRight = { }
TblNameForeRight = { }
TblNameForeOther = { }
TblNameSoundex = { }
TblNameLastWrong = { }
TblNameMinimum = { }
TblNameDeduction = { }
TblNameMaximum = { }
TblNameThreshold = { }
-- Event Matching Defaults -- Default Value -- Description of Default
IntDatesToleranceDef = 50 -- ±50 Days -- Tolerance to grant a Lower or Upper Date Timespan match
IntDatesMatchedDef = 2 -- 2 Points -- Addition for a tolerant Lower/Upper Date Timespan match
IntDatesOverlapDef = 2 -- 2 Points -- Addition for an overlapping Date Timespan
IntDatesMinimumDef = 1 -- 1 Point -- Minimum Dates score to avoid entire Dates mismatch
IntDatesDeductionDef = -15 -- -15 Points -- Deduction for entire Dates mismatch
IntPlacePartRightDef = 3 -- 3 Points -- Addition for Place Part perfect match in the right position
IntPlacePartOtherDef = 2 -- 2 Points -- Addition for Place Part perfect match but in other position
IntPlaceSoundexDef = 1 -- 1 Point -- Addition for Place Part Soundex match only
IntEventMaximumDef = 10 -- 10 Points -- Maximum for entire Event match to avoid overwhelming result
IntBoostedBirthDef = 1 -- 1 Times -- Boost score multiplier for Birth Events -- V3.8
IntBoostedBapChDef = 1 -- 1 Times -- Boost score multiplier for Baptism/Christening -- V3.8
IntBoostedMarryDef = 1 -- 1 Times -- Boost score multiplier for Marriage Events -- V3.8
IntBoostedDeathDef = 1 -- 1 Times -- Boost score multiplier for Death/Burial/Cremate -- V3.8
-- Date Chronology Defaults -- Default Value -- Description of Default
IntDatesTimespanDef = 50 -- ±50 Years -- Timespan to extend 'After', 'Before', 'From', 'To' Period & Range Dates
IntDatesVarianceDef = 5 -- ±5 Years -- Variance for 'Approximate', 'Calculated', 'Estimated' Year only Dates
IntDatesPregnantDef = 9 -- 9 Months -- Pregnancy duration for synthesised Dates for Chronology checks
IntDatesPubertyDef = 12 -- 12 Years -- Minimum puberty age for synthesised Dates for Chronology checks
IntDatesMarriageDef = 16 -- 16 Years -- Minimum marriage age for synthesised Dates for Chronology checks
IntDatesFertileDef = 50 -- 50 Years -- Maximum fertile age for synthesised Dates for Chronology checks
IntDatesLifespanDef = 100 -- 100 Years -- Maximum lifespan age for synthesised Dates for Chronology checks
IntChronMagnitudeDef = 12 -- 12 Months -- Magnitude of Date Chronology mismatch to deduct 1 point
IntChronToleranceDef = -20 -- -20 Points -- Degree of mismatch tolerated before excluding Individuals
IntDaysPerYear = 365.242199 -- Days per year taking account of leap years
-- Generation Gap Defaults -- Default Value -- Description of Default
IntGenGapFamilyDef = 2 -- 2 Gen Gap -- Largest Generations Up + Down that defines immediate Family to exclude ( < IntGenGapRelative )
IntGenGapRelativeDef = 6 -- 6 Gen Gap -- Largest Generations Up + Down that defines a close Relative
IntGenGapDeductDef = -5 -- -5 Points -- Deduction for a close Relative as defined above
-- Gender Mismatch Default
IntGenderDeductDef = -10 -- -10 Points -- Deduction for Individual gender mismatch and Child gender mismatch
end -- function PresetGlobalDefaults()
function SetUserInterfaceDefaults() -- Set User Interface Defaults
IntIndiScoreMin = IntIndiScoreMinDef
IntLeastResults = IntLeastResultsDef
IntLimitResults = IntLimitResultsDef
IntPruneResults = IntPruneResultsDef
IntProgBarStart = IntProgBarStartDef
end -- function SetUserInterfaceDefaults
function SetNamesMatchDefaults() -- Set Names Matching Defaults
for intRelation = IntIndivi, IntChild do
TblLastNameRight[intRelation] = IntLastNameRightDef
TblForeNameRight[intRelation] = IntForeNameRightDef
TblForeNameOther[intRelation] = IntForeNameOtherDef
TblNameSoundex [intRelation] = IntNameSoundexDef
TblNameLastWrong[intRelation] = IntNameLastWrongDef
TblNameMinimum [intRelation] = IntNameMinimumDef
TblNameDeduction[intRelation] = IntNameDeductionDef
TblNameMaximum [intRelation] = IntNameMaximumDef
TblNameThreshold[intRelation] = IntNameThresholdDef
SetNamesPoints(intRelation)
end
end -- function SetNamesMatchDefaults
function SetEventMatchDefaults() -- Set Event Matching Defaults
IntDatesTolerance = IntDatesToleranceDef
IntDatesMatched = IntDatesMatchedDef
IntDatesOverlap = IntDatesOverlapDef
IntDatesMinimum = IntDatesMinimumDef
IntDatesDeduction = IntDatesDeductionDef
IntPlacePartRight = IntPlacePartRightDef
IntPlacePartOther = IntPlacePartOtherDef
IntPlaceSoundex = IntPlaceSoundexDef
IntEventMaximum = IntEventMaximumDef
IntBoostedBirth = IntBoostedBirthDef -- V3.8
IntBoostedBapCh = IntBoostedBapChDef -- V3.8
IntBoostedMarry = IntBoostedMarryDef -- V3.8
IntBoostedDeath = IntBoostedDeathDef -- V3.8
end -- function SetEventMatchDefaults
function SetChronologyDefaults() -- Set Date Chronology Defaults
IntDatesTimespan = IntDatesTimespanDef
IntDatesVariance = IntDatesVarianceDef
IntDatesPregnant = IntDatesPregnantDef
IntDatesPuberty = IntDatesPubertyDef
IntDatesMarriage = IntDatesMarriageDef
IntDatesFertile = IntDatesFertileDef
IntDatesLifespan = IntDatesLifespanDef
IntChronMagnitude = IntChronMagnitudeDef
IntChronTolerance = IntChronToleranceDef
end -- function SetChronologyDefaults
function SetOtherMatchDefaults() -- Set Generation Gap & Gender Mismatch Defaults
IntGenGapFamily = IntGenGapFamilyDef
IntGenGapRelative = IntGenGapRelativeDef
IntGenGapDeduct = IntGenGapDeductDef
IntGenderDeduct = IntGenderDeductDef
end -- function SetOtherMatchDefaults
function SetNamesPoints(intRelation) -- Set other Names values from sticky preference values
TblNameForeOther[intRelation] = TblForeNameOther[intRelation] - TblNameSoundex[intRelation]
TblNameForeRight[intRelation] = TblForeNameRight[intRelation] - TblNameSoundex[intRelation] - TblNameForeOther[intRelation]
TblNameLastRight[intRelation] = TblLastNameRight[intRelation] - TblNameSoundex[intRelation]
end -- function SetNamesPoints
function SetEventPoints(flag) -- Set other Event values from sticky preference values
IntPartOther = IntPlacePartOther - IntPlaceSoundex
IntPartRight = IntPlacePartRight - IntPlaceSoundex - IntPartOther
if flag == nil then SaveSettings() end
end -- function SetEventPoints
function SetChronology(flag) -- Set other Chronology values from sticky preference values
IntTimespanDays = math.floor( IntDatesTimespan * IntDaysPerYear )
IntVarianceDays = math.floor( IntDatesVariance * IntDaysPerYear )
IntPregnantDays = math.floor( IntDatesPregnant * IntDaysPerYear / 12 )
IntPubertyDays = math.floor( IntDatesPuberty * IntDaysPerYear )
IntMarriageDays = math.floor( IntDatesMarriage * IntDaysPerYear )
IntFertileDays = math.floor( IntDatesFertile * IntDaysPerYear )
IntLifespanDays = math.floor( IntDatesLifespan * IntDaysPerYear )
IntChronMagDays = math.floor( IntChronMagnitude* IntDaysPerYear / 12 )
if flag == nil then SaveSettings() end
end -- function SetChronology
function SetGenerations(flag) -- Set other Generations values from sticky preference values
IntGenGapDeduction = math.abs(IntGenGapDeduct) -- The 0.01 offset below is to cater for IntGenGapDeduction = 0
IntFamGenGapMax = -math.floor( ( IntGenGapRelative - IntGenGapFamily - 0.5 ) * IntGenGapDeduction ) - 0.01
if flag == nil then SaveSettings() end
end -- function SetGenerations
function ResetDefaultSettings() -- Reset GUI Sticky Settings to Default Values
iup_gui.CustomDialogue("Main","0x0") -- Custom "Main" dialogue minimum size & centralisation
iup_gui.CustomDialogue("Font","0x0") -- Custom "Font" dialogue minimum size & centralisation
iup_gui.DefaultDialogue("Bars","Memo") -- GUI window rastersize and X & Y co-ordinates for "Main","Font","Bars","Memo" dialogues
StrLast = "Plugin Not Run Yet" -- Plugin last successful run Date
StrDate = "1 January 1900" -- Individual Date Updated threshold
StrTick = "OFF" -- Toggle tick for Last = "OFF" versus Date = "ON"
StrDiag = "OFF" -- Diagnostic Mode toggle
StrSpan = "OFF" -- Timespan Dates toggle
end -- function ResetDefaultSettings
function LoadSettings(strFileName) -- Load Sticky Settings from File
iup_gui.LoadSettings() -- Includes "Main","Font" dialogues and "FontSet" & "History"
StrDate = iup_gui.LoadGlobal("Dated",StrDate) -- Legacy for before V3.0
StrLast = iup_gui.LoadGlobal("Last" ,StrLast)
StrDate = iup_gui.LoadGlobal("Date" ,StrDate)
StrTick = iup_gui.LoadGlobal("Tick" ,StrTick)
--? StrDiag = iup_gui.LoadGlobal("Diag" ,StrDiag)
--? StrSpan = iup_gui.LoadGlobal("Span" ,StrSpan)
if StrDate == StrLast then -- Prior to V3.4 the StrLast date was moved to StrDate to use it as threshold
StrDate = "1 January 1900" -- Adjust to V3.4 arrangement keeping the dates separate selected by toggles
StrTick = "ON"
end
IntIndiScoreMin = iup_gui.LoadGlobal("IndiScoreMin" , IntIndiScoreMin)
IntLeastResults = iup_gui.LoadGlobal("LeastResults" , IntLeastResults)
IntLimitResults = iup_gui.LoadGlobal("LimitResults" , IntLimitResults)
IntPruneResults = iup_gui.LoadGlobal("PruneResults" , IntPruneResults)
TblLastNameRight = iup_gui.LoadGlobal("LastNameRight" , TblLastNameRight)
TblForeNameRight = iup_gui.LoadGlobal("ForeNameRight" , TblForeNameRight)
TblForeNameOther = iup_gui.LoadGlobal("ForeNameOther" , TblForeNameOther)
TblNameSoundex = iup_gui.LoadGlobal("NameSoundex" , TblNameSoundex)
TblNameLastWrong = iup_gui.LoadGlobal("NameLastWrong" , TblNameLastWrong)
TblNameMinimum = iup_gui.LoadGlobal("NameMinimum" , TblNameMinimum)
TblNameDeduction = iup_gui.LoadGlobal("NameDeduction" , TblNameDeduction)
TblNameMaximum = iup_gui.LoadGlobal("NameMaximum" , TblNameMaximum)
TblNameThreshold = iup_gui.LoadGlobal("NameThreshold" , TblNameThreshold)
IntDatesTolerance = iup_gui.LoadGlobal("DatesTolerance", IntDatesTolerance)
IntDatesMatched = iup_gui.LoadGlobal("DatesMatched" , IntDatesMatched)
IntDatesOverlap = iup_gui.LoadGlobal("DatesOverlap" , IntDatesOverlap)
IntDatesMinimum = iup_gui.LoadGlobal("DatesMinimum" , IntDatesMinimum)
IntDatesDeduction = iup_gui.LoadGlobal("DatesDeduction", IntDatesDeduction)
IntPlacePartRight = iup_gui.LoadGlobal("PlacePartRight", IntPlacePartRight)
IntPlacePartOther = iup_gui.LoadGlobal("PlacePartOther", IntPlacePartOther)
IntPlaceSoundex = iup_gui.LoadGlobal("PlaceSoundex" , IntPlaceSoundex)
IntEventMaximum = iup_gui.LoadGlobal("EventMaximum" , IntEventMaximum)
IntBoostedBirth = iup_gui.LoadGlobal("BoostedBirth" , IntBoostedBirth) -- V3.8
IntBoostedBapCh = iup_gui.LoadGlobal("BoostedBapCh" , IntBoostedBapCh) -- V3.8
IntBoostedMarry = iup_gui.LoadGlobal("BoostedMarry" , IntBoostedMarry) -- V3.8
IntBoostedDeath = iup_gui.LoadGlobal("BoostedDeath" , IntBoostedDeath) -- V3.8
IntDatesTimespan = iup_gui.LoadGlobal("DatesTimespan" , IntDatesTimespan)
IntDatesVariance = iup_gui.LoadGlobal("DatesVariance" , IntDatesVariance)
IntDatesPregnant = iup_gui.LoadGlobal("DatesPregnant" , IntDatesPregnant)
IntDatesPuberty = iup_gui.LoadGlobal("DatesPuberty" , IntDatesPuberty)
IntDatesMarriage = iup_gui.LoadGlobal("DatesMarriage" , IntDatesMarriage)
IntDatesFertile = iup_gui.LoadGlobal("DatesFertile" , IntDatesFertile)
IntDatesLifespan = iup_gui.LoadGlobal("DatesLifespan" , IntDatesLifespan)
IntChronMagnitude = iup_gui.LoadGlobal("ChronMagnitude", IntChronMagnitude)
IntChronTolerance = iup_gui.LoadGlobal("ChronTolerance", IntChronTolerance)
IntGenGapFamily = iup_gui.LoadGlobal("GenGapFamily" , IntGenGapFamily)
IntGenGapRelative = iup_gui.LoadGlobal("GenGapRelative", IntGenGapRelative)
IntGenGapDeduct = iup_gui.LoadGlobal("GenGapDeduct" , IntGenGapDeduct)
IntGenderDeduct = iup_gui.LoadGlobal("GenderDeduct" , IntGenderDeduct)
for intRelation = IntIndivi, IntChild do
SetNamesPoints(intRelation) -- Assign derived User Preference settings
end
SetEventPoints("No SaveSettings")
SetChronology ("No SaveSettings")
SetGenerations("No SaveSettings")
if general.FlgFileExists(StrNonDupsFile) then
TblNonDups, StrErr = table.load(StrNonDupsFile) -- Load Non-Duplicates table
end
SaveSettings() -- Save Sticky Data settings (must be last)
end -- function LoadSettings
function SaveSettings(strFileName) -- Save Sticky Settings to File
iup_gui.SaveGlobal("Last",StrLast)
iup_gui.SaveGlobal("Date",StrDate)
iup_gui.SaveGlobal("Tick",StrTick)
--? iup_gui.SaveGlobal("Diag",StrDiag)
--? iup_gui.SaveGlobal("Span",StrSpan)
iup_gui.SaveGlobal("IndiScoreMin" , IntIndiScoreMin)
iup_gui.SaveGlobal("LeastResults" , IntLeastResults)
iup_gui.SaveGlobal("LimitResults" , IntLimitResults)
iup_gui.SaveGlobal("PruneResults" , IntPruneResults)
iup_gui.SaveGlobal("LastNameRight" , TblLastNameRight)
iup_gui.SaveGlobal("ForeNameRight" , TblForeNameRight)
iup_gui.SaveGlobal("ForeNameOther" , TblForeNameOther)
iup_gui.SaveGlobal("NameSoundex" , TblNameSoundex)
iup_gui.SaveGlobal("NameLastWrong" , TblNameLastWrong)
iup_gui.SaveGlobal("NameMinimum" , TblNameMinimum)
iup_gui.SaveGlobal("NameDeduction" , TblNameDeduction)
iup_gui.SaveGlobal("NameMaximum" , TblNameMaximum)
iup_gui.SaveGlobal("NameThreshold" , TblNameThreshold)
iup_gui.SaveGlobal("DatesTolerance", IntDatesTolerance)
iup_gui.SaveGlobal("DatesMatched" , IntDatesMatched)
iup_gui.SaveGlobal("DatesOverlap" , IntDatesOverlap)
iup_gui.SaveGlobal("DatesMinimum" , IntDatesMinimum)
iup_gui.SaveGlobal("DatesDeduction", IntDatesDeduction)
iup_gui.SaveGlobal("PlacePartRight", IntPlacePartRight)
iup_gui.SaveGlobal("PlacePartOther", IntPlacePartOther)
iup_gui.SaveGlobal("PlaceSoundex" , IntPlaceSoundex)
iup_gui.SaveGlobal("EventMaximum" , IntEventMaximum)
iup_gui.SaveGlobal("BoostedBirth" , IntBoostedBirth) -- V3.8
iup_gui.SaveGlobal("BoostedBapCh" , IntBoostedBapCh) -- V3.8
iup_gui.SaveGlobal("BoostedMarry" , IntBoostedMarry) -- V3.8
iup_gui.SaveGlobal("BoostedDeath" , IntBoostedDeath) -- V3.8
iup_gui.SaveGlobal("DatesTimespan" , IntDatesTimespan)
iup_gui.SaveGlobal("DatesVariance" , IntDatesVariance)
iup_gui.SaveGlobal("DatesPregnant" , IntDatesPregnant)
iup_gui.SaveGlobal("DatesPuberty" , IntDatesPuberty)
iup_gui.SaveGlobal("DatesMarriage" , IntDatesMarriage)
iup_gui.SaveGlobal("DatesFertile" , IntDatesFertile)
iup_gui.SaveGlobal("DatesLifespan" , IntDatesLifespan)
iup_gui.SaveGlobal("ChronMagnitude", IntChronMagnitude)
iup_gui.SaveGlobal("ChronTolerance", IntChronTolerance)
iup_gui.SaveGlobal("GenGapFamily" , IntGenGapFamily)
iup_gui.SaveGlobal("GenGapRelative", IntGenGapRelative)
iup_gui.SaveGlobal("GenGapDeduct" , IntGenGapDeduct)
iup_gui.SaveGlobal("GenderDeduct" , IntGenderDeduct)
iup_gui.SaveSettings() -- Includes "Main","Font" dialogues and "FontSet" & "History"
table.save(TblNonDups,StrNonDupsFile) -- Save Non-Duplicates table
end -- function SaveSettings
function GUI_MainDialogue() -- Graphical User Interface
local function setNameItem(tblName,intRelation,intItem) -- Set Name for Relation spin value item
tblName[intRelation] = intItem
SetNamesPoints(intRelation)
SaveSettings()
end -- local function setNameItem
-- Create the Find Duplicates controls with title/value
local tglLast = iup.toggle { Title="Tick to include only Individuals updated after Plugin last run Date :"; RightButton="YES"; }
local lblLast = iup.label { Title=StrLast; Size="80"; }
local tglDate = iup.toggle { Title="Tick to include only Individuals updated after this adjustable Date :"; RightButton="YES"; }
local lblDate = iup.label { Title=StrDate; Size="80"; }
local btnPick = iup.button { Title=" Click here to select any subset of Individuals to be included "; }
local lblPick = iup.label { Title="0 Records"; }
local lblLine = iup.label { Separator="HORIZONTAL"; }
local btnFind = iup.button { Expand="YES"; Title="Find any Duplicates constrained by the Included subset of Individuals chosen above"; }
local lblTime = iup.label { Title="Estimated run time to check Individuals for Duplicates is 99 min 99 sec"; }
local btnShow = iup.button { Title="Show the previous Result Set of Duplicates in Family Historian"; }
local tglDiag = iup.toggle { Title="Enable Diagnostic Mode :"; Value=StrDiag; RightButton="YES"; }
local lblNull = iup.label { Title=" "; }
local tglSpan = iup.toggle { Title="Including Timespan Dates :"; Value=StrSpan; RightButton="YES"; Active="NO"; }
-- Create the Omit Non-Duplicates controls with title/value
local lblResult = iup.label { Title="Result Set Entries"; }
local lstResult = iup.list { Value=""; VisibleLines=9; Multiple="YES"; }
local btnSetAll = iup.button { Title="Select All"; }
local btnSetNil = iup.button { Title="Select None"; }
local btnMovAll = iup.button { Title="Move All"; }
local btnMovSet = iup.button { Title="Move Selected"; }
local lblNonDup = iup.label { Title="Non-Duplicates List"; }
local lstNonDup = iup.list { Value=""; VisibleLines=9; Multiple="YES"; }
local btnSelAll = iup.button { Title="Select All"; }
local btnSelNil = iup.button { Title="Select None"; }
local btnDelAll = iup.button { Title="Erase List"; }
local btnDelSel = iup.button { Title="Erase Selected"; }
-- Create the Dialogue Common controls with title/value
local btnUpdates = iup.button { Title="Check for Updates"; } -- V3.9
local btnGetHelp = iup.button { Title="Help && Advice"; }
local btnDestroy = iup.button { Title="Close Plugin"; }
-- Create the Set Preferences controls with title/value
local tblSet = {} -- Table to hold all the Set Preference tab controls
local intTab = 0 -- Index to Tab number on Set Preference tab
local tblTab = {} -- Table of current Preference Tab number
local intRow = 0 -- Index to Row of controls on each Tab
local tblRow = {} -- Table of current Row of controls
for intTab = 1, 5 do -- Set the default attributes for up to 5 Tabs and 15 Rows of controls
tblSet[intTab] = {}
tblTab = tblSet[intTab]
for intRow = 1, 15 do -- Local function setControls() sets Font & FgColor & BgColor & Padding
tblTab[intRow] = {}
tblRow = tblTab[intRow]
tblRow.Heading = iup.label { Title=" "; Expand="HORIZONTAL"; Padding="x1"; }
if intTab == 2 then -- Defaults for Names Matching 2nd tab
if intRow == 1 then -- 1st Row has Relations titles, and Defaults button
local tblTitle = { "Individual "; "Father "; "Mother "; "Spouse "; "Child "; }
for intRel = IntIndivi, IntChild do
tblRow[intRel]=iup.label{ Title=tblTitle[intRel]; Alignment="ACENTER:ACENTER"; }
end
tblRow.Default = iup.button{ Title="Defaults "; Tip="Restore defaults below"; }
else -- Other Rows have Relations spin controls, and default integers & measurements
for intRel = IntIndivi, IntChild do
tblRow[intRel] = iup.text { Spin="YES"; Border="NO"; Alignment="ARIGHT"; RasterSize=90; ReadOnly="YES"; SpinAlign="RIGHT"; SpinValue=0; SpinInc=1; SpinMin=0; SpinMax=100; }
end
tblRow.Integer = iup.label { Title="9"; Expand="YES"; Alignment="ARIGHT:ATOP"; } -- V3.8
tblRow.Measure = iup.label { Title="Points"; }
tblRow.Default = iup.hbox { Homogeneous="YES"; tblRow.Integer; tblRow.Measure; Margin=0; }
end
tblRow.Overall = iup.hbox { Homogeneous="YES"; tblRow.Heading; tblRow[IntIndivi]; tblRow[IntFather]; tblRow[IntMother]; tblRow[IntSpouse]; tblRow[IntChild]; tblRow.Default; }
else -- Defaults for all except Names Matching 2nd tab
if intRow == 1 then -- 1st Row has bold title, Current Settings title, and Default Settings button
tblRow.Current = iup.label { Title=" Current Settings "; }
tblRow.Default = iup.button{ Title=" Default Settings "; Tip="Restore defaults below"; }
else -- Other Rows have Settings spin control, and default integers & measurements
tblRow.Current = iup.text { Spin="YES"; Border="NO"; Alignment="ARIGHT"; RasterSize=110; ReadOnly="YES"; SpinAlign="RIGHT"; SpinValue=0; SpinInc=1; SpinMin=0; SpinMax=100; }
tblRow.Integer = iup.label { Title="9"; Expand="YES"; Alignment="ARIGHT:ATOP"; } -- V3.8
tblRow.Measure = iup.label { Title=" Points "; }
tblRow.Default = iup.hbox { Homogeneous="YES"; tblRow.Integer; tblRow.Measure; Margin=0; }
end
tblRow.Overall = iup.hbox { Homogeneous="YES"; tblRow.Heading; tblRow.Current; tblRow.Default; }
end
end
end
intTab = intTab + 1 -- User Interface is 1st tab
tblTab = tblSet[intTab]
intRow = 1
local intInter = intTab -- Save tab number for Preferences tab control
local tblResultSetLim = tblTab[intRow]
tblResultSetLim.Heading.Title = "Result Set Limits" -- 1st row with Default Settings button
intRow = intRow + 1
local tblIndiScoreMin = tblTab[intRow] -- 2nd row
tblIndiScoreMin.Heading.Title = "Individual Threshold"
tblIndiScoreMin.Current.SpinMin = -100
tblIndiScoreMin.Current.SpinMax = 100
tblIndiScoreMin.Current.spin_cb = function(self,intItem) IntIndiScoreMin=intItem SaveSettings() end
tblIndiScoreMin.Integer.Title = tostring(IntIndiScoreMinDef)
intRow = intRow + 1
local tblLeastResults = tblTab[intRow] -- 3rd row
tblLeastResults.Heading.Title = "Results Minimum Score"
tblLeastResults.Current.SpinMin = -100
tblLeastResults.Current.SpinMax = 100
tblLeastResults.Current.spin_cb = function(self,intItem) IntLeastResults=intItem SaveSettings() end
tblLeastResults.Integer.Title = tostring(IntLeastResultsDef)
tblLeastResults.Measure.Title = " Point "
intRow = intRow + 1
local tblLimitResults = tblTab[intRow] -- 4th row
tblLimitResults.Heading.Title = "Results Maximum Rows"
tblLimitResults.Current.SpinMin = 20
tblLimitResults.Current.SpinMax = 500
tblLimitResults.Integer.Title = tostring(IntLimitResultsDef)
tblLimitResults.Measure.Title = " Rows "
intRow = intRow + 1
local tblPruneResults = tblTab[intRow] -- 5th row
tblPruneResults.Heading.Title = "Memory Conservation"
tblPruneResults.Current.SpinMin = 20
tblPruneResults.Current.SpinMax = 1000
tblPruneResults.Current.spin_cb = function(self,intItem) IntPruneResults=intItem SetEventPoints() end
tblPruneResults.Integer.Title = tostring(IntPruneResultsDef)
tblPruneResults.Measure.Title = " Entries "
intRow = intRow + 1
iup.Destroy(tblTab[intRow].Overall) -- 6th row replaces defaults with a horizontal separator
tblTab[intRow].Overall = iup.vbox { iup.hbox { Margin="8x15"; }; iup.label { Separator="HORIZONTAL"; }; Margin="1x1"; }
intRow = intRow + 1
local btnDefault = iup.button { Title="Restore GUI Defaults"; }
local btnSoundex = iup.button { Title="Erase Soundex Cache"; } -- 7th row replaces defaults with three buttons
local btnSetFont = iup.button { Title="Set Window Fonts"; }
iup.Destroy(tblTab[intRow].Overall)
tblTab[intRow].Overall = iup.hbox { btnDefault; btnSoundex; btnSetFont; Homogeneous="YES"; Margin="30x50"; Gap="10"; }
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Names Matching 2nd tab --
tblTab = tblSet[intTab]
intRow = 1
local intNames = intTab -- Save tab number for Preferences tab control
local tblNamesDefaults = tblTab[intRow]
tblNamesDefaults.Heading.Title = "Names" -- 1st row with Defaults button
intRow = intRow + 1
local tblLastNameRight = tblTab[intRow] -- 2nd row
tblLastNameRight.Heading.Title = "Last Right"
for intRel = IntIndivi, IntChild do
tblLastNameRight[intRel].spin_cb = function(self,intItem) setNameItem(TblLastNameRight,intRel,intItem) end
end
tblLastNameRight.Integer.Title = tostring(IntLastNameRightDef)
intRow = intRow + 1
local tblForeNameRight = tblTab[intRow] -- 3rd row
tblForeNameRight.Heading.Title = "Fore Right"
for intRel = IntIndivi, IntChild do
tblForeNameRight[intRel].spin_cb = function(self,intItem) setNameItem(TblForeNameRight,intRel,intItem) end
end
tblForeNameRight.Integer.Title = tostring(IntForeNameRightDef)
intRow = intRow + 1
local tblForeNameOther = tblTab[intRow] -- 4th row
tblForeNameOther.Heading.Title = "Fore Other"
for intRel = IntIndivi, IntChild do
tblForeNameOther[intRel].SpinMin = -100
tblForeNameOther[intRel].spin_cb = function(self,intItem) setNameItem(TblForeNameOther,intRel,intItem) end
end
tblForeNameOther.Integer.Title = tostring(IntForeNameOtherDef)
intRow = intRow + 1
local tblNameSoundex = tblTab[intRow] -- 5th row
tblNameSoundex.Heading.Title = "Soundex"
for intRel = IntIndivi, IntChild do
tblNameSoundex[intRel].spin_cb = function(self,intItem) setNameItem(TblNameSoundex,intRel,intItem) end
end
tblNameSoundex.Integer.Title = tostring(IntNameSoundexDef)
intRow = intRow + 1
local tblNameLastWrong = tblTab[intRow] -- 6th row
tblNameLastWrong.Heading.Title = "Last Wrong"
for intRel = IntIndivi, IntChild do
tblNameLastWrong[intRel].SpinMin = -100
tblNameLastWrong[intRel].SpinMax = 0
tblNameLastWrong[intRel].spin_cb = function(self,intItem) setNameItem(TblNameLastWrong,intRel,intItem) end
end
tblNameLastWrong.Integer.Title = tostring(IntNameLastWrongDef)
intRow = intRow + 1
local tblNameMinimum = tblTab[intRow] -- 7th row
tblNameMinimum.Heading.Title = "Minimum"
tblNameMinimum[IntIndivi].visible = "NO"
for intRel = IntIndivi, IntChild do
tblNameMinimum[intRel].spin_cb = function(self,intItem) setNameItem(TblNameMinimum,intRel,intItem) end
end
tblNameMinimum.Integer.Title = tostring(IntNameMinimumDef)
tblNameMinimum.Measure.Title = "Point "
intRow = intRow + 1
local tblNameDeduction = tblTab[intRow] -- 8th row
tblNameDeduction.Heading.Title = "Deduction"
tblNameDeduction[IntIndivi].visible= "NO"
for intRel = IntFather, IntChild do
tblNameDeduction[intRel].SpinMin = -100
tblNameDeduction[intRel].SpinMax = 0
tblNameDeduction[intRel].spin_cb = function(self,intItem) setNameItem(TblNameDeduction,intRel,intItem) end
end
tblNameDeduction.Integer.Title = tostring(IntNameDeductionDef)
intRow = intRow + 1
local tblNameMaximum = tblTab[intRow] -- 9th row
tblNameMaximum.Heading.Title = "Maximum"
for intRel = IntIndivi, IntChild do
tblNameMaximum[intRel].spin_cb = function(self,intItem) setNameItem(TblNameMaximum,intRel,intItem) end
end
tblNameMaximum.Integer.Title = tostring(IntNameMaximumDef)
intRow = intRow + 1
local tblNameThreshold = tblTab[intRow] -- 10th row
tblNameThreshold.Heading.Title = "Threshold"
for intRel = IntIndivi, IntChild do
tblNameThreshold[intRel].spin_cb = function(self,intItem) setNameItem(TblNameThreshold,intRel,intItem) end
end
tblNameThreshold.Integer.Title = tostring(IntNameThresholdDef)
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Event Matching 3rd tab --
tblTab = tblSet[intTab]
intRow = 1
local intEvent = intTab -- Save tab number for Preferences tab control
local tblEventDefault = tblTab[intRow]
tblEventDefault.Heading.Title = "Events" -- 1st row with Default Settings button
intRow = intRow + 1
local tblDatesTolerance = tblTab[intRow] -- 2nd row
tblDatesTolerance.Heading.Title = "Dates Tolerance"
tblDatesTolerance.Current.SpinMax = 200
tblDatesTolerance.Current.spin_cb = function(self,intItem) IntDatesTolerance=intItem SetEventPoints() end
tblDatesTolerance.Integer.Title = StrPlusMinus..tostring(IntDatesToleranceDef)
tblDatesTolerance.Measure.Title = " Days "
intRow = intRow + 1
local tblDatesMatched = tblTab[intRow] -- 3rd row
tblDatesMatched.Heading.Title = "Dates Matched"
tblDatesMatched.Current.spin_cb = function(self,intItem) IntDatesMatched=intItem SetEventPoints() end
tblDatesMatched.Integer.Title = tostring(IntDatesMatchedDef)
intRow = intRow + 1
local tblDatesOverlap = tblTab[intRow] -- 4th row
tblDatesOverlap.Heading.Title = "Dates Overlap"
tblDatesOverlap.Current.spin_cb = function(self,intItem) IntDatesOverlap=intItem SetEventPoints() end
tblDatesOverlap.Integer.Title = tostring(IntDatesOverlapDef)
intRow = intRow + 1
local tblDatesMinimum = tblTab[intRow] -- 5th row
tblDatesMinimum.Heading.Title = "Dates Minimum"
tblDatesMinimum.Current.spin_cb = function(self,intItem) IntDatesMinimum=intItem SetEventPoints() end
tblDatesMinimum.Integer.Title = tostring(IntDatesMinimumDef)
tblDatesMinimum.Measure.Title = " Point "
intRow = intRow + 1
local tblDatesDeduction = tblTab[intRow] -- 6th row
tblDatesDeduction.Heading.Title = "Dates Deduction"
tblDatesDeduction.Current.SpinMin = -100
tblDatesDeduction.Current.SpinMax = 0
tblDatesDeduction.Current.spin_cb = function(self,intItem) IntDatesDeduction=intItem SetEventPoints() end
tblDatesDeduction.Integer.Title = tostring(IntDatesDeductionDef)
intRow = intRow + 1
local tblPlacePartRight = tblTab[intRow] -- 7th row
tblPlacePartRight.Heading.Title = "Place Part Right"
tblPlacePartRight.Current.spin_cb = function(self,intItem) IntPlacePartRight=intItem SetEventPoints() end
tblPlacePartRight.Integer.Title = tostring(IntPlacePartRightDef)
intRow = intRow + 1
local tblPlacePartOther = tblTab[intRow] -- 8th row
tblPlacePartOther.Heading.Title = "Place Part Other"
tblPlacePartOther.Current.spin_cb = function(self,intItem) IntPlacePartOther=intItem SetEventPoints() end
tblPlacePartOther.Integer.Title = tostring(IntPlacePartOtherDef)
intRow = intRow + 1
local tblPlaceSoundex = tblTab[intRow] -- 9th row
tblPlaceSoundex.Heading.Title = "Place Part Soundex"
tblPlaceSoundex.Current.spin_cb = function(self,intItem) IntPlaceSoundex=intItem SetEventPoints() end
tblPlaceSoundex.Integer.Title = tostring(IntPlaceSoundexDef)
tblPlaceSoundex.Measure.Title = " Point "
intRow = intRow + 1
local tblEventMaximum = tblTab[intRow] -- 10th row
tblEventMaximum.Heading.Title = "Event Maximum"
tblEventMaximum.Current.spin_cb = function(self,intItem) IntEventMaximum=intItem SetEventPoints() end
tblEventMaximum.Integer.Title = tostring(IntEventMaximumDef)
intRow = intRow + 1
local tblBoostedBirth = tblTab[intRow] -- 11th row -- V3.8
tblBoostedBirth.Heading.Title = "Boost Birth Events"
tblBoostedBirth.Current.spin_cb = function(self,intItem) IntBoostedBirth=intItem SetEventPoints() end
tblBoostedBirth.Integer.Title = tostring(IntBoostedBirthDef)
tblBoostedBirth.Measure.Title = " Times "
intRow = intRow + 1
local tblBoostedBapCh = tblTab[intRow] -- 12th row -- V3.8
tblBoostedBapCh.Heading.Title = "Boost Baptism/Christening"
tblBoostedBapCh.Current.spin_cb = function(self,intItem) IntBoostedBapCh=intItem SetEventPoints() end
tblBoostedBapCh.Integer.Title = tostring(IntBoostedBapChDef)
tblBoostedBapCh.Measure.Title = " Times "
intRow = intRow + 1
local tblBoostedMarry = tblTab[intRow] -- 13th row -- V3.8
tblBoostedMarry.Heading.Title = "Boost Marriage Events"
tblBoostedMarry.Current.spin_cb = function(self,intItem) IntBoostedMarry=intItem SetEventPoints() end
tblBoostedMarry.Integer.Title = tostring(IntBoostedMarryDef)
tblBoostedMarry.Measure.Title = " Times "
intRow = intRow + 1
local tblBoostedDeath = tblTab[intRow] -- 14th row -- V3.8
tblBoostedDeath.Heading.Title = "Boost Death/Burial/Cremate"
tblBoostedDeath.Current.spin_cb = function(self,intItem) IntBoostedDeath=intItem SetEventPoints() end
tblBoostedDeath.Integer.Title = tostring(IntBoostedDeathDef)
tblBoostedDeath.Measure.Title = " Times "
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Date Chronology 4th tab --
tblTab = tblSet[intTab]
intRow = 1
local intChron = intTab -- Save tab number for Preferences tab control
local tblChronDefault = tblTab[intRow]
tblChronDefault.Heading.Title = "Dates" -- 1st row with Default Settings button
intRow = intRow + 1
local tblDatesTimespan = tblTab[intRow] -- 2nd row
tblDatesTimespan.Heading.Title = "Dates Timespan"
tblDatesTimespan.Current.SpinMax = 200
tblDatesTimespan.Current.spin_cb = function(self,intItem) IntDatesTimespan=intItem SetChronology() end
tblDatesTimespan.Integer.Title = StrPlusMinus..tostring(IntDatesTimespanDef)
tblDatesTimespan.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesVariance = tblTab[intRow] -- 3rd row
tblDatesVariance.Heading.Title = "Dates Variance"
tblDatesVariance.Current.spin_cb = function(self,intItem) IntDatesVariance=intItem SetChronology() end
tblDatesVariance.Integer.Title = StrPlusMinus..tostring(IntDatesVarianceDef)
tblDatesVariance.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesPregnant = tblTab[intRow] -- 4th row
tblDatesPregnant.Heading.Title = "Pregnancy Duration"
tblDatesPregnant.Current.SpinMin = 6
tblDatesPregnant.Current.SpinMax = 12
tblDatesPregnant.Current.spin_cb = function(self,intItem) IntDatesPregnant=intItem SetChronology() end
tblDatesPregnant.Integer.Title = tostring(IntDatesPregnantDef)
tblDatesPregnant.Measure.Title = " Months "
intRow = intRow + 1
local tblDatesPuberty = tblTab[intRow] -- 5th row
tblDatesPuberty.Heading.Title = "Min Puberty Age"
tblDatesPuberty.Current.SpinMin = 10
tblDatesPuberty.Current.SpinMax = 20
tblDatesPuberty.Current.spin_cb = function(self,intItem) IntDatesPuberty=intItem SetChronology() end
tblDatesPuberty.Integer.Title = tostring(IntDatesPubertyDef)
tblDatesPuberty.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesMarriage = tblTab[intRow] -- 6th row
tblDatesMarriage.Heading.Title = "Min Marriage Age"
tblDatesMarriage.Current.SpinMin = 10
tblDatesMarriage.Current.SpinMax = 20
tblDatesMarriage.Current.spin_cb = function(self,intItem) IntDatesMarriage=intItem SetChronology() end
tblDatesMarriage.Integer.Title = tostring(IntDatesMarriageDef)
tblDatesMarriage.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesFertile = tblTab[intRow] -- 7th row
tblDatesFertile.Heading.Title = "Max Fertile Age"
tblDatesFertile.Current.SpinMin = 40
tblDatesFertile.Current.SpinMax = 80
tblDatesFertile.Current.spin_cb = function(self,intItem) IntDatesFertile=intItem SetChronology() end
tblDatesFertile.Integer.Title = tostring(IntDatesFertileDef)
tblDatesFertile.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesLifespan = tblTab[intRow] -- 8th row
tblDatesLifespan.Heading.Title = "Max Lifespan Age"
tblDatesLifespan.Current.SpinMin = 60
tblDatesLifespan.Current.SpinMax = 140
tblDatesLifespan.Current.spin_cb = function(self,intItem) IntDatesLifespan=intItem SetChronology() end
tblDatesLifespan.Integer.Title = tostring(IntDatesLifespanDef)
tblDatesLifespan.Measure.Title = " Years "
intRow = intRow + 1
local tblChronMagnitude = tblTab[intRow] -- 9th row
tblChronMagnitude.Heading.Title = "Chron Magnitude"
tblChronMagnitude.Current.SpinMin = 1
tblChronMagnitude.Current.SpinMax = 120
local txtChMag = tblChronMagnitude.Current
tblChronMagnitude.Integer.Title = tostring(IntChronMagnitudeDef)
tblChronMagnitude.Measure.Title = " Months "
intRow = intRow + 1
local tblChronTolerance = tblTab[intRow] -- 10th row
tblChronTolerance.Heading.Title = "Chron Tolerance"
tblChronTolerance.Current.SpinMin = -100
tblChronTolerance.Current.SpinMax = 0
tblChronTolerance.Current.spin_cb = function(self,intItem) IntChronTolerance=intItem SetChronology() end
tblChronTolerance.Integer.Title = tostring(IntChronToleranceDef)
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Family & Gender 5th tab --
tblTab = tblSet[intTab]
intRow = 1
local intOther = intTab -- Save tab number for Preferences tab control
local tblOtherDefault = tblTab[intRow]
tblOtherDefault.Heading.Title = "Generation Gap" -- 1st row with Default Settings button
intRow = intRow + 1
local tblGenGapFamily = tblTab[intRow] -- 2nd row
tblGenGapFamily.Heading.Title = "Family Generations"
tblGenGapFamily.Current.SpinMax = 10
tblGenGapFamily.Current.spin_cb = function(self,intItem) IntGenGapFamily=intItem SetGenerations() end
tblGenGapFamily.Integer.Title = tostring(IntGenGapFamilyDef)
tblGenGapFamily.Measure.Title = " Gen Gap "
intRow = intRow + 1
local tblGenGapRelative = tblTab[intRow] -- 3rd row
tblGenGapRelative.Heading.Title = "Relatives Generations"
tblGenGapRelative.Current.SpinMax = 10
tblGenGapRelative.Current.spin_cb = function(self,intItem) IntGenGapRelative=intItem SetGenerations() end
tblGenGapRelative.Integer.Title = tostring(IntGenGapRelativeDef)
tblGenGapRelative.Measure.Title = " Gen Gap "
intRow = intRow + 1
local tblGenGapDeduct = tblTab[intRow] -- 4th row
tblGenGapDeduct.Heading.Title = "Relatives Deduction"
tblGenGapDeduct.Current.SpinMin = -100
tblGenGapDeduct.Current.SpinMax = 0
tblGenGapDeduct.Current.spin_cb = function(self,intItem) IntGenGapDeduct=intItem SetGenerations() end
tblGenGapDeduct.Integer.Title = tostring(IntGenGapDeductDef)
intRow = intRow + 1
iup.Destroy(tblTab[intRow].Overall) -- 5th row replaces defaults with horizontal separator
tblTab[intRow].Overall = iup.vbox { iup.hbox { Margin="8x8"; }; iup.label { Separator="HORIZONTAL"; }; iup.hbox { Margin="8x8" }; Margin="1x1"; }
intRow = intRow + 1
local tblGenderMismatch = tblTab[intRow] -- 6th row
iup.Destroy(tblGenderMismatch.Heading)
iup.Destroy(tblGenderMismatch.Current)
iup.Destroy(tblGenderMismatch.Default)
tblGenderMismatch.Heading = iup.label { Title="Gender Mismatch"; }
tblGenderMismatch.Overall = iup.hbox { Homogeneous="YES"; tblGenderMismatch.Heading; iup.label { Title=" Current Settings "; Expand="YES"; }; iup.label { Title=" " }; }
intRow = intRow + 1
local tblGenderDeduct = tblTab[intRow] -- 7th row
tblGenderDeduct.Heading.Title = "Gender Deduction"
tblGenderDeduct.Current.SpinMin = -100
tblGenderDeduct.Current.SpinMax = 0
tblGenderDeduct.Current.spin_cb = function(self,intItem) IntGenderDeduct=intItem SaveSettings() end
tblGenderDeduct.Integer.Title = tostring(IntGenderDeductDef)
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Signal end of table of Preferences tab controls
tblSet[intTab] = nil
-- Create the Find Duplicates tab layout
local vboxFind = iup.vbox { Gap="8"; Margin="8x8";
iup.hbox { Margin="10x0"; iup.label{Expand="YES";}; tglLast; lblLast; };
iup.hbox { Margin="10x0"; iup.label{Expand="YES";}; tglDate; lblDate; };
iup.hbox { Margin="10x0"; btnPick; lblPick; };
iup.hbox { Margin="10x20";lblLine; };
iup.hbox { Margin="10x0"; btnFind; };
iup.hbox { Margin="10x0"; lblTime; };
iup.hbox { Margin="10x0"; btnShow; };
iup.hbox { Margin="10x0"; iup.label{Expand="YES";}; tglDiag; iup.label{Expand="YES";}; tglSpan; iup.label {Expand="YES";}; };
}
-- Create the Omit Non-Duplicates tab layout
local vboxOmit = iup.vbox { Gap="4"; Margin="4x4";
iup.vbox { Margin="10x0";
lblResult; lstResult;
iup.hbox { Margin="0x0"; btnSetAll; btnSetNil; btnMovAll; btnMovSet; Homogeneous="YES"; Expand="HORIZONTAL"; };
lblNonDup; lstNonDup;
iup.hbox { Margin="0x0"; btnSelAll; btnSelNil; btnDelAll; btnDelSel; Homogeneous="YES"; Expand="HORIZONTAL"; };
};
}
-- Create the Set Preferences tab layout
local tblPref = {}
for intTab = 1, #tblSet do
tblTab = tblSet[intTab]
if tblTab == nil then break end
local strMargin = "40x1"
if intTab == intNames then strMargin = "2x1" end
tblPref[intTab] = iup.vbox { Gap="8"; Margin=strMargin; }
for intRow = 1, #tblTab do
if tblTab[intRow] == nil then break end
iup.Append( tblPref[intTab], tblTab[intRow].Overall )
end
end
local tabPref = iup.tabs {
tblPref[intInter]; TabTitle0=" User Interface ";
tblPref[intNames]; TabTitle1=" Names Matching ";
tblPref[intEvent]; TabTitle2=" Event Matching ";
tblPref[intChron]; TabTitle3=" Date Chronology ";
tblPref[intOther]; TabTitle4=" Family && Gender ";
}
local vboxPref = iup.vbox { Gap="10";
tabPref;
}
-- Create the Tab controls layout
local tabCont = iup.tabs {
vboxFind; TabTitle0=" Find Duplicates ";
vboxOmit; TabTitle1="Omit Non-Duplicates";
vboxPref; TabTitle2=" Set Preferences ";
}
-- Combine all the above controls
local allCont = iup.vbox {
tabCont;
iup.hbox { btnUpdates; btnGetHelp; btnDestroy; Homogeneous="YES"; Margin="90x10"; Gap="10"; Expand="HORIZONTAL"; } -- V3.9
}
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local dialogMain = iup.dialog { Title=iup_gui.Plugin..iup_gui.Version;
allCont;
}
-- Local Variables and Functions
local intTotal = 0 -- Total number of Idividual Records
local intPick = 0 -- Picked number of Individual Records
local tblIndi = { } -- User selection of Individual Records
local intDate = 0 -- Date threshold for last Updated value
local tblResults = { } -- Results List for Omit Non-Duplicates tab, and Show previous Result Set button
local intTabPosn = 0
local tblControls = { } -- GUI control attributes used by doSetFont
local function setMonospacedFont() -- Monospaced font "Consolas, " or "Lucida Console, " or "Lucida Sans Typewriter, " or "DejaVu Sans Mono, "
local strFont = iup_gui.FontBody:gsub(".-, ","Lucida Console, ")
lstResult.Font = strFont
lstNonDup.Font = strFont
end -- local function setMonospacedFont
local function setControls() -- Set control attributes mainly for Preference tab Font & FgColor & BgColor & Padding
local function setAttribs(iupControl,intRow) -- pcall(setAttribs,iupControl,intRow) prevents missing control handle errors propagating
if intRow == 1 then
if iupControl.Title:match("Default") then
iupControl.FgColor = iup_gui.Safe -- Set all Default button attributes
iupControl.Expand = "HORIZONTAL"
iupControl.Padding = "10x2"
else
iupControl.Expand = "YES" -- Set all Spin heading attributes
iupControl.Padding = "x3"
end
elseif iupControl.Spin == "YES" then
iupControl.Font = iup_gui.FontHead -- Set all Spin control attributes
iupControl.FgColor = iup_gui.Safe
iupControl.BgColor = iup_gui.Smoke
iupControl.Padding = "20"
iupControl.Size = "42"
end
end -- local function setAttribs()
if fhGetAppVersion() > 6 then -- FH V7 IUP 3.28 -- V3.8
tabPref.TabPadding = "8x4"
tabCont.TabPadding = "8x4"
else -- FH V6 IUP 3.11 -- V3.8
tabPref.Padding = "8x8"
tabCont.Padding = "8x8"
end
for i, iupControl in ipairs({ tblResultSetLim.Heading; tblNamesDefaults.Heading; tblEventDefault.Heading; tblChronDefault.Heading; tblOtherDefault.Heading; tblGenderMismatch.Heading; }) do
iupControl.Font = iup_gui.FontHead
iupControl.FgColor = iup_gui.Head -- Set top left Header attributes
iupControl.Expand = "YES"
iupControl.Padding = "x4"
end
for i, iupControl in ipairs({ tblPref[intInter]; tblPref[intNames]; tblPref[intEvent]; tblPref[intChron]; tblPref[intOther]; }) do
iupControl.Font = iup_gui.FontBody
iupControl.FgColor = iup_gui.Body -- Set each Preference tab default attributes
end
for intTab = 1, #tblSet do -- Reset the attributes of Defaults buttons and Spin headings/controls
tblTab = tblSet[intTab]
if tblTab == nil then break end
for intRow = 1, #tblTab do
tblRow = tblTab[intRow]
if tblRow == nil then break end
if intRow == 1 then
pcall(setAttribs,tblRow.Default,intRow) -- pcall for Defaults button prevents missing handle errors propagating
end
if intTab == intNames then
for intRel = IntIndivi, IntChild do
pcall(setAttribs,tblRow[intRel],intRow) -- pcall for Spin heading/control prevents missing handle errors propagating
end
else
pcall(setAttribs,tblRow.Current,intRow) -- pcall for Spin heading/control prevents missing handle errors propagating
end
end
end
setMonospacedFont()
end -- local function setControls
local function doResetRecords() -- Reset all Records to excluded
intTotal = 0
local ptrIndi = fhNewItemPtr()
ptrIndi:MoveToFirstRecord("INDI")
while ptrIndi:IsNotNull() do
local intRecId = fhGetRecordId(ptrIndi)
if not TblData[intRecId] then TblData[intRecId] = {} end -- Create table of Data entries per Record Id
TblData[intRecId].Chosen = false -- Flag all Records as excluded
intTotal = intTotal + 1 -- Count the Total number of Records
ptrIndi:MoveNext()
end
end -- local function doResetRecords
local function intChosenRecord(ptrIndi) -- Check if Record last Updated after chosen Date
local intRecId = fhGetRecordId(ptrIndi)
if ( fhCallBuiltInFunction("DayNumber",fhCallBuiltInFunction("LastUpdated",ptrIndi)) or 999999 ) >= intDate then -- 0 => 999999 to allow undated records -- V3.7
TblData[intRecId].Chosen = true -- Flag the chosen Records to include
return 1
end
TblData[intRecId].Chosen = false -- Flag the other Records to exclude
return 0
end -- local function intChosenRecord
local function setEstimatedTime() -- Set estimated run time based on chosen & total records
local strTime = "less than a few minutes"
local intScale = 60000000
if tglDiag.Value == "ON" then intScale = intScale / 20 end -- Lengthen estimate in Diagnostic Mode
local intMins = math.floor( intPick * intTotal / intScale )
if intMins <= 0 then strTime = "only a few seconds" end
if intMins >= 2 then strTime = "from "..intMins.." minutes to "..(intMins*4).." minutes" end
lblTime.Title = "Estimated run time to check for Duplicates is "..strTime
end -- local function setEstimatedTime
local function doPickRecords() -- Pick chosen Individual Records
intPick = 0
if #tblIndi == 0 then -- No selection
local ptrIndi = fhNewItemPtr()
ptrIndi:MoveToFirstRecord("INDI")
while ptrIndi:IsNotNull() do
intPick = intPick + intChosenRecord(ptrIndi) -- So check all the Records
ptrIndi:MoveNext()
end
else
for intIndi = 1, #tblIndi do
intPick = intPick + intChosenRecord(tblIndi[intIndi]) -- Check just selected Records
end
end
local strRecords = " Records Chosen"
if intPick == 1 then strRecords = strRecords:gsub("s "," ") end
lblPick.Title = intPick..strRecords
setEstimatedTime()
if intPick == 0 then btnFind.Active = "NO" else btnFind.Active = "YES" end
return intPick
end -- local function doPickRecords
local function setDateValue() -- Set Date Value and toggle ticks
local datDate = fhNewDate(9999)
if StrTick == "ON" and StrLast:match("^%d") then -- Use the Plugin Last run Date if it exists
tglLast.Value = "ON"
tglDate.Value = "OFF"
lblLast.Active = "YES" -- Set mode of toggles & Dates
lblDate.Active = "NO"
datDate:SetValueAsText(StrLast)
intDate = fhCallBuiltInFunction("DayNumber",datDate:GetDatePt1()) or 999999
else
StrTick = "OFF" -- Use the adjustable Date
tglLast.Value = "OFF"
tglDate.Value = "ON"
lblLast.Active = "NO" -- Set mode of toggles & Dates
lblDate.Active = "YES"
local strDate = lblDate.Title
datDate:SetValueAsText(strDate) -- Check that Date has valid format
intDate = general.GetDayNumber(datDate:GetDatePt1())
if intDate == 0 then
iup_gui.MemoDialogue("\n Unrecognised Date \n "..strDate.." \n")
intDate = 999999
else
StrDate = strDate
SaveSettings() -- Save the adjustable Date
end
--[==[
if datDate:SetValueAsText(strDate) then -- Check that Date has valid format
intDate = fhCallBuiltInFunction("DayNumber",datDate:GetDatePt1())
else
intDate = nil
end
if intDate then
StrDate = strDate
SaveSettings() -- Save the adjustable Date
else
iup_gui.MemoDialogue("\n Unrecognised Date \n "..strDate.." \n")
intDate = 999999
end
--]==]
end
end -- local function setDateValue
local function getEntry(intRecId) -- From Rec Id get Name & Format Size for Entry
local ptrName = fhNewItemPtr()
ptrName:MoveToRecordById("INDI",intRecId or 0)
local strName = fhGetDisplayText(ptrName)
local intSize = strName:length() -- Name length in characters
if string.encoding() == "UTF-8" then
while intSize > 30 do -- Remove trailing UTF-8 chars beyond 30th
intSize = intSize - 1
strName = strName:gsub("[%z\1-\127\194-\244][\128-\191]*$","")
end
end
return intRecId,strName,( 30 + strName:len() - intSize ) -- Format size adjusted for UTF-8 chars
end -- local function getEntry
local function strFormatResult(tblEntry) -- Format a pair of Duplicate Records
if not tblEntry then return " " end
local intRecIdA,strNameA,intSizeA = getEntry(tblEntry.RecordIdA)
local intRecIdB,strNameB,intSizeB = getEntry(tblEntry.RecordIdB)
local strFormat = ("%6d %-{A}.{A}s%6d %-.{B}s"):gsub("{A}",intSizeA):gsub("{B}",intSizeB)
return string.format(strFormat,intRecIdA,strNameA,intRecIdB,strNameB)
end -- local function strFormatResult
local function doDisplayTables() -- Display both Results List and Non-Duplicates tables in Omit Non-Duplicates tab
setMonospacedFont()
local intValue = lstResult.Value
lstResult.removeitem = nil
for intEntry = 1, #tblResults do
lstResult[intEntry] = strFormatResult(tblResults[intEntry]) -- Results List candidate pairs
end
lstResult.Value = intValue
local intValue = lstNonDup.Value
lstNonDup.removeitem = nil
for intEntry = 1, #TblNonDups do
lstNonDup[intEntry] = strFormatResult(TblNonDups[intEntry]) -- Non-Duplicates list of pairs
end
lstNonDup.Value = intValue
end -- local function doDisplayTables
local function doLoadLists() -- Load the Result List excluding missing Records and Non-Duplicate pairs
if general.FlgFileExists(StrResultsFile) then
local tblResultsFile, strErr = table.load(StrResultsFile) -- Retrieve saved previous Result Set file
local intEntry = 1
for i, tblResultsFile in ipairs( tblResultsFile ) do
local intDataA = tblResultsFile.RecordIdA -- Get RecordIds of each Result Set pair in turn
local intDataB = tblResultsFile.RecordIdB
local ptrIndiA = fhNewItemPtr()
local ptrIndiB = fhNewItemPtr()
ptrIndiA:MoveToRecordById('INDI',intDataA) -- Do both Individual Records exist i.e. have not been Merged
ptrIndiB:MoveToRecordById('INDI',intDataB)
if ptrIndiA:IsNotNull() and ptrIndiB:IsNotNull() then -- Both the Individual Records do exist in GEDCOM
tblResults[intEntry] = tblResultsFile
for i, tblNonDups in ipairs( TblNonDups ) do -- Search Non-Duplicates table
if intDataA == tblNonDups.RecordIdA and intDataB == tblNonDups.RecordIdB then
tblResults[intEntry] = nil
intEntry = intEntry - 1 -- Exclude Non-Duplicate pairs
break
end
end
intEntry = intEntry + 1 -- Step onto next internal Results List entry
end
end
doDisplayTables()
if #TblNonDups > 0 then btnDelAll.Active = "YES" end
end
end -- local function doLoadLists
local function doTick(tblArg,intMode) -- Action for Last run Date & adjustable Date toggles
local intState = tblArg[2]
if intState == intMode then StrTick = "ON" else StrTick = "OFF" end
setDateValue()
doPickRecords() -- Pick and count the Records
SaveSettings()
end -- local function doTick
function lblDate:button_cb(iupButton,intPressed,intPosX,intPosY,strStatus)
if iupButton == iup.BUTTON1 and intPressed == 1 then -- Left mouse button is pressed within Date label
local datDate = fhNewDate(0000)
local isOK = datDate:SetValueAsText(StrDate)
datDate = fhPromptUserForDate(datDate) -- Prompt user with adjustable Date as default
if datDate
and datDate:GetType() == "Simple"
and datDate:GetSubtype() == "" then
lblDate.Title = datDate:GetDisplayText() -- New adjusted Date is plain simple
setDateValue()
doPickRecords() -- Pick and count the Records
else
iup_gui.MemoDialogue("\n The adjustable Date must be a Simple date and \n not a Period, Phrase, Range, or Quarter Date. \n")
end
end
end -- function lblDate:button_cb
local function doPick() -- Action for Pick button
dialogMain.Active = "NO"
tblIndi = fhPromptUserForRecordSel('INDI')
if #tblIndi > 0 then doResetRecords() end
doPickRecords() -- Pick and count the Records
dialogMain.bringfront = "YES"
dialogMain.Active = "YES"
end -- local function doPick
local function doFind() -- Action for Find any Duplicates button
local tglSpanActive = tglSpan.Active
tglSpan.Active = "NO"
dialogMain.Active = "NO"
if intPick > 0 then -- If any Records chosen, then run Find Duplicates, which returns true if any found and not stopped
if FindDuplicateRecords(intTotal,tglDiag.Value=="ON",tglSpan.Value=="ON") then
local dateToday = fhNewDate(0000)
dateToday:SetSimpleDate(fhCallBuiltInFunction("Today"))
StrLast = dateToday:GetDisplayText() -- Set date Today as last run date
SaveSettings()
return iup.CLOSE
else
local ptrIndi = fhNewItemPtr()
ptrIndi:MoveToFirstRecord("INDI")
while ptrIndi:IsNotNull() do -- Loop through every Individual Record
TblData[fhGetRecordId(ptrIndi)].Names = nil -- Clear individual records
ptrIndi:MoveNext()
end
end
end
dialogMain.bringfront = "YES"
dialogMain.Active = "YES"
tglSpan.Active = tglSpanActive
end -- local function doFind
local function doShow() -- Action for Show previous Result Set button
local tglSpanActive = tglSpan.Active
tglSpan.Active = "NO"
dialogMain.Active = "NO"
if general.FlgFileExists(StrResultsFile) then
doLoadLists() -- Load the Results List and display as Result Set
if DisplayResultSet(tblResults,tglDiag.Value=="ON",tglSpan.Value=="ON") then
return iup.CLOSE
end
end
dialogMain.bringfront = "YES"
dialogMain.Active = "YES"
tglSpan.Active = tglSpanActive
end -- local function doShow
local function doDiag() -- Action for Diagnostic toggle
StrDiag = tglDiag.Value
if StrDiag == "ON" then tglSpan.Active = "YES" end
if StrDiag == "OFF" then tglSpan.Active = "NO" end
setEstimatedTime() -- Increase run time estimate
--? SaveSettings() -- Enable StrDiag in doDefault/Load/SaveSettings() to make it sticky
end -- local function doDiag
local function doSpan() -- Action for Timespan toggle
StrSpan = tglSpan.Value
--? SaveSettings() -- Enable StrSpan in doDefault/Load/SaveSettings() to make it sticky
end -- local function doSpan
local function setButtons() -- Set Non-Duplicates tab buttons active or not
local function setButton(isMatch,btnName)
if isMatch then btnName.Active = "YES" else btnName.Active = "NO" end
end -- local function setButton
local strMove = lstResult.Value
setButton(strMove:match("%-"),btnSetAll) -- Need some unselected to enable Select All
setButton(strMove:match("%+"),btnSetNil) -- Need some selected to enable Select None
setButton(strMove:match("^."),btnMovAll) -- Need some entries to enable Move All
setButton(strMove:match("%+"),btnMovSet) -- Need some selected to enable Move Selected
local strDrop = lstNonDup.Value
setButton(strDrop:match("%-"),btnSelAll) -- Need some unselected to enable Select All
setButton(strDrop:match("%+"),btnSelNil) -- Need some selected to enable Select None
setButton(strDrop:match("^."),btnDelAll) -- Need some entries to enable Erase List
setButton(strDrop:match("%+"),btnDelSel) -- Need some selected to enable Erase Selected
end -- local function setButtons
local function doSelect(iupList,strChar) -- Select All or None of List entries for btnSetAll, btnSetNil, btnSelAll, btnSelNil
iupList.Value = string.rep(strChar,iupList.Value:len())
setButtons()
end -- local function doSelect
local function doChosen(strMatch) -- Move chosen Result Set entries to Non-Duplicates for btnMovAll & btnMovSet
local strMove = lstResult.Value
local intMove = strMove:len()
for intEntry = intMove, 1, -1 do
strChar = strMove:sub(intEntry,intEntry)
if strChar:match(strMatch) then
table.insert(TblNonDups,1,tblResults[intEntry]) -- Add to Non-Duplicates and remove from Results Set
table.remove(tblResults,intEntry)
end
end
doDisplayTables()
lstResult.Value = string.rep("-",lstResult.Value:len()) -- Clear all selections
setButtons()
end -- local function doChosen
local function doDelete(strMatch) -- Delete chosen Non-Duplicates entries for btnDelAll & btnDelSel
local strMove = lstNonDup.Value
local intMove = strMove:len()
local intButton = 1
if strMatch == "^." or strMove == string.rep("+",intMove) then -- If btnDelAll or all entries selected then prompt for approval
intButton = iup_gui.MemoDialogue("\n Continue to ERASE the entire Non-Duplicates list ? \n","Yes, Erase","No, Cancel")
end
if intButton == 1 then
for intEntry = intMove, 1, -1 do -- If approved then delete all matching Non-Duplicates entries
strChar = strMove:sub(intEntry,intEntry)
if strChar:match(strMatch) then
table.remove(TblNonDups,intEntry)
end
end
doLoadLists()
lstNonDup.Value = string.rep("-",lstNonDup.Value:len()) -- Clear all selections
setButtons()
end
end -- local function doDelete
local function doDefault() -- Handle the Restore GUI Defaults button on Set Preferences tab
ResetDefaultSettings()
--? tglDiag.Value = StrDiag -- Reset controls
--? tglSpan.Value = StrSpan
lblDate.Title = StrDate
setDateValue()
tblIndi = {}
doPickRecords() -- Pick and count the Records
iup_gui.ShowDialogue("Main")
iup_gui.DefaultDialogue() -- V3.9
SaveSettings() -- Save sticky data settings
end -- local function doDefault
local function doSoundex() -- Handle the Erase Soundex Cache button on Set Preferences tab
TblSoundex = { } -- Soundex dictionary codes cache of previously coded Names & Places
table.save(TblSoundex,StrSoundexFile)
end -- local function doSoundex
local function doSetFont() -- Handle the Set Window Font button on Set Preferences tab
btnSetFont.Active = "NO"
iup_gui.FontDialogue(tblControls)
SaveSettings() -- Save sticky data settings
btnSetFont.Active = "YES"
end -- local function doSetFont
function btnSetFont:button_cb(intButton,intPress) -- Action for mouse right-click on Set Window Fonts button
if intButton == iup.BUTTON3 and intPress == 0 then
iup_gui.BalloonToggle() -- Toggle tooltips Balloon mode
end
end -- function btnSetFont:button_cb
function tblLimitResults.Current:spin_cb(intItem) -- Call back for Result Set Maximum Rows spin control on Set Preferences tab
IntLimitResults = intItem
tblPruneResults.Current.SpinMin = intItem
intItem = intItem * 2
if IntPruneResults < intItem then
IntPruneResults = intItem
tblPruneResults.Current.SpinValue = intItem
end
SaveSettings() -- Save sticky data settings
end -- function tblLimitResults.Current:spin_cb
local function setResultSet() -- Set the Result Set spin values
tblIndiScoreMin.Current.SpinValue = IntIndiScoreMin
tblLeastResults.Current.SpinValue = IntLeastResults
tblLimitResults.Current.SpinValue = IntLimitResults
tblPruneResults.Current.SpinMin = IntLimitResults
tblPruneResults.Current.SpinValue = IntPruneResults
end -- local function setResultSet
function tblResultSetLim.Default:action() -- Action for Result Set Limits Default Settings button on Set Preferences tab
SetUserInterfaceDefaults()
setResultSet()
SaveSettings() -- Save sticky data settings
end -- function tblResultSetLim.Default:action
local function setNamesValues() -- Set the Names Matching spin Values
for intRelation = IntIndivi, IntChild do
tblLastNameRight[intRelation].SpinValue = TblLastNameRight[intRelation]
tblForeNameRight[intRelation].SpinValue = TblForeNameRight[intRelation]
tblForeNameOther[intRelation].SpinValue = TblForeNameOther[intRelation]
tblNameSoundex [intRelation].SpinValue = TblNameSoundex [intRelation]
tblNameLastWrong[intRelation].SpinValue = TblNameLastWrong[intRelation]
tblNameMinimum [intRelation].SpinValue = TblNameMinimum [intRelation]
tblNameDeduction[intRelation].SpinValue = TblNameDeduction[intRelation]
tblNameMaximum [intRelation].SpinValue = TblNameMaximum [intRelation]
tblNameThreshold[intRelation].SpinValue = TblNameThreshold[intRelation]
end
end -- local function setNamesValues
function tblNamesDefaults.Default:action() -- Action for Names Matching Default Settings button on Set Preferences tab
SetNamesMatchDefaults()
setNamesValues()
SaveSettings() -- Save sticky data settings
end -- function tblNamesDefaults.Default:action
local function setEventValues() -- Set the Event Matching spin Values
tblDatesTolerance.Current.SpinValue = IntDatesTolerance
tblDatesMatched .Current.SpinValue = IntDatesMatched
tblDatesOverlap .Current.SpinValue = IntDatesOverlap
tblDatesMinimum .Current.SpinValue = IntDatesMinimum
tblDatesDeduction.Current.SpinValue = IntDatesDeduction
tblPlacePartRight.Current.SpinValue = IntPlacePartRight
tblPlacePartOther.Current.SpinValue = IntPlacePartOther
tblPlaceSoundex .Current.SpinValue = IntPlaceSoundex
tblEventMaximum .Current.SpinValue = IntEventMaximum
tblBoostedBirth .Current.SpinValue = IntBoostedBirth -- V3.8
tblBoostedBapCh .Current.SpinValue = IntBoostedBapCh -- V3.8
tblBoostedMarry .Current.SpinValue = IntBoostedMarry -- V3.8
tblBoostedDeath .Current.SpinValue = IntBoostedDeath -- V3.8
end -- local function setEventValues
function tblEventDefault.Default:action() -- Action for Event Matching Default Settings button on Set Preferences tab
SetEventMatchDefaults()
setEventValues()
SetEventPoints() -- Update and Save sticky data settings
end -- function tblEventDefault.Default:action
local intSpinLo = 1
local intSpinHi = 6
function tblChronMagnitude.Current:spin_cb(intItem) -- Call back for Chronology Magnitude spin control on Set Preferences tab
local intSpininc = tonumber(txtChMag.SpinInc) -- Magnitudes = 1, 2, 3, 4, 6, 12, 18 and increments of intSpinHi=6 to 120 Months
if intSpininc > intSpinLo and intItem < 6 then
txtChMag.SpinInc = intSpinLo -- When value decreases below 6 set decrement = intSpinLo=1 and adjust value
intItem = 4
txtChMag.SpinValue = intItem + intSpininc
elseif intSpininc < intSpinHi and intItem > 4 then
txtChMag.SpinInc = intSpinHi -- When value increases above 4 set increment = intSpinHi=6 and adjust value
intItem = math.ceil( intItem / intSpinHi ) * intSpinHi
txtChMag.SpinValue = intItem - intSpininc
end
if intItem == 6 then txtChMag.SpinInc = 2 end -- Needed to allow spin decrement below 6 to reduce to 1
IntChronMagnitude = intItem
SetChronology() -- Update and Save sticky data settings
end -- function tblChronMagnitude.Current:spin_cb
local function setChronValues() -- Set the Date Chronology spin Values
tblDatesTimespan .Current.SpinValue = IntDatesTimespan
tblDatesVariance .Current.SpinValue = IntDatesVariance
tblDatesPregnant .Current.SpinValue = IntDatesPregnant
tblDatesPuberty .Current.SpinValue = IntDatesPuberty
tblDatesMarriage .Current.SpinValue = IntDatesMarriage
tblDatesFertile .Current.SpinValue = IntDatesFertile
tblDatesLifespan .Current.SpinValue = IntDatesLifespan
tblChronMagnitude.Current.SpinValue = IntChronMagnitude
tblChronTolerance.Current.SpinValue = IntChronTolerance
local intSpininc = 2
if IntChronMagnitude < 6 then intSpininc = intSpinLo end
if IntChronMagnitude > 6 then intSpininc = intSpinHi end
tblChronMagnitude.Current.SpinInc = intSpininc
end -- local function setChronValues
function tblChronDefault.Default:action() -- Action for Date Chronology Default Settings button on Set Preferences tab
SetChronologyDefaults()
setChronValues()
SetChronology() -- Update and Save sticky data settings
end -- function tblChronDefault.Default:action
local function setOtherValues() -- Set Family & Gender spin Values
tblGenGapFamily .Current.SpinValue = IntGenGapFamily
tblGenGapRelative.Current.SpinValue = IntGenGapRelative
tblGenGapDeduct .Current.SpinValue = IntGenGapDeduct
tblGenderDeduct .Current.SpinValue = IntGenderDeduct
end -- local function setOtherValues
function tblOtherDefault.Default:action() -- Action for Date Chronology Default Settings button on Set Preferences tab
SetOtherMatchDefaults()
setOtherValues()
SetGenerations() -- Update and Save sticky data settings
end -- function tblOtherDefault.Default:action
function btnUpdates:action() -- Action for Check for Updates button -- V3.9
iup_gui.CheckVersionInStore() -- Notify if later Version
end -- function btnUpdates:action
local function doExecute(strExecutable, strParameter) -- Invoke FH Shell Execute API -- V3.8
local function ReportError(strMessage)
iup_gui.WarnDialogue( "Shell Execute Error",
"ERROR: "..strMessage.." :\n"..strExecutable.."\n"..strParameter.."\n\n",
"OK" )
end -- local function ReportError
return general.DoExecute(strExecutable, strParameter, ReportError)
end -- local function doExecute
local strHelp = "https://pluginstore.family-historian.co.uk/page/help/find-duplicate-individuals"
local arrHelp = { "-find-duplicates-tab"; "-omit-non-duplicates-tab"; "-set-preferences-tab"; }
function btnGetHelp:action() -- Action for Help and Advice button according to current tab -- V3.8
local strPage = arrHelp[intTabPosn] or ""
doExecute( strHelp..strPage )
fhSleep(2000,500)
dialogMain.BringFront="YES"
end -- function btnGetHelp:action
function tabCont:tabchangepos_cb(intNew,intOld) -- Call back when Main tab position is changed
intTabPosn = intNew + 1 -- 31 July 2013
if intNew == 1 then
doLoadLists() -- Load the Omit Non-Duplicates tab
end
setButtons()
end -- function tabCont:tabchangepos_cb
-- Set other GUI control attributes
local strMiddle = "ACENTER:ACENTER"
tblControls = { { "Font"; "FgColor"; "Alignment"; "Padding"; "Tip"; "action"; {"TipBalloon";"Balloon";}; {"Expand";"YES";}; {"help_cb";function() iup_gui.HelpDialogue(intTabPosn) end;}; setControls; };
-- Find Duplicates tab
[vboxFind] = { "FontBody"; "Body"; "ARIGHT" ; };
[tglLast] = { "FontBody"; "Safe"; false ; false; "Include Individuals updated after Plugin last run Date" ; function(...) doTick({...},1) end; };
[lblLast] = { "FontBody"; "Body"; strMiddle; "10" ; "Date the Plugn was last run successfully" ; };
[tglDate] = { "FontBody"; "Safe"; false ; false; "Include Individuals updated after this adjustable Date" ; function(...) doTick({...},0) end; };
[lblDate] = { "FontBody"; "Safe"; strMiddle; "10" ; "User adjustable Date threshold for Individuals" ; };
[btnPick] = { "FontBody"; "Safe"; false ; "10" ; "Select any subset of Individuals to include" ; function() return doPick() end; };
[lblPick] = { "FontBody"; "Body"; strMiddle; "10" ; "Number of Individual records included" ; };
[btnFind] = { "FontBody"; "Safe"; false ; "10" ; "Find Duplicates from Included subset of Individuals" ; function() return doFind() end; };
[lblTime] = { "FontBody"; "Body"; strMiddle; "10" ; "Estimated run time to find Duplicates" ; };
[btnShow] = { "FontBody"; "Safe"; false ; "10" ; "Show previous Result Set without using Find again" ; function() return doShow() end; };
[tglDiag] = { "FontBody"; "Safe"; false ; false; "Enable Diagnostic Mode showing details in Result Set" ; function(...) doDiag({...}) end; };
[tglSpan] = { "FontBody"; "Safe"; false ; false; "Include Timespan Dates in the Result Set details" ; function(...) doSpan({...}) end; };
-- Omit Non-Duplicates tab
[vboxOmit] = { "FontBody"; "Body"; "ARIGHT" ; };
[lblResult] = { "FontHead"; "Head"; strMiddle; "0" ; "Result Set of possible duplicates" ; };
[lstResult] = { "FontBody"; "Body"; "ALEFT" ; "0" ; "Use Leftclick and Shft+Leftclick and Ctrl+Leftclick" ; function() setButtons() end; };
[btnSetAll] = { "FontBody"; "Safe"; false ; "10" ; "Select all of the Result Set entries" ; function() doSelect(lstResult,"+") end; };
[btnSetNil] = { "FontBody"; "Safe"; false ; "10" ; "Select none of the Result Set entries" ; function() doSelect(lstResult,"-") end; };
[btnMovAll] = { "FontBody"; "Safe"; false ; "10" ; "Move all Result Set entries to Non-Duplicates" ; function() doChosen("^.") end; };
[btnMovSet] = { "FontBody"; "Safe"; false ; "10" ; "Move selected Result Set entries to Non-Duplicates" ; function() doChosen("%+") end; };
[lblNonDup] = { "FontHead"; "Head"; strMiddle; "0" ; "List of Non-Duplicates to ignore" ; };
[lstNonDup] = { "FontBody"; "Body"; "ALEFT" ; "0" ; "Use Leftclick and Shft+Leftclick and Ctrl+Leftclick" ; function() setButtons() end; };
[btnSelAll] = { "FontBody"; "Safe"; false ; "10" ; "Select all of the Non-Duplicates list" ; function() doSelect(lstNonDup,"+") end; };
[btnSelNil] = { "FontBody"; "Safe"; false ; "10" ; "Select none of the Non-Duplicates list" ; function() doSelect(lstNonDup,"-") end; };
[btnDelAll] = { "FontBody"; "Safe"; false ; "10" ; "Erase all of the Non-Duplicates list" ; function() doDelete("^.") end; };
[btnDelSel] = { "FontBody"; "Safe"; false ; "10" ; "Erase selected Non-Duplicates list entries" ; function() doDelete("%+") end; };
-- Preferences tab
[tabPref] = { "FontHead"; "Head"; false ; false; "Select preference settings to adjust scoring" ; }; -- V3.8
[vboxPref] = { "FontBody"; "Body"; "ARIGHT" ; };
[btnDefault] = { "FontBody"; "Safe"; false ; "10" ; "Restore GUI defaults and Window sizes and positions" ; function() doDefault() end; };
[btnSoundex] = { "FontBody"; "Safe"; false ; "10" ; "Erase the Soundex Cache history file" ; function() doSoundex() end; };
[btnSetFont] = { "FontBody"; "Safe"; false ; "10" ; "Choose user interface window font styles" ; function() doSetFont() end; };
-- Dialogue Common controls
[tabCont] = { "FontHead"; "Head"; false ; false; "Find Duplicates or Omit Non-Duplicates or Set Preferences" ; }; -- V3.8
[allCont] = { "FontBody"; "Body"; "ARIGHT" ; };
[btnUpdates] = { "FontBody"; "Safe"; false ; "4" ; "Check for a later plugin version in the Plugin Store"; }; -- V3.9
[btnGetHelp] = { "FontBody"; "Safe"; false ; "4" ; "Obtain online Help and Advice from the Plugin Store" ; };
[btnDestroy] = { "FontBody"; "Risk"; false ; "4" ; "Close the plugin and keep all edits" ; function() return iup.CLOSE end; };
}
iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes
doResetRecords()
lblDate.Title = StrDate
setDateValue()
doPickRecords() -- Pick and count the Records
if not general.FlgFileExists(StrResultsFile) then btnShow.Active = "NO" end
setResultSet()
setNamesValues()
setEventValues()
setChronValues()
setOtherValues()
iup_gui.ShowDialogue("Main",dialogMain,btnDestroy,"Map") -- Map Main GUI Dialogue
iup_gui.RefreshDialogue("Main") -- Adjust GUI size after adding Preferences
iup_gui.ShowDialogue("Main") -- Display Main GUI Dialogue
end -- function GUI_MainDialogue
function NewSoundex(tblSoundex) -- Prototype for Soundex Calculator
-- See http://en.wikipedia.org/wiki/Soundex and http://creativyst.com/Doc/Articles/SoundEx1/SoundEx1.htm#SoundExAndCensus
-- This Soundex variant converts all characters to unaccented upper case ASCII,
-- and encodes 1st letter into its code number e.g. Gill (G400) & Jill (J400) are now both 2400.
local tblSoundex = tblSoundex or { } -- Soundex dictionary cache of previously coded Names
tblSoundex[""] = "0000" -- Seed with null string special case
local tblCodeNum = { -- Soundex code number table
A=0;E=0;I=0;O=0;U=0;Y=0;H=0; -- H=0;W=0; -- H & W are ignored after first character
B=1;F=1;P=1;V=1;W=1; -- H=1;W=1; -- W treated as B,F,P,V so BILL=WILL=1400
C=2;G=2;J=2;K=2;Q=2;S=2;X=2;Z=2;
D=3;T=3;
L=4;
M=5;N=5;
R=6;
}
local function getSoundex(strAnyName) -- Get the Soundex code for any Name
local strNewName = encoder.StrEncode_ASCII(strAnyName):upper() -- Convert to ASCII upper case characters
local strSoundex = tblCodeNum[strNewName:sub(1,1)] or "" -- Soundex starts with leading letter code number
local strLastNum = strSoundex -- Set initial Soundex code number
tblCodeNum.H = nil -- Ignore H after first character
tblCodeNum.W = nil -- Ignore W after first character
for i = 2, string.len(strNewName) do
local strCodeNum = tblCodeNum[strNewName:sub(i,i)] -- Step through Soundex code of each subsequent letter
if strCodeNum then
if strCodeNum > 0 and strCodeNum ~= strLastNum then -- Not a vowel nor same as Soundex preceeding code
strSoundex = strSoundex..strCodeNum -- So append Soundex code until 4 chars long
if string.len(strSoundex) == 4 then break end
end
strLastNum = strCodeNum -- Save as Soundex preceeding code, unless H or W
end
end
tblCodeNum.H = 0 -- Reinstate initial H code number
tblCodeNum.W = 1 -- Reinstate initial W code number
strSoundex = string.sub(strSoundex.."0000",1,4) -- Pad code with zeroes to 4 chars long
tblSoundex[strAnyName] = strSoundex -- Save code in cache for future quick lookup
return strSoundex
end -- local function getSoundex
return function(strAnyName) -- Convert a Name to Soundex
return tblSoundex[strAnyName] or getSoundex(strAnyName) -- If already in cache then return previous code else get new code
end -- anonymous function
end -- function NewSoundex
function NewNamesData() -- Prototype to Make Names & Soundex Dictionary per Individual
local tblNameLastRight = TblNameLastRight -- Duplicate globals as locals
local tblNameForeRight = TblNameForeRight
local tblNameForeOther = TblNameForeOther
local tblNameSoundex = TblNameSoundex
local intIndivi = IntIndivi
local intChild = IntChild
return function(ptrInd) -- Make Names & Soundex Dictionary per Individual
local tblName = {}
local ptrName = fhGetItemPtr(ptrInd,"~.NAME")
while ptrName:IsNotNull() do -- Loop through every NAME tag instance replacing punctuation such as - , . [ ] ( ) with a space
local strSurname = fhGetItemText(ptrName,"~:SURNAME")
local strSURNAME = " "..strSurname:upper():gsub(StrS,""):gsub(StrP," ").." " -- Strip spaces in Surnames, so "Van Dyke" becomes "VANDYKE", but "Smith-Jones" becomes "SMITH JONES"
strSurname = " "..strSurname:lower():gsub(StrP," ").." "
for intRef, strRef in ipairs({ "~:ADORNED_FULL"; "~.NICK"; "~._USED"; "~.FONE"; "~.ROMN"; }) do -- V3.8
local strName = fhGetItemText(ptrName,strRef):lower():gsub(StrP," ").." " -- Remove unnamed Names, ensure Forenames are lowercase, and Surname is uppercase
--# strName = strName:gsub("%[unnamed person%]",""):gsub(strSurname,strSURNAME) -- gsub() is faster than replace() and no magic symbols in surname by now?
strName = strName:gsub("%[unnamed person%]",""):replace(strSurname,strSURNAME) -- gsub() is faster than replace() and no magic symbols in surname by now?
for intName, strName in ipairs(strName:split(" ")) do -- Extract Names separated by space, and more than 2 chars long
if string.len(strName) > 2 and not tblName[strName] then -- Ensure replicated Names are skipped
if strName:match("[A-Z]") then
tblName["0"..strName] = tblNameLastRight -- Lastname gets most points, has "0" upper case ( default 7 total = 5 + 2 for Soundex )
else
tblName[intName..strName] = tblNameForeRight -- Forename in right position, has leading 1 - 9 ( default 6 total = 3 + 1 below + 2 for Soundex )
tblName[strName] = tblNameForeOther -- Forename in other position, is all lower case ( default 3 total = 1 + 2 for Soundex )
end
local strSoundex = StrSoundex(strName)
if not tblName[strSoundex] then
tblName[strSoundex] = tblNameSoundex -- Soundex match only points, has capital + digits ( default 2 total )
else
local tblSoundex = {} -- Different Name part but replicated Soundex, so points must accumulate to achieve correct total
for intRelation = intIndivi, intChild do
tblSoundex[intRelation] = tblName[strSoundex][intRelation] + tblNameSoundex[intRelation]
end
tblName[strSoundex] = tblSoundex
end
end
end
end
ptrName:MoveNext("SAME_TAG")
end
return tblName
end -- anonymous function
end -- function NewNamesData
function NewEventData() -- Prototype to Make Event Date Timespan & Place Parts per Individual
local intPartRight = IntPartRight -- Duplicate globals as locals
local intPartOther = IntPartOther
local intPlaceSoundex = IntPlaceSoundex
local tblLower = { Before=IntTimespanDays; To=IntTimespanDays; Approximate0=IntVarianceDays; Calculated0=IntVarianceDays; Estimated0=IntVarianceDays; }
local tblUpper = { After=IntTimespanDays; From=IntTimespanDays; Approximate0=IntVarianceDays; Calculated0=IntVarianceDays; Estimated0=IntVarianceDays; }
return function(ptrEvent) -- Make Event Date Timespan & Place Parts per Individual
local dateDate = fhGetValueAsDate(fhGetItemPtr(ptrEvent,"~.DATE"))
if dateDate:IsNull() then return nil end -- If no Event Date, then return nil
local pntLower = dateDate:GetDatePt1() -- Lower Date whether Type is Simple or Period or Range
--# local intLower = fhCallBuiltInFunction("DayNumber",pntLower) or 0 -- Lower Date with Month missing uses Jan, and with Day missing uses 1st
local intLower = general.GetDayNumber(pntLower) or 0 -- Lower Date with Month missing uses Jan, and with Day missing uses 1st
local pntUpper = dateDate:GetDatePt2()
--# local intUpper = fhCallBuiltInFunction("DayNumber",pntUpper)
local intUpper = general.GetDayNumber(pntUpper) or 0
--# if intUpper then -- Upper Date for Period(From-To) or Range(Between)=Quarter Date
if intUpper > 0 then -- Upper Date for Period(From-To) or Range(Between)=Quarter Date
if pntUpper:GetMonth() == 0 then intUpper = intUpper + 364 -- Upper Date with Month missing, so extend to end of Year
elseif pntUpper:GetDay() == 0 then intUpper = intUpper + 30 end -- Upper Date with Day missing, so extend to end of Month (could adjust according to Month?)
else -- No Upper Date for Simple(Approximate,Calculated,Extimated) or Period(From,To) or Range(After,Before)
intUpper = intLower -- So Upper Date = Lower Date and extend as above
if pntLower:GetMonth() == 0 then intUpper = intUpper + 364
elseif pntLower:GetDay() == 0 then intUpper = intUpper + 30 end
local strSubType = dateDate:GetSubtype() -- Subtype is Approximate, Calculated, Estimated, From, To, After, Before, or ""
if string.len(strSubType) > 7 then
strSubType = strSubType..pntLower:GetMonth() -- Simple(Approximate,Calculated,Estimated) Year Date needs Upper/Lower = +/- half Timespan
end
intLower = intLower - ( tblLower[strSubType] or 0 ) -- Period(To) or Range(Before) Date needs Lower = Lower - Timespan
intUpper = intUpper + ( tblUpper[strSubType] or 0 ) -- Period(From) or Range(After) Date needs Upper = Upper + Timespan
end
local tblPlace = {}
for _, strRef in ipairs({ "~.PLAC"; "~.PLAC.FONE"; "~.PLAC.ROMN"; }) do -- Cater for multiple FONE & ROMN variants -- V3.8
local ptrPlace = fhGetItemPtr(ptrEvent,strRef)
while ptrPlace:IsNotNull() do
for intPlace, strPlace in ipairs(fhGetValueAsText(ptrPlace):split(",")) do
strPlace = strPlace:lower():gsub(StrSP,"") -- Remove all spaces and punctuation from Place part and ensure lowercase
if string.length(strPlace) > 1 and not tblPlace[strPlace] then -- Ensure replicated Places & Soundex are eliminated, and part > 1 char
tblPlace[strPlace] = intPartOther -- Place part in other position, is all lower case ( default 2 total = 1 + 1 for Soundex )
tblPlace[intPlace..strPlace] = intPartRight -- Place part in right position, has leading digit ( default 3 total = 1 + 1 above + 1 for Soundex )
tblPlace[StrSoundex(strPlace)] = intPlaceSoundex -- Similar sounding Place part, has capital+digits ( default 1 total )
end
end
ptrPlace:MoveNext("SAME_TAG")
end
end
return TblMakeEvent(intLower,intUpper,tblPlace) -- Save Lower & Upper Date Timespan and Place & Soundex for each comma separated Place part
end -- anonymous function
end -- function NewEventData
function NewPersonData() -- Prototype to Make Person Database for Individual
local tblFact = { BIRT="Birth"; BAPM="BapCh"; CHR="BapCh"; FAMS="Marry"; DEAT="Death"; BURI="Death"; CREM="Death"; }
local tblRel = { FAMCHUSB="Father"; FAMCWIFE="Mother"; FAMSHUSB="Spouse"; FAMSWIFE="Spouse"; FAMSCHIL="Child"; }
return function(ptrInd,intRid) -- Make Person Database for Individual
local tblInd = TblData[intRid]
tblInd.Indiv = ptrInd:Clone() -- Save Individual Record pointer
tblInd.Gender= fhGetItemText(ptrInd,"~.SEX") -- Save Individual Gender
tblInd.Names = TblNamesData(ptrInd) -- Save Individual Names data table
tblInd.Birth = {}
tblInd.BapCh = {} -- Save an Event table for each type of BMD Event in tblFact
tblInd.Marry = {}
tblInd.Death = {}
tblInd.Father= {} -- Save a Record Id table for each type of Relation in tblRel
tblInd.Mother= {}
tblInd.Spouse= {}
tblInd.Child = {}
local ptrItem = fhNewItemPtr()
ptrItem:MoveToFirstChildItem(ptrInd)
while ptrItem:IsNotNull() do -- Loop through all instances of each Item
local strTag = fhGetTag(ptrItem)
local strFact = tblFact[strTag] -- Lookup the Fact name for Item Tag (if any)
if strFact then
local ptrFact = ptrItem:Clone()
if strFact == "Marry" then ptrFact:MoveTo(fhGetValueAsLink(ptrFact),"~.MARR") end
table.insert(tblInd[strFact],TblEventData(ptrFact)) -- Save each Event table of Dates & Places
end
if strTag:match("FAM") then -- Family link Item
local ptrRel = fhNewItemPtr()
ptrRel:MoveToFirstChildItem(fhGetValueAsLink(ptrItem))
while ptrRel:IsNotNull() do -- Loop through all instances of each Item
local strRel = tblRel[strTag..fhGetTag(ptrRel)] -- Lookup the Relation name for Tags (if any)
if strRel then
local ptrLnk = fhGetValueAsLink(ptrRel) -- Ensure link pointer has a value
if ptrLnk:IsNotNull() then
local intRel = fhGetRecordId(ptrLnk) -- Obtain the Record Id of Relation
if intRel ~= intRid then -- Relation is not original Individual
table.insert(tblInd[strRel],intRel) -- Save their Record Id
TblData[intRel].Indiv = ptrLnk:Clone() -- Save their Record pointer
end
end
end
ptrRel:MoveNext("ANY")
end
end
ptrItem:MoveNext("ANY")
end
end -- anonymous function
end -- function NewPersonData
function TblMakeEvent(intLower,intUpper,tblPlace) -- Make Event from Date Timespan & Place Parts
local tblEvent = {}
tblEvent.Lower = intLower
tblEvent.Upper = intUpper
tblEvent.Place = tblPlace
return tblEvent
end -- function TblMakeEvent
function NewScoreNamesIndi() -- Prototype to Score Names of primary Individuals
local intLastWrongIndi = TblNameLastWrong[IntIndivi]
local intNameMaxIndi = TblNameMaximum[IntIndivi]
return function(tblListA,tblListB) -- Calculate the Score for Comparing two Individual Name Lists
local intScore = 0
local intLastWrong = intLastWrongIndi -- Deduction if no Lastname match (default is 0)
for strName in pairs(tblListA) do
local tblName = tblListB[strName]
if tblName then
intLastWrong = strName:match("^(0)") or intLastWrong -- Note if Lastname matches to inhibit deduction
intScore = intScore + tblName[IntIndivi] -- Increase score for any Name or Soundex matches
end
end
if tonumber(intLastWrong) < 0 then return intLastWrong end -- Reduce score if no Lastname match and deduction exists
return math.min(intScore,intNameMaxIndi) -- Limit prevents multiple Alternate Name matches overwhelming result
end -- anonymous function
end -- function NewScoreNamesIndi
function IntScoreNamesData(tblListA,tblListB,intRelation) -- Calculate the Score for Comparing two Name Lists of Relations
local intScore = 0
local intLastWrong = TblNameLastWrong[intRelation] -- Deduction if no Lastname match (default is 0)
for strName in pairs(tblListA) do
local tblName = tblListB[strName]
if tblName then
intLastWrong = strName:match("^(0)") or intLastWrong -- Note if Lastname matches to inhibit deduction
intScore = intScore + tblName[intRelation] -- Increase score for any Name or Soundex matches
end
end
if tonumber(intLastWrong) < 0 then return intLastWrong end -- Reduce score if no Lastname match and deduction exists
if intScore < TblNameMinimum[intRelation] then return TblNameDeduction[intRelation] end -- Reduce score for Name matches below minimum threshold
return math.min(intScore,TblNameMaximum[intRelation]) -- Limit prevents multiple Alternate Name matches overwhelming result
end -- function IntScoreNamesData
function IntScoreEventData(tblEventA,tblEventB) -- Calculate the Score for Comparing two Events
if tblEventA.Place and tblEventB.Place then -- Both the Event Dates exist and are Real not Synthetic
local intScore = 0
if math.abs(tblEventA.Lower-tblEventB.Lower) <= IntDatesTolerance then intScore = intScore + IntDatesMatched end
if math.abs(tblEventA.Upper-tblEventB.Upper) <= IntDatesTolerance then intScore = intScore + IntDatesMatched end
if math.max(tblEventA.Lower,tblEventB.Lower) <= math.min(tblEventA.Upper,tblEventB.Upper) then intScore = intScore + IntDatesOverlap end
if intScore < IntDatesMinimum then return IntDatesDeduction end -- Dates completely different, so reduce score
for strPlace in pairs(tblEventA.Place) do
intScore = intScore + ( tblEventB.Place[strPlace] or 0 ) -- Increase score for Place part or Soundex matches
end
return math.min(intScore,IntEventMaximum) -- Limit prevents multiple Place part matches overwhelming result
end
return 0
end -- function IntScoreEventData
function IntScoreBestEvents(tblEventsA,tblEventsB) -- Calculate best Score for Comparing all Events
local intScore = nil
for intEventA, tblEventA in ipairs(tblEventsA) do
for intEventB, tblEventB in ipairs(tblEventsB) do
intScore = math.max(intScore or -999,IntScoreEventData(tblEventA,tblEventB))
end
end
return intScore or 0
end -- function IntScoreBestEvents
function TblScoreRelatives(tblRidA,tblRidB,intRelation) -- Calculate best Score for Comparing all Relatives
local tblScore = { 0; 0; 0; 0; 0; 0; }
tblRidA.Best = tblRidA[1] -- Record Id of Best matching Relatives
tblRidB.Best = tblRidB[1]
for intRelA, intRidA in ipairs(tblRidA) do
for intRelB, intRidB in ipairs(tblRidB) do
if intRidA and intRidB then -- Both the Record Id exist
if intRidA == intRidB and IntGenGapFamily > 0 then -- Both the same person and immediate family excluded -- V3.7
return { 0; 0; 0; 0; 0; 0; } -- So return zero, as they are not duplicates to be scored, and negative score upsets close Family preferences
end
local tblIndA = TblData[intRidA]
local tblIndB = TblData[intRidB] -- Score Names data
if not tblIndA.Names then GetPersonData(tblIndA.Indiv,intRidA) end
if not tblIndB.Names then GetPersonData(tblIndB.Indiv,intRidB) end
local intNames = IntScoreNamesData(tblIndA.Names,tblIndB.Names,intRelation)
if intNames >= TblNameThreshold[intRelation] then -- Threshold has been reached, so score Event data
local intBirth = IntScoreBestEvents(tblIndA.Birth,tblIndB.Birth)
local intBapCh = IntScoreBestEvents(tblIndA.BapCh,tblIndB.BapCh)
local intMarry = IntScoreBestEvents(tblIndA.Marry,tblIndB.Marry)
local intDeath = IntScoreBestEvents(tblIndA.Death,tblIndB.Death)
local intScore = intNames+intBirth+intBapCh+intMarry+intDeath
if intScore > tblScore[1] then -- Save the best Scores and pair of Relatives Id
tblScore = { intScore; intNames; intBirth; intBapCh; intMarry; intDeath; }
tblRidA.Best = intRidA
tblRidB.Best = intRidB
end
else
if intNames > tblScore[1] then
tblScore = { intNames; intNames; }
tblRidA.Best = intRidA
tblRidB.Best = intRidB
end
end
end
end
end
return tblScore
end -- function TblScoreRelatives
function TblScoreGender(tblIndA,tblIndB) -- Calculate Score for Comparing Gender of Individuals and Best Children
local intIndivGend = 0
if tblIndA.Gender ~= tblIndB.Gender then intIndivGend = IntGenderDeduct end
local intChildGend = 0
local intChildA = tblIndA.Child.Best -- Retrieve the Best matching Child Record Id
local intChildB = tblIndB.Child.Best
if intChildA and intChildB then
if TblData[intChildA].Gender ~= TblData[intChildB].Gender then intChildGend = IntGenderDeduct end
end
return { intIndivGend+intChildGend; intIndivGend; intChildGend; }
end -- function TblScoreGender
function IntLower(tblEvent) -- Lower date range value
if tblEvent then return tblEvent.Lower end
return -99999
end -- function IntLower
function IntUpper(tblEvent) -- Upper date range value
if tblEvent then return tblEvent.Upper end
return 999999
end -- function IntUpper
function TblDataIndi(intRid) -- Create Person Database if not yet compiled
local tblInd = TblData[intRid]
if not tblInd.Names then GetPersonData(tblInd.Indiv,intRid) end
return tblInd
end -- function TblDataIndi(intRid)
function SynthesiseDates(tblInd) -- Synthesise missing Event Dates from other Event Dates
local tblBirth = tblInd.Birth[1] -- Only consider 1st Date for each type of Event
local tblBapCh = tblInd.BapCh[1]
local tblMarry = tblInd.Marry[1]
local tblDeath = tblInd.Death[1]
local intSpouse = tblInd.Spouse[1] -- Cannot cope with multiple Spouses, so only synthesise Marriage Date when one Spouse
if #tblInd.Spouse == 1 and not tblMarry then -- Single Spouse but no Marriage Date, so synthesise from own/spouse BMD events
local tblSpouse = TblDataIndi(intSpouse)
local intLower = math.max( IntLower(tblBirth), IntLower(tblBapCh), IntLower(tblSpouse.Birth[1]), IntLower(tblSpouse.BapCh[1]) )
local intUpper = math.min( IntUpper(tblDeath), IntUpper(tblSpouse.Death[1]) )
if intLower > 0 or intUpper < 999999 then -- Synthetic Lower or Upper date exists
if intLower <= 0 then intLower = intUpper - IntLifespanDays -- No Lower date so Lower = Upper - Lifespan
else intLower = intLower + IntMarriageDays end -- Lower Birth/Baptism date exists so add Age of Consent
if intUpper >= 999999 then intUpper = intLower + IntLifespanDays end -- No upper date so Upper = Lower + Lifespan
tblInd.Marry[1] = TblMakeEvent(intLower,intUpper) -- Synthesise Event with no Place
tblMarry = tblInd.Marry[1]
end
end
if not tblBirth then -- No Birth Date, so synthesise from own/child/parent BMD events
local intLower = -99999
local intUpper = math.min( IntUpper(tblDeath), IntUpper(tblMarry) - IntMarriageDays, IntUpper(tblBapCh) )
local intChild1 = tblInd.Child[1]
if intChild1 then
local tblChild1 = TblDataIndi(intChild1) -- Latest Birth is 1st Child's latest Birth/Baptism/Death - Age of Puberty, or Marriage - Age of Puberty - Age of Consent
intUpper = math.min( intUpper, IntUpper(tblChild1.Birth[1]) - IntPubertyDays, IntUpper(tblChild1.BapCh[1]) - IntPubertyDays, IntUpper(tblChild1.Death[1]) - IntPubertyDays, IntUpper(tblChild1.Marry[1]) - IntPubertyDays - IntMarriageDays )
end
local intMother = tblInd.Mother[1]
if intMother then
local tblMother = TblDataIndi(intMother)
intLower = math.max( intLower, IntLower(tblMother.Birth[1]) + IntPubertyDays ) -- Earliest Birth is 1st Mother's earliest Birth + Age of Puberty
intUpper = math.min( intUpper, IntUpper(tblMother.Birth[1]) + IntFertileDays, IntUpper(tblMother.Death[1]) )
end -- Latest Birth is earliest of 1st Mother's latest Birth + Age of Fertility, or 1st Mother's latest Death
local intFather = tblInd.Father[1]
if intFather then
local tblFather = TblDataIndi(intFather)
intLower = math.max( intLower, IntLower(tblFather.Birth[1]) + IntPubertyDays ) -- Earliest Birth is 1st Father's earliest Birth + Age of Puberty
intUpper = math.min( intUpper, IntUpper(tblFather.Death[1]) + IntPregnantDays ) -- Latest Birth is 1st Father's latest Death + Pregnancy
end
if intLower > 0 or intUpper < 999999 then -- Synthetic Lower or Upper date exists
if intLower <= 0 then intLower = intUpper - IntLifespanDays end -- No Lower date so Lower = Upper - Lifespan
if intUpper >= 999999 then intUpper = intLower + IntLifespanDays end -- No upper date so Upper = Lower + Lifespan
tblInd.Birth[1] = TblMakeEvent(intLower,intUpper) -- Synthesise Event with no Place
tblBirth = tblInd.Birth[1]
end
end
if tblBirth and not tblDeath then
tblInd.Death[1] = TblMakeEvent(tblBirth.Lower,tblBirth.Upper+IntLifespanDays) -- Birth Date but no Death Date, so synthesise from own Birth event
end
end -- function SynthesiseDates
function IntDateChronCheck(intDateA,intDateB) -- Check date chronology and return proportional points score
return math.floor( math.min( ( intDateA - intDateB ), 0 ) / IntChronMagDays )
end -- function IntDateChronCheck
function IntScoreDateChron(tblIndA,tblIndB) -- Calculate the Score for the Chronology of Event Dates i.e. Is Birth after Baptism after Marriage after Death ?
local intLowerBirthA = IntLower(tblIndA.Birth[1]) -- Lower Date for each Event for both Individuals
local intLowerBirthB = IntLower(tblIndB.Birth[1])
local intLowerBapChA = IntLower(tblIndA.BapCh[1])
local intLowerBapChB = IntLower(tblIndB.BapCh[1])
local intLowerMarryA = IntLower(tblIndA.Marry[1])
local intLowerMarryB = IntLower(tblIndB.Marry[1])
local intLowerDeathA = IntLower(tblIndA.Death[1])
local intLowerDeathB = IntLower(tblIndB.Death[1])
local intUpperBirthA = IntUpper(tblIndA.Birth[#tblIndA.Birth]) -- Upper Date for each Event for both Individuals
local intUpperBirthB = IntUpper(tblIndB.Birth[#tblIndB.Birth])
local intUpperBapChA = IntUpper(tblIndA.BapCh[#tblIndA.BapCh])
local intUpperBapChB = IntUpper(tblIndB.BapCh[#tblIndB.BapCh])
local intUpperMarryA = IntUpper(tblIndA.Marry[#tblIndA.Marry])
local intUpperMarryB = IntUpper(tblIndB.Marry[#tblIndB.Marry])
local intUpperDeathA = IntUpper(tblIndA.Death[#tblIndA.Death])
local intUpperDeathB = IntUpper(tblIndB.Death[#tblIndB.Death])
local intScore =
IntDateChronCheck( intUpperBirthA, intLowerBirthB ) + -- Individual A latest birth before Individual B earliest birth
IntDateChronCheck( intUpperBirthB, intLowerBirthA ) + -- Individual B latest birth before Individual A earliest birth
IntDateChronCheck( intUpperBapChA, intLowerBirthB ) + -- Individual A baptised before Individual B born
IntDateChronCheck( intUpperBapChB, intLowerBirthA ) + -- Individual B baptised before Individual A born
IntDateChronCheck( intUpperMarryA, intLowerBirthB ) + -- Individual A married before Individual B born
IntDateChronCheck( intUpperMarryB, intLowerBirthA ) + -- Individual B married before Individual A born
IntDateChronCheck( intUpperDeathA, intLowerBirthB ) + -- Individual A died before Individual B born
IntDateChronCheck( intUpperDeathB, intLowerBirthA ) + -- Individual B died before Individual A born
IntDateChronCheck( intUpperBapChA, intLowerBapChB ) + -- Individual A latest baptised before Individual B earliest baptised
IntDateChronCheck( intUpperBapChB, intLowerBapChA ) + -- Individual B latest baptised before Individual A earliest baptised
IntDateChronCheck( intUpperMarryA, intLowerBapChB ) + -- Individual A married before Individual B baptised
IntDateChronCheck( intUpperMarryB, intLowerBapChA ) + -- Individual B married before Individual A baptised
IntDateChronCheck( intUpperDeathA, intLowerBapChB ) + -- Individual A died before Individual B baptised
IntDateChronCheck( intUpperDeathB, intLowerBapChA ) + -- Individual B died before Individual A baptised
IntDateChronCheck( intUpperMarryA, intLowerMarryB ) + -- Individual A latest marriage before Individual B earliest marriage
IntDateChronCheck( intUpperMarryB, intLowerMarryA ) + -- Individual B latest marriage before Individual A earliest marriage
IntDateChronCheck( intUpperDeathA, intLowerMarryB ) + -- Individual A died before Individual B married
IntDateChronCheck( intUpperDeathB, intLowerMarryA ) + -- Individual B died before Individual A married
IntDateChronCheck( intUpperDeathA, intLowerDeathB ) + -- Individual A latest died before Individual B earliest died
IntDateChronCheck( intUpperDeathB, intLowerDeathA ) -- Individual B latest died before Individual A earliest died
if intScore > IntChronTolerance then
local intMother = tblIndB.Mother.Best
if intMother then
tblEvent = TblDataIndi(intMother)
intScore = intScore +
IntDateChronCheck( intUpperBirthA, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual A born before Mother of B mature
IntDateChronCheck( IntUpper(tblEvent.Birth[#tblEvent.Birth])+IntFertileDays, intLowerBirthA ) + -- Mother of B infertile before Individual A born
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthA ) -- Mother of B died before Individual A born
end
local intMother = tblIndA.Mother.Best
if intMother then
tblEvent = TblDataIndi(intMother)
intScore = intScore +
IntDateChronCheck( intUpperBirthB, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual B born before Mother of A mature
IntDateChronCheck( IntUpper(tblEvent.Birth[#tblEvent.Birth])+IntFertileDays, intLowerBirthA ) + -- Mother of A infertile before Individual B born
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthA ) -- Mother of A died before Individual B born
end
local intFather = tblIndB.Father.Best
if intFather then
tblEvent = TblDataIndi(intFather)
intScore = intScore +
IntDateChronCheck( intUpperBirthA, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual A born before Father of B mature
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthA-IntPregnantDays ) -- Father of B died before Individual A conceived
end
local intFather = tblIndA.Father.Best
if intFather then
tblEvent = TblDataIndi(intFather)
intScore = intScore +
IntDateChronCheck( intUpperBirthB, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual B born before Father of A mature
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthB-IntPregnantDays ) -- Father of A died before Individual B conceived
end
local intChild = tblIndB.Child.Best
if intChild then
tblEvent = TblDataIndi(intChild)
intScore = intScore +
IntDateChronCheck( intUpperDeathA, IntLower(tblEvent.Birth[1])-IntPregnantDays ) -- Individual A died before Child of B conceived
end
local intChild = tblIndA.Child.Best
if intChild then
tblEvent = TblDataIndi(intChild)
intScore = intScore +
IntDateChronCheck( intUpperDeathB, IntLower(tblEvent.Birth[1])-IntPregnantDays ) -- Individual B died before Child of A conceived
end
end
return intScore
end -- function IntScoreDateChron
function TblScoreGenGap(ptrIndA,ptrIndB) -- Calculate the Score for Comparing Gender Gap
local intGensUp = fhCallBuiltInFunction("RelationCode",ptrIndA,ptrIndB,"GENS_UP",1) -- Always positive or nil if unrelated
local intGensDn = fhCallBuiltInFunction("RelationCode",ptrIndA,ptrIndB,"GENS_DOWN",1) -- Always positive or nil if unrelated
if intGensUp and intGensDn then
local intGenGap = intGensUp + intGensDn - IntGenGapRelative -- Spouse = -6, Parent:Child = -5, Sibling/Gparent:Gchild = -4, -3, -2, -1 , 0 , 1, etc
return { math.min( intGenGap * IntGenGapDeduction, 0 ); intGensUp; intGensDn; }
end
return { 0; intGensUp or ""; intGensDn or ""; }
end -- function TblScoreGenGap
function SortResults(tblResults) -- Sort the Results into Descending Order of Full Score and then by Individual Score
table.sort( tblResults, function(tblA,tblB) if tblA.FullScore ~= tblB.FullScore then return tblA.FullScore > tblB.FullScore else return tblA.IndiScore > tblB.IndiScore end end )
end -- function SortResults
-- Find Duplicate Records --
function FindDuplicateRecords(intTotal,flgDiag,flgSpan) -- Total records, Diagnostic mode, with Timespans
local tblData = TblData -- Data table of Record Id to include in candidate checks
local tblRecId = {} -- Individual Record Id pointers to Individual Record Data saved for comparisons
local tblResults = {} -- Results for Individual Record matches
local intBestScore = 0
local intMinimum = IntLeastResults -- Minimum initial score to retain in Results
local isResultOK = true -- Flag if completed run with Result Set
local intStartTime = os.time() -- Time search started for Result Set duration Message
local intComparisons = intTotal * ( intTotal-1 ) / 2 -- Number of Individual versus Individual comparisons
local intProgressStep = 0 -- Progress count of inner loop Individual comparisons
local intIndThreshold = TblNameThreshold[IntIndivi] -- Local copies of Global values
local intIndiScoreMin = IntIndiScoreMin
local intLeastResults = IntLeastResults
local intLimitResults = IntLimitResults
local intPruneResults = IntPruneResults
local intProgBarStart = IntProgBarStart
if flgDiag then
intIndThreshold = -100 -- points -- Adjusted values for Diagnostic Mode to retain many more Results
intIndiScoreMin = -100 -- points
intLeastResults = -100 -- points
intLimitResults = 200 -- rows
intPruneResults = intLimitResults * 2
intProgBarStart = intProgBarStart / 10
end
progbar.Setup(iup_gui.DialogueAttributes("Bars")) -- Create the Progress Bar functions
if intComparisons > intProgBarStart then -- Optionally start Progress Bar for Comparsions
progbar.Start("Finding Duplicates",intComparisons)
end
TblSoundex = { } -- Soundex dictionary codes cache of previously coded Names & Places
if general.FlgFileExists(StrSoundexFile) then
progbar.Focus()
progbar.Message("Loading Soundex Cache")
TblSoundex, StrErr = table.load(StrSoundexFile) -- Load Soundex dictionary codes cache table
if TblSoundex[""] == "Z000" then
TblSoundex = { } -- Erase old initial letter Soundex cache
end
end
StrSoundex = NewSoundex(TblSoundex) -- Prototype for Soundex Calculator using Soundex dictionary codes cache
TblNamesData = NewNamesData() -- Prototype to Make Names & Soundex Dictionary per Individual
TblEventData = NewEventData() -- Prototype to Make Event Date Timespan & Place Parts per Individual
GetPersonData = NewPersonData() -- Prototype to Make Personal Profile Database per Individual
IntScoreNamesIndi = NewScoreNamesIndi() -- Prototype to Score Names of primary Individuals
local ptrIndA = fhNewItemPtr()
ptrIndA:MoveToFirstRecord("INDI")
while ptrIndA:IsNotNull() do -- Loop through every Individual Record
local intRidA = fhGetRecordId(ptrIndA) -- Obtain the Record Id of Individual
local tblIndA = tblData[intRidA]
if not tblIndA.Names then GetPersonData(ptrIndA,intRidA) end -- Make the Person Database for Individual
table.insert(tblRecId,intRidA) -- Save the Data Record Id in consecutively indexed table
SynthesiseDates(tblIndA) -- Synthesise missing Event Dates from other Events
for intIndB = 1, #tblRecId - 1 do -- Loop through prior Individual Record entries
local intRidB = tblRecId[intIndB] -- Record Id from consecutively indexed table
local tblIndB = tblData[intRidB] -- Lookup Individual Record Data
if tblIndA.Chosen or tblIndB.Chosen then -- Only score Records that were Chosen in GUI
local intIndiNames = IntScoreNamesIndi(tblIndA.Names,tblIndB.Names)
if intIndiNames >= intIndThreshold then -- If enough Individual Names match, assess score for Individual BMD Events, etc
local intIndiBirth = IntScoreBestEvents(tblIndA.Birth,tblIndB.Birth) * IntBoostedBirth -- V3.8
local intIndiBapCh = IntScoreBestEvents(tblIndA.BapCh,tblIndB.BapCh) * IntBoostedBapCh -- V3.8
local intIndiMarry = IntScoreBestEvents(tblIndA.Marry,tblIndB.Marry) * IntBoostedMarry -- V3.8
local intIndiDeath = IntScoreBestEvents(tblIndA.Death,tblIndB.Death) * IntBoostedDeath -- V3.8
local intIndiScore = intIndiNames + intIndiBirth + intIndiBapCh + intIndiMarry + intIndiDeath
if intIndiScore >= intIndiScoreMin then -- If Individual Score exceeds minimum, assess score for Relatives, Gender, etc
local tblFather = TblScoreRelatives(tblIndA.Father,tblIndB.Father,IntFather)
local tblMother = TblScoreRelatives(tblIndA.Mother,tblIndB.Mother,IntMother)
local tblSpouse = TblScoreRelatives(tblIndA.Spouse,tblIndB.Spouse,IntSpouse)
local tblChild = TblScoreRelatives(tblIndA.Child ,tblIndB.Child ,IntChild )
local tblGender = TblScoreGender(tblIndA,tblIndB)
local intFullScore = intIndiScore + tblFather[1] + tblMother[1] + tblSpouse[1] + tblChild[1] + tblGender[1]
if intFullScore >= intMinimum then -- Continue if score is above lowest retained Results entry
local ptrIndB = tblIndB.Indiv -- Get other Individual Record pointer
local intDateChron = IntScoreDateChron(tblIndA,tblIndB) -- Check date chronology using Best match Relations
local tblGenGap = TblScoreGenGap(ptrIndA,ptrIndB) -- Only check generation gap now as it has a high run time overhead
if intDateChron > IntChronTolerance -- Exclude major chronology mismatch
and tblGenGap[1] > IntFamGenGapMax then -- Exclude spouse (including spouse's spouses), parent/child, sibling, gparent/gchild
intFullScore = intFullScore + intDateChron + tblGenGap[1]
if intFullScore >= intMinimum then
local isCandidate = true
for i, tblNonDups in ipairs( TblNonDups ) do
if intRidA == tblNonDups.RecordIdA and intRidB == tblNonDups.RecordIdB then
isCandidate = false -- Exclude Non-Duplicate pairs
break
end
end
if isCandidate then
table.insert( tblResults,
{
FullScore=intFullScore; RecordIdA=intRidA; RecordIdB=intRidB;
IndiScore=intIndiScore; IndiNames=intIndiNames; IndiBirth=intIndiBirth; IndiBapCh=intIndiBapCh; IndiMarry=intIndiMarry; IndiDeath=intIndiDeath;
FathScore=tblFather[1]; FathNames=tblFather[2]; FathBirth=tblFather[3]; FathBapCh=tblFather[4]; FathMarry=tblFather[5]; FathDeath=tblFather[6];
MothScore=tblMother[1]; MothNames=tblMother[2]; MothBirth=tblMother[3]; MothBapCh=tblMother[4]; MothMarry=tblMother[5]; MothDeath=tblMother[6];
SpouScore=tblSpouse[1]; SpouNames=tblSpouse[2]; SpouBirth=tblSpouse[3]; SpouBapCh=tblSpouse[4]; SpouMarry=tblSpouse[5]; SpouDeath=tblSpouse[6];
ChilScore=tblChild [1]; ChilNames=tblChild [2]; ChilBirth=tblChild [3]; ChilBapCh=tblChild [4]; ChilMarry=tblChild [5]; ChilDeath=tblChild [6];
FamGenGap=tblGenGap[1]; FamGensUp=tblGenGap[2]; FamGensDn=tblGenGap[3]; DateChron=intDateChron;
GendScore=tblGender[1]; IndivGend=tblGender[2]; ChildGend=tblGender[3];
} )
if #tblResults >= intPruneResults then -- Prune low scores from Results to avoid exceeding memory
SortResults(tblResults)
for intEntry = intLimitResults + 1, #tblResults do tblResults[intEntry] = nil end -- table.remove(tblResults) end
intMinimum = tblResults[#tblResults].FullScore
end
end
end
end
end
end
end
end
end
progbar.Message("Individual Record Id "..intRidA) -- Report progress
progbar.Step(intProgressStep)
intProgressStep = intProgressStep + 1 -- Each inner loop performs incrementing number of comparisons
if progbar.Stop() then -- Break out of outer loop
isResultOK = false
break
end
ptrIndA:MoveNext()
end
progbar.Focus()
progbar.Message("Saving Soundex Cache") -- Save Soundex dictionary codes cache table
table.save(TblSoundex,StrSoundexFile)
if isResultOK then -- Dislay Result Set unless Progress Bar was Stopped
progbar.Message("Composing Result Set")
SortResults(tblResults) -- Sort the final results
local dateOrigin = fhNewDate(0001,01,10):GetDatePt1() -- Date origin for Timespan Dates is 10-Jan-0001 = 1-Jan-0001 + 9 days for Gregorian calendar offset
local dateTimespan = fhNewDate(0000) -- Date Timespan pointer for Timespan Date entries
for intEntry = 1, #tblResults do -- Extract highest scoring entries & populate table with Timespan Dates
local tblEntry = tblResults[intEntry] -- Timespan Dates only added now to avoid slowing down main search loop above
if tblEntry then intFullScore = tblEntry.FullScore end
if intFullScore < intLeastResults
or intEntry > intLimitResults then
table.remove(tblResults) -- When low Score or Results limit reached, purge remainder of results
else
intRecordIdA = tblEntry.RecordIdA
intRecordIdB = tblEntry.RecordIdB
-- Populate this Entry in Results table with Event Date Timespans
for strEvent, strPrefix in pairs( { Birth="B_"; BapCh="C_"; Marry="M_"; Death="D_"; } ) do
for strRecId, intRecId in pairs( { A_=intRecordIdA; B_=intRecordIdB; } ) do
local strIndex = strPrefix..strRecId.."Span"
local tblIndi = tblData[intRecId]
local tblEvent = tblIndi[strEvent][1] -- Get earliest Lower/Upper Timespan Dates for each Event for both Records
if tblEvent then -- Event Timespan Date exists
local intLower = tblEvent.Lower
local intUpper = tblEvent.Upper
if intLower == intUpper then -- Lower = Upper so Simple Date
dateTimespan:SetSimpleDate(fhCallBuiltInFunction("CalcDate",dateOrigin,0,0,intLower))
else -- Lower < Upper so Date Range
dateTimespan:SetRange("between",fhCallBuiltInFunction("CalcDate",dateOrigin,0,0,intLower),fhCallBuiltInFunction("CalcDate",dateOrigin,0,0,intUpper))
end
local strType = " Synth"
if tblEvent.Place then strType = " Actual" end -- Date is Actual and not Synthetic
tblEntry[strIndex] = dateTimespan:GetDisplayText("COMPACT")..strType
else
tblEntry[strIndex] = " " -- Event Timespan Date missing
end
end
end
progbar.Step(intProgressStep)
end
end
table.save(tblResults,StrResultsFile) -- Save results so can show again later
isResultOK = DisplayResultSet(tblResults,flgDiag,flgSpan,intStartTime)
end
progbar.Close() -- Close the Progress Bar and retrieve its window position
return isResultOK
end -- function FindDuplicateRecords
function DisplayResultSet(tblResults,flgDiag,flgSpan,intStartTime) -- Display the Result Set in FH
if #tblResults == 0 then
iup_gui.MemoDialogue("\n No duplicate Individual Records found. \n","OK")
return false
end
local tblColumnKey =
{ -- Column Title ; Index ; Type ;Width; Alignment; -- Result Set Column parameters allow easy changes
{ "Individual" ; "IndiScore"; };
{ "I-Names" ; "IndiNames"; };
{ "I-Birth" ; "IndiBirth"; };
{ "I-Birth Timespan-A " ; "B_A_Span" ; "text" ;142 ; "align_right"; }; -- Birth Timespan Record A
{ "I-Birth Timespan-B " ; "B_B_Span" ; "text" ;142 ; "align_right"; }; -- Birth Timespan Record B
{ "I-BapCh" ; "IndiBapCh"; };
{ "I-BapCh Timespan-A " ; "C_A_Span" ; "text" ;142 ; "align_right"; }; -- Bap/Chr Timespan Record A
{ "I-BapCh Timespan-B " ; "C_B_Span" ; "text" ;142 ; "align_right"; }; -- Bap/Chr Timespan Record B
{ "I-Marry" ; "IndiMarry"; };
{ "I-Marry Timespan-A " ; "M_A_Span" ; "text" ;142 ; "align_right"; }; -- Marry Timespan Record A
{ "I-Marry Timespan-B " ; "M_B_Span" ; "text" ;142 ; "align_right"; }; -- Marry Timespan Record B
{ "I-Death" ; "IndiDeath"; };
{ "I-Death Timespan-A " ; "D_A_Span" ; "text" ;142 ; "align_right"; }; -- Death Timespan Record A
{ "I-Death Timespan-B " ; "D_B_Span" ; "text" ;142 ; "align_right"; }; -- Death Timespan Record B
{ "Father" ; "FathScore"; };
{ "F-Names" ; "FathNames"; };
{ "F-Birth" ; "FathBirth"; };
{ "F-BapCh" ; "FathBapCh"; };
{ "F-Marry" ; "FathMarry"; };
{ "F-Death" ; "FathDeath"; };
{ "Mother" ; "MothScore"; };
{ "M-Names" ; "MothNames"; };
{ "M-Birth" ; "MothBirth"; };
{ "M-BapCh" ; "MothBapCh"; };
{ "M-Marry" ; "MothMarry"; };
{ "M-Death" ; "MothDeath"; };
{ "Spouse" ; "SpouScore"; };
{ "S-Names" ; "SpouNames"; };
{ "S-Birth" ; "SpouBirth"; };
{ "S-BapCh" ; "SpouBapCh"; };
{ "S-Marry" ; "SpouMarry"; };
{ "S-Death" ; "SpouDeath"; };
{ "Child" ; "ChilScore"; };
{ "C-Names" ; "ChilNames"; };
{ "C-Birth" ; "ChilBirth"; };
{ "C-BapCh" ; "ChilBapCh"; };
{ "C-Marry" ; "ChilMarry"; };
{ "C-Death" ; "ChilDeath"; };
{ "Chrono" ; "DateChron"; };
{ "Gen.Gap" ; "FamGenGap"; };
{ "Gens-Up" ; "FamGensUp"; };
{ "Gens-Dn" ; "FamGensDn"; };
{ "Gender" ; "GendScore"; };
{ "I-Gend" ; "IndivGend"; };
{ "C-Gend" ; "ChildGend"; };
}
local intFullScore = 0 -- Full Score
local tblFullScore = {}
local tblPercentage = {}
local intRecordIdA = 0 -- Record Id A & Individual A
local tblRecordIdA = {}
local tblPossibleA = {}
local intRecordIdB = 0 -- Record Id B & Individual B
local tblRecordIdB = {}
local tblPossibleB = {}
local tblResultSet = {} -- Table of Result Set Columns for Diagnostic sub-scores
local intMaxScore = 0
local intBoosted = IntBoostedBirth + IntBoostedBapCh + IntBoostedMarry + IntBoostedDeath -- V3.8
for intRelation = IntIndivi, IntChild do -- Max score for Percentage calculation = Event*Boost & Name maxima for each relation
intMaxScore = intMaxScore + (IntEventMaximum * intBoosted) + TblNameMaximum[intRelation]
intBoosted = 4 -- V3.8
end
for intEntry = 1, #tblResults do -- Create Result Set Column tables
local tblEntry = tblResults[intEntry]
intRecordIdA = tblEntry.RecordIdA
local ptrPossibleA = fhNewItemPtr() -- Record Id and Pointer of Individual A
ptrPossibleA:MoveToRecordById("INDI",intRecordIdA)
intRecordIdB = tblEntry.RecordIdB
local ptrPossibleB = fhNewItemPtr() -- Record Id and Pointer of Individual B
ptrPossibleB:MoveToRecordById("INDI",intRecordIdB)
intFullScore = tblEntry.FullScore
table.insert(tblPercentage,math.floor(intFullScore*100/intMaxScore)) -- Percentage Full Score
table.insert(tblRecordIdA,intRecordIdA) -- Individual Rec Id A
table.insert(tblPossibleA,ptrPossibleA) -- Individual Record A
table.insert(tblRecordIdB,intRecordIdB) -- Individual Rec Id B
table.insert(tblPossibleB,ptrPossibleB) -- Individual Record B
table.insert(tblFullScore,intFullScore) -- Points Full Score
for i, tblColumn in ipairs( tblColumnKey ) do
local strIndex = tblColumn[2]
if intEntry==1 then tblResultSet[strIndex] = {} end -- Create Diagnostic sub-score Column tables
table.insert(tblResultSet[strIndex],tblEntry[strIndex])
end
end
local intSize = #tblFullScore -- Output primary Result Set Columns unconditionally
local strMessage = ""
if intStartTime then
local intTime = os.difftime(os.time(),intStartTime)
strMessage = " in "..intTime.." seconds"
end
strMessage = "found "..intSize.." candidates"..strMessage
if intSize == 1 then strMessage = strMessage:gsub("candidates","candidate") end
if intTime == 1 then strMessage = strMessage:gsub("seconds","second") end
fhOutputResultSetTitles(iup_gui.Plugin..iup_gui.Version..strMessage)
fhOutputResultSetColumn("Percent" , "integer" , tblPercentage, intSize, 31, "align_mid" )
fhOutputResultSetColumn("Rec Id A" , "integer" , tblRecordIdA , intSize, 31, "align_mid" )
fhOutputResultSetColumn("Record A" , "item" , tblPossibleA , intSize, 140, "align_left")
fhOutputResultSetColumn("Rec Id B" , "integer" , tblRecordIdB , intSize, 31, "align_mid" )
fhOutputResultSetColumn("Record B" , "item" , tblPossibleB , intSize, 140, "align_left")
fhOutputResultSetColumn(" Total " , "integer" , tblFullScore , intSize, 31, "align_mid" )
if intSize > 0 then
for i, tblColumn in ipairs( tblColumnKey ) do -- Output Diagnostic sub-score Columns only if they exist
local strTitle = tblColumn[1]
local strIndex = tblColumn[2]
local strType = tblColumn[3]
local intWidth = tblColumn[4]
local strAlign = tblColumn[5]
if flgDiag or not strTitle:match("%-") then -- Only output Diagnostic or Timespan columns if requested
if flgSpan or not strTitle:match("Timespan") then
if strType and intWidth and strAlign then
fhOutputResultSetColumn(strTitle, strType, tblResultSet[strIndex], intSize, intWidth, strAlign )
else
fhOutputResultSetColumn(strTitle, "integer", tblResultSet[strIndex], intSize, 31, "align_mid" )
end
end
end
end
end
return true
end -- function DisplayResultSet
-- Main Code Section Starts Here --
if fhGetContextInfo("CI_APP_MODE") ~= "Project Mode" then -- V3.9
fhMessageBox("\nThis plugin cannot operate on a Standalone GEDCOM File.\n")
return
end
fhInitialise(5,0,8,"save_recommended")
PresetGlobalData() -- Preset global data settings
ResetDefaultSettings() -- Preset default sticky settings
LoadSettings() -- Load sticky data settings
GUI_MainDialogue() -- Invoke graphical user interface
SaveSettings() -- Save sticky data settings
if IntFhVersion > 5 then fhSetConversionLossFlag(false) end -- Inhibit loss of accents message
--[[
@V3.9: Library 4.1; Check for Updates button; Centre window on Parent FH window; Exclude Standalone GEDCOM Files;
@V3.8: Updated library to Functions Prototypes v3.0; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; Boost event scores so for instance duplicate Marriages are given precedence; Add FONE & ROMN to NAME & PLAC; FH V7 Lua 3.5 IUP 3.28;
@V3.7: Fix "±" character in Set Preferences tab Event Matching & Date Chronology, include immediate family in TblScoreRelatives(...), include undated records in intChosenRecord(...).
@V3.6: Both ANSI FH V5 & UTF-8 FH V6 IUP 3.11.2 plus new Soundex all numeric coding for Unicode, etc.
@V3.5: Latest GUI Library, add F1 help_cb, add BalloonToggle().
@V3.4: Fix close relations problem, improve Find Duplicates date selection using toggles & prompt, allow multiple selection from lists in Omit Non-Duplicate tab.
@V3.4: Fix stack overflow problem, Version History help pages, and new GUI library, Library modules, iup_gui.Balloon = "YES", auto Plugin Title & Version, GUI attribute mixed case.
@V3.3: Better Progress Bar message while loading large family Relation Pool.
@V3.2: More efficient Record Pool Database construction and Individual comparison loop, detects all same sex parents, fix minor ProgressBar bug, include CREMation="Death".
@V3.1: Includes all multiple Relations and all multiple Events in assessments, and adds some function prototypes.
@V3.0: Update to Omit Non-Duplicates list, remove Named List option, add Erase Soundex Cache, finish Help & Advice, and first Plugin Store release.
@V2.6: Minor updates to the Soundex function and its file load/save, plus better Progress Bar presentation.
@V2.5: Minor updates to Name & Event checks and ProgressBar to improve performance, extra ProgressBar.Messages and moved ProgressBar.Stop() after output of Result Set.
@V2.4: Minor adjustment to Set Preferences, structured Help & Advice, small corrections to Name & Event checks, plus Soundex and ProgressBar function prototypes.
@V2.3: Add extra chronology date checks & remove obsolete checks, add CheckVersionInStore, strip all spaces & ignore case in Place parts, better Result Set sort, add complete sticky Set Preferences.
@V2.2: Bug fix for Non-Duplicates list, and some minor improvements.
@V2.1: Adjust Soundex scoring for similar Lastname & Forename case, treat space separated Surnames as one Name, ignore case when matching Fornames,
monospaced font for Non-Duplicates tab, added some Set Preferences sticky options and tabs.
@V2.0: Add Non-Duplicates list management tab & simple Set Preferences tab, deduction for Surname mismatch per relative, proportional Chronology in Months.
@V1.9: Fix bug in scoring values per relative.
@V1.8: Add name scoring values per relative, separate Period/Range and Approx/Calc/Est timespans, proportional Chronology scores, show previous Result Set.
@V1.7: Add Forename positional scores, increase score for Surname match, reduce Name mismatch to 0,
add Place positional scores, drop Child Count, reduce Generation Gap deductions, include Percentage result score,
new Date timespans for Approx/Calc/Est & Before/After & From/To, check 1st Child Gender, User Preference Settings,
synthesise missing Event Dates, extra Chronology checks, exclude multiple Chronology errors, exclude close Family,
sticky Settings and Soundex & NonDups tables, Date Timespans in diagnostic Results, GUI Last Run Date and bug fixes.
@V1.6: Fix bug with selecting subset, extra Date chronology checks.
@V1.5: 1st Child name check, corrected Gen Gap check, better Date chronology check, tweaked other scores, improved run time estimate, Diagnostic mode.
@V1.4: Fixed GUI size on Font change, fixed bug in last Update check, corrected run time estimate, added Burial Event, added Date chronology check.
@V1.3: Proportional Generation gap score, separate Names table, Father & Mother Name & Soundex check, Result Set diagnostic sub-scores, GUI with selection options.
@V1.2: Soundex checks runtime improvements, limit Name checks score, Event Date +/-50 days check, Place Name & Soundex check, Spouse Name & Soundex check, Generation gap check.
@V1.1: Added child count comparison. Prune low scores from results to prevent exceeding memory.
@V1.0: Initial version.
]]
--[[
@Title: Find Duplicate Individuals
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 3.9
@Keywords:
@LastUpdated: 25 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: Find duplicated Individual Records
@Version Log: See end of file.
@TBD: www.behindthename.com/api for Related Names as per http://www.fhug.org.uk/forum/viewtopic.php?f=42&t=6089&p=26187#p26187 et seq ...
@TBD: SynthesiseDates(tblInd) to use datBirthDate:SetSimpleDate(fhCallBuiltInFunction('EstimatedBirthDate',ptrIndi,'EARLIEST')) and datDeathDate:SetSimpleDate(fhCallBuiltInFunction('EstimatedDeathDate',ptrIndi,'LATEST'))
@TBD: but only once those functions estimate the Spouse dates when needed to estimate Individual dates as per FH support log EstimatedBirth/DeathDate Functions [#622325].
]]
if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end
--[[
@Title: aa Library Functions Preamble
@Author: Mike Tate
@Version: 4.1
@LastUpdated: 18 Feb 2026
@Description: All the library functions prototype closures for Plugins.
]]
--[[
@Module: +fh+stringx_v3
@Author: Mike Tate
@Version: 3.0
@LastUpdated: 19 Sep 2020
@Description: Extended string functions to supplement LUA string library.
@V3.0: Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility; Added inert(strTxt) function;
@V2.5: Support FH V6 Encoding = UTF-8;
@V2.4: Tolerant of integer & nil parameters just link match & gsub;
@V1.0: Initial version.
]]
local function stringx_v3()
local fh = {} -- Local environment table
-- Supply current file encoding format --
function fh.encoding()
if fhGetAppVersion() > 5 then return fhGetStringEncoding() end
return "ANSI"
end -- function encoding
-- Split a string using "," or chosen separator --
function fh.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
-- Split a string into numbers using " " or "," or "x" separators -- Any non-number remains as a string
function fh.splitnumbers(strTxt)
local tblNum = {}
strTxt = tostring(strTxt or "")
strTxt:gsub("([^ ,x]+)", function(strNum) tblNum[#tblNum+1] = tonumber(strNum) or strNum end)
return tblNum
end -- function splitnumbers
local strMagic = "([%^%$%(%)%%%.%[%]%*%+%-%?])" -- UTF-8 replacement for "(%W)"
-- Hide magic pattern symbols ^ $ ( ) % . [ ] * + - ?
function fh.plain(strTxt)
-- Prefix every magic pattern character with a % escape character,
-- where %% is the % escape, and %1 is the original character capture.
strTxt = tostring(strTxt or ""):gsub(strMagic,"%%%1")
return strTxt
end -- function plain
-- matches is plain text version of string.match()
function fh.matches(strTxt,strFind,intInit)
strFind = tostring(strFind or ""):gsub(strMagic,"%%%1") -- Hide magic pattern symbols
return tostring(strTxt or ""):match(strFind,tonumber(intInit))
end -- function matches
-- replace is plain text version of string.gsub()
function fh.replace(strTxt,strOld,strNew,intNum)
strOld = tostring(strOld or ""):gsub(strMagic,"%%%1") -- Hide magic pattern symbols
return tostring(strTxt or ""):gsub(strOld,function() return strNew end,tonumber(intNum)) -- Hide % capture symbols
end -- function replace
-- Hide % escape/capture symbols in replacement so they are inert
function fh.inert(strTxt)
strTxt = tostring(strTxt or ""):gsub("%%","%%%%") -- Hide all % symbols
return strTxt
end -- function inert
-- convert is pattern without captures version of string.gsub()
function fh.convert(strTxt,strOld,strNew,intNum)
return tostring(strTxt or ""):gsub(tostring(strOld or ""),function() return strNew end,tonumber(intNum)) -- Hide % capture symbols
end -- function convert
local dicUpper = { }
local dicLower = { }
local dicCaseX = { }
-- ASCII unaccented letter translations for Upper, Lower, and Case Insensitive
for intUpper = string.byte("A"), string.byte("Z") do
local strUpper = string.char(intUpper)
local strLower = string.char(intUpper - string.byte("A") + string.byte("a"))
dicUpper[strLower] = strUpper
dicLower[strUpper] = strLower
local strCaseX = "["..strUpper..strLower.."]"
dicCaseX[strLower] = strCaseX
dicCaseX[strUpper] = strCaseX
end
-- Supply character length of ANSI text --
function fh.length(strTxt)
return string.len(strTxt or "")
end -- function length
-- Supply character substring of ANSI text --
function fh.substring(strTxt,i,j)
return string.sub(strTxt or "",i,j)
end -- function substring
-- Translate upper/lower case ANSI letters to pattern that matches both --
function fh.caseless(strTxt)
strTxt = tostring(strTxt or ""):gsub("[A-Za-z]",dicCaseX)
return strTxt
end -- function caseless
if fh.encoding() == "UTF-8" then
-- Supply character length of UTF-8 text --
function fh.length(strTxt)
isFlag = fhIsConversionLossFlagSet()
strTxt = fhConvertUTF8toANSI(strTxt or "")
fhSetConversionLossFlag(isFlag)
return string.len(strTxt)
end -- function length
local strUTF8 = "([%z\1-\127\194-\244][\128-\191]*)" -- Cater for Lua 5.1 %z or Lua 5.3 \0
if fhGetAppVersion() > 6 then
strUTF8 = "([\0-\127\194-\244][\128-\191]*)"
end
-- Supply character substring of UTF-8 text --
function fh.substring(strTxt,i,j)
local strSub = ""
j = j or -1
if j < 0 then j = j + length(strTxt) + 1 end
if i < 0 then i = i + length(strTxt) + 1 end
for strChr in string.gmatch(strTxt or "",strUTF8) do
if j <= 0 then break end
j = j - 1
i = i - 1
if i <= 0 then strSub = strSub..strChr end
end
return strSub
end -- function substring
-- Translate lower case to upper case UTF-8 letters --
function fh.upper(strTxt)
strTxt = tostring(strTxt or ""):gsub("([a-z\194-\244][\128-\191]*)",dicUpper)
return strTxt
end -- function upper
-- Translate upper case to lower case UTF-8 letters --
function fh.lower(strTxt)
strTxt = tostring(strTxt or ""):gsub("([A-Z\194-\244][\128-\191]*)",dicLower)
return strTxt
end -- function lower
-- Translate upper/lower case UTF-8 letters to pattern that matches both --
function fh.caseless(strTxt)
strTxt = tostring(strTxt or ""):gsub("([A-Za-z\194-\244][\128-\191]*)",dicCaseX)
return strTxt
end -- function caseless
-- Following tables use ASCII numeric coding to be immune from ANSI/UTF-8 encoding --
local arrPairs = -- Upper & Lower case groups of UTF-8 letters with same prefix --
{-- { Prefix; Beg ; End ; Inc; Offset Upper > Lower }; -- These include all ANSI letters and many more
{ "\195"; 0x80; 0x96; 1 ; 32 }; -- 195=0xC3 À U+00C0 to Ö U+00D6 and à U+00E0 to ö U+00F6
{ "\195"; 0x98; 0x9E; 1 ; 32 }; -- 195=0xC3 Ø U+00D8 to Þ U+00DE and ø U+00F8 to þ U+00FE
{ "\196"; 0x80; 0xB6; 2 ; 1 }; -- 196=0xC4 A U+0100 to k U+0137 in pairs
{ "\196"; 0xB9; 0xBD; 2 ; 1 }; -- 196=0xC4 L U+0139 to l U+013E in pairs
{ "\197"; 0x81; 0x87; 2 ; 1 }; -- 197=0xC5 L U+0141 to n U+0148 in pairs
{ "\197"; 0x8A; 0xB6; 2 ; 1 }; -- 197=0xC5 ? U+014A to y U+0177 in pairs
{ "\197"; 0xB9; 0xBD; 2 ; 1 }; -- 197=0xC5 Z U+0179 to ž U+017E in pairs
{ "\198"; 0x82; 0x84; 2 ; 1 }; -- 198=0xC6 ? U+0182 to ? U+0185 in pairs
-- Add more Unicode groups here as usage increases --
}
local dicPairs = -- Upper v Lower case UTF-8 letters that don't fit groups above --
{ [string.char(0xC4,0xBF)] = string.char(0xC5,0x80); -- ? U+013F and ? U+0140
[string.char(0xC5,0xB8)] = string.char(0xC3,0xBF); -- Ÿ U+0178 and ÿ U+00FF
}
local intBeg1 = string.byte(string.sub("À",1))
local intBeg2 = string.byte(string.sub("À",2))
local intEnd1 = string.byte(string.sub("Z",1))
local intEnd2 = string.byte(string.sub("Z",2))
-- print(string.format("%#x %#x %#x %#x",intBeg1,intBeg2,intEnd1,intEnd2)) -- Useful to work out numeric coding
-- Populate the UTF-8 letter translation dictionaries --
for intGroup, tblGroup in ipairs ( arrPairs ) do -- UTF-8 accented letter groups
local strPrefix = tblGroup[1]
for intUpper = tblGroup[2], tblGroup[3], tblGroup[4] do
local strUpper = string.char(intUpper)
local strLower = string.char(intUpper + tblGroup[5])
local strCaseX = strPrefix.."["..strUpper..strLower.."]"
strUpper = strPrefix..strUpper
strLower = strPrefix..strLower
dicUpper[strLower] = strUpper
dicLower[strUpper] = strLower
dicCaseX[strLower] = strCaseX
dicCaseX[strUpper] = strCaseX
end
end
for strUpper, strLower in pairs ( dicPairs ) do -- UTF-8 accented letters where upper & lower have different prefix
dicUpper[strLower] = strUpper
dicLower[strUpper] = strLower
local strCaseX = ""
for intByte = 1, #strUpper do -- Matches more than just the two letters, but can't do any better
strCaseX = strCaseX.."["..strUpper:sub(intByte,intByte)..strLower:sub(intByte,intByte).."]"
end
dicCaseX[strLower] = strCaseX
dicCaseX[strUpper] = strCaseX
end
end
-- overload fh functions into string table
for strIndex, anyValue in pairs(fh) do
if type(anyValue) == "function" then
string[strIndex] = anyValue
end
end
return fh
end -- local function stringx_v3
local stringx = stringx_v3() -- To access FH string extension module
--[[
@Module: +fh+iterate_v3
@Author: Mike Tate
@Version: 3.0
@LastUpdated: 25 Aug 2020
@Description: An iterater functions module to supplement LUA functions.
@V3.0: Function Prototype Closure version.
@V1.2: RecordTypes() includes HEAD tag.
@V1.1: ?
@V1.0: Initial version.
]]
local function iterate_v3()
local fh = {} -- Local environment table
-- Iterator for all records of one chosen type --
function fh.Records(strType)
local ptrAll = fhNewItemPtr() -- Pointer to all records in turn
local ptrRec = fhNewItemPtr() -- Pointer to record returned to user
ptrAll:MoveToFirstRecord(strType)
return function ()
ptrRec:MoveTo(ptrAll)
ptrAll:MoveNext()
if ptrRec:IsNotNull() then return ptrRec end
end
end -- function Records
-- Iterator for all the record types --
function fh.RecordTypes()
local intNext = -1 -- Next record type number
local intLast = fhGetRecordTypeCount() -- Last record type number
return function()
intNext = intNext + 1
if intNext == 0 then -- Includes HEAD tag -- V1.2
return "HEAD"
elseif intNext <= intLast then
return fhGetRecordTypeTag(intNext) -- Return record type tag
end
end
end -- function RecordTypes
-- Iterator for all items in all records of chosen types --
function fh.Items(...)
local arg = {...}
local intType = 1 -- Integer record type number
local tblType = {} -- Table of record type tags
local ptrNext = fhNewItemPtr() -- Pointer to next item in turn
local ptrItem = fhNewItemPtr() -- Pointer to item returned to user
if #arg == 0 then
for intType = 1, fhGetRecordTypeCount() do -- No parameters so use all record types
tblType[intType] = fhGetRecordTypeTag(intType)
end
else
tblType = arg -- Got parameters so use them instead
end
-- print(tblType[intType],intType)
ptrNext:MoveToFirstRecord(tblType[intType]) -- Get first record of first type
return function()
repeat
while ptrNext:IsNotNull() do -- Loop through all items
ptrItem:MoveTo(ptrNext)
ptrNext:MoveNextSpecial()
if ptrItem:IsNotNull() then return ptrItem end
end
intType = intType + 1 -- Loop through each record type
if intType <= #tblType then
ptrNext:MoveToFirstRecord(tblType[intType])
end
until intType > #tblType
end
end -- function Items
-- Iterator for all facts of an individual --
function fh.Facts(ptrIndi)
local ptrItem = fhNewItemPtr() -- Pointer to each item at level 1
local ptrFact = fhNewItemPtr() -- Pointer to each fact returned to user
ptrItem:MoveToFirstChildItem(ptrIndi)
return function ()
while ptrItem:IsNotNull() do
ptrFact:MoveTo(ptrItem)
ptrItem:MoveNext()
if fhIsFact(ptrFact) then return ptrFact end
end
end
end -- function Facts
return fh
end -- local function iterate_v3
local iterate = iterate_v3() -- To access FH iterate items module
--[[
@Module: +fh+general_v3
@Author: Mike Tate
@Version: 3.5
@LastUpdated: 12 Dec 2024
@Description: A general functions module to supplement LUA functions, where filenames use UTF-8 but for a few exceptions.
@V3.5: Further fix for Crossover/WINE file attributes;
@V3.4: Further fix for Unix/WINE file attributes;
@V3.3: Fix problem in fh.MakeFolder(...) when folder path is invalid; Remove Type, ShortName & ShortPath from attributes(...) as unsupported in WINE;
@V3.2: Added function DetectOldModules(); Updated functions RenameFile(), RenameFolder() & GetFolderContents();
@V3.1: Functions derived from FH V7 fhFileUtils library using File System Objects, plus additional features;
@V3.0: Function Prototype Closure version; GetDayNumber() error message reasons;
@V1.5: Revised SplitFilename(strFilename) for missing extension.
@V1.4: Revised EstimatedBirthDates() & EstimatedDeathDates() to fix null Dates.
@V1.3: Add GetDayNumber(), EstimatedBirthDates(), EstimatedDeathDates().
@V1.2: SplitFilename() updated for directory only paths, and MakeFolder() added.
@V1.1: pl.path experiment revoked. New DirTree with omit branch option. Avoid using stringx_v2.
@V1.0: Initial version.
]]
local function general_v3()
local fh = {} -- Local environment table
require("luacom") -- To create File System Object
fh.FSO = luacom.CreateObject("Scripting.FileSystemObject")
-- 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 fh.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 fh.GetParentFolder(strFileName)
-- strFileName ~ full file path
-- return value ~ parent folder path
local strParent = fh.FSO:GetParentFolderName(strFileName) --! Faulty in FH v6 with Unicode chars in path
if fhGetAppVersion() == 6 then
local _, wasAnsi = fh.FileNameToANSI(strFileName)
if not wasAnsi then
strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
end
end
return strParent
end -- function GetParentFolder
-- Check if file exists --
function fh.FlgFileExists(strFileName)
-- strFileName ~ full file path
-- return value ~ true if it exists
return fh.FSO:FileExists(strFileName)
end -- function FlgFileExists
-- Check if folder exists --
function fh.FlgFolderExists(strFolderName)
-- strFolderName ~ full file path
-- return value ~ true if it exists
return fh.FSO:FolderExists(strFolderName)
end -- function FlgFolderExists
-- Delete a file if it exists --
function fh.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 fh.FSO:FileExists(strFileName) then
fh.FSO:DeleteFile(strFileName,true)
if fh.FSO:FileExists(strFileName) then
doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
return false
end
end
return true
end -- function DeleteFile
-- Delete a folder if it exists including contents --
function fh.DeleteFolder(strFolderName,errFunction)
-- strFolderName ~ full folder path
-- errFunction ~ optional error reporting function
-- return value ~ true if folder does not exist or is deleted else false
if fh.FSO:FolderExists(strFolderName) then
fh.FSO:DeleteFolder(strFolderName,true)
if fh.FSO:FolderExists(strFolderName) then
doError("Folder Not Deleted:\n"..strFolderName.."\n",errFunction)
return false
end
end
return true
end -- function DeleteFolder
-- Rename a file if it exists --
function fh.RenameFile(strFileName,strNewName)
-- strFileName ~ full file path
-- strNewName ~ new file name & type
-- return value ~ true if file exists but new name does not and rename is OK else false
local strNewFile = fh.GetParentFolder(strFileName).."\\"..strNewName
if fh.FSO:FileExists(strFileName) and not fh.FSO:FileExists(strNewFile) then
local fileObject = fh.FSO:GetFile(strFileName)
fileObject.Name = strNewName
if fh.FSO:FileExists(strNewFile) then
return true
end
end
return false
end -- function RenameFile
-- Rename a folder if it exists --
function fh.RenameFolder(strFolderName,strNewName)
-- strFolderName ~ full folder path
-- strNewName ~ new folder name
-- return value ~ true if folder exists but new name does not and rename is OK else false
local strNewFolder = fh.GetParentFolder(strFolderName).."\\"..strNewName
if fh.FSO:FolderExists(strFolderName) and not fh.FSO:FolderExists(strNewFolder) then
local folderObject = fh.FSO:GetFolder(strFolderName)
folderObject.Name = strNewName
if fh.FSO:FolderExists(strNewFolder) then
return true
end
end
return false
end -- function RenameFolder
-- Copy a file if it exists and destination is not a folder --
function fh.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 fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
fh.FSO:CopyFile(strFileName,strDestination)
if fh.FSO:FileExists(strDestination) then
return true
end
end
return false
end -- function CopyFile
-- Copy a folder if it exists and destination is not a file --
function fh.CopyFolder(strFolderName,strDestination)
-- strFolderName ~ full source folder path
-- strDestination ~ full target folder path
-- return value ~ true if folder exists and is copied else false
if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
fh.FSO:CopyFolder(strFolderName,strDestination)
if fh.FSO:FolderExists(strDestination) then
return true
end
end
return false
end -- function CopyFolder
-- Move a file if it exists and destination is not a folder --
function fh.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 fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
if fh.DeleteFile(strDestination) then
fh.FSO:MoveFile(strFileName,strDestination)
if fh.FSO:FileExists(strDestination) then
return true
end
end
end
return false
end -- function MoveFile
-- Move a folder if it exists and destination is not a file --
function fh.MoveFolder(strFolderName,strDestination)
-- strFolderName ~ full source folder path
-- strDestination ~ full target folder path
-- return value ~ true if folder exists and is moved else false
if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
if fh.DeleteFolder(strDestination) then
fh.FSO:MoveFolder(strFolderName,strDestination)
if fh.FSO:FolderExists(strDestination) then
return true
end
end
end
return false
end -- function MoveFolder
local function CreateFolder(strFolderName) -- V3.3
fh.FSO:CreateFolder(strFolderName)
end -- local function CreateFolder(strFolderName)
-- Make subfolder recursively if does not exist --
function fh.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 fh.FSO:FolderExists(strFolderName) then
if #strFolderName > 4 -- V3.3
and not fh.MakeFolder(fh.GetParentFolder(strFolderName),errFunction) then
return false
end
if not pcall(CreateFolder,strFolderName) -- V3.3
or not fh.FSO:FolderExists(strFolderName) then
doError("Cannot Make Folder Path: \n"..strFolderName.." \n",errFunction)
return false
end
end
return true
end -- function MakeFolder
-- Check if folder writable --
function fh.FlgFolderWrite(strFolderName)
-- strFolderName ~ full source folder path
-- return value ~ true if folder writable else false
if fh.FlgFolderExists(strFolderName) then
if fh.MakeFolder(strFolderName.."\\vwxyz") then
fh.FSO:DeleteFolder(strFolderName.."\\vwxyz",true)
return true
end
end
return false
end -- function FlgFolderWrite
-- Open File with ANSI path and return Handle --
function fh.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 fh.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 = fh.FileNameToANSI(strFileName)
local fileHandle = fh.OpenFile(strAnsi,"w")
fileHandle:write(strContents)
assert(fileHandle:close())
if not wasAnsi then
fh.MoveFile(strAnsi,strFileName)
end
return true
end -- function SaveStringToFile
-- Load string from file --
function fh.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 = fh.FileNameToANSI(strFileName)
if not wasAnsi then
fh.CopyFile(strFileName,strAnsi)
end
local fileHandle = fh.OpenFile(strAnsi,"r")
local strContents = fileHandle:read("*all")
assert(fileHandle:close())
return strContents
end -- function StrLoadFromFile
-- Returns the Path, Filename, and Extension as 3 values --
function fh.SplitFilename(strFileName)
-- strFileName ~ full file path
-- return values ~ path, name.type, type
if fh.FSO:FolderExists(strFileName) then
local strPath = strFileName:gsub("[\\/]$","")
return strPath.."\\","",""
end
strFileName = strFileName.."."
return strFileName:match("^(.-)([^\\/]-%.([^\\/%.]-))%.?$")
end -- function SplitFilename
-- Convert dd/mm/yyyy hh:mm:ss format to integer seconds -- (DateTime format is used in attributes returned by GetFolderContents and DirTree below)
function fh.IntTime(strDateTime)
-- strDateTime ~ date time string
-- return value ~ integer seconds since 01/01/1970 00:00:00
local strDay,strMonth,strYear,strHour,strMin,strSec = strDateTime:match("^(%d%d)/(%d%d)/(%d+) (%d%d):(%d%d):(%d%d)")
if tonumber(strYear) < 1970 then return 0 end
local isDST = false
if tonumber(strMonth) > 4 and tonumber(strMonth) < 11 then isDST = true end -- Approximation is sometimes wrong
local intTime = os.time( { year=strYear; month=strMonth; day=strDay; hour=strHour; min=strMin; sec=strSec; isdst=isDST; } )
local tblDat = os.date("*t",intTime)
if tblDat.isdst then
intTime = intTime + 3600
isDST = true
end
return intTime
end -- function IntTime
-- Return table of attributes for folder --
local function attribFolder(tblAttr)
-- tblAttr ~ folder attributes table
-- return value ~ attributes table like LFS except datetimes
-- WINE only supports the tblAttr.name & tblAttr.path
return { mode="directory"; name=tblAttr.name; path=tblAttr.path; }
end -- local function attribFolder
-- Return table of attributes for file --
local function attribFile(tblAttr)
-- tblAttr ~ file attributes table
-- return value ~ attributes table like LFS except datetimes
-- WINE does not support the tblAttr.type, tblAttr.shortname, tblAttr.shortpath & sometimes tblAttr.datecreated
return { mode="file"; name=tblAttr.name; path=tblAttr.path; size=tblAttr.size; modified=tblAttr.datelastmodified; attributes=tblAttr.attributes; }
end -- local function attribFile
-- Return attributes table of all files and folders in a specified folder --
function fh.GetFolderContents(strFolder,doRecurse)
-- strFolder ~ full folder path
-- doRecurse ~ true for recursion
-- return value ~ attributes table
local arrList = {}
if fh.FSO:FolderExists(strFolder) then
local function getFileList(strFolder)
local tblList = fh.FSO:GetFolder(strFolder)
local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
local tblAttr = tblEnum:Next()
while tblAttr do
table.insert(arrList,attribFolder(tblAttr))
if doRecurse then getFileList(tblAttr.path) end
tblAttr = tblEnum:Next()
end
local tblEnum = luacom.GetEnumerator(tblList.Files)
local tblAttr = tblEnum:Next()
while tblAttr do
table.insert(arrList,attribFile(tblAttr))
tblAttr = tblEnum:Next()
end
end
getFileList(strFolder)
end
return arrList
end -- function GetFolderContents
-- Return a Directory Tree entry & attributes on each iteration --
function fh.DirTree(strDir,...)
-- strDir ~ full folder path
-- ... ~ list of folders to omit
-- return value ~ full path, attributes table
local arg = {...}
assert( fh.FSO:FolderExists(strDir), "directory parameter is missing or empty" )
local function yieldtree(strDir)
local tblList = fh.FSO:GetFolder(strDir)
local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
local tblAttr = tblEnum:Next()
while tblAttr do -- for _,tblAttr in luacom.pairs(tblList.SubFolders) do -- pairs not working in FH v6 so use tblEnum code
coroutine.yield(tblAttr.path,attributes(tblAttr,"directory"))
local isOK = true
for _,strOmit in ipairs (arg) do
if tblAttr.path:match(strOmit) then -- Omit tree branch
isOK = false
break
end
end
if isOK then yieldtree(tblAttr.path) end
tblAttr = tblEnum:Next()
end
local tblEnum = luacom.GetEnumerator(tblList.Files)
local tblAttr = tblEnum:Next()
while tblAttr do -- for _,tblAttr in luacom.pairs(tblList.Files) do -- pairs not working in FH v6 so use tblEnum code
coroutine.yield(tblAttr.path,attributes(tblAttr,"file"))
tblAttr = tblEnum:Next()
end
end
return coroutine.wrap(function() yieldtree(strDir) end)
end -- function DirTree
-- Detect FH V5/6 old library modules and advise removal --
function fh.DetectOldModules()
if fhGetAppVersion() > 6 then
local strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugins\\"
local arrFile = { "compat53.lua"; "ltn12.lua"; "luasql\\sqlite3.dll"; "md5.lua"; "pl\\init.lua"; "socket.lua"; "utf8.lua"; "zip.dll"; }
for _, strFile in ipairs (arrFile) do
if fh.FSO:FileExists(strPath..strFile) then
fhMessageBox("\n Detected some old FH V6 library modules. \n\nPlease remove them by running the plugin: \n\n 'Delete old FH6 Plugin Module Files' \n","MB_OK","MB_ICONEXCLAMATION")
break
end
end
end
end -- function DetectOldModules
if fhGetAppVersion() > 6 then unpack = table.unpack end
-- Invoke FH Shell Execute API --
function fh.DoExecute(strExecutable,...)
-- strExecutable ~ full path of executable
-- ... ~ parameter list and optional error reporting function
-- return value ~ true if successful else false
local arg = {...}
local errFunction = fhMessageBox
if type(arg[#arg]) == 'function' then
errFunction = arg[#arg]
table.remove(arg)
end
local isOK, intErrorCode, strErrorText = fhShellExecute(strExecutable,unpack(arg))
if not isOK then
errFunction(tostring(strErrorText).." ("..tostring(intErrorCode)..")")
end
return isOK
end -- function DoExecute
-- Obtain the Day Number for any Date Point -- -- Fix problems with invalid dates in DayNumber function
function fh.GetDayNumber(datDate)
-- datDate ~ date point
-- return value ~ day number
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
fhMessageBox("\n Get Day Number issue for date \n "..oldDate:GetDisplayText().." \n "..strError.." \n So replaced it with date \n "..newDate:GetDisplayText().." \n "..strIsBC,"MB_OK","MB_ICONEXCLAMATION")
end
return intDay
end -- function GetDayNumber
local dtpYearMin = fhNewDatePt(1000) -- Minimum year to use when earliest estimate is null
local dtpYearMax = fhNewDatePt(2000) -- Maximum year to use when latest estimate is null
function fh.GetYearToday() -- Get the Year for Today
-- return value ~ integer year today
local intYearToday = fhCallBuiltInFunction("Year",fhCallBuiltInFunction("Today"))
dtpYearMax = fhNewDatePt(intYearToday) -- Set maximum year date point
return intYearToday
end -- function GetYearToday()
local function getDeathFacts(ptrIndi) -- Iterate Death, Burial, Cremation facts
-- ptrIndi ~ pointer to individual
-- return value ~ pointer to fact
local arrFact = { "~.DEAT"; "~.BURI"; "~.CREM"; }
local intFact = 0
local ptrFact = fhNewItemPtr() -- Pointer to each fact returned to user
return function ()
while intFact < #arrFact do
intFact = intFact + 1
ptrFact = fhGetItemPtr(ptrIndi,arrFact[intFact])
if ptrFact:IsNotNull() then return ptrFact end
end
end
end -- local function getDeathFacts
-- Ensure Estimated Date EARLIEST <= LATEST <= Fact Date -- -- Fix errors in EstimatedBirth/DeathDate function
local function estimatedDates(strFunc,ptrIndi,intGens,getFact,intYrs)
-- strFunc ~ "EstimatedBirthDate" or "EstimatedDeathDate"
-- ptrIndi ~ Individual of interest
-- intGens ~ Number of generations (may be nil)
-- getFact ~ Iterator function for facts
-- intYrs ~ Years to add to After dates
-- return values ~ EARLIEST, MID, LATEST dates
intGens = intGens or 2
local dtpMin = fhCallBuiltInFunction(strFunc,ptrIndi,"EARLIEST",intGens)
local dtpMax = fhCallBuiltInFunction(strFunc,ptrIndi,"LATEST",intGens)
local dtpMid = fhNewDatePt()
if not ( dtpMin:IsNull() and dtpMax:IsNull() ) then -- Skip if both null
if dtpMax:IsNull() then dtpMax = dtpYearMax elseif dtpMin:IsNull() then dtpMin = dtpYearMin end
for ptrFact in getFact(ptrIndi) do
local datFact = fhGetValueAsDate(fhGetItemPtr(ptrFact,"~.DATE"))
if not datFact:IsNull() then -- Find 1st Fact Date
local dtpLast = datFact:GetDatePt1() -- Last date = DatePt1 for Simple, Range, and Before
local strType = datFact:GetSubtype() -- Between = DatePt2 and After = DatePt1 + intYrs
if strType == "Between" then dtpLast = datFact:GetDatePt2()
elseif strType == "After" then dtpLast = fhNewDatePt(dtpLast:GetYear()+intYrs,dtpLast:GetMonth(),dtpLast:GetDay()) end -- Compare only uses Year, Month, Day so omitted ,dtpLast:GetYearDD(),dtpLast:GetBC(),dtpLast:GetCalendar()
if dtpMax:Compare(dtpLast) > 0 then dtpMax = dtpLast end
if dtpMin:Compare(dtpMax) > 0 then dtpMin = dtpMax end
if strType ~= "After" then break end -- Now EARLIEST <= LATEST <= Last date
end
end
local intDays = ( fh.GetDayNumber(dtpMax) - fh.GetDayNumber(dtpMin) ) / 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 ) + 0.1 ) -- Offset month is remainder fraction of year * 12
dtpMid = fhCallBuiltInFunction("CalcDate",dtpMin,intYear,intMnth) -- Need approximate MID year & month
end
return { Min=dtpMin; Mid=dtpMid; Max=dtpMax; } -- Return EARLIEST, MID, LATEST dates
end -- local function estimatedDates
-- Make EstimatedBirthDate EARLIEST <= LATEST <= 1st Fact Date -- -- Fix errors in EstimatedBirthDate function
function fh.EstimatedBirthDates(ptrIndi,intGens)
-- ptrInd ~ pointer to individual
-- intGens ~ generations to include
-- return values ~ EARLIEST, MID, LATEST dates
return estimatedDates("EstimatedBirthDate",ptrIndi,intGens,iterate.Facts,10)
end -- function EstimatedBirthDates
-- Make EstimatedDeathDate EARLIEST <= LATEST <= DEAT/BURI/CREM Date -- -- Fix errors in EstimatedDeathDate function
function fh.EstimatedDeathDates(ptrIndi,intGens)
-- ptrInd ~ pointer to individual
-- intGens ~ generations to include
-- return values ~ EARLIEST, MID, LATEST dates
return estimatedDates("EstimatedDeathDate",ptrIndi,intGens,getDeathFacts,100)
end -- function EstimatedDeathDates
--[[
@function: BuildDataRef
@description: Get Full Data Reference for Pointer
@parameters: Item Pointer
@returns: Data Reference String, Record Id Integer, Record Type Tag String
@requires: None
]]
function fh.BuildDataRef(ptrRef)
local strDataRef = "" -- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
local intRecId = 0 -- Record Id for associated Record
local strRecTag = "" -- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc
-- getDataRef() is called recursively per level of the Data Ref
-- ptrRef points to the upper Data Ref levels yet to be analysed
-- strRef compiles the lower Data Ref levels including instances
local function getDataRef(ptrRef,strRef)
local ptrTag = ptrRef:Clone()
local strTag = fhGetTag(ptrTag) -- Current level Tag
ptrTag:MoveToParentItem(ptrTag)
if ptrTag:IsNotNull() then -- Parent level exists
local intSib = 1
local ptrSib = ptrRef:Clone() -- Pointer to siblings with same Tag
ptrSib:MovePrev("SAME_TAG")
while ptrSib:IsNotNull() do -- Count previous siblings with same Tag
intSib = intSib + 1
ptrSib:MovePrev("SAME_TAG")
end
if intSib > 1 then strTag = strTag.."["..intSib.."]" end
getDataRef(ptrTag,"."..strTag..strRef) -- Now analyse the parent level
else
strDataRef = strTag..strRef -- Record level reached, so set return values
intRecId = fhGetRecordId(ptrRef)
strRecTag = strTag
if not fhIsValidDataRef(strDataRef) then print("BuildDataRef: "..strDataRef.." is Invalid") end
end
end -- local function getDataRef
if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end
return strDataRef, intRecId, strRecTag
end -- function BuildDataRef
--[[
@function: GetDataRefPtr
@description: Get Pointer for Full Data Reference
@parameters: Data Reference String, Record Id Integer, Record Type Tag String (optional)
@returns: Item Pointer which IsNull() if any parameters are invalid
@requires: None
]]
function fh.GetDataRefPtr(strDataRef,intRecId,strRecTag)
strDataRef = strDataRef or ""
if not strRecTag then
strRecTag = strDataRef:gsub("^(%u+).*$","%1") -- Extract Record Tag from Data Ref
end
local ptrRef = fhNewItemPtr()
ptrRef:MoveToRecordById(strRecTag,intRecId or 0) -- Lookup the Record by Id
ptrRef:MoveTo(ptrRef,strDataRef) -- Move to the Data Ref
return ptrRef
end -- function GetDataRefPtr
function fh.TblDataRef(ptrRef)
local tblRef = {}
tblRef.DataRef, tblRef.RecId, tblRef.RecTag = BuildDataRef(ptrRef)
return tblRef
end -- function TblDataRef
function fh.PtrDataRef(tblRef)
local tblRef = tblRef or {} -- Ensure table and its fields exist
return GetDataRefPtr(tblRef.DataRef or "",tblRef.RecId or 0,tblRef.RecTag or "")
end -- function PtrDataRef
return fh
end -- local function general_v3
local general = general_v3() -- To access FH general tools module
--[[
@Module: +fh+tablex_v3
@Author: Mike Tate
@Version: 3.1
@LastUpdated: 08 Jan 2022
@Description: A Table Load Save Module.
@V3.1: Cater for full UTF-8 filenames.
@V3.0: Function Prototype Closure version.
@V1.2: Added local definitions of _ to ensure nil gets returned on error.
@V1.1: ?
@V1.0: Initial version 0.94 is Lua 5.1 compatible.
]]
local function tablex_v3()
local fh = {} -- Local environment table
------------------------------------------------------ Start Table Load Save
-- require "_tableloadsave"
--[[
Save Table to File/Stringtable
Load Table from File/Stringtable
v 0.94
Lua 5.1 compatible
Userdata and indices of these are not saved
Functions are saved via string.dump, so make sure it has no upvalues
References are saved
----------------------------------------------------
table.save( table [, filename] )
Saves a table so it can be called via the table.load function again
table must a object of type 'table'
filename is optional, and may be a string representing a filename or true/1
table.save( table )
on success: returns a string representing the table (stringtable)
(uses a string as buffer, ideal for smaller tables)
table.save( table, true or 1 )
on success: returns a string representing the table (stringtable)
(uses io.tmpfile() as buffer, ideal for bigger tables)
table.save( table, "filename" )
on success: returns 1
(saves the table to file "filename")
on failure: returns as second argument an error msg
----------------------------------------------------
table.load( filename or stringtable )
Loads a table that has been saved via the table.save function
on success: returns a previously saved table
on failure: returns as second argument an error msg
----------------------------------------------------
chillcode, http://lua-users.org/wiki/SaveTableToFile
Licensed under the same terms as Lua itself.
]]--
-- declare local variables
--// exportstring( string )
--// returns a "Lua" portable version of the string
local function exportstring( s )
s = string.format( "%q",s )
-- to replace
s = string.gsub( s,"\\\n","\\n" )
s = string.gsub( s,"\r","\\r" )
s = string.gsub( s,string.char(26),"\"..string.char(26)..\"" )
return s
end
--// The Save Function
function fh.save( tbl,filename )
local charS,charE = " ","\n"
local file,err,_,stransi,wasansi -- V1.2 -- V3.1 -- Added _,stransi,wasansi --!
-- create a pseudo file that writes to a string and return the string
if not filename then
file = { write = function( self,newstr ) self.str = self.str..newstr end, str = "" }
charS,charE = "",""
-- write table to tmpfile
elseif filename == true or filename == 1 then
charS,charE,file = "","",io.tmpfile()
-- write table to file
-- use io.open here rather than io.output, since in windows when clicking on a file opened with io.output will create an error
else
stransi,wasansi = general.FileNameToANSI(filename) -- V3.1 -- Cater for non-ANSI filename --!
file,err = io.open( stransi, "w" )
if err then return _,err end
end
-- initiate variables for save procedure
local tables,lookup = { tbl },{ [tbl] = 1 }
file:write( "return {"..charE )
for idx,t in ipairs( tables ) do
if filename and filename ~= true and filename ~= 1 then
file:write( "-- Table: {"..idx.."}"..charE )
end
file:write( "{"..charE )
local thandled = {}
for i,v in ipairs( t ) do
thandled[i] = true
-- escape functions and userdata
if type( v ) ~= "userdata" then
-- only handle value
if type( v ) == "table" then
if not lookup[v] then
table.insert( tables, v )
lookup[v] = #tables
end
file:write( charS.."{"..lookup[v].."},"..charE )
elseif type( v ) == "function" then
file:write( charS.."loadstring("..exportstring(string.dump( v )).."),"..charE )
else
local value = ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
file:write( charS..value..","..charE )
end
end
end
for i,v in pairs( t ) do
-- escape functions and userdata
if (not thandled[i]) and type( v ) ~= "userdata" then
-- handle index
if type( i ) == "table" then
if not lookup[i] then
table.insert( tables,i )
lookup[i] = #tables
end
file:write( charS.."[{"..lookup[i].."}]=" )
else
local index = ( type( i ) == "string" and "["..exportstring( i ).."]" ) or string.format( "[%d]",i )
file:write( charS..index.."=" )
end
-- handle value
if type( v ) == "table" then
if not lookup[v] then
table.insert( tables,v )
lookup[v] = #tables
end
file:write( "{"..lookup[v].."},"..charE )
elseif type( v ) == "function" then
file:write( "loadstring("..exportstring(string.dump( v )).."),"..charE )
else
local value = ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
file:write( value..","..charE )
end
end
end
file:write( "},"..charE )
end
file:write( "}" )
-- Return Values
-- return stringtable from string
if not filename then
-- set marker for stringtable
return file.str.."--|"
-- return stringttable from file
elseif filename == true or filename == 1 then
file:seek ( "set" )
-- no need to close file, it gets closed and removed automatically
-- set marker for stringtable
return file:read( "*a" ).."--|"
-- close file and return 1
else
file:close()
if not ( wasansi ) then -- V3.1 -- Cater for non-ANSI filename --!
general.MoveFile(stransi,filename)
end
return 1
end
end
--// The Load Function
function fh.load( sfile )
local tables,err,_ -- V1.2 -- Added _
-- catch marker for stringtable
if string.sub( sfile,-3,-1 ) == "--|" then
tables,err = loadstring( sfile )
else
local stransi,wasansi = general.FileNameToANSI(sfile) -- V3.1 -- Cater for non-ANSI filename --!
if not ( wasansi ) then
general.CopyFile(sfile,stransi)
end
tables,err = loadfile( stransi )
if not ( wasansi ) then
general.DeleteFile(stransi) -- V3.1 -- Cater for non-ANSI filename --!
end
end
if err then return _,err
end
tables = tables()
for idx = 1,#tables do
local tolinkv,tolinki = {},{}
for i,v in pairs( tables[idx] ) do
if type( v ) == "table" and tables[v[1]] then
table.insert( tolinkv,{ i,tables[v[1]] } )
end
if type( i ) == "table" and tables[i[1]] then
table.insert( tolinki,{ i,tables[i[1]] } )
end
end
-- link values, first due to possible changes of indices
for _,v in ipairs( tolinkv ) do
tables[idx][v[1]] = v[2]
end
-- link indices
for _,v in ipairs( tolinki ) do
tables[idx][v[2]],tables[idx][v[1]] = tables[idx][v[1]],nil
end
end
return tables[1]
end
------------------------------------------------------ End Table Load Save
-- overload fh functions into table
for strIndex, anyValue in pairs(fh) do
if type(anyValue) == "function" then
table[strIndex] = anyValue
end
end
return fh
end -- local function tablex_v3
local tablex = tablex_v3 () -- To access FH table extension module
--[[
@Module: +fh+encoder_v3
@Author: Mike Tate
@Version: 3.6
@LastUpdated: 27 Aug 2024
@Description: Text encoder module for HTML XHTML XML URI UTF8 UTF16 ISO CP1252/ANSI character codings.
@V3.6: In fh.FileLines(...) cater for empty file;
@V3.5: Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility.
@V3.4: Ensure expressions involving gsub return just text parameter.
@V3.3: Adds UNICODE U+10000 to U+10FFFF UTF-16 Supplementary Planes.
@V3.2: Update for ANSI & Unicode to ASCII for sorting, Soundex, etc.
@V3.1: Update for Unicode UTF-16 & UTF-8 and fhConvertANSItoUTF8 & fhConvertUTF8toANSI, name change UTF to UTF8 & CP to ANSI.
@V2.0: StrUTF8_Encode() replaced by StrUTF_CP1252() for entire UTF-8 range, plus new StrCP1252_ISO().
@V1.0: Initial version.
]]
local function encoder_v3()
local fh = {} -- Local environment table
local fhVersion = fhGetAppVersion()
local br_Tag = "
" -- Markup language break tag default
local br_Lua = "
" -- Lua pattern for break tag recognition
local tblCodePage = {} -- Code Page to XML/XHTML/HTML/URI/UTF8 encodings: http://en.wikipedia.org/wiki/Windows-1252 & 1250 & etc
-- Control characters "\000" to "\031" for URI & Markup "[%c]" encodings are disallowed except for "\t" to "\r"
tblCodePage["\000"] = "" -- NUL
tblCodePage["\001"] = "" -- SOH
tblCodePage["\002"] = "" -- STX
tblCodePage["\003"] = "" -- ETX
tblCodePage["\004"] = "" -- EOT
tblCodePage["\005"] = "" -- ENQ
tblCodePage["\006"] = "" -- ACK
tblCodePage["\a"] = "" -- BEL
tblCodePage["\b"] = "" -- BS
tblCodePage["\t"] = "+" -- HT space in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["\n"] = "%0A" -- LF br_Tag in Markup
tblCodePage["\v"] = "%0A" -- VT br_Tag in Markup
tblCodePage["\f"] = "%0A" -- FF br_Tag in Markup
tblCodePage["\r"] = "%0D" -- CR br_Tag in Markup
tblCodePage["\014"] = "" -- SO
tblCodePage["\015"] = "" -- SI
tblCodePage["\016"] = "" -- DLE
tblCodePage["\017"] = "" -- DC1
tblCodePage["\018"] = "" -- DC2
tblCodePage["\019"] = "" -- DC3
tblCodePage["\020"] = "" -- DC4
tblCodePage["\021"] = "" -- NAK
tblCodePage["\022"] = "" -- SYN
tblCodePage["\023"] = "" -- ETB
tblCodePage["\024"] = "" -- CAN
tblCodePage["\025"] = "" -- EM
tblCodePage["\026"] = "" -- SUB
tblCodePage["\027"] = "" -- ESC
tblCodePage["\028"] = "" -- FS
tblCodePage["\029"] = "" -- GS
tblCodePage["\030"] = "" -- RS
tblCodePage["\031"] = "" -- US
-- ASCII characters "\032" to "\127" for URI "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding
tblCodePage[" "] = "+" -- or "%20" Space
tblCodePage["!"] = "%21" -- Reserved character
tblCodePage['"'] = "%22" -- """ in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["#"] = "%23" -- Reserved character
tblCodePage["$"] = "%24" -- Reserved character
tblCodePage["%"] = "%25" -- Must be encoded
tblCodePage["&"] = "%26" -- Reserved character -- "&" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["'"] = "%27" -- Reserved character -- "'" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["("] = "%28" -- Reserved character
tblCodePage[")"] = "%29" -- Reserved character
tblCodePage["*"] = "%2A" -- Reserved character
tblCodePage["+"] = "%2B" -- Reserved character
tblCodePage[","] = "%2C" -- Reserved character
-- tblCodePage["-"] = "%2D" -- Unreserved character not encoded
-- tblCodePage["."] = "%2E" -- Unreserved character not encoded
tblCodePage["/"] = "%2F" -- Reserved character
-- Digits 0 to 9 -- Unreserved characters not encoded
tblCodePage[":"] = "%3A" -- Reserved character
tblCodePage[";"] = "%3B" -- Reserved character
tblCodePage["<"] = "%3C" -- "<" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["="] = "%3D" -- Reserved character
tblCodePage[">"] = "%3E" -- ">" in Markup see setURIEncodings() and setMarkupEncodings() below
tblCodePage["?"] = "%3F" -- Reserved character
tblCodePage["@"] = "%40" -- Reserved character
-- Letters A to Z -- Unreserved characters not encoded
tblCodePage["["] = "%5B" -- Reserved character
tblCodePage["\\"]= "%5C"
tblCodePage["]"] = "%5D" -- Reserved character
tblCodePage["^"] = "%5E"
-- tblCodePage["_"] = "%5F" -- Unreserved character not encoded
tblCodePage["`"] = "%60"
-- Letters a to z -- Unreserved characters not encoded
tblCodePage["{"] = "%7B"
tblCodePage["|"] = "%7C"
tblCodePage["}"] = "%7D"
-- tblCodePage["~"] = "%7E" -- Unreserved character not encoded
tblCodePage["\127"] = "" -- DEL
-- Code Page 1252 Unicode characters "\128" to "\255" for UTF-8 scheme "[€-ÿ]" encodings: http://en.wikipedia.org/wiki/UTF-8
tblCodePage["€"] = string.char(0xE2,0x82,0xAC) -- "€"
tblCodePage["\129"] = "" -- Undefined
tblCodePage["‚"] = string.char(0xE2,0x80,0x9A)
tblCodePage["ƒ"] = string.char(0xC6,0x92)
tblCodePage["„"] = string.char(0xE2,0x80,0x9E)
tblCodePage["…"] = string.char(0xE2,0x80,0xA6)
tblCodePage["†"] = string.char(0xE2,0x80,0xA0)
tblCodePage["‡"] = string.char(0xE2,0x80,0xA1)
tblCodePage["ˆ"] = string.char(0xCB,0x86)
tblCodePage["‰"] = string.char(0xE2,0x80,0xB0)
tblCodePage["Š"] = string.char(0xC5,0xA0)
tblCodePage["‹"] = string.char(0xE2,0x80,0xB9)
tblCodePage["Œ"] = string.char(0xC5,0x92)
tblCodePage["\141"] = "" -- Undefined
tblCodePage["Ž"] = string.char(0xC5,0xBD)
tblCodePage["\143"] = "" -- Undefined
tblCodePage["\144"] = "" -- Undefined
tblCodePage["‘"] = string.char(0xE2,0x80,0x98)
tblCodePage["’"] = string.char(0xE2,0x80,0x99)
tblCodePage["“"] = string.char(0xE2,0x80,0x9C)
tblCodePage["”"] = string.char(0xE2,0x80,0x9D)
tblCodePage["•"] = string.char(0xE2,0x80,0xA2)
tblCodePage["–"] = string.char(0xE2,0x80,0x93)
tblCodePage["—"] = string.char(0xE2,0x80,0x94)
tblCodePage["\152"] = string.char(0xCB,0x9C) -- Small Tilde
tblCodePage["™"] = string.char(0xE2,0x84,0xA2)
tblCodePage["š"] = string.char(0xC5,0xA1)
tblCodePage["›"] = string.char(0xE2,0x80,0xBA)
tblCodePage["œ"] = string.char(0xC5,0x93)
tblCodePage["\157"] = "" -- Undefined
tblCodePage["ž"] = string.char(0xC5,0xBE)
tblCodePage["Ÿ"] = string.char(0xC5,0xB8)
tblCodePage["\160"] = string.char(0xC2,0xA0) -- " " No Break Space
tblCodePage["¡"] = string.char(0xC2,0xA1) -- "¡"
tblCodePage["¢"] = string.char(0xC2,0xA2) -- "¢"
tblCodePage["£"] = string.char(0xC2,0xA3) -- "£"
tblCodePage["¤"] = string.char(0xC2,0xA4) -- "¤"
tblCodePage["¥"] = string.char(0xC2,0xA5) -- "¥"
tblCodePage["¦"] = string.char(0xC2,0xA6)
tblCodePage["§"] = string.char(0xC2,0xA7)
tblCodePage["¨"] = string.char(0xC2,0xA8)
tblCodePage["©"] = string.char(0xC2,0xA9)
tblCodePage["ª"] = string.char(0xC2,0xAA)
tblCodePage["«"] = string.char(0xC2,0xAB)
tblCodePage["¬"] = string.char(0xC2,0xAC)
tblCodePage[""] = string.char(0xC2,0xAD) -- "" Soft Hyphen
tblCodePage["®"] = string.char(0xC2,0xAE)
tblCodePage["¯"] = string.char(0xC2,0xAF)
tblCodePage["°"] = string.char(0xC2,0xB0)
tblCodePage["±"] = string.char(0xC2,0xB1)
tblCodePage["²"] = string.char(0xC2,0xB2)
tblCodePage["³"] = string.char(0xC2,0xB3)
tblCodePage["´"] = string.char(0xC2,0xB4)
tblCodePage["µ"] = string.char(0xC2,0xB5)
tblCodePage["¶"] = string.char(0xC2,0xB6)
tblCodePage["·"] = string.char(0xC2,0xB7)
tblCodePage["¸"] = string.char(0xC2,0xB8)
tblCodePage["¹"] = string.char(0xC2,0xB9)
tblCodePage["º"] = string.char(0xC2,0xBA)
tblCodePage["»"] = string.char(0xC2,0xBB)
tblCodePage["¼"] = string.char(0xC2,0xBC)
tblCodePage["½"] = string.char(0xC2,0xBD)
tblCodePage["¾"] = string.char(0xC2,0xBE)
tblCodePage["¿"] = string.char(0xC2,0xBF)
tblCodePage["À"] = string.char(0xC3,0x80)
tblCodePage["Á"] = string.char(0xC3,0x81)
tblCodePage["Â"] = string.char(0xC3,0x82)
tblCodePage["Ã"] = string.char(0xC3,0x83)
tblCodePage["Ä"] = string.char(0xC3,0x84)
tblCodePage["Å"] = string.char(0xC3,0x85)
tblCodePage["Æ"] = string.char(0xC3,0x86)
tblCodePage["Ç"] = string.char(0xC3,0x87)
tblCodePage["È"] = string.char(0xC3,0x88)
tblCodePage["É"] = string.char(0xC3,0x89)
tblCodePage["Ê"] = string.char(0xC3,0x8A)
tblCodePage["Ë"] = string.char(0xC3,0x8B)
tblCodePage["Ì"] = string.char(0xC3,0x8C)
tblCodePage["Í"] = string.char(0xC3,0x8D)
tblCodePage["Î"] = string.char(0xC3,0x8E)
tblCodePage["Ï"] = string.char(0xC3,0x8F)
tblCodePage["Ð"] = string.char(0xC3,0x90)
tblCodePage["Ñ"] = string.char(0xC3,0x91)
tblCodePage["Ò"] = string.char(0xC3,0x92)
tblCodePage["Ó"] = string.char(0xC3,0x93)
tblCodePage["Ô"] = string.char(0xC3,0x94)
tblCodePage["Õ"] = string.char(0xC3,0x95)
tblCodePage["Ö"] = string.char(0xC3,0x96)
tblCodePage["×"] = string.char(0xC3,0x97)
tblCodePage["Ø"] = string.char(0xC3,0x98)
tblCodePage["Ù"] = string.char(0xC3,0x99)
tblCodePage["Ú"] = string.char(0xC3,0x9A)
tblCodePage["Û"] = string.char(0xC3,0x9B)
tblCodePage["Ü"] = string.char(0xC3,0x9C)
tblCodePage["Ý"] = string.char(0xC3,0x9D)
tblCodePage["Þ"] = string.char(0xC3,0x9E)
tblCodePage["ß"] = string.char(0xC3,0x9F)
tblCodePage["à"] = string.char(0xC3,0xA0)
tblCodePage["á"] = string.char(0xC3,0xA1)
tblCodePage["â"] = string.char(0xC3,0xA2)
tblCodePage["ã"] = string.char(0xC3,0xA3)
tblCodePage["ä"] = string.char(0xC3,0xA4)
tblCodePage["å"] = string.char(0xC3,0xA5)
tblCodePage["æ"] = string.char(0xC3,0xA6)
tblCodePage["ç"] = string.char(0xC3,0xA7)
tblCodePage["è"] = string.char(0xC3,0xA8)
tblCodePage["é"] = string.char(0xC3,0xA9)
tblCodePage["ê"] = string.char(0xC3,0xAA)
tblCodePage["ë"] = string.char(0xC3,0xAB)
tblCodePage["ì"] = string.char(0xC3,0xAC)
tblCodePage["í"] = string.char(0xC3,0xAD)
tblCodePage["î"] = string.char(0xC3,0xAE)
tblCodePage["ï"] = string.char(0xC3,0xAF)
tblCodePage["ð"] = string.char(0xC3,0xB0)
tblCodePage["ñ"] = string.char(0xC3,0xB1)
tblCodePage["ò"] = string.char(0xC3,0xB2)
tblCodePage["ó"] = string.char(0xC3,0xB3)
tblCodePage["ô"] = string.char(0xC3,0xB4)
tblCodePage["õ"] = string.char(0xC3,0xB5)
tblCodePage["ö"] = string.char(0xC3,0xB6)
tblCodePage["÷"] = string.char(0xC3,0xB7)
tblCodePage["ø"] = string.char(0xC3,0xB8)
tblCodePage["ù"] = string.char(0xC3,0xB9)
tblCodePage["ú"] = string.char(0xC3,0xBA)
tblCodePage["û"] = string.char(0xC3,0xBB)
tblCodePage["ü"] = string.char(0xC3,0xBC)
tblCodePage["ý"] = string.char(0xC3,0xBD)
tblCodePage["þ"] = string.char(0xC3,0xBE)
tblCodePage["ÿ"] = string.char(0xC3,0xBF)
-- Set XML/XHTML/HTML "[%c\"&'<>]" Markup encodings: http://en.wikipedia.org/wiki/XML and http://en.wikipedia.org/wiki/HTML
local function setMarkupEncodings()
tblCodePage["\t"] = " " -- HT "\t" to "\r" are treated as white space in Markup Languages by default
tblCodePage["\n"] = br_Tag -- LF
tblCodePage["\v"] = br_Tag -- VT line break tag "
" or "
" or "
" or "
" is better
tblCodePage["\f"] = br_Tag -- FF
tblCodePage["\r"] = br_Tag -- CR
tblCodePage['"'] = """
tblCodePage["&"] = "&"
tblCodePage["'"] = "'"
tblCodePage["<"] = "<"
tblCodePage[">"] = ">"
end -- local function setMarkupEncodings
-- Set URI/URL/URN "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding
local function setURIEncodings()
tblCodePage["\t"] = "+" -- HT space
tblCodePage["\n"] = "%0A" -- LF newline
tblCodePage["\v"] = "%0A" -- VT newline
tblCodePage["\f"] = "%0A" -- FF newline
tblCodePage["\r"] = "%0D" -- CR return
tblCodePage['"'] = "%22"
tblCodePage["&"] = "%26"
tblCodePage["'"] = "%27"
tblCodePage["<"] = "%3C"
tblCodePage[">"] = "%3E"
end -- local function setURIEncodings
-- Encode characters according to gsub pattern & lookup table --
local function strEncode(strText,strPattern,tblPattern)
return ( (strText or ""):gsub(strPattern,tblPattern) ) -- V3.4
end -- local function strEncode
-- Encode CP1252/ANSI characters into UTF-8 codes --
function fh.StrANSI_UTF8(strText)
if fhVersion > 5 then
strText = fhConvertANSItoUTF8(strText)
else
strText = strEncode(strText,"[\127-ÿ]",tblCodePage)
end
return strText
end -- function StrANSI_UTF8
function fh.StrCP_UTF(strText) -- Legacy
return fh.StrANSI_UTF8(strText)
end -- function StrCP1252_UTF8
function fh.StrCP1252_UTF(strText) -- Legacy
return fh.StrANSI_UTF8(strText)
end -- function StrCP1252_UTF
-- Encode CP1252/ANSI or UTF-8 characters into UTF-8 --
function fh.StrEncode_UTF8(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_UTF8(strText)
else
return strText
end
end -- function StrEncode_UTF8
-- Encode CP1252/ANSI characters into XML/XHTML/HTML/UTF8 codes --
local strANSI_XML = "[%z\001-\031\"&'<>\127-ÿ]"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strANSI_XML = "[\000-\031\"&'<>\127-ÿ]"
end
function fh.StrANSI_XML(strText)
setMarkupEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes br_Tag
strText = strEncode(strText,strANSI_XML,tblCodePage)
return strText
end -- function StrANSI_XML
function StrCP_XML(strText) -- Legacy
return fh.StrANSI_XML(strText)
end -- function StrCP_XML
function StrCP1252_XML(strText) -- Legacy
return fh.StrANSI_XML(strText)
end -- function StrCP1252_XML
-- Encode UTF-8 ASCII characters into XML/XHTML/HTML codes --
local strUTF8_XML = "[%z\001-\031\"&'<>\127]"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strUTF8_XML = "[\000-\031\"&'<>\127]"
end
function fh.StrUTF8_XML(strText)
setMarkupEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes br_Tag
strText = strEncode(strText,strUTF8_XML,tblCodePage)
return strText
end -- function StrUTF8_XML
-- Encode CP1252/ANSI or UTF-8 ASCII characters into XML/XHTML/HTML codes --
function fh.StrEncode_XML(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_XML(strText)
else
return fh.StrUTF8_XML(strText)
end
end -- function StrEncode_XML
-- Encode Item Text characters into XML/HTML/UTF-8 codes --
function fh.StrGetItem_XML(ptrItem,strTags)
return fh.StrEncode_XML(fhGetItemText(ptrItem,strTags))
end -- function StrGetItem_XML
-- Encode CP1252/ANSI characters into URI codes --
function fh.StrANSI_URI(strText)
setURIEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes %0A
strText = strEncode(strText,"[^0-9A-Za-z]",tblCodePage)
return strText
end -- function StrANSI_URI
function fh.StrCP_URI(strText)
return fh.StrANSI_URI(strText)
end -- function StrCP_URI
function fh.StrCP1252_URI(strText)
return fh.StrANSI_URI(strText)
end -- function StrCP1252_URI
-- Encode UTF-8 ASCII characters into URI codes --
local strUTF8_URI = "[%z\001-\127]"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strUTF8_URI = "[\000-\127]"
end
function fh.StrUTF8_URI(strText)
setURIEncodings()
strText = (strText or ""):gsub(br_Lua,"\n") -- Convert
&
&
&
to \n that becomes br_Tag
strText = strEncode(strText,strUTF8_URI,tblCodePage)
return strText
end -- function StrUTF8_URI
-- Encode CP1252/ANSI or UTF-8 ASCII characters into URI codes --
function fh.StrEncode_URI(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_URI(strText)
else
return fh.StrUTF8_URI(strText)
end
end -- function StrEncode_URI
function fh.StrUTF8_Encode(strText) -- Legacy from V1.0
return fh.StrUTF8_ANSI(strText)
end -- function StrUTF8_Encode
-- Encode UTF-8 bytes into single CP1252/ANSI character V2.0 upvalues --
local strByteRange = "["..string.char(0xC0).."-"..string.char(0xFF).."]"
local tblBytePoint = {0xC0;0xE0;0xF0;0xF8;0xFC;} -- Byte codes for 2-byte, 3-byte, 4-byte, 5-byte, 6-byte UTF-8
local tblUTF8 = {}
for strByte = string.byte("€"), string.byte("ÿ") do
local strChar = string.char(strByte) -- Use CodePage to UTF-8 table to populate UTF-8 to CodePage table
local strCode = tblCodePage[strChar]
tblUTF8[strCode] = strChar
end
-- Encode UTF-8 bytes into single CP1252/ANSI character --
function fh.StrUTF8_ANSI(strText)
strText = strText or ""
if fhVersion > 5 then return fhConvertUTF8toANSI(strText) end
if strText:match(strByteRange) then -- If text contains characters that need translating then
local intChar = 0 -- Input character index
local strChar = "" -- Current character
local strCode = "" -- UTF-8 multi-byte code
local tblLine = {} -- Translated output line
repeat
intChar = intChar + 1 -- Step through each character in text
strChar = strText:sub(intChar,intChar)
if strChar:match(strByteRange) then -- Convert UTF-8 bytes into CP character
strCode = strChar -- First UTF-8 byte code, whose top bits say how many bytes to append
for intByte, strByte in ipairs(tblBytePoint) do
if string.byte(strChar) >= strByte then
intChar = intChar + 1 -- Append next UTF-8 byte code character
strCode = strCode..strText:sub(intChar,intChar)
else
break
end
end
strChar = tblUTF8[strCode] or "¿" -- Translate UTF-8 code into CP character
end
table.insert(tblLine,strChar) -- Accumulate output char by char
until intChar >= #strText
strText = table.concat(tblLine)
end
return strText
end -- function StrUTF8_ANSI
function fh.StrUTF_CP(strText) -- Legacy
return fh.StrUTF8_ANSI(strText)
end -- function StrUTF_CP
function fh.StrUTF_CP1252(strText) -- Legacy
return fh.StrUTF8_ANSI(strText)
end -- function StrUTF_CP1252
-- Encode CP1252/ANSI or UTF-8 characters into ANSI --
function fh.StrEncode_ANSI(strText)
if stringx.encoding() == "ANSI" then
return strText or ""
else
return fh.StrUTF8_ANSI(strText)
end
end -- function StrEncode_ANSI
-- Set ISO-8859-1 "[\127-Ÿ]" encodings: http://en.wikipedia.org/wiki/ISO/IEC_8859-1
local tblISO8859 = { }
tblISO8859["\127"]="" -- DEL
tblISO8859["€"] = "EUR"
tblISO8859["\129"]="" -- Undefined
tblISO8859["‚"] = "¸"
tblISO8859["ƒ"] = "f"
tblISO8859["„"] = "¸¸"
tblISO8859["…"] = "..."
tblISO8859["†"] = "+"
tblISO8859["‡"] = "±"
tblISO8859["ˆ"] = "^"
tblISO8859["‰"] = "%"
tblISO8859["Š"] = "S"
tblISO8859["‹"] = "<"
tblISO8859["Œ"] = "OE"
tblISO8859["\141"]="" -- Undefined
tblISO8859["Ž"] = "Z"
tblISO8859["\143"]="" -- Undefined
tblISO8859["\144"]="" -- Undefined
tblISO8859["‘"] = "'"
tblISO8859["’"] = "'"
tblISO8859["“"] = '"'
tblISO8859["”"] = '"'
tblISO8859["•"] = "º"
tblISO8859["–"] = "-"
tblISO8859["—"] = "-"
tblISO8859["\152"]="~" -- Small Tilde
tblISO8859["™"] = "TM"
tblISO8859["š"] = "s"
tblISO8859["›"] = ">"
tblISO8859["œ"] = "oe"
tblISO8859["\157"]="" -- Undefined
tblISO8859["ž"] = "z"
tblISO8859["Ÿ"] = "Y"
-- Encode CP1252/ANSI characters into ISO-8859-1 codes --
function fh.StrANSI_ISO(strText)
return strEncode(strText,"[\127-Ÿ]",tblISO8859)
end -- function StrANSI_ISO
function fh.StrCP_ISO(strText) -- Legacy
return fh.StrANSI_ISO(strText)
end -- function StrCP_ISO
function fh.StrCP1252_ISO(strText) -- Legacy
return fh.StrANSI_ISO(strText)
end -- function StrCP1252_ISO
function fh.StrUTF8_ISO(strText)
return fh.StrANSI_ISO(fh.StrUTF8_ANSI(strText))
end -- function StrUTF8_ISO
-- Encode CP1252/ANSI or UTF-8 ASCII characters into ISO-8859-1 codes --
function fh.StrEncode_ISO(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_ISO(strText)
else
return fh.StrUTF8_ISO(strText)
end
end -- function StrEncode_ISO
-- Convert UTF-8 bytes to a UTF-16 word or pair --
local tblByte = {}
local tblLead = { 0x80; 0xC0; 0xE0; 0xF0; 0xF8; 0xFC; }
function fh.StrUtf8toUtf16(strChar)
-- Convert any UTF-8 multibytes to UTF-16 --
local function strUtf8()
if #tblByte > 0 then
local intUtf16 = 0
for intIndex, intByte in ipairs (tblByte) do -- Convert UTF-8 bytes to UNICODE U+0080 to U+10FFFF
if intIndex == 1 then
intUtf16 = intByte - tblLead[#tblByte]
else
intUtf16 = intUtf16 * 0x40 + intByte - 0x80
end
end
if intUtf16 > 0xFFFF then -- U+10000 to U+10FFFF Supplementary Planes -- V2.6
tblByte = {}
intUtf16 = intUtf16 - 0x10000
local intLow10 = 0xDC00 + ( intUtf16 % 0x400 ) -- Low 16-bit Surrogate
local intTop10 = 0xD800 + math.floor( intUtf16 / 0x400 ) -- High 16-bit Surrogate
local intChar1 = intTop10 % 0x100
local intChar2 = math.floor( intTop10 / 0x100 )
local intChar3 = intLow10 % 0x100
local intChar4 = math.floor( intLow10 / 0x100 )
return string.char(intChar1,intChar2,intChar3,intChar4) -- Surrogate 16-bit Pair
end
if intUtf16 < 0xD800 -- U+0080 to U+FFFF (except U+D800 to U+DFFF) -- V2.6
or intUtf16 > 0xDFFF then -- Basic Multilingual Plane
tblByte = {}
local intChar1 = intUtf16 % 0x100
local intChar2 = math.floor( intUtf16 / 0x100 )
return string.char(intChar1,intChar2) -- BPL 16-bit
end
local strUtf8 = "" -- U+D800 to U+DFFF Reserved Code Points -- V2.6
for intIndex, intByte in ipairs (tblByte) do
strUtf8 = strUtf8..string.format("%.2X ",intByte)
end
local strUtf16 = string.format("%.4X ",intUtf16)
fhMessageBox("\n UTF-16 Reserved Code Point U+D800 to U+DFFF \n UTF-16 = "..strUtf16.." UTF-8 = "..strUtf8.."\n Character will be replaced by a question mark. \n","MB_OK","MB_ICONEXCLAMATION")
tblByte = {}
return "?\0"
end
return ""
end -- local function strUtf8
local intUtf8 = string.byte(strChar)
if intUtf8 < 0x80 then -- U+0000 to U+007F (ASCII)
return strUtf8()..strChar.."\0" -- Previous UTF-8 multibytes + current ASCII char
end
if intUtf8 >= 0xC0 then -- Next UTF-8 multibyte start
local strUtf16 = strUtf8()
table.insert(tblByte,intUtf8)
return strUtf16 -- Previous UTF-8 multibytes
end
table.insert(tblByte,intUtf8)
return ""
end -- function StrUtf8toUtf16
-- Encode UTF-8 bytes into UTF-16 words --
function fh.StrUTF8_UTF16(strText)
tblByte = {} -- (0xFF) flushes last UTF-8 character
return ( ((strText or "")..string.char(0xFF)):gsub("(.)",fh.StrUtf8toUtf16) ) -- V3.4
end -- function StrUTF8_UTF16
-- Encode CP1252/ANSI or UTF-8 characters into UTF-16 words --
function fh.StrEncode_UTF16(strText)
if stringx.encoding() == "ANSI" then
strText = fh.StrANSI_UTF8(strText)
end
return fh.StrUTF8_UTF16(strText)
end -- function StrEncode_UTF16
local intTop10 = 0
-- Convert a UTF-16 word or pair to UTF-8 bytes --
function fh.StrUtf16toUtf8(strChar1,strChar2)
local intUtf16 = string.byte(strChar2) * 0x100 + string.byte(strChar1)
if intUtf16 < 0x80 then -- U+0000 to U+007F (ASCII)
return string.char(intUtf16)
end
if intUtf16 < 0x800 then -- U+0080 to U+07FF
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16
return string.char( intByte2 + 0xC0, intByte1 + 0x80 )
end
if intUtf16 < 0xD800 -- U+0800 to U+FFFF
or intUtf16 > 0xDFFF then
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16
return string.char( intByte3 + 0xE0, intByte2 + 0x80, intByte1 + 0x80 )
end
if intUtf16 < 0xDC00 then -- U+10000 to U+10FFFF High 16-bit Surrogate Supplementary Planes -- V2.6
intTop10 = ( intUtf16 - 0xD800 ) * 0x400 + 0x10000
return ""
end
intUtf16 = intUtf16 - 0xDC00 + intTop10 -- U+10000 to U+10FFFF Low 16-bit Surrogate Supplementary Planes -- V2.6
local intByte1 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte2 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte3 = intUtf16 % 0x40
intUtf16 = math.floor( intUtf16 / 0x40 )
local intByte4 = intUtf16
return string.char( intByte4 + 0xF0, intByte3 + 0x80, intByte2 + 0x80, intByte1 + 0x80 )
end -- function StrUtf16toUtf8
-- Encode UTF-16 words into UTF-8 bytes --
function fh.StrUTF16_UTF8(strText)
return ( (strText or ""):gsub("(.)(.)",fh.StrUtf16toUtf8) ) -- V3.4
end -- function StrUTF16_UTF8
-- Encode UTF-16 words into ANSI characters --
function fh.StrUTF16_ANSI(strText)
return fh.StrUTF8_ANSI(fh.StrUTF16_UTF8(strText))
end -- function StrUTF16_ANSI
-- Read UTF-16/UTF-8/ANSI file converted to chosen encoding via line iterator --
local strUtf16 = "^.%z"
if fhVersion > 6 then -- Cater for Lua 5.1 %z or Lua 5.3 \0
strUtf16 = "^.\0"
end
function fh.FileLines(strFileName,strEncoding) -- Derived from http://lua-users.org/wiki/EnhancedFileLines
local bomUtf16= "^"..string.char(0xFF,0xFE) -- "ÿþ"
local bomUtf8 = "^"..string.char(0xEF,0xBB,0xBF) -- ""
local fncConv = tostring -- Function to convert input to current encoding
local intHead = 1 -- Index to start of current text line
local intLump = 1024
local fHandle = general.OpenFile(strFileName,"rb")
local strText = fHandle:read(1024) or "" -- Read first lump from file and cater for empty file
local intBOM = 0
strEncoding = strEncoding or string.encoding()
if strText:match(bomUtf16)
or strText:match(strUtf16) then
strText,intBOM = strText:gsub(bomUtf16,"") -- Strip UTF-16 BOM
if strEncoding == "ANSI" then -- Define UTF-16 conversion to current encoding
fncConv = fh.StrUTF16_ANSI
else
fncConv = fh.StrUTF16_UTF8
end
elseif strText:match(bomUtf8) then
strText,intBOM = strText:gsub(bomUtf8,"") -- Strip UTF-8 BOM
if strEncoding == "ANSI" then -- Define UTF-8 conversion to current encoding
fncConv = fh.StrUTF8_ANSI
end
else
if strEncoding == "UTF-8" and #strText > 0 then -- Define ANSI conversion to current encoding and cater for empty file
fncConv = fh.StrANSI_UTF8
end
end
strText = fncConv(strText) -- Convert first lump of text
return function() -- Iterator function
local intTail,strTail -- Index to end of current text line, and terminating characters
while true do
intTail, strTail = strText:match("()([\r\n].)",intHead)
if intTail or not fHandle then
if intHead > 1 then intLump = 0 end
break -- End of line or end of file
elseif fHandle then
local strLump = fHandle:read(1024) -- Read next lump from file
if strLump then -- Strip old text and add converted lump
strText = strText:sub(intHead)..fncConv(strLump)
intHead = 1
intLump = 1024
else
assert(fHandle:close()) -- End of file
fHandle = nil
end
end
end
if not intTail then
intTail = #strText -- Last fragment of file
elseif strTail == "\r\n" then
intTail = intTail + 1 -- Adjust tail for both \r & \n
end
local strLine = strText:sub(intHead,intTail) -- Extract line from text
intHead = intTail + 1
if #strLine > 0 then -- Return pruned line, tail chars, lump bytes read
local strBody, strTail = strLine:match("^(.-)([\r\n]+)$")
return strBody, strTail, intLump
end
end
end -- function FileLines
-- Set "[€-ÿ]" ASCII encodings same as Unidecode below
local tblASCII = { }
tblASCII["€"] = "=E"
tblASCII["\129"]="" -- Undefined
tblASCII["‚"] = ","
tblASCII["ƒ"] = "f"
tblASCII["„"] = ",,"
tblASCII["…"] = "..."
tblASCII["†"] = "|+"
tblASCII["‡"] = "|++"
tblASCII["ˆ"] = "^"
tblASCII["‰"] = "%0"
tblASCII["Š"] = "S"
tblASCII["‹"] = "<"
tblASCII["Œ"] = "OE"
tblASCII["\141"]="" -- Undefined
tblASCII["Ž"] = "Z"
tblASCII["\143"]="" -- Undefined
tblASCII["\144"]="" -- Undefined
tblASCII["‘"] = "'"
tblASCII["’"] = "'"
tblASCII["“"] = "\""
tblASCII["”"] = "\""
tblASCII["•"] = "*"
tblASCII["–"] = "-"
tblASCII["—"] = "--"
tblASCII["\152"]="~" -- Small Tilde
tblASCII["™"] = "TM"
tblASCII["š"] = "s"
tblASCII["›"] = ">"
tblASCII["œ"] = "oe"
tblASCII["\157"]="" -- Undefined
tblASCII["ž"] = "z"
tblASCII["Ÿ"] = "Y"
tblASCII["\160"]=" " -- " " No Break Space
tblASCII["¡"] = "!" -- "¡"
tblASCII["¢"] = "=c" -- "¢"
tblASCII["£"] = "=L" -- "£"
tblASCII["¤"] = "=$" -- "¤"
tblASCII["¥"] = "=Y" -- "¥"
tblASCII["¦"] = "|"
tblASCII["§"] = "=SS"
tblASCII["¨"] = "\""
tblASCII["©"] = "(C)"
tblASCII["ª"] = "a"
tblASCII["«"] = "<<"
tblASCII["¬"] = "-"
tblASCII[""] = "-" -- "" Soft Hyphen
tblASCII["®"] = "(R)"
tblASCII["¯"] = "-"
tblASCII["°"] = "=o"
tblASCII["±"] = "+-"
tblASCII["²"] = "2"
tblASCII["³"] = "3"
tblASCII["´"] = "'"
tblASCII["µ"] = "=u"
tblASCII["¶"] = "=p"
tblASCII["·"] = "*"
tblASCII["¸"] = ","
tblASCII["¹"] = "1"
tblASCII["º"] = "o"
tblASCII["»"] = ">>"
tblASCII["¼"] = "1/4"
tblASCII["½"] = "1/2"
tblASCII["¾"] = "3/4"
tblASCII["¿"] = "?"
tblASCII["À"] = "A"
tblASCII["Á"] = "A"
tblASCII["Â"] = "A"
tblASCII["Ã"] = "A"
tblASCII["Ä"] = "A"
tblASCII["Å"] = "A"
tblASCII["Æ"] = "AE"
tblASCII["Ç"] = "C"
tblASCII["È"] = "E"
tblASCII["É"] = "E"
tblASCII["Ê"] = "E"
tblASCII["Ë"] = "E"
tblASCII["Ì"] = "I"
tblASCII["Í"] = "I"
tblASCII["Î"] = "I"
tblASCII["Ï"] = "I"
tblASCII["Ð"] = "D"
tblASCII["Ñ"] = "N"
tblASCII["Ò"] = "O"
tblASCII["Ó"] = "O"
tblASCII["Ô"] = "O"
tblASCII["Õ"] = "O"
tblASCII["Ö"] = "O"
tblASCII["×"] = "*"
tblASCII["Ø"] = "O"
tblASCII["Ù"] = "U"
tblASCII["Ú"] = "U"
tblASCII["Û"] = "U"
tblASCII["Ü"] = "U"
tblASCII["Ý"] = "Y"
tblASCII["Þ"] = "TH"
tblASCII["ß"] = "ss"
tblASCII["à"] = "a"
tblASCII["á"] = "a"
tblASCII["â"] = "a"
tblASCII["ã"] = "a"
tblASCII["ä"] = "a"
tblASCII["å"] = "a"
tblASCII["æ"] = "ae"
tblASCII["ç"] = "c"
tblASCII["è"] = "e"
tblASCII["é"] = "e"
tblASCII["ê"] = "e"
tblASCII["ë"] = "e"
tblASCII["ì"] = "i"
tblASCII["í"] = "i"
tblASCII["î"] = "i"
tblASCII["ï"] = "i"
tblASCII["ð"] = "d"
tblASCII["ñ"] = "n"
tblASCII["ò"] = "o"
tblASCII["ó"] = "o"
tblASCII["ô"] = "o"
tblASCII["õ"] = "o"
tblASCII["ö"] = "o"
tblASCII["÷"] = "/"
tblASCII["ø"] = "o"
tblASCII["ù"] = "u"
tblASCII["ú"] = "u"
tblASCII["û"] = "u"
tblASCII["ü"] = "u"
tblASCII["ý"] = "y"
tblASCII["þ"] = "th"
tblASCII["ÿ"] = "y"
-- Encode CP1252/ANSI characters into ASCII codes [\000-\127] --
function fh.StrANSI_ASCII(strText)
return strEncode(strText,"[€-ÿ]",tblASCII)
end -- function StrANSI_ASCII
--[=[
Unidecode converts each codepoint into a few ASCII characters.
Lookup table indexed by codepoint [0x0000]-[0xFFFF] gives an ASCII string.
i.e. strASCII = Unidecode[intByte2][intByte1] or "=?" allowing for partially populated table.
See http://search.cpan.org/dist/Text-Unidecode/ and follow Browse to:
See http://cpansearch.perl.org/src/SBURKE/Text-Unidecode-1.22/lib/Text/Unidecode/
where each x??.pm gives 256 ASCII conversions.
Start with the first few European accented characters, and add the others later.
--]=]
local Unidecode = { }
function fh.StrUnidecode(strChar1,strChar2) -- Decode UTF-16 byte pair into ASCII characters
return Unidecode[string.byte(strChar2)][string.byte(strChar1)] or "=?"
end -- function StrUnidecode
-- Encode UTF-8 characters into ASCII codes [\000-\126] --
function fh.StrUTF8_ASCII(strText)
strText = fh.StrUTF8_UTF16(strText) -- Convert to UTF-16 Unicode and then to ASCII
return ( strText:gsub("(.)(.)",fh.StrUnidecode) )
end -- function StrUTF8_ASCII
-- Encode CP1252/ANSI or UTF-8 into ASCII codes [\000-\126] --
function fh.StrEncode_ASCII(strText)
if stringx.encoding() == "ANSI" then
return fh.StrANSI_ASCII(strText)
else
return fh.StrUTF8_ASCII(strText)
end
end -- function StrEncode_ASCII
-- Set markup language break tag --
function fh.SetBreakTag(br_New)
if not (br_New or ""):match(br_Lua) then -- Ensure new break tag is "
" or "
" or "
" or "
"
br_New = "
"
end
br_Tag = br_New
end -- function SetBreakTag
for intByte = 0x00, 0xFF do Unidecode[intByte] = { } end
Unidecode[0x00] =
{[0]="\00";"\01";"\02";"\03";"\04";"\05";"\06";"\a";"\b";"\t";"\n";"\v";"\f";"\r";"\14";"\15";"\16";"\17";"\18";"\19";"\20";"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\30";"\31";
" ";"!";'"';"#";"$";"%";"&";"'";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"?"; -- 0x20 to 0x3F
"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_"; -- 0x40 to 0x5F
"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~";"\127"; -- 0x60 to 0x7F
""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; -- 0x80 to 0x9F
" ";"!";"=c";"=L";"=$";"=Y";"|";"=SS";'"';"(C)";"a";"<<";"-";"-";"(R)";"-";"=o";"+-";"2";"3";"'";"=u";"=P";"*";",";"1";"o";">>";"1/4";"1/2";"3/4";"?"; -- 0xA0 to 0xBF
"A";"A";"A";"A";"A";"A";"AE";"C";"E";"E";"E";"E";"I";"I";"I";"I";"D";"N";"O";"O";"O";"O";"O";"*";"O";"U";"U";"U";"U";"Y";"TH";"ss"; -- 0xC0 to 0xDF
"a";"a";"a";"a";"a";"a";"ae";"c";"e";"e";"e";"e";"i";"i";"i";"i";"d";"n";"o";"o";"o";"o";"o";"/";"o";"u";"u";"u";"u";"y";"th";"y"; -- 0xE0 to 0xFF
}
Unidecode[0x01] =
{[0]="A";"a";"A";"a";"A";"a";"C";"c";"C";"c";"C";"c";"C";"c";"D";"d";"D";"d";"E";"e";"E";"e";"E";"e";"E";"e";"E";"e";"G";"g";"G";"g"; -- 0x00 to 0x1F
"G";"g";"G";"g";"H";"h";"H";"h";"I";"i";"I";"i";"I";"i";"I";"i";"I";"i";"IJ";"ij";"J";"j";"K";"k";"k";"L";"l";"L";"l";"L";"l";"L"; -- 0x20 to 0x3F
"l";"L";"l";"N";"n";"N";"n";"N";"n";"'n";"ng";"NG";"O";"o";"O";"o";"O";"o";"OE";"oe";"R";"r";"R";"r";"R";"r";"S";"s";"S";"s";"S";"s"; -- 0x40 to 0x5F
"S";"s";"T";"t";"T";"t";"T";"t";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"W";"w";"Y";"y";"Y";"Z";"z";"Z";"z";"Z";"z";"s"; -- 0x60 to 0x7F
"b";"B";"B";"b";"6";"6";"O";"C";"c";"D";"D";"D";"d";"d";"3";"@";"E";"F";"f";"G";"G";"hv";"I";"I";"K";"k";"l";"l";"W";"N";"n";"O"; -- 0x80 to 0x9F
"O";"o";"OI";"oi";"P";"p";"YR";"2";"2";"SH";"sh";"t";"T";"t";"T";"U";"u";"Y";"V";"Y";"y";"Z";"z";"ZH";"ZH";"zh";"zh";"2";"5";"5";"ts";"w"; -- 0xA0 to 0xBF
"|";"||";"|=";"!";"DZ";"Dz";"dz";"LJ";"Lj";"lj";"NJ";"Nj";"nj";"A";"a";"I";"i";"O";"o";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"@";"A";"a"; -- 0xC0 to 0xDF
"A";"a";"AE";"ae";"G";"g";"G";"g";"K";"k";"O";"o";"O";"o";"ZH";"zh";"j";"DZ";"Dz";"dz";"G";"g";"HV";"W";"N";"n";"A";"a";"AE";"ae";"O";"o"; -- 0xE0 to 0xFF
}
Unidecode[0x02] =
{[0]="A";"a";"A";"a";"E";"e";"E";"e";"I";"i";"I";"i";"O";"o";"O";"o";"R";"r";"R";"r";"U";"u";"U";"u";"S";"s";"T";"t";"Y";"y";"H";"h"; -- 0x00 to 0x1F
"N";"d";"OU";"ou";"Z";"z";"A";"a";"E";"e";"O";"o";"O";"o";"O";"o";"O";"o";"Y";"y";"l";"n";"t";"j";"db";"qp";"A";"C";"c";"L";"T";"s"; -- 0x20 to 0x3F
"z";"[?]";"[?]";"B";"U";"^";"E";"e";"J";"j";"q";"q";"R";"r";"Y";"y";"a";"a";"a";"b";"o";"c";"d";"d";"e";"@";"@";"e";"e";"e";"e";"j"; -- 0x40 to 0x5F
"g";"g";"g";"g";"u";"Y";"h";"h";"i";"i";"I";"l";"l";"l";"lZ";"W";"W";"m";"n";"n";"n";"o";"OE";"O";"F";"r";"r";"r";"r";"r";"r";"r"; -- 0x60 to 0x7F
"R";"R";"s";"S";"j";"S";"S";"t";"t";"u";"U";"v";"^";"w";"y";"Y";"z";"z";"Z";"Z";"?";"?";"?";"C";"@";"B";"E";"G";"H";"j";"k";"L"; -- 0x80 to 0x9F
"q";"?";"?";"dz";"dZ";"dz";"ts";"tS";"tC";"fN";"ls";"lz";"WW";"]]";"h";"h";"h";"h";"j";"r";"r";"r";"r";"w";"y";"'";'"';"`";"'";"`";"`";"'"; -- 0xA0 to 0xBF
"?";"?";"<";">";"^";"V";"^";"V";"'";"-";"/";"\\";",";"_";"\\";"/";":";".";"`";"'";"^";"V";"+";"-";"V";".";"@";",";"~";'"';"R";"X"; -- 0xC0 to 0xDF
"G";"l";"s";"x";"?";"";"";"";"";"";"";"";"V";"=";'"';"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0xE0 to 0xFF
}
Unidecode[0x03] =
{
}
Unidecode[0x04] =
{
}
Unidecode[0x20] =
{[0]=" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";"";"";"";"";"-";"-";"-";"-";"--";"--";"||";"_";"'";"'";",";"'";'"';'"';",,";'"'; -- 0x00 to 0x1F
"|+";"|++";"*";"*>";".";"..";"...";".";"\n";"\n\n";"";"";"";"";"";" ";"%0";"%00";"'";"''";"'''";"`";"``";"```";"^";"<";">";"*";"!!";"!?";"-";"_"; -- 0x20 to 0x3F
"-";"^";"***";"--";"/";"-[";"]-";"[?]";"?!";"!?";"7";"PP";"(]";"[)";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0x40 to 0x5F
"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"0";"";"";"";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"n"; -- 0x60 to 0x7F
"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0x80 to 0x9F
"ECU";"CL";"Cr";"FF";"L";"mil";"N";"Pts";"Rs";"W";"NS";"D";"=E";"K";"T";"Dr";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0xA0 to 0xBF
"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";""; -- 0xC0 to 0xDF
"";"";"";"";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]"; -- 0xE0 to 0xFF
}
Unidecode[0x21] =
{[34]="TM";
}
return fh
end -- local function encoder_v3
local encoder = encoder_v3() -- To access FH encoder chars module
--[[
@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
--[[
@Module: +fh+iup_gui_v3
@Author: Mike Tate
@Version: 4.6
@LastUpdated: 18 Feb 2026
@Description: Graphical User Interface Library Module
@V4.6: Improve handling of centred window RasterSize in fh.ShowDialogue(...);
@V4.5: Adjust CheckVersionInStore() for dedicated button use;
@V4.4: Introduce use of NATIVEPARENT and CENTERPARENT to centre on parent window by default; Ensure not off screen; Monitors with -ve X;
@V4.3: Added memo options to CheckVersionInStore;
@V4.2: Skip if standalone GEDCOM in fh.SaveSettings() and getDataFiles();
@V4.1: CheckVersionInStore() save & retrieve latest version in file; Remove old wiki Help features;
@V4.0: Cater for full UTF-8 filenames;
@V3.9: ShowDialogue() popup closure fhSleep() added; CheckVersionInStore() at monthly intervals;
@V3.8: Function Prototype Closure version.
@V3.7: AssignAttributes(tblControls) now allows any string attribute to invoke a function.
@V3.6: anyMemoDialogue() sets TopMost attribute.
@V3.5: Replace IsNormalWindow(iupDialog) with SetWindowCoord(tblName) and update CheckWindowPosition(tblName) to prevent negative values freezing main dialog.
@V3.4: Use general.MakeFolder() to ensure key folders exist, add Get/PutRegKey(), check Registry IE Shell Version in HelpDialogue(), better error handling in LoadSettings().
@V3.3: LoadFolder() and SaveFolder() use global folder as default for local folder to improve synch across PC.
@V3.2: Load & Save settings now use a single clipboard so Local PC settings are preserved across synchronised PC.
@V3.1: IUP 3.11.2 iup.GetGlobal("VERSION") to top, HelpDialogue conditional ExpandChildren="YES/NO", RefreshDialogue uses NaturalSize, SetUtf8Mode(), Load/SaveFolder(), etc
@V3.0: ShowDialogue "dialog" mode for Memo, new DestroyDialogue, NewHelpDialogue tblAttr for Font, AssignAttributes intSkip, CustomDialogue iup.CENTERPARENT+, IUP Workaround, BalloonToggle, Initialise test Plugin file exists.
@V2.0: Support for Plugin Data scope, new FontDialogue, RefreshDialogue, AssignAttributes, httpRequest handler, keep "dialog" mode.
@V1.0: Initial version.
]]
local function iup_gui_v3()
local fh = {} -- Local environment table
require "iuplua" -- To access GUI window builder
require "iupluacontrols" -- To access GUI window controls
require "lfs" -- To access LUA filing system
require "iupluaole" -- To access OLE subsystem
require "luacom" -- To access COM subsystem
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local iupVersion = iup.GetGlobal("VERSION") -- Obtain IUP module version
-- "iuplua" Omitted Constants Workaround --
iup.TOP = iup.LEFT
iup.BOTTOM = iup.RIGHT
iup.RED = iup.RGB(1,0,0)
iup.GREEN = iup.RGB(0,1,0)
iup.BLUE = iup.RGB(0,0,1)
iup.BLACK = iup.RGB(0,0,0)
iup.WHITE = iup.RGB(1,1,1)
iup.YELLOW = iup.RGB(1,1,0)
-- Shared Interface Attributes & Functions --
fh.Version = " " -- Plugin Version
fh.History = fh.Version -- Version History
fh.Red = "255 0 0" -- Color attributes (must exclude leading zeros & spaces to allow value comparisons)
fh.Maroon = "128 0 0"
fh.Amber = "250 160 0"
fh.Orange = "255 165 0"
fh.Yellow = "255 255 0"
fh.Olive = "128 128 0"
fh.Lime = "0 255 0"
fh.Green = "0 128 0"
fh.Cyan = "0 255 255"
fh.Teal = "0 128 128"
fh.Blue = "0 0 255"
fh.Navy = "0 0 128"
fh.Magenta = "255 0 255"
fh.Purple = "128 0 128"
fh.Black = "0 0 0"
fh.Gray = "128 128 128"
fh.Silver = "192 192 192"
fh.Smoke = "240 240 240"
fh.White = "255 255 255"
fh.Risk = fh.Red -- Risk colour for hazardous controls such as Close/Delete buttons
fh.Warn = fh.Magenta -- Warn colour for caution controls and warnings
fh.Safe = fh.Green -- Safe colour for active controls such as most buttons
fh.Info = fh.Black -- Info colour for text controls such as labels/tabs
fh.Head = fh.Black -- Head colour for headings
fh.Body = fh.Black -- Body colour for body text
fh.Back = fh.White -- Background colour for all windows
fh.Gap = "8" -- Layout attributes Gap was "10"
fh.Border = "8x8" -- was BigMargin="10x10"
fh.Margin = "1x1" -- was MinMargin
fh.Balloon = "NO" -- Tooltip balloon mode
fh.FontSet = 0 -- Legacy GUI font set assigned by FontAssignment but used globally
fh.FontHead = ""
fh.FontBody = ""
local GUI = { } -- Sub-table for GUI Dialogue attributes to allow any "Name"
--[[
GUI.Name table of dialogue attributes, where Name is Font, Main, Memo, Bars, etc
GUI.Name.CoordX x co-ordinate ( Loaded & Saved by default )
GUI.Name.CoordY y co-ordinate ( Loaded & Saved by default )
GUI.Name.Dialog dialogue handle
GUI.Name.Focus focus button handle
GUI.Name.Frame dialogframe mode, "normal" = dialogframe="NO" else "YES", "showxy" = showxy(), "popup" or "keep" = popup(), default is "normal & showxy"
GUI.Name.Height height
GUI.Name.Raster rastersize ( Loaded & Saved by default )
GUI.Name.Width width
GUI.Name.Back ProgressBar background colour
GUI.Name.Body ProgressBar body text colour
GUI.Name.Font ProgressBar font style
GUI.Name.Stop ProgressBar Stop button colour
GUI.Name.GUI Module table usable by other modules e.g. progbar.Setup
--]]
-- tblScrn[1] = origin x, tblScrn[2] = origin y, tblScrn[3] = width, tblScrn[4] = height
local tblScrn = stringx.splitnumbers(iup.GetGlobal("VIRTUALSCREEN")) -- Used by CustomDialogue() and CheckWindowPosition() and ShowDialogue() below
local intMinX = tblScrn[1]
local intMinY = tblScrn[2] -- V4.4
local intMaxW = tblScrn[3]
local intMaxH = tblScrn[4]
function fh.BalloonToggle() -- Toggle tooltips Balloon mode
local tblToggle = { YES="NO"; NO="YES"; }
fh.Balloon = tblToggle[fh.Balloon]
fh.SaveSettings()
end -- function BalloonToggle
iup.SetGlobal("UTF8MODE","NO")
iup.SetGlobal("UTF8MODE_FILE","NO") -- V4.0
function fh.SetUtf8Mode() -- Set IUP into UTF-8 mode
if iupVersion == "3.5" or stringx.encoding() == "ANSI" then return false end
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("UTF8MODE_FILE","YES") -- V4.0
return true
end -- function SetUtf8Mode
local function tblOfNames(...) -- Get table of dialogue Names including "Font","Main" by default -- V4.4
local arg = {...}
local tblNames = {"Font";"Main";}
for intName, strName in ipairs(arg) do
if type(strName) == "string"
and strName ~= "Font"
and strName ~= "Main" then
table.insert(tblNames,strName)
end
end
return tblNames
end -- local function tblOfNames
local function tblNameFor(strName) -- Get table of parameters for chosen dialogue Name
strName = tostring(strName)
if not GUI[strName] then -- Need new table with default minimum & raster size, and X & Y co-ordinates
GUI[strName] = { }
local tblName = GUI[strName]
tblName.Raster = "x"
tblName.CoordX = iup.CENTERPARENT
tblName.CoordY = iup.CENTERPARENT
end
return GUI[strName]
end -- local function tblNameFor
local function intDimension(intMin,intVal,intMax) -- Return a number bounded by intMin and intMax
if not intVal then return 0 end -- Except if no value then return 0
intVal = tonumber(intVal) or (intMin+intMax)/2
return math.max(intMin,math.min(intVal,intMax))
end -- local function intDimension
function fh.CustomDialogue(strName,strRas,intX,intY) -- GUI custom window raster size, and X & Y co-ordinates
-- strRas nil = old size, "x" or "0x0" = min size, "999x999" = new size
-- intX/Y nil = central, "99" = co-ordinate position
local tblName = tblNameFor(strName)
local tblSize = {}
local intWide = 0
local intHigh = 0
strRas = strRas or tblName.Raster
if strRas then -- Ensure raster size is between minimum and screen size
tblSize = stringx.splitnumbers(strRas)
intWide = intDimension(intWide,tblSize[1],intMaxW)
intHigh = intDimension(intHigh,tblSize[2],intMaxH)
strRas = tostring(intWide.."x"..intHigh)
end
if intX and intX < iup.CENTERPARENT then
intX = intDimension(intMinX,intX,intMaxW-intWide) -- Ensure X co-ordinate positions window on screen -- V4.4
end
if intY and intY < iup.CENTERPARENT then
intY = intDimension(intMinY,intY,intMaxH-intHigh) -- Ensure Y co-ordinate positions window on screen -- V4.4
end
tblName.Raster = strRas or "x"
tblName.CoordX = tonumber(intX) or iup.CENTERPARENT -- V4.4
tblName.CoordY = tonumber(intY) or iup.CENTERPARENT -- V4.4
end -- function CustomDialogue
function fh.DefaultDialogue(...) -- GUI default window minimum & raster size, and X & Y co-ordinates
for intName, strName in ipairs(tblOfNames(...)) do
fh.CustomDialogue(strName)
end
end -- function DefaultDialogue
function fh.DialogueAttributes(strName) -- Provide named Dialogue Attributes
local tblName = tblNameFor(strName) -- tblName.Dialog = dialog handle, so any other attributes could be retrieved
local tblSize = stringx.splitnumbers(tblName.Raster or "x") -- Split Raster Size into width=tblSize[1] and height=tblSize[2]
tblName.Width = tblSize[1]
tblName.Height= tblSize[2]
tblName.Back = fh.Back -- Following only needed for NewProgressBar
tblName.Body = fh.Body
tblName.Font = fh.FontBody
tblName.Stop = fh.Risk
tblName.GUI = fh -- Module table
return tblName
end -- function DialogueAttributes
local strDefaultScope = "Project" -- Default scope for Load/Save data is per Project/User/Machine as set by PluginDataScope()
local tblClipProj = { }
local tblClipUser = { } -- Clipboards of sticky data for each Plugin Data scope -- V3.2
local tblClipMach = { }
local function doLoadData(strParam,strDefault,strScope) -- Load sticky data for Plugin Data scope
strScope = tostring(strScope or strDefaultScope):lower()
local tblClipData = tblClipProj
if strScope:match("user") then tblClipData = tblClipUser
elseif strScope:match("mach") then tblClipData = tblClipMach
end
return tblClipData[strParam] or strDefault
end -- local function doLoadData
function fh.LoadGlobal(strParam,strDefault,strScope) -- Load Global Parameter for all PC
return doLoadData(strParam,strDefault,strScope)
end -- function LoadGlobal
function fh.LoadLocal(strParam,strDefault,strScope) -- Load Local Parameter for this PC
return doLoadData(fh.ComputerName.."-"..strParam,strDefault,strScope)
end -- function LoadLocal
local function doLoadFolder(strFolder) -- Use relative paths to let Paths change -- V3.3
strFolder = strFolder:gsub("^FhDataPath",function() return fh.FhDataPath end) -- Full path to .fh_data folder
strFolder = strFolder:gsub("^PublicPath",function() return fh.PublicPath end) -- Full path to Public folder
strFolder = strFolder:gsub("^FhProjPath",function() return fh.FhProjPath end) -- Full path to project folder
return strFolder
end -- local function doLoadFolder
function fh.LoadFolder(strParam,strDefault,strScope) -- Load Folder Parameter for this PC -- V3.3
local strFolder = doLoadFolder(fh.LoadLocal(strParam,"",strScope))
if not general.FlgFolderExists(strFolder) then -- If no local folder try global folder
strFolder = doLoadFolder(fh.LoadGlobal(strParam,strDefault,strScope))
end
return strFolder
end -- function LoadFolder
function fh.LoadDialogue(...) -- Load Dialogue Parameters for "Font","Main" by default
for intName, strName in ipairs(tblOfNames(...)) do
local tblName = tblNameFor(strName)
tblName.Raster = tostring(fh.LoadLocal(strName.."R",tblName.Raster))
tblName.CoordX = tonumber(fh.LoadLocal(strName.."X",tblName.CoordX))
tblName.CoordY = tonumber(fh.LoadLocal(strName.."Y",tblName.CoordY))
fh.CheckWindowPosition(tblName)
end
end -- function LoadDialogue
function fh.LoadSettings(...) -- Load Sticky Settings from File
for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
strFileName = fh[strFileName]
if general.FlgFileExists(strFileName) then -- Load Settings File in table lines with key & val fields
local tblField = {}
local strClip = general.StrLoadFromFile(strFileName) -- V4.0
for strLine in strClip:gmatch("[^\r\n]+") do -- V4.0
if #tblField == 0
and strLine:match("^return {") -- Unless entire Sticky Data table was saved
and type(table.load) == "function" then
local tblClip, strErr = table.load(strFileName) -- Load Settings File table
if strErr then error(strErr.."\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.") end
for i,j in pairs (tblClip) do
tblClipData[i] = tblClip[i]
end
break
end
tblField = stringx.split(strLine,"=")
if tblField[1] then tblClipData[tblField[1]] = tblField[2] end
end
else
for i,j in pairs (tblClipData) do
tblClipData[i] = nil -- Restore defaults and clear any junk -- V4.0
end
end
end
fh.Safe = tostring(fh.LoadGlobal("SafeColor",fh.Safe))
fh.Warn = tostring(fh.LoadGlobal("WarnColor",fh.Warn))
fh.Risk = tostring(fh.LoadGlobal("RiskColor",fh.Risk))
fh.Head = tostring(fh.LoadGlobal("HeadColor",fh.Head))
fh.Body = tostring(fh.LoadGlobal("BodyColor",fh.Body))
fh.FontHead= tostring(fh.LoadGlobal("FontHead" ,fh.FontHead))
fh.FontBody= tostring(fh.LoadGlobal("FontBody" ,fh.FontBody))
fh.History = tostring(fh.LoadGlobal("History" ,fh.History))
fh.Balloon = tostring(fh.LoadGlobal("Balloon" ,fh.Balloon, "Machine"))
fh.LoadDialogue(...)
end -- function LoadSettings
local function doSaveData(strParam,anyValue,strScope) -- Save sticky data for Plugin Data scope
strScope = tostring(strScope or strDefaultScope):lower()
local tblClipData = tblClipProj
if strScope:match("user") then tblClipData = tblClipUser
elseif strScope:match("mach") then tblClipData = tblClipMach
end
tblClipData[strParam] = anyValue
end -- local function doSaveData
function fh.SaveGlobal(strParam,anyValue,strScope) -- Save Global Parameter for all PC
doSaveData(strParam,anyValue,strScope)
end -- function SaveGlobal
function fh.SaveLocal(strParam,anyValue,strScope) -- Save Local Parameter for this PC
doSaveData(fh.ComputerName.."-"..strParam,anyValue,strScope)
end -- function SaveLocal
function fh.SaveFolder(strParam,strFolder,strScope) -- Save Folder Parameter for this PC
strFolder = stringx.replace(strFolder,fh.FhDataPath,"FhDataPath") -- Full path to .fh_data folder
strFolder = stringx.replace(strFolder,fh.PublicPath,"PublicPath") -- Full path to Public folder
strFolder = stringx.replace(strFolder,fh.FhProjPath,"FhProjPath") -- Full path to project folder
fh.SaveGlobal(strParam,strFolder,strScope) -- V3.3
fh.SaveLocal(strParam,strFolder,strScope) -- Uses relative paths to let Paths change
end -- function SaveFolder
function fh.SaveDialogue(...) -- Save Dialogue Parameters for "Font","Main" by default
for intName, strName in ipairs(tblOfNames(...)) do
local tblName = tblNameFor(strName)
fh.SaveLocal(strName.."R",tblName.Raster)
fh.SaveLocal(strName.."X",tblName.CoordX)
fh.SaveLocal(strName.."Y",tblName.CoordY)
end
end -- function SaveDialogue
function fh.SaveSettings(...) -- Save Sticky Settings to File
fh.SaveDialogue(...)
fh.SaveGlobal("SafeColor",fh.Safe)
fh.SaveGlobal("WarnColor",fh.Warn)
fh.SaveGlobal("RiskColor",fh.Risk)
fh.SaveGlobal("HeadColor",fh.Head)
fh.SaveGlobal("BodyColor",fh.Body)
fh.SaveGlobal("FontHead" ,fh.FontHead)
fh.SaveGlobal("FontBody" ,fh.FontBody)
fh.SaveGlobal("History" ,fh.History)
fh.SaveGlobal("Balloon" ,fh.Balloon, "Machine")
for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
for i,j in pairs (tblClipData) do -- Check if table has any entries
strFileName = fh[strFileName]
if #strFileName > 0 then -- Skip for standalone GEDCOM -- V4.2
if type(table.save) == "function" then -- Save entire Settings File table per Project/User/Machine
table.save(tblClipData,strFileName)
else
local tblClip = {}
for strKey,strVal in pairs(tblClipData) do -- Else save Settings File lines with key & val fields -- V4.0
table.insert(tblClip,strKey.."="..strVal.."\n") -- V4.0
end
local strClip = table.concat(tblClip,"\n") -- V4.0
if not general.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
end
break
end
end
end -- function SaveSettings
function fh.CheckWindowPosition(tblName) -- Ensure dialogue window coordinates are on Screen
local tblSize = stringx.splitnumbers(tblName.Raster or "600x400","x") -- Get window dimensions from the previous use of plugin -- V4.4
local intWinW = tblSize[1]
local intWinH = tblSize[2]
if tonumber(tblName.CoordX) == nil
or tonumber(tblName.CoordY) == nil
or tonumber(tblName.CoordX) < intMinX -- V4.4 -- V3.5
or tonumber(tblName.CoordY) < intMinY -- V4.4 -- V3.5
or tonumber(tblName.CoordX) + intWinW > intMaxW -- V4.4
or tonumber(tblName.CoordY) + intWinH > intMaxH then -- V4.4
tblName.CoordX = iup.CENTERPARENT -- V4.4
tblName.CoordY = iup.CENTERPARENT -- V4.4
end
end -- function CheckWindowPosition
function fh.SetWindowCoord(tblName) -- Set the Window coordinates if not Maximised or Minimised -- V3.5
-- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height
local tblPosn = stringx.splitnumbers(tblName.Dialog.ScreenPosition)
local intPosX = tblPosn[1] or -1
local intPosY = tblPosn[2] or -1
if intPosX < 0 and intPosY < 0 then -- If origin is negative (-8, -8 = Maximised, -3200, -3200 = Minimised)
return false -- then is Maximised or Minimised
end
tblName.CoordX = intPosX -- Otherwise set the Window coordinates
tblName.CoordY = intPosY
return true
end -- function SetWindowCoord
function fh.ShowDialogue(strName,iupDialog,btnFocus,strFrame) -- Set standard frame attributes and display dialogue window
local tblName = tblNameFor(strName)
iupDialog = iupDialog or tblName.Dialog -- Retrieve previous parameters if needed
btnFocus = btnFocus or tblName.Focus
strFrame = strFrame or tblName.Frame
strFrame = strFrame or "show norm" -- Default frame mode is dialog:showxy(X,Y) with DialogFrame="NO" ("normal" to vary size, otherwise fixed size)
strFrame = strFrame:lower() -- Other modes are "show", "popup" & "keep" with DialogFrame="YES", or with "normal" for DialogFrame="NO" ("show" for active windows, "popup"/"keep" for modal windows)
if strFrame:gsub("%s-%a-map%a*[%s%p]*","") == "" then -- May be prefixed with "map" mode to just map dialogue initially, also may be suffixed with "dialog" to inhibit iup.MainLoop() to allow progress messages
strFrame = "map show norm" -- If only "map" mode then default to "map show norm"
end
if type(iupDialog) == "userdata" then
tblName.Dialog = iupDialog
tblName.Focus = btnFocus -- Preserve parameters
tblName.Frame = strFrame
iupDialog.Background = fh.Back -- Background colour
iupDialog.Shrink = "YES" -- Sometimes needed to shrink controls to raster size
if type(btnFocus) == "userdata" then -- Set button as focus for Esc and Enter keys
iupDialog.StartFocus = iupDialog.StartFocus or btnFocus
iupDialog.DefaultEsc = iupDialog.DefaultEsc or btnFocus
iupDialog.DefaultEnter = iupDialog.DefaultEnter or btnFocus
end
if tblName.CoordX == iup.CENTERPARENT
or tblName.CoordY == iup.CENTERPARENT then -- When centred on parent refresh this window size -- V4.4 -- V4.6
fh.RefreshDialogue(strName)
else
iupDialog.MinSize = "x" -- Minimum size (default "x" becomes nil)
iupDialog.RasterSize = tblName.Raster or "x" -- Raster size (default "x" becomes nil)
end
iupDialog.MaxSize = intMaxW.."x"..intMaxH -- Maximum size is virtual screen size
if strFrame:match("norm") then -- DialogFrame mode is "NO" by default for variable size window
if strFrame:match("pop") or strFrame:match("keep") then
iupDialog.MinBox = "NO" -- For "popup" and "keep" hide Minimize and Maximize icons
iupDialog.MaxBox = "NO"
else
strFrame = strFrame.." show" -- If not "popup" nor "keep" then use "showxy" mode
end
else
iupDialog.DialogFrame = "YES" -- Define DialogFrame mode for fixed size window
end
iupDialog.close_cb = iupDialog.close_cb or function() return iup.CLOSE end -- Define default window X close, move, and resize actions
iupDialog.move_cb = iupDialog.move_cb or function(self) if iup.MainLoopLevel() > 0 then fh.SetWindowCoord(tblName) end end -- V3.5 -- V4.4
iupDialog.resize_cb = iupDialog.resize_cb or function(self) if iup.MainLoopLevel() > 0 then tblName.Raster=self.RasterSize end end -- V3.5 -- V4.4 -- V4.6
fh.RefreshDialogue(strName) -- Refresh to set Natural Size as Minimum Size
if strName == "Main" then
if fhGetAppVersion() > 6 then -- Main window centres on FH parent -- V4.4
iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
else
local tblMain = tblNameFor("Main") -- Others popup centrally in Main -- V4.4
local iupMain = tblMain.Dialog
if iupMain then -- Centre based on size of windows -- V1.4
local arrName = stringx.splitnumbers(tblName.Raster or "0x0")
local arrMain = stringx.splitnumbers(tblMain.Raster or arrName[1].."x"..arrName[2])
tblName.CoordX = iupMain.X + math.floor( (arrMain[1] - arrName[1]) / 2 )
tblName.CoordY = iupMain.Y + math.floor( (arrMain[2] - arrName[2]) / 2 )
elseif fhGetAppVersion() > 6 then -- This window centres on FH parent -- V4.4
iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
end
if strFrame:match("map") then -- Only dialogue mapping is required
iupDialog:map()
tblName.Frame = strFrame:gsub("%s-%a-map%a*[%s%p]*","") -- Remove "map" from frame mode ready for subsequent call
return
end
if iup.MainLoopLevel() == 0 -- Called from outside Main GUI, so must use showxy() and not popup()
or strFrame:match("dialog")
or strFrame:match("sho") then -- Use showxy() to dispay dialogue window for "showxy" or "dialog" mode
iupDialog:showxy(tblName.CoordX,tblName.CoordY)
if not strFrame:match("dialog") -- Inhibit MainLoop if "dialog" mode -- V4.1
and iup.MainLoopLevel() == 0 then iup.MainLoop() end
else
iupDialog:popup(tblName.CoordX,tblName.CoordY) -- Use popup() to display dialogue window for "popup" or "keep" modes
fhSleep(200,150) -- Sometimes needed to prevent MainLoop() closure! -- V3.9
end
if not strFrame:match("dialog") and strFrame:match("pop") then
tblName.Dialog = nil -- When popup closed, clear key parameters, but not for "keep" mode
tblName.Raster = nil
tblName.CoordX = nil -- iup.CENTERPARENT
tblName.CoordY = nil -- iup.CENTERPARENT
elseif tblName.CoordX ~= iup.CENTERPARENT -- V4.4
and tblName.CoordY ~= iup.CENTERPARENT then
fh.SetWindowCoord(tblName) -- Set Window coordinate pixel values -- V3.5
end
end
end -- function ShowDialogue
function fh.DestroyDialogue(strName) -- Destroy existing dialogue
local tblName = tblNameFor(strName)
if tblName then
local iupDialog = tblName.Dialog
if type(iupDialog) == "userdata" then
iupDialog:destroy()
tblName.Dialog = nil -- Prevent future misuse of handle -- 22 Jul 2014
end
end
end -- function DestroyDialogue
local function strDialogueArgs(strArgA,strArgB,comp) -- Compare two argument pairs and return matching pair
local tblArgA = stringx.splitnumbers(strArgA)
local tblArgB = stringx.splitnumbers(strArgB)
local strArgX = tostring(comp(tblArgA[1] or 100,tblArgB[1] or 100))
local strArgY = tostring(comp(tblArgA[2] or 100,tblArgB[2] or 100))
return strArgX.."x"..strArgY
end -- local function strDialogueArgs
function fh.RefreshDialogue(strName) -- Refresh dialogue window size after Font change, etc
local tblName = tblNameFor(strName)
local iupDialog = tblName.Dialog -- Retrieve the dialogue handle
if type(iupDialog) == "userdata" then
iupDialog.Size = iup.NULL
iupDialog.MinSize = iup.NULL -- V3.1
iup.Refresh(iupDialog) -- Refresh window to Natural Size and set as Minimum Size
if not iupDialog.RasterSize then
iupDialog:map()
iup.Refresh(iupDialog)
end
local strSize = iupDialog.NaturalSize or iupDialog.RasterSize -- IUP 3.5 NaturalSize = nil, IUP 3.11 needs NaturalSize -- V3.1
iupDialog.MinSize = strDialogueArgs(iupDialog.MaxSize,strSize,math.min) -- Set Minimum Size to smaller of Maximm Size or Natural/Raster Size -- V3.1
iupDialog.RasterSize = strDialogueArgs(tblName.Raster,strSize,math.max) -- Set Current Size to larger of Current Size or Natural/Raster Size -- V3.1
iup.Refresh(iupDialog)
tblName.Raster = iupDialog.RasterSize
if iupDialog.Visible == "YES" then -- Ensure visible dialogue origin is on screen
tblName.CoordX = math.max(tblName.CoordX,10)
tblName.CoordY = math.max(tblName.CoordY,10) -- Set both coordinates to larger of current value or 10 pixels
if iupDialog.Modal then -- V3.8
if iupDialog.Modal == "NO" then
iupDialog.ZOrder = "BOTTOM" -- Ensure dialogue is subservient to any popup
iupDialog:showxy(tblName.CoordX,tblName.CoordY) -- Use showxy() to reposition main window
else
iupDialog:popup(tblName.CoordX,tblName.CoordY) -- Use popup() to reposition modal window
end
end
else
iupDialog.BringFront="YES"
end
end
end -- function RefreshDialogue
function fh.AssignAttributes(tblControls) -- Assign the attributes of all controls supplied
local anyFunction = nil
for iupName, tblAttr in pairs ( tblControls or {} ) do
if type(iupName) == "userdata" and type(tblAttr) == "table" then -- Loop through each iup control
local intSkip = 0 -- Skip counter for attributes same for all controls
for intAttr, anyName in ipairs ( tblControls[1] or {} ) do -- Loop through each iup attribute
local strName = nil
local strAttr = nil
local strType = type(anyName)
if strType == "string" then -- Attribute is different for each control in tblControls
strName = anyName
strAttr = tblAttr[intAttr-intSkip]
elseif strType == "table" then -- Attribute is same for all controls as per tblControls[1]
intSkip = intSkip + 1
strName = anyName[1]
strAttr = anyName[2]
elseif strType == "function" then
intSkip = intSkip + 1
anyFunction = anyName
break
end
if type(strName) == "string" and ( type(strAttr) == "string" or type(strAttr) == "function" ) then
local anyRawGet = rawget(fh,strAttr) -- Use rawget() to stop require("pl.strict") complaining
if type(anyRawGet) == "string" then
strAttr = anyRawGet -- Use internal module attribute such as Head or FontBody
elseif type(iupName[strName]) == "string"
and type(strAttr) == "function" then -- Allow string attributes to invoke a function -- V3.7
strAttr = strAttr()
end
iupName[strName] = strAttr -- Assign attribute to control
end
end
end
end
if anyFunction then anyFunction() end -- Perform any control assignment function
end -- function AssignAttributes
-- Font Dialogue Attributes and Functions --
fh.FontBody = iup.GetGlobal("DEFAULTFONT") -- Set default font for Body and Head text
fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold ")
function fh.FontDialogue(tblAttr,strName) -- GUI Font Face & Style Dialogue
tblAttr = tblAttr or {}
strName = strName or "Main"
local isFontChosen = false
local btnFontHead = iup.button { Title="Choose Headings Font and default Colour"; }
local btnFontBody = iup.button { Title="Choose Body text Font and default Colour"; }
local btnCol_Safe = iup.button { Title=" Safe Colour "; }
local btnCol_Warn = iup.button { Title=" Warning Colour "; }
local btnCol_Risk = iup.button { Title=" Risky Colour "; }
local btnDefault = iup.button { Title=" Default Fonts "; }
local btnMinimum = iup.button { Title=" Minimum Size "; }
local btnDestroy = iup.button { Title=" Close Dialogue "; }
local frmSetFonts = iup.frame { Title=" Set Window Fonts & Colours ";
iup.vbox { Alignment="ACENTER"; Margin=fh.Margin; Homogeneous="YES";
btnFontHead;
btnFontBody;
iup.hbox { btnCol_Safe; btnCol_Warn; btnCol_Risk; Homogeneous="YES"; };
iup.hbox { btnDefault ; btnMinimum ; btnDestroy ; Homogeneous="YES"; };
} -- iup.vbox end
} -- iup.frame end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local dialogFont = iup.dialog { Title=" Set Window Fonts & Colours "; Gap=fh.Gap; Margin=fh.Border; frmSetFonts; }
local tblButtons = { }
local function setDialogues() -- Refresh the Main dialogue -- V4.4
fh.AssignAttributes(tblAttr) -- Assign parent dialogue attributes
fh.RefreshDialogue(strName) -- Refresh parent window size & position and bring infront of other window
fh.RefreshDialogue("Font") -- Refresh Font window size & position and bring infront of parent window
end -- local function setDialogues
local function getFont(strColor) -- Set font button function
local strTitle = " Choose font style & default colour for "..strColor:gsub("Head","Heading").." text "
local strValue = "Font"..strColor -- The font codes below are not recognised by iupFontDlg and result in empty font face!
local strFont = rawget(fh,strValue):gsub(" Black,",","):gsub(" Light, Bold",","):gsub(" Extra Bold,",","):gsub(" Semibold,",",")
local iupFontDlg = iup.fontdlg { Title=strTitle; Color=rawget(fh,strColor); Value=strFont; }
iupFontDlg:popup() -- Popup predefined font dialogue
if iupFontDlg.Status == "1" then
if iupFontDlg.Value:match("^,") then -- Font face missing so revert to original font
iupFontDlg.Value = rawget(fh,strValue)
end
fh[strColor] = iupFontDlg.Color -- Set Head or Body color attribute
fh[strValue] = iupFontDlg.Value -- Set FontHead or FontBody font style
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
setDialogues()
isFontChosen = true
end
end -- local function getFont
local function getColor(strColor) -- Set colour button function
local strTitle = " Choose colour for "..strColor:gsub("Warn","Warning"):gsub("Risk","Risky").." button & message text "
local iupColorDlg = iup.colordlg { Title=strTitle; Value=rawget(fh,strColor); ShowColorTable="YES"; }
iupColorDlg.DialogFrame="YES"
iupColorDlg:popup() -- Popup predefined color dialogue fixed size window
if iupColorDlg.Status == "1" then
fh[strColor] = iupColorDlg.Value -- Set Safe or Warn or Risk color attribute
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
setDialogues()
isFontChosen = true
end
end -- local function getColor
local function setDefault() -- Action for Default Fonts button
fh.Safe = fh.Green
fh.Warn = fh.Magenta
fh.Risk = fh.Red -- Set default colours
fh.Body = fh.Black
fh.Head = fh.Black
fh.FontBody = iup.GetGlobal("DEFAULTFONT") -- Set default fonts for Body and Head text
fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold")
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
setDialogues()
isFontChosen = true
end -- local function setDefault
local function setMinimum() -- Action for Minimum Size button
local tblName = tblNameFor(strName)
local iupDialog = tblName.Dialog -- Retrieve the parent dialogue handle
if type(iupDialog) == "userdata" then
tblName.Raster = "10x10" -- Refresh parent window to Minimum Size & adjust position
fh.RefreshDialogue(strName)
end
local tblFont = tblNameFor("Font")
tblFont.Raster = "10x10" -- Refresh Font window to Minimum Size & adjust position
fh.RefreshDialogue("Font")
end -- local function setMinimum
tblButtons = { { "Font" ; "FgColor" ; "Tip" ; "action" ; {"TipBalloon";"Balloon";} ; {"Expand";"YES";} ; };
[btnFontHead] = { "FontHead"; "Head"; "Choose the Heading text Font Face, Style, Size, Effects, and default Colour"; function() getFont("Head") end; };
[btnFontBody] = { "FontBody"; "Body"; "Choose the Body text Font Face, Style, Size, Effects, and default Colour" ; function() getFont("Body") end; };
[btnCol_Safe] = { "FontBody"; "Safe"; "Choose the colour for Safe operations" ; function() getColor("Safe") end; };
[btnCol_Warn] = { "FontBody"; "Warn"; "Choose the colour for Warning operations"; function() getColor("Warn") end; };
[btnCol_Risk] = { "FontBody"; "Risk"; "Choose the colour for Risky operations" ; function() getColor("Risk") end; };
[btnDefault ] = { "FontBody"; "Safe"; "Restore default Fonts and Colours"; function() setDefault() end; };
[btnMinimum ] = { "FontBody"; "Safe"; "Reduce window to its minimum size"; function() setMinimum() end; };
[btnDestroy ] = { "FontBody"; "Risk"; "Close this dialogue "; function() return iup.CLOSE end; };
[frmSetFonts] = { "FontHead"; "Head"; };
}
fh.AssignAttributes(tblButtons) -- Assign the button & frame attributes
fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep normal") -- Popup the Set Window Fonts dialogue: "keep normal" : vary size & posn, and remember size & posn
-- fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup normal") -- Popup the Set Window Fonts dialogue: "popup normal" : vary size & posn, but redisplayed centred
-- fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep") -- Popup the Set Window Fonts dialogue: "keep" : fixed size, vary posn, and only remember posn
-- fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup") -- Popup the Set Window Fonts dialogue: "popup": fixed size, vary posn, but redisplayed centred
dialogFont:destroy()
return isFontChosen
end -- function FontDialogue
local function anyMemoControl(anyName,fgColor) -- Compose any control Title and FgColor
local strName = tostring(anyName) -- anyName may be a string, and fgColor is default FgColor
local tipText = nil
if type(anyName) == "table" then -- anyName may be a table = { Title string ; FgColor string ; ToolTip string (optional); }
strName = anyName[1]
fgColor = anyName[2]:match("%d* %d* %d*") or fgColor
tipText = anyName[3]
end
return strName, fgColor, tipText
end -- local function anyMemoControl
local function anyMemoDialogue(strHead,anyHead,strMemo,anyMemo,...) -- Display framed memo dialogue with buttons
local arg = {...} -- Fix for Lua 5.2+
local intButt = 0 -- Returned value if "X Close" button is used
local tblButt = { [0]="X Close"; } -- Button names lookup table
local strHead, fgcHead, tipHead = anyMemoControl(anyHead or "",strHead)
local strMemo, fgcMemo, tipMemo = anyMemoControl(anyMemo or "",strMemo)
-- Create the GUI labels and buttons
local lblMemo = iup.label { Title=strMemo; FgColor=fgcMemo; Tip=tipMemo; TipBalloon=fh.Balloon; Alignment="ACENTER"; Padding=fh.Margin; Expand="YES"; WordWrap="YES"; }
local lblLine = iup.label { Separator="HORIZONTAL"; }
local iupHbox = iup.hbox { Homogeneous="YES"; }
local btnButt = iup.button { }
local strTop = "YES" -- Make dialogue TopMost -- V3.6
local strMode = "popup"
if arg[1] == "Keep Dialogue" then -- Keep dialogue open for a progress message
strMode = "keep dialogue"
lblLine = iup.label { }
if not arg[2] then strTop = "NO" end -- User chooses TopMost -- V3.6
else
if #arg == 0 then arg[1] = "OK" end -- If no buttons listed then default to an "OK" button
for intArg, anyButt in ipairs(arg) do
local strButt, fgcButt, tipButt = anyMemoControl(anyButt,fh.Safe)
tblButt[intArg] = strButt
btnButt = iup.button { Title=strButt; FgColor=fgcButt; Tip=tipButt; TipBalloon=fh.Balloon; Expand="NO"; MinSize="80"; Padding=fh.Margin; action=function() intButt=intArg return iup.CLOSE end; }
iup.Append( iupHbox, btnButt )
end
end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local iupMemo = iup.dialog { Title=fh.Plugin..fh.Version..strHead; TopMost=strTop; -- TopMost added -- V3.6
iup.vbox { Alignment="ACENTER"; Gap=fh.Gap; Margin=fh.Margin;
iup.frame { Title=strHead; FgColor=fgcHead; Font=fh.FontHead;
iup.vbox { Alignment="ACENTER"; Font=fh.FontBody; lblMemo; lblLine; iupHbox; };
};
};
}
fh.ShowDialogue("Memo",iupMemo,btnButt,strMode) -- Show popup Memo dialogue window with righthand button in focus (if any)
if strMode == "keep dialogue" then return lblMemo, iupMemo end -- Return label & dialogue controls so message can be changed and dialogue destroyed
iupMemo:destroy()
return intButt, tblButt[intButt] -- Return button number & title that was pressed
end -- local function anyMemoDialogue
function fh.MemoDialogue(anyMemo,...) -- Multi-Button GUI like iup.Alarm and fhMessageBox, with "Memo" in frame
return anyMemoDialogue(fh.Head,"Memo",fh.Body,anyMemo,...)
end -- function MemoDialogue
function fh.WarnDialogue(anyHead,anyMemo,...) -- Multi-Button GUI like iup.Alarm and fhMessageBox, with heading in frame
return anyMemoDialogue(fh.Warn,anyHead,fh.Warn,anyMemo,...)
end -- function WarnDialogue
function fh.GetRegKey(strKey) -- Read Windows Registry Key Value
local luaShell = luacom.CreateObject("WScript.Shell")
local anyValue = nil
if pcall( function() anyValue = luaShell:RegRead(strKey) end ) then
return anyValue -- Return Key Value if found
end
return nil
end -- function GetRegKey
function fh.PutRegKey(strKey,anyValue,strType) -- Write Windows Registry Key Value
local luaShell = luacom.CreateObject("WScript.Shell")
local strAns = nil
if pcall( function() strAns = luaShell:RegWrite(strKey,anyValue,strType) end ) then
return true
end
return nil
end -- function PutRegKey
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
function fh.VersionInStore(strPlugin) -- Obtain the Version in Plugin Store by Name only -- V4.5
local strVersion = "0"
if strPlugin then
local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat"
general.DeleteFile(strFile)
local lblMemo, iupMemo = fh.MemoDialogue("Checking for updated version in the Family Historian 'Plugin Store'.","Keep Dialogue")
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin
local isOK, strReturn = pcall(httpRequest,strRequest)
iupMemo:destroy()
if not isOK then -- Problem with Internet access
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION")
elseif strReturn ~= nil then
strVersion = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
end
end
return strVersion or "0"
end -- function VersionInStore
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) -- V4.1
for i=1,5 do
intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
end
return intVersion
end -- local function intVersion
function fh.CheckVersionInStore() -- Check if later Version available in Plugin Store -- V4.5
local strNewVer = fh.VersionInStore(fh.Plugin:gsub(" %- .*",""))
local strOldVer = fh.Version
if intVersion(strNewVer) > intVersion(strOldVer:match("%D*([%d%.]*)")) then
fh.MemoDialogue("Later Version "..strNewVer.." of this Plugin is available from the 'Plugin Store'.")
else
fh.MemoDialogue("No later Version of this Plugin is available from the 'Plugin Store'.")
end
end -- function CheckVersionInStore
function fh.PluginDataScope(strScope) -- Set default Plugin Data scope to per-Project, or per-User, or per-Machine
strScope = tostring(strScope):lower()
if strScope:match("mach") then -- Per-Machine
strDefaultScope = "Machine"
elseif strScope:match("user") then -- Per-User
strDefaultScope = "User"
end -- Per-Project is default
end -- function PluginDataScope
local function getPluginDataFileName(strScope) -- Get plugin data filename for chosen scope
local isOK, strDataFile = pcall(fhGetPluginDataFileName,strScope)
if not isOK then strDataFile = fhGetPluginDataFileName() end -- Before V5.0.8 parameter is disallowed and default = CURRENT_PROJECT
return strDataFile
end -- local function getPluginDataFileName
local function getDataFiles(strScope) -- Compose the Plugin Data file & path & root names
local strPluginName = fh.Plugin
local strPluginPlain = stringx.plain(strPluginName)
local strDataRoot = "" -- Plugin data file root name -- V4.2
local strDataPath = "" -- Plugin data folder path name
local strDataFile = getPluginDataFileName(strScope) -- Allow plugins with variant filenames to use same plugin data files
strDataFile = strDataFile:gsub("\\"..strPluginPlain:gsub(" ","_"):lower(),"\\"..strPluginName)
strDataFile = strDataFile:gsub("\\"..strPluginPlain..".+%.[D,d][A,a][T,t]$","\\"..strPluginName..".dat")
if #strDataFile > 0 then -- Standalone GEDCOM path is ""
strDataPath = strDataFile:gsub("\\"..strPluginPlain.."%.[D,d][A,a][T,t]$","")
strDataRoot = strDataPath.."\\"..strPluginName
general.MakeFolder(strDataPath) -- V3.4
end
return strDataFile, strDataPath, strDataRoot
end -- local function getDataFiles
function fh.Initialise(strVersion,strPlugin) -- Initialise the GUI module with optional Version & Plugin name
local strAppData = fhGetContextInfo("CI_APP_DATA_FOLDER")
fh.Plugin = fhGetContextInfo("CI_PLUGIN_NAME") -- Plugin Name from file
fh.Version = strVersion or " " -- Plugin Version
if fh.Version == " " then
local strTitle = "\n@Title is missing"
local strAuthor = "\n@Author is missing"
local strVersion = "\n@Version is missing"
local strPlugin = strAppData.."\\Plugins\\"..fh.Plugin..".fh_lua"
if general.FlgFileExists(strPlugin) then
for strLine in io.lines(strPlugin) do -- Read each line from the Plugin file
strPlugin = strLine:match("^@Title:[\t-\r ]*(.*)")
if strPlugin then
strPlugin = strPlugin:gsub("&&","&")
--? if fh.Plugin:match("^"..strPlugin:gsub("(%W)","%%%1")) then
if fh.Plugin:match("^"..stringx.plain(strPlugin)) then
fh.Plugin = strPlugin -- Prefer Title to Filename if it matches
strTitle = nil
else
strTitle = "\n@Title differs from Filename" -- Report abnormality
end
end
if strLine:match("^@Author:%s*(.*)") then -- Check @Author exists
strAuthor = nil
end
fh.Version = strLine:gsub("^@Version:%D*([%d%.]*)%D*"," %1 ")
if fh.Version ~= strLine then -- Obtain the @Version from Plugin file
strVersion = nil
break
end
end
if strTitle or strAuthor or strVersion then -- Report any header abnormalities
fhMessageBox("\nScript Header: "..fh.Plugin..(strTitle or "")..(strAuthor or "")..(strVersion or ""),"MB_OK","MB_ICONEXCLAMATION")
end
else
fhMessageBox("\nPlugin has not been saved!","MB_OK","MB_ICONEXCLAMATION")
end
end
fh.History = fh.Version -- Version History
fh.Plugin = strPlugin or fh.Plugin -- Plugin Name from argument or default from file
fh.DefaultDialogue() -- Default "Font","Main" dialogues
fh.MachineFile,fh.MachinePath,fh.MachineRoot = getDataFiles("LOCAL_MACHINE") -- Plugin data names per machine
fh.PerUserFile,fh.PerUserPath,fh.PerUserRoot = getDataFiles("CURRENT_USER") -- Plugin data names per user
fh.ProjectFile,fh.ProjectPath,fh.ProjectRoot = getDataFiles("CURRENT_PROJECT") -- Plugin data names per project
fh.FhDataPath = fhGetContextInfo("CI_PROJECT_DATA_FOLDER") -- Paths used by Load/SaveFolder for relative folders -- V4.0
fh.PublicPath = fhGetContextInfo("CI_PROJECT_PUBLIC_FOLDER") -- Public data folder path name -- V4.0
if fh.FhDataPath == "" then
fh.FhDataPath = fh.ProjectPath:gsub("\\Plugin Data$","")
end
if fh.PublicPath == "" then
fh.PublicPath = fh.ProjectPath
fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Plugin Data$","%1")
else
general.MakeFolder(fh.PublicPath) -- V3.4
fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Public$","%1")
end
fh.CalicoPie = strAppData:gsub("\\Calico Pie\\.*","\\Calico Pie") -- Program Data Calico Pie path name
fh.ComputerName = os.getenv("COMPUTERNAME") -- Local PC Computer Name
end -- function Initialise
fh.Initialise() -- Initialise module with default values
return fh
end -- local function iup_gui_v3
local iup_gui = iup_gui_v3() -- To access FH IUP GUI build module
-- Preset Global Data Definitions --
function PresetGlobalData()
iup_gui.Gap = "2" -- GUI defaults
iup_gui.Balloon = "NO" -- Needed for PlayOnLinux/Mac -- V3.8
iup_gui.SetUtf8Mode()
IntFhVersion = fhGetAppVersion()
StrPlusMinus = "±"
if IntFhVersion > 5 then
StrPlusMinus = fhConvertANSItoUTF8(StrPlusMinus) -- Fix "±" -- V3.7
end
StrC = "[%z-\031\127]" -- LUA control chars pattern becasue "%c" mishandles UTF-8 codes \129 \141 \143 \144 \157 \173
StrP = "[!-/:-@%[\\%]^_`%]{-~]" -- LUA punction chars pattern because "%p" mishandles UTF-8 codes
-- i.e. = "[\033-\047\058-\064\091\092\093\094\095\096\123-\126]"
StrS = "[\t-\r ]" -- i.e. = "[\009-\013\032]" -- LUA space chars pattern because "%s" mishandles UTF-8 code (ANSI nbsp=\160)
StrSP = "[\t-\r !-/:-@%[\\%]^_`%]{-~]"
StrNonDupsFile = iup_gui.ProjectRoot..".nondups" -- File names for saved data always ANSI
StrResultsFile = iup_gui.ProjectRoot..".results"
StrSoundexFile = iup_gui.ProjectRoot..".soundex"
PresetGlobalDefaults() -- User preferences defaults
SetUserInterfaceDefaults()
SetNamesMatchDefaults()
SetEventMatchDefaults()
SetChronologyDefaults()
SetOtherMatchDefaults()
end -- function PresetGlobalData
-- Global User Defaults Definition --
function PresetGlobalDefaults()
TblData = {} -- Data table per Individual Record Id of key information
TblNonDups = {} -- Non-Duplicate pairs of Record Id to exclude
-- User Interface Defaults -- Default Value -- Description of Default
IntIndiScoreMinDef = 12 -- 12 Points -- Minimum score needed for Individual to assess Relations
IntLeastResultsDef = 1 -- 1 Point -- Result Set lowest score to display
IntLimitResultsDef = 100 -- 100 Rows -- Result Set limit of rows to display
IntPruneResultsDef = 200 -- 200 Entries -- Results table threshold to avoid exhausting memory about twice IntLimitResults
IntProgBarStartDef = 400000 -- 400000 Compares -- Threshold of Comparisons at which to start Progress Bar
-- Comparisons = R * (R-1) / 2 where R = Total Individual Records
-- Names Matching Defaults -- Default Value -- Description of Default
IntLastNameRightDef = 7 -- 7 Points -- Addition for Lastname perfect match
IntForeNameRightDef = 6 -- 6 Points -- Addition for Forename perfect match in the right position
IntForeNameOtherDef = 3 -- 3 Points -- Addition for Forename perfect match but in other position
IntNameSoundexDef = 2 -- 2 Points -- Addition for any Name Soundex match only
IntNameLastWrongDef = -0 -- -0 Points -- Deduction for Lastname total mismatch
IntNameMinimumDef = 1 -- 1 Point -- Minimum needed to avoid entire Names mismatch
IntNameDeductionDef = -0 -- -0 Points -- Deduction for Relation entire Names mismatch (not used for Individual)
IntNameMaximumDef = 20 -- 20 Points -- Maximum entire Names match to avoid overwhelming result
IntNameThresholdDef = 6 -- 6 Points -- Threshold needed to proceed with Event assessments, etc
IntIndivi = 1 -- Table index per Relation
IntFather = 2
IntMother = 3
IntSpouse = 4
IntChild = 5
TblLastNameRight = { } -- Table entry per Relation
TblForeNameRight = { }
TblForeNameOther = { }
TblNameLastRight = { }
TblNameForeRight = { }
TblNameForeOther = { }
TblNameSoundex = { }
TblNameLastWrong = { }
TblNameMinimum = { }
TblNameDeduction = { }
TblNameMaximum = { }
TblNameThreshold = { }
-- Event Matching Defaults -- Default Value -- Description of Default
IntDatesToleranceDef = 50 -- ±50 Days -- Tolerance to grant a Lower or Upper Date Timespan match
IntDatesMatchedDef = 2 -- 2 Points -- Addition for a tolerant Lower/Upper Date Timespan match
IntDatesOverlapDef = 2 -- 2 Points -- Addition for an overlapping Date Timespan
IntDatesMinimumDef = 1 -- 1 Point -- Minimum Dates score to avoid entire Dates mismatch
IntDatesDeductionDef = -15 -- -15 Points -- Deduction for entire Dates mismatch
IntPlacePartRightDef = 3 -- 3 Points -- Addition for Place Part perfect match in the right position
IntPlacePartOtherDef = 2 -- 2 Points -- Addition for Place Part perfect match but in other position
IntPlaceSoundexDef = 1 -- 1 Point -- Addition for Place Part Soundex match only
IntEventMaximumDef = 10 -- 10 Points -- Maximum for entire Event match to avoid overwhelming result
IntBoostedBirthDef = 1 -- 1 Times -- Boost score multiplier for Birth Events -- V3.8
IntBoostedBapChDef = 1 -- 1 Times -- Boost score multiplier for Baptism/Christening -- V3.8
IntBoostedMarryDef = 1 -- 1 Times -- Boost score multiplier for Marriage Events -- V3.8
IntBoostedDeathDef = 1 -- 1 Times -- Boost score multiplier for Death/Burial/Cremate -- V3.8
-- Date Chronology Defaults -- Default Value -- Description of Default
IntDatesTimespanDef = 50 -- ±50 Years -- Timespan to extend 'After', 'Before', 'From', 'To' Period & Range Dates
IntDatesVarianceDef = 5 -- ±5 Years -- Variance for 'Approximate', 'Calculated', 'Estimated' Year only Dates
IntDatesPregnantDef = 9 -- 9 Months -- Pregnancy duration for synthesised Dates for Chronology checks
IntDatesPubertyDef = 12 -- 12 Years -- Minimum puberty age for synthesised Dates for Chronology checks
IntDatesMarriageDef = 16 -- 16 Years -- Minimum marriage age for synthesised Dates for Chronology checks
IntDatesFertileDef = 50 -- 50 Years -- Maximum fertile age for synthesised Dates for Chronology checks
IntDatesLifespanDef = 100 -- 100 Years -- Maximum lifespan age for synthesised Dates for Chronology checks
IntChronMagnitudeDef = 12 -- 12 Months -- Magnitude of Date Chronology mismatch to deduct 1 point
IntChronToleranceDef = -20 -- -20 Points -- Degree of mismatch tolerated before excluding Individuals
IntDaysPerYear = 365.242199 -- Days per year taking account of leap years
-- Generation Gap Defaults -- Default Value -- Description of Default
IntGenGapFamilyDef = 2 -- 2 Gen Gap -- Largest Generations Up + Down that defines immediate Family to exclude ( < IntGenGapRelative )
IntGenGapRelativeDef = 6 -- 6 Gen Gap -- Largest Generations Up + Down that defines a close Relative
IntGenGapDeductDef = -5 -- -5 Points -- Deduction for a close Relative as defined above
-- Gender Mismatch Default
IntGenderDeductDef = -10 -- -10 Points -- Deduction for Individual gender mismatch and Child gender mismatch
end -- function PresetGlobalDefaults()
function SetUserInterfaceDefaults() -- Set User Interface Defaults
IntIndiScoreMin = IntIndiScoreMinDef
IntLeastResults = IntLeastResultsDef
IntLimitResults = IntLimitResultsDef
IntPruneResults = IntPruneResultsDef
IntProgBarStart = IntProgBarStartDef
end -- function SetUserInterfaceDefaults
function SetNamesMatchDefaults() -- Set Names Matching Defaults
for intRelation = IntIndivi, IntChild do
TblLastNameRight[intRelation] = IntLastNameRightDef
TblForeNameRight[intRelation] = IntForeNameRightDef
TblForeNameOther[intRelation] = IntForeNameOtherDef
TblNameSoundex [intRelation] = IntNameSoundexDef
TblNameLastWrong[intRelation] = IntNameLastWrongDef
TblNameMinimum [intRelation] = IntNameMinimumDef
TblNameDeduction[intRelation] = IntNameDeductionDef
TblNameMaximum [intRelation] = IntNameMaximumDef
TblNameThreshold[intRelation] = IntNameThresholdDef
SetNamesPoints(intRelation)
end
end -- function SetNamesMatchDefaults
function SetEventMatchDefaults() -- Set Event Matching Defaults
IntDatesTolerance = IntDatesToleranceDef
IntDatesMatched = IntDatesMatchedDef
IntDatesOverlap = IntDatesOverlapDef
IntDatesMinimum = IntDatesMinimumDef
IntDatesDeduction = IntDatesDeductionDef
IntPlacePartRight = IntPlacePartRightDef
IntPlacePartOther = IntPlacePartOtherDef
IntPlaceSoundex = IntPlaceSoundexDef
IntEventMaximum = IntEventMaximumDef
IntBoostedBirth = IntBoostedBirthDef -- V3.8
IntBoostedBapCh = IntBoostedBapChDef -- V3.8
IntBoostedMarry = IntBoostedMarryDef -- V3.8
IntBoostedDeath = IntBoostedDeathDef -- V3.8
end -- function SetEventMatchDefaults
function SetChronologyDefaults() -- Set Date Chronology Defaults
IntDatesTimespan = IntDatesTimespanDef
IntDatesVariance = IntDatesVarianceDef
IntDatesPregnant = IntDatesPregnantDef
IntDatesPuberty = IntDatesPubertyDef
IntDatesMarriage = IntDatesMarriageDef
IntDatesFertile = IntDatesFertileDef
IntDatesLifespan = IntDatesLifespanDef
IntChronMagnitude = IntChronMagnitudeDef
IntChronTolerance = IntChronToleranceDef
end -- function SetChronologyDefaults
function SetOtherMatchDefaults() -- Set Generation Gap & Gender Mismatch Defaults
IntGenGapFamily = IntGenGapFamilyDef
IntGenGapRelative = IntGenGapRelativeDef
IntGenGapDeduct = IntGenGapDeductDef
IntGenderDeduct = IntGenderDeductDef
end -- function SetOtherMatchDefaults
function SetNamesPoints(intRelation) -- Set other Names values from sticky preference values
TblNameForeOther[intRelation] = TblForeNameOther[intRelation] - TblNameSoundex[intRelation]
TblNameForeRight[intRelation] = TblForeNameRight[intRelation] - TblNameSoundex[intRelation] - TblNameForeOther[intRelation]
TblNameLastRight[intRelation] = TblLastNameRight[intRelation] - TblNameSoundex[intRelation]
end -- function SetNamesPoints
function SetEventPoints(flag) -- Set other Event values from sticky preference values
IntPartOther = IntPlacePartOther - IntPlaceSoundex
IntPartRight = IntPlacePartRight - IntPlaceSoundex - IntPartOther
if flag == nil then SaveSettings() end
end -- function SetEventPoints
function SetChronology(flag) -- Set other Chronology values from sticky preference values
IntTimespanDays = math.floor( IntDatesTimespan * IntDaysPerYear )
IntVarianceDays = math.floor( IntDatesVariance * IntDaysPerYear )
IntPregnantDays = math.floor( IntDatesPregnant * IntDaysPerYear / 12 )
IntPubertyDays = math.floor( IntDatesPuberty * IntDaysPerYear )
IntMarriageDays = math.floor( IntDatesMarriage * IntDaysPerYear )
IntFertileDays = math.floor( IntDatesFertile * IntDaysPerYear )
IntLifespanDays = math.floor( IntDatesLifespan * IntDaysPerYear )
IntChronMagDays = math.floor( IntChronMagnitude* IntDaysPerYear / 12 )
if flag == nil then SaveSettings() end
end -- function SetChronology
function SetGenerations(flag) -- Set other Generations values from sticky preference values
IntGenGapDeduction = math.abs(IntGenGapDeduct) -- The 0.01 offset below is to cater for IntGenGapDeduction = 0
IntFamGenGapMax = -math.floor( ( IntGenGapRelative - IntGenGapFamily - 0.5 ) * IntGenGapDeduction ) - 0.01
if flag == nil then SaveSettings() end
end -- function SetGenerations
function ResetDefaultSettings() -- Reset GUI Sticky Settings to Default Values
iup_gui.CustomDialogue("Main","0x0") -- Custom "Main" dialogue minimum size & centralisation
iup_gui.CustomDialogue("Font","0x0") -- Custom "Font" dialogue minimum size & centralisation
iup_gui.DefaultDialogue("Bars","Memo") -- GUI window rastersize and X & Y co-ordinates for "Main","Font","Bars","Memo" dialogues
StrLast = "Plugin Not Run Yet" -- Plugin last successful run Date
StrDate = "1 January 1900" -- Individual Date Updated threshold
StrTick = "OFF" -- Toggle tick for Last = "OFF" versus Date = "ON"
StrDiag = "OFF" -- Diagnostic Mode toggle
StrSpan = "OFF" -- Timespan Dates toggle
end -- function ResetDefaultSettings
function LoadSettings(strFileName) -- Load Sticky Settings from File
iup_gui.LoadSettings() -- Includes "Main","Font" dialogues and "FontSet" & "History"
StrDate = iup_gui.LoadGlobal("Dated",StrDate) -- Legacy for before V3.0
StrLast = iup_gui.LoadGlobal("Last" ,StrLast)
StrDate = iup_gui.LoadGlobal("Date" ,StrDate)
StrTick = iup_gui.LoadGlobal("Tick" ,StrTick)
--? StrDiag = iup_gui.LoadGlobal("Diag" ,StrDiag)
--? StrSpan = iup_gui.LoadGlobal("Span" ,StrSpan)
if StrDate == StrLast then -- Prior to V3.4 the StrLast date was moved to StrDate to use it as threshold
StrDate = "1 January 1900" -- Adjust to V3.4 arrangement keeping the dates separate selected by toggles
StrTick = "ON"
end
IntIndiScoreMin = iup_gui.LoadGlobal("IndiScoreMin" , IntIndiScoreMin)
IntLeastResults = iup_gui.LoadGlobal("LeastResults" , IntLeastResults)
IntLimitResults = iup_gui.LoadGlobal("LimitResults" , IntLimitResults)
IntPruneResults = iup_gui.LoadGlobal("PruneResults" , IntPruneResults)
TblLastNameRight = iup_gui.LoadGlobal("LastNameRight" , TblLastNameRight)
TblForeNameRight = iup_gui.LoadGlobal("ForeNameRight" , TblForeNameRight)
TblForeNameOther = iup_gui.LoadGlobal("ForeNameOther" , TblForeNameOther)
TblNameSoundex = iup_gui.LoadGlobal("NameSoundex" , TblNameSoundex)
TblNameLastWrong = iup_gui.LoadGlobal("NameLastWrong" , TblNameLastWrong)
TblNameMinimum = iup_gui.LoadGlobal("NameMinimum" , TblNameMinimum)
TblNameDeduction = iup_gui.LoadGlobal("NameDeduction" , TblNameDeduction)
TblNameMaximum = iup_gui.LoadGlobal("NameMaximum" , TblNameMaximum)
TblNameThreshold = iup_gui.LoadGlobal("NameThreshold" , TblNameThreshold)
IntDatesTolerance = iup_gui.LoadGlobal("DatesTolerance", IntDatesTolerance)
IntDatesMatched = iup_gui.LoadGlobal("DatesMatched" , IntDatesMatched)
IntDatesOverlap = iup_gui.LoadGlobal("DatesOverlap" , IntDatesOverlap)
IntDatesMinimum = iup_gui.LoadGlobal("DatesMinimum" , IntDatesMinimum)
IntDatesDeduction = iup_gui.LoadGlobal("DatesDeduction", IntDatesDeduction)
IntPlacePartRight = iup_gui.LoadGlobal("PlacePartRight", IntPlacePartRight)
IntPlacePartOther = iup_gui.LoadGlobal("PlacePartOther", IntPlacePartOther)
IntPlaceSoundex = iup_gui.LoadGlobal("PlaceSoundex" , IntPlaceSoundex)
IntEventMaximum = iup_gui.LoadGlobal("EventMaximum" , IntEventMaximum)
IntBoostedBirth = iup_gui.LoadGlobal("BoostedBirth" , IntBoostedBirth) -- V3.8
IntBoostedBapCh = iup_gui.LoadGlobal("BoostedBapCh" , IntBoostedBapCh) -- V3.8
IntBoostedMarry = iup_gui.LoadGlobal("BoostedMarry" , IntBoostedMarry) -- V3.8
IntBoostedDeath = iup_gui.LoadGlobal("BoostedDeath" , IntBoostedDeath) -- V3.8
IntDatesTimespan = iup_gui.LoadGlobal("DatesTimespan" , IntDatesTimespan)
IntDatesVariance = iup_gui.LoadGlobal("DatesVariance" , IntDatesVariance)
IntDatesPregnant = iup_gui.LoadGlobal("DatesPregnant" , IntDatesPregnant)
IntDatesPuberty = iup_gui.LoadGlobal("DatesPuberty" , IntDatesPuberty)
IntDatesMarriage = iup_gui.LoadGlobal("DatesMarriage" , IntDatesMarriage)
IntDatesFertile = iup_gui.LoadGlobal("DatesFertile" , IntDatesFertile)
IntDatesLifespan = iup_gui.LoadGlobal("DatesLifespan" , IntDatesLifespan)
IntChronMagnitude = iup_gui.LoadGlobal("ChronMagnitude", IntChronMagnitude)
IntChronTolerance = iup_gui.LoadGlobal("ChronTolerance", IntChronTolerance)
IntGenGapFamily = iup_gui.LoadGlobal("GenGapFamily" , IntGenGapFamily)
IntGenGapRelative = iup_gui.LoadGlobal("GenGapRelative", IntGenGapRelative)
IntGenGapDeduct = iup_gui.LoadGlobal("GenGapDeduct" , IntGenGapDeduct)
IntGenderDeduct = iup_gui.LoadGlobal("GenderDeduct" , IntGenderDeduct)
for intRelation = IntIndivi, IntChild do
SetNamesPoints(intRelation) -- Assign derived User Preference settings
end
SetEventPoints("No SaveSettings")
SetChronology ("No SaveSettings")
SetGenerations("No SaveSettings")
if general.FlgFileExists(StrNonDupsFile) then
TblNonDups, StrErr = table.load(StrNonDupsFile) -- Load Non-Duplicates table
end
SaveSettings() -- Save Sticky Data settings (must be last)
end -- function LoadSettings
function SaveSettings(strFileName) -- Save Sticky Settings to File
iup_gui.SaveGlobal("Last",StrLast)
iup_gui.SaveGlobal("Date",StrDate)
iup_gui.SaveGlobal("Tick",StrTick)
--? iup_gui.SaveGlobal("Diag",StrDiag)
--? iup_gui.SaveGlobal("Span",StrSpan)
iup_gui.SaveGlobal("IndiScoreMin" , IntIndiScoreMin)
iup_gui.SaveGlobal("LeastResults" , IntLeastResults)
iup_gui.SaveGlobal("LimitResults" , IntLimitResults)
iup_gui.SaveGlobal("PruneResults" , IntPruneResults)
iup_gui.SaveGlobal("LastNameRight" , TblLastNameRight)
iup_gui.SaveGlobal("ForeNameRight" , TblForeNameRight)
iup_gui.SaveGlobal("ForeNameOther" , TblForeNameOther)
iup_gui.SaveGlobal("NameSoundex" , TblNameSoundex)
iup_gui.SaveGlobal("NameLastWrong" , TblNameLastWrong)
iup_gui.SaveGlobal("NameMinimum" , TblNameMinimum)
iup_gui.SaveGlobal("NameDeduction" , TblNameDeduction)
iup_gui.SaveGlobal("NameMaximum" , TblNameMaximum)
iup_gui.SaveGlobal("NameThreshold" , TblNameThreshold)
iup_gui.SaveGlobal("DatesTolerance", IntDatesTolerance)
iup_gui.SaveGlobal("DatesMatched" , IntDatesMatched)
iup_gui.SaveGlobal("DatesOverlap" , IntDatesOverlap)
iup_gui.SaveGlobal("DatesMinimum" , IntDatesMinimum)
iup_gui.SaveGlobal("DatesDeduction", IntDatesDeduction)
iup_gui.SaveGlobal("PlacePartRight", IntPlacePartRight)
iup_gui.SaveGlobal("PlacePartOther", IntPlacePartOther)
iup_gui.SaveGlobal("PlaceSoundex" , IntPlaceSoundex)
iup_gui.SaveGlobal("EventMaximum" , IntEventMaximum)
iup_gui.SaveGlobal("BoostedBirth" , IntBoostedBirth) -- V3.8
iup_gui.SaveGlobal("BoostedBapCh" , IntBoostedBapCh) -- V3.8
iup_gui.SaveGlobal("BoostedMarry" , IntBoostedMarry) -- V3.8
iup_gui.SaveGlobal("BoostedDeath" , IntBoostedDeath) -- V3.8
iup_gui.SaveGlobal("DatesTimespan" , IntDatesTimespan)
iup_gui.SaveGlobal("DatesVariance" , IntDatesVariance)
iup_gui.SaveGlobal("DatesPregnant" , IntDatesPregnant)
iup_gui.SaveGlobal("DatesPuberty" , IntDatesPuberty)
iup_gui.SaveGlobal("DatesMarriage" , IntDatesMarriage)
iup_gui.SaveGlobal("DatesFertile" , IntDatesFertile)
iup_gui.SaveGlobal("DatesLifespan" , IntDatesLifespan)
iup_gui.SaveGlobal("ChronMagnitude", IntChronMagnitude)
iup_gui.SaveGlobal("ChronTolerance", IntChronTolerance)
iup_gui.SaveGlobal("GenGapFamily" , IntGenGapFamily)
iup_gui.SaveGlobal("GenGapRelative", IntGenGapRelative)
iup_gui.SaveGlobal("GenGapDeduct" , IntGenGapDeduct)
iup_gui.SaveGlobal("GenderDeduct" , IntGenderDeduct)
iup_gui.SaveSettings() -- Includes "Main","Font" dialogues and "FontSet" & "History"
table.save(TblNonDups,StrNonDupsFile) -- Save Non-Duplicates table
end -- function SaveSettings
function GUI_MainDialogue() -- Graphical User Interface
local function setNameItem(tblName,intRelation,intItem) -- Set Name for Relation spin value item
tblName[intRelation] = intItem
SetNamesPoints(intRelation)
SaveSettings()
end -- local function setNameItem
-- Create the Find Duplicates controls with title/value
local tglLast = iup.toggle { Title="Tick to include only Individuals updated after Plugin last run Date :"; RightButton="YES"; }
local lblLast = iup.label { Title=StrLast; Size="80"; }
local tglDate = iup.toggle { Title="Tick to include only Individuals updated after this adjustable Date :"; RightButton="YES"; }
local lblDate = iup.label { Title=StrDate; Size="80"; }
local btnPick = iup.button { Title=" Click here to select any subset of Individuals to be included "; }
local lblPick = iup.label { Title="0 Records"; }
local lblLine = iup.label { Separator="HORIZONTAL"; }
local btnFind = iup.button { Expand="YES"; Title="Find any Duplicates constrained by the Included subset of Individuals chosen above"; }
local lblTime = iup.label { Title="Estimated run time to check Individuals for Duplicates is 99 min 99 sec"; }
local btnShow = iup.button { Title="Show the previous Result Set of Duplicates in Family Historian"; }
local tglDiag = iup.toggle { Title="Enable Diagnostic Mode :"; Value=StrDiag; RightButton="YES"; }
local lblNull = iup.label { Title=" "; }
local tglSpan = iup.toggle { Title="Including Timespan Dates :"; Value=StrSpan; RightButton="YES"; Active="NO"; }
-- Create the Omit Non-Duplicates controls with title/value
local lblResult = iup.label { Title="Result Set Entries"; }
local lstResult = iup.list { Value=""; VisibleLines=9; Multiple="YES"; }
local btnSetAll = iup.button { Title="Select All"; }
local btnSetNil = iup.button { Title="Select None"; }
local btnMovAll = iup.button { Title="Move All"; }
local btnMovSet = iup.button { Title="Move Selected"; }
local lblNonDup = iup.label { Title="Non-Duplicates List"; }
local lstNonDup = iup.list { Value=""; VisibleLines=9; Multiple="YES"; }
local btnSelAll = iup.button { Title="Select All"; }
local btnSelNil = iup.button { Title="Select None"; }
local btnDelAll = iup.button { Title="Erase List"; }
local btnDelSel = iup.button { Title="Erase Selected"; }
-- Create the Dialogue Common controls with title/value
local btnUpdates = iup.button { Title="Check for Updates"; } -- V3.9
local btnGetHelp = iup.button { Title="Help && Advice"; }
local btnDestroy = iup.button { Title="Close Plugin"; }
-- Create the Set Preferences controls with title/value
local tblSet = {} -- Table to hold all the Set Preference tab controls
local intTab = 0 -- Index to Tab number on Set Preference tab
local tblTab = {} -- Table of current Preference Tab number
local intRow = 0 -- Index to Row of controls on each Tab
local tblRow = {} -- Table of current Row of controls
for intTab = 1, 5 do -- Set the default attributes for up to 5 Tabs and 15 Rows of controls
tblSet[intTab] = {}
tblTab = tblSet[intTab]
for intRow = 1, 15 do -- Local function setControls() sets Font & FgColor & BgColor & Padding
tblTab[intRow] = {}
tblRow = tblTab[intRow]
tblRow.Heading = iup.label { Title=" "; Expand="HORIZONTAL"; Padding="x1"; }
if intTab == 2 then -- Defaults for Names Matching 2nd tab
if intRow == 1 then -- 1st Row has Relations titles, and Defaults button
local tblTitle = { "Individual "; "Father "; "Mother "; "Spouse "; "Child "; }
for intRel = IntIndivi, IntChild do
tblRow[intRel]=iup.label{ Title=tblTitle[intRel]; Alignment="ACENTER:ACENTER"; }
end
tblRow.Default = iup.button{ Title="Defaults "; Tip="Restore defaults below"; }
else -- Other Rows have Relations spin controls, and default integers & measurements
for intRel = IntIndivi, IntChild do
tblRow[intRel] = iup.text { Spin="YES"; Border="NO"; Alignment="ARIGHT"; RasterSize=90; ReadOnly="YES"; SpinAlign="RIGHT"; SpinValue=0; SpinInc=1; SpinMin=0; SpinMax=100; }
end
tblRow.Integer = iup.label { Title="9"; Expand="YES"; Alignment="ARIGHT:ATOP"; } -- V3.8
tblRow.Measure = iup.label { Title="Points"; }
tblRow.Default = iup.hbox { Homogeneous="YES"; tblRow.Integer; tblRow.Measure; Margin=0; }
end
tblRow.Overall = iup.hbox { Homogeneous="YES"; tblRow.Heading; tblRow[IntIndivi]; tblRow[IntFather]; tblRow[IntMother]; tblRow[IntSpouse]; tblRow[IntChild]; tblRow.Default; }
else -- Defaults for all except Names Matching 2nd tab
if intRow == 1 then -- 1st Row has bold title, Current Settings title, and Default Settings button
tblRow.Current = iup.label { Title=" Current Settings "; }
tblRow.Default = iup.button{ Title=" Default Settings "; Tip="Restore defaults below"; }
else -- Other Rows have Settings spin control, and default integers & measurements
tblRow.Current = iup.text { Spin="YES"; Border="NO"; Alignment="ARIGHT"; RasterSize=110; ReadOnly="YES"; SpinAlign="RIGHT"; SpinValue=0; SpinInc=1; SpinMin=0; SpinMax=100; }
tblRow.Integer = iup.label { Title="9"; Expand="YES"; Alignment="ARIGHT:ATOP"; } -- V3.8
tblRow.Measure = iup.label { Title=" Points "; }
tblRow.Default = iup.hbox { Homogeneous="YES"; tblRow.Integer; tblRow.Measure; Margin=0; }
end
tblRow.Overall = iup.hbox { Homogeneous="YES"; tblRow.Heading; tblRow.Current; tblRow.Default; }
end
end
end
intTab = intTab + 1 -- User Interface is 1st tab
tblTab = tblSet[intTab]
intRow = 1
local intInter = intTab -- Save tab number for Preferences tab control
local tblResultSetLim = tblTab[intRow]
tblResultSetLim.Heading.Title = "Result Set Limits" -- 1st row with Default Settings button
intRow = intRow + 1
local tblIndiScoreMin = tblTab[intRow] -- 2nd row
tblIndiScoreMin.Heading.Title = "Individual Threshold"
tblIndiScoreMin.Current.SpinMin = -100
tblIndiScoreMin.Current.SpinMax = 100
tblIndiScoreMin.Current.spin_cb = function(self,intItem) IntIndiScoreMin=intItem SaveSettings() end
tblIndiScoreMin.Integer.Title = tostring(IntIndiScoreMinDef)
intRow = intRow + 1
local tblLeastResults = tblTab[intRow] -- 3rd row
tblLeastResults.Heading.Title = "Results Minimum Score"
tblLeastResults.Current.SpinMin = -100
tblLeastResults.Current.SpinMax = 100
tblLeastResults.Current.spin_cb = function(self,intItem) IntLeastResults=intItem SaveSettings() end
tblLeastResults.Integer.Title = tostring(IntLeastResultsDef)
tblLeastResults.Measure.Title = " Point "
intRow = intRow + 1
local tblLimitResults = tblTab[intRow] -- 4th row
tblLimitResults.Heading.Title = "Results Maximum Rows"
tblLimitResults.Current.SpinMin = 20
tblLimitResults.Current.SpinMax = 500
tblLimitResults.Integer.Title = tostring(IntLimitResultsDef)
tblLimitResults.Measure.Title = " Rows "
intRow = intRow + 1
local tblPruneResults = tblTab[intRow] -- 5th row
tblPruneResults.Heading.Title = "Memory Conservation"
tblPruneResults.Current.SpinMin = 20
tblPruneResults.Current.SpinMax = 1000
tblPruneResults.Current.spin_cb = function(self,intItem) IntPruneResults=intItem SetEventPoints() end
tblPruneResults.Integer.Title = tostring(IntPruneResultsDef)
tblPruneResults.Measure.Title = " Entries "
intRow = intRow + 1
iup.Destroy(tblTab[intRow].Overall) -- 6th row replaces defaults with a horizontal separator
tblTab[intRow].Overall = iup.vbox { iup.hbox { Margin="8x15"; }; iup.label { Separator="HORIZONTAL"; }; Margin="1x1"; }
intRow = intRow + 1
local btnDefault = iup.button { Title="Restore GUI Defaults"; }
local btnSoundex = iup.button { Title="Erase Soundex Cache"; } -- 7th row replaces defaults with three buttons
local btnSetFont = iup.button { Title="Set Window Fonts"; }
iup.Destroy(tblTab[intRow].Overall)
tblTab[intRow].Overall = iup.hbox { btnDefault; btnSoundex; btnSetFont; Homogeneous="YES"; Margin="30x50"; Gap="10"; }
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Names Matching 2nd tab --
tblTab = tblSet[intTab]
intRow = 1
local intNames = intTab -- Save tab number for Preferences tab control
local tblNamesDefaults = tblTab[intRow]
tblNamesDefaults.Heading.Title = "Names" -- 1st row with Defaults button
intRow = intRow + 1
local tblLastNameRight = tblTab[intRow] -- 2nd row
tblLastNameRight.Heading.Title = "Last Right"
for intRel = IntIndivi, IntChild do
tblLastNameRight[intRel].spin_cb = function(self,intItem) setNameItem(TblLastNameRight,intRel,intItem) end
end
tblLastNameRight.Integer.Title = tostring(IntLastNameRightDef)
intRow = intRow + 1
local tblForeNameRight = tblTab[intRow] -- 3rd row
tblForeNameRight.Heading.Title = "Fore Right"
for intRel = IntIndivi, IntChild do
tblForeNameRight[intRel].spin_cb = function(self,intItem) setNameItem(TblForeNameRight,intRel,intItem) end
end
tblForeNameRight.Integer.Title = tostring(IntForeNameRightDef)
intRow = intRow + 1
local tblForeNameOther = tblTab[intRow] -- 4th row
tblForeNameOther.Heading.Title = "Fore Other"
for intRel = IntIndivi, IntChild do
tblForeNameOther[intRel].SpinMin = -100
tblForeNameOther[intRel].spin_cb = function(self,intItem) setNameItem(TblForeNameOther,intRel,intItem) end
end
tblForeNameOther.Integer.Title = tostring(IntForeNameOtherDef)
intRow = intRow + 1
local tblNameSoundex = tblTab[intRow] -- 5th row
tblNameSoundex.Heading.Title = "Soundex"
for intRel = IntIndivi, IntChild do
tblNameSoundex[intRel].spin_cb = function(self,intItem) setNameItem(TblNameSoundex,intRel,intItem) end
end
tblNameSoundex.Integer.Title = tostring(IntNameSoundexDef)
intRow = intRow + 1
local tblNameLastWrong = tblTab[intRow] -- 6th row
tblNameLastWrong.Heading.Title = "Last Wrong"
for intRel = IntIndivi, IntChild do
tblNameLastWrong[intRel].SpinMin = -100
tblNameLastWrong[intRel].SpinMax = 0
tblNameLastWrong[intRel].spin_cb = function(self,intItem) setNameItem(TblNameLastWrong,intRel,intItem) end
end
tblNameLastWrong.Integer.Title = tostring(IntNameLastWrongDef)
intRow = intRow + 1
local tblNameMinimum = tblTab[intRow] -- 7th row
tblNameMinimum.Heading.Title = "Minimum"
tblNameMinimum[IntIndivi].visible = "NO"
for intRel = IntIndivi, IntChild do
tblNameMinimum[intRel].spin_cb = function(self,intItem) setNameItem(TblNameMinimum,intRel,intItem) end
end
tblNameMinimum.Integer.Title = tostring(IntNameMinimumDef)
tblNameMinimum.Measure.Title = "Point "
intRow = intRow + 1
local tblNameDeduction = tblTab[intRow] -- 8th row
tblNameDeduction.Heading.Title = "Deduction"
tblNameDeduction[IntIndivi].visible= "NO"
for intRel = IntFather, IntChild do
tblNameDeduction[intRel].SpinMin = -100
tblNameDeduction[intRel].SpinMax = 0
tblNameDeduction[intRel].spin_cb = function(self,intItem) setNameItem(TblNameDeduction,intRel,intItem) end
end
tblNameDeduction.Integer.Title = tostring(IntNameDeductionDef)
intRow = intRow + 1
local tblNameMaximum = tblTab[intRow] -- 9th row
tblNameMaximum.Heading.Title = "Maximum"
for intRel = IntIndivi, IntChild do
tblNameMaximum[intRel].spin_cb = function(self,intItem) setNameItem(TblNameMaximum,intRel,intItem) end
end
tblNameMaximum.Integer.Title = tostring(IntNameMaximumDef)
intRow = intRow + 1
local tblNameThreshold = tblTab[intRow] -- 10th row
tblNameThreshold.Heading.Title = "Threshold"
for intRel = IntIndivi, IntChild do
tblNameThreshold[intRel].spin_cb = function(self,intItem) setNameItem(TblNameThreshold,intRel,intItem) end
end
tblNameThreshold.Integer.Title = tostring(IntNameThresholdDef)
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Event Matching 3rd tab --
tblTab = tblSet[intTab]
intRow = 1
local intEvent = intTab -- Save tab number for Preferences tab control
local tblEventDefault = tblTab[intRow]
tblEventDefault.Heading.Title = "Events" -- 1st row with Default Settings button
intRow = intRow + 1
local tblDatesTolerance = tblTab[intRow] -- 2nd row
tblDatesTolerance.Heading.Title = "Dates Tolerance"
tblDatesTolerance.Current.SpinMax = 200
tblDatesTolerance.Current.spin_cb = function(self,intItem) IntDatesTolerance=intItem SetEventPoints() end
tblDatesTolerance.Integer.Title = StrPlusMinus..tostring(IntDatesToleranceDef)
tblDatesTolerance.Measure.Title = " Days "
intRow = intRow + 1
local tblDatesMatched = tblTab[intRow] -- 3rd row
tblDatesMatched.Heading.Title = "Dates Matched"
tblDatesMatched.Current.spin_cb = function(self,intItem) IntDatesMatched=intItem SetEventPoints() end
tblDatesMatched.Integer.Title = tostring(IntDatesMatchedDef)
intRow = intRow + 1
local tblDatesOverlap = tblTab[intRow] -- 4th row
tblDatesOverlap.Heading.Title = "Dates Overlap"
tblDatesOverlap.Current.spin_cb = function(self,intItem) IntDatesOverlap=intItem SetEventPoints() end
tblDatesOverlap.Integer.Title = tostring(IntDatesOverlapDef)
intRow = intRow + 1
local tblDatesMinimum = tblTab[intRow] -- 5th row
tblDatesMinimum.Heading.Title = "Dates Minimum"
tblDatesMinimum.Current.spin_cb = function(self,intItem) IntDatesMinimum=intItem SetEventPoints() end
tblDatesMinimum.Integer.Title = tostring(IntDatesMinimumDef)
tblDatesMinimum.Measure.Title = " Point "
intRow = intRow + 1
local tblDatesDeduction = tblTab[intRow] -- 6th row
tblDatesDeduction.Heading.Title = "Dates Deduction"
tblDatesDeduction.Current.SpinMin = -100
tblDatesDeduction.Current.SpinMax = 0
tblDatesDeduction.Current.spin_cb = function(self,intItem) IntDatesDeduction=intItem SetEventPoints() end
tblDatesDeduction.Integer.Title = tostring(IntDatesDeductionDef)
intRow = intRow + 1
local tblPlacePartRight = tblTab[intRow] -- 7th row
tblPlacePartRight.Heading.Title = "Place Part Right"
tblPlacePartRight.Current.spin_cb = function(self,intItem) IntPlacePartRight=intItem SetEventPoints() end
tblPlacePartRight.Integer.Title = tostring(IntPlacePartRightDef)
intRow = intRow + 1
local tblPlacePartOther = tblTab[intRow] -- 8th row
tblPlacePartOther.Heading.Title = "Place Part Other"
tblPlacePartOther.Current.spin_cb = function(self,intItem) IntPlacePartOther=intItem SetEventPoints() end
tblPlacePartOther.Integer.Title = tostring(IntPlacePartOtherDef)
intRow = intRow + 1
local tblPlaceSoundex = tblTab[intRow] -- 9th row
tblPlaceSoundex.Heading.Title = "Place Part Soundex"
tblPlaceSoundex.Current.spin_cb = function(self,intItem) IntPlaceSoundex=intItem SetEventPoints() end
tblPlaceSoundex.Integer.Title = tostring(IntPlaceSoundexDef)
tblPlaceSoundex.Measure.Title = " Point "
intRow = intRow + 1
local tblEventMaximum = tblTab[intRow] -- 10th row
tblEventMaximum.Heading.Title = "Event Maximum"
tblEventMaximum.Current.spin_cb = function(self,intItem) IntEventMaximum=intItem SetEventPoints() end
tblEventMaximum.Integer.Title = tostring(IntEventMaximumDef)
intRow = intRow + 1
local tblBoostedBirth = tblTab[intRow] -- 11th row -- V3.8
tblBoostedBirth.Heading.Title = "Boost Birth Events"
tblBoostedBirth.Current.spin_cb = function(self,intItem) IntBoostedBirth=intItem SetEventPoints() end
tblBoostedBirth.Integer.Title = tostring(IntBoostedBirthDef)
tblBoostedBirth.Measure.Title = " Times "
intRow = intRow + 1
local tblBoostedBapCh = tblTab[intRow] -- 12th row -- V3.8
tblBoostedBapCh.Heading.Title = "Boost Baptism/Christening"
tblBoostedBapCh.Current.spin_cb = function(self,intItem) IntBoostedBapCh=intItem SetEventPoints() end
tblBoostedBapCh.Integer.Title = tostring(IntBoostedBapChDef)
tblBoostedBapCh.Measure.Title = " Times "
intRow = intRow + 1
local tblBoostedMarry = tblTab[intRow] -- 13th row -- V3.8
tblBoostedMarry.Heading.Title = "Boost Marriage Events"
tblBoostedMarry.Current.spin_cb = function(self,intItem) IntBoostedMarry=intItem SetEventPoints() end
tblBoostedMarry.Integer.Title = tostring(IntBoostedMarryDef)
tblBoostedMarry.Measure.Title = " Times "
intRow = intRow + 1
local tblBoostedDeath = tblTab[intRow] -- 14th row -- V3.8
tblBoostedDeath.Heading.Title = "Boost Death/Burial/Cremate"
tblBoostedDeath.Current.spin_cb = function(self,intItem) IntBoostedDeath=intItem SetEventPoints() end
tblBoostedDeath.Integer.Title = tostring(IntBoostedDeathDef)
tblBoostedDeath.Measure.Title = " Times "
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Date Chronology 4th tab --
tblTab = tblSet[intTab]
intRow = 1
local intChron = intTab -- Save tab number for Preferences tab control
local tblChronDefault = tblTab[intRow]
tblChronDefault.Heading.Title = "Dates" -- 1st row with Default Settings button
intRow = intRow + 1
local tblDatesTimespan = tblTab[intRow] -- 2nd row
tblDatesTimespan.Heading.Title = "Dates Timespan"
tblDatesTimespan.Current.SpinMax = 200
tblDatesTimespan.Current.spin_cb = function(self,intItem) IntDatesTimespan=intItem SetChronology() end
tblDatesTimespan.Integer.Title = StrPlusMinus..tostring(IntDatesTimespanDef)
tblDatesTimespan.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesVariance = tblTab[intRow] -- 3rd row
tblDatesVariance.Heading.Title = "Dates Variance"
tblDatesVariance.Current.spin_cb = function(self,intItem) IntDatesVariance=intItem SetChronology() end
tblDatesVariance.Integer.Title = StrPlusMinus..tostring(IntDatesVarianceDef)
tblDatesVariance.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesPregnant = tblTab[intRow] -- 4th row
tblDatesPregnant.Heading.Title = "Pregnancy Duration"
tblDatesPregnant.Current.SpinMin = 6
tblDatesPregnant.Current.SpinMax = 12
tblDatesPregnant.Current.spin_cb = function(self,intItem) IntDatesPregnant=intItem SetChronology() end
tblDatesPregnant.Integer.Title = tostring(IntDatesPregnantDef)
tblDatesPregnant.Measure.Title = " Months "
intRow = intRow + 1
local tblDatesPuberty = tblTab[intRow] -- 5th row
tblDatesPuberty.Heading.Title = "Min Puberty Age"
tblDatesPuberty.Current.SpinMin = 10
tblDatesPuberty.Current.SpinMax = 20
tblDatesPuberty.Current.spin_cb = function(self,intItem) IntDatesPuberty=intItem SetChronology() end
tblDatesPuberty.Integer.Title = tostring(IntDatesPubertyDef)
tblDatesPuberty.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesMarriage = tblTab[intRow] -- 6th row
tblDatesMarriage.Heading.Title = "Min Marriage Age"
tblDatesMarriage.Current.SpinMin = 10
tblDatesMarriage.Current.SpinMax = 20
tblDatesMarriage.Current.spin_cb = function(self,intItem) IntDatesMarriage=intItem SetChronology() end
tblDatesMarriage.Integer.Title = tostring(IntDatesMarriageDef)
tblDatesMarriage.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesFertile = tblTab[intRow] -- 7th row
tblDatesFertile.Heading.Title = "Max Fertile Age"
tblDatesFertile.Current.SpinMin = 40
tblDatesFertile.Current.SpinMax = 80
tblDatesFertile.Current.spin_cb = function(self,intItem) IntDatesFertile=intItem SetChronology() end
tblDatesFertile.Integer.Title = tostring(IntDatesFertileDef)
tblDatesFertile.Measure.Title = " Years "
intRow = intRow + 1
local tblDatesLifespan = tblTab[intRow] -- 8th row
tblDatesLifespan.Heading.Title = "Max Lifespan Age"
tblDatesLifespan.Current.SpinMin = 60
tblDatesLifespan.Current.SpinMax = 140
tblDatesLifespan.Current.spin_cb = function(self,intItem) IntDatesLifespan=intItem SetChronology() end
tblDatesLifespan.Integer.Title = tostring(IntDatesLifespanDef)
tblDatesLifespan.Measure.Title = " Years "
intRow = intRow + 1
local tblChronMagnitude = tblTab[intRow] -- 9th row
tblChronMagnitude.Heading.Title = "Chron Magnitude"
tblChronMagnitude.Current.SpinMin = 1
tblChronMagnitude.Current.SpinMax = 120
local txtChMag = tblChronMagnitude.Current
tblChronMagnitude.Integer.Title = tostring(IntChronMagnitudeDef)
tblChronMagnitude.Measure.Title = " Months "
intRow = intRow + 1
local tblChronTolerance = tblTab[intRow] -- 10th row
tblChronTolerance.Heading.Title = "Chron Tolerance"
tblChronTolerance.Current.SpinMin = -100
tblChronTolerance.Current.SpinMax = 0
tblChronTolerance.Current.spin_cb = function(self,intItem) IntChronTolerance=intItem SetChronology() end
tblChronTolerance.Integer.Title = tostring(IntChronToleranceDef)
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Family & Gender 5th tab --
tblTab = tblSet[intTab]
intRow = 1
local intOther = intTab -- Save tab number for Preferences tab control
local tblOtherDefault = tblTab[intRow]
tblOtherDefault.Heading.Title = "Generation Gap" -- 1st row with Default Settings button
intRow = intRow + 1
local tblGenGapFamily = tblTab[intRow] -- 2nd row
tblGenGapFamily.Heading.Title = "Family Generations"
tblGenGapFamily.Current.SpinMax = 10
tblGenGapFamily.Current.spin_cb = function(self,intItem) IntGenGapFamily=intItem SetGenerations() end
tblGenGapFamily.Integer.Title = tostring(IntGenGapFamilyDef)
tblGenGapFamily.Measure.Title = " Gen Gap "
intRow = intRow + 1
local tblGenGapRelative = tblTab[intRow] -- 3rd row
tblGenGapRelative.Heading.Title = "Relatives Generations"
tblGenGapRelative.Current.SpinMax = 10
tblGenGapRelative.Current.spin_cb = function(self,intItem) IntGenGapRelative=intItem SetGenerations() end
tblGenGapRelative.Integer.Title = tostring(IntGenGapRelativeDef)
tblGenGapRelative.Measure.Title = " Gen Gap "
intRow = intRow + 1
local tblGenGapDeduct = tblTab[intRow] -- 4th row
tblGenGapDeduct.Heading.Title = "Relatives Deduction"
tblGenGapDeduct.Current.SpinMin = -100
tblGenGapDeduct.Current.SpinMax = 0
tblGenGapDeduct.Current.spin_cb = function(self,intItem) IntGenGapDeduct=intItem SetGenerations() end
tblGenGapDeduct.Integer.Title = tostring(IntGenGapDeductDef)
intRow = intRow + 1
iup.Destroy(tblTab[intRow].Overall) -- 5th row replaces defaults with horizontal separator
tblTab[intRow].Overall = iup.vbox { iup.hbox { Margin="8x8"; }; iup.label { Separator="HORIZONTAL"; }; iup.hbox { Margin="8x8" }; Margin="1x1"; }
intRow = intRow + 1
local tblGenderMismatch = tblTab[intRow] -- 6th row
iup.Destroy(tblGenderMismatch.Heading)
iup.Destroy(tblGenderMismatch.Current)
iup.Destroy(tblGenderMismatch.Default)
tblGenderMismatch.Heading = iup.label { Title="Gender Mismatch"; }
tblGenderMismatch.Overall = iup.hbox { Homogeneous="YES"; tblGenderMismatch.Heading; iup.label { Title=" Current Settings "; Expand="YES"; }; iup.label { Title=" " }; }
intRow = intRow + 1
local tblGenderDeduct = tblTab[intRow] -- 7th row
tblGenderDeduct.Heading.Title = "Gender Deduction"
tblGenderDeduct.Current.SpinMin = -100
tblGenderDeduct.Current.SpinMax = 0
tblGenderDeduct.Current.spin_cb = function(self,intItem) IntGenderDeduct=intItem SaveSettings() end
tblGenderDeduct.Integer.Title = tostring(IntGenderDeductDef)
intRow = intRow + 1
tblTab[intRow] = nil
intTab = intTab + 1 -- Signal end of table of Preferences tab controls
tblSet[intTab] = nil
-- Create the Find Duplicates tab layout
local vboxFind = iup.vbox { Gap="8"; Margin="8x8";
iup.hbox { Margin="10x0"; iup.label{Expand="YES";}; tglLast; lblLast; };
iup.hbox { Margin="10x0"; iup.label{Expand="YES";}; tglDate; lblDate; };
iup.hbox { Margin="10x0"; btnPick; lblPick; };
iup.hbox { Margin="10x20";lblLine; };
iup.hbox { Margin="10x0"; btnFind; };
iup.hbox { Margin="10x0"; lblTime; };
iup.hbox { Margin="10x0"; btnShow; };
iup.hbox { Margin="10x0"; iup.label{Expand="YES";}; tglDiag; iup.label{Expand="YES";}; tglSpan; iup.label {Expand="YES";}; };
}
-- Create the Omit Non-Duplicates tab layout
local vboxOmit = iup.vbox { Gap="4"; Margin="4x4";
iup.vbox { Margin="10x0";
lblResult; lstResult;
iup.hbox { Margin="0x0"; btnSetAll; btnSetNil; btnMovAll; btnMovSet; Homogeneous="YES"; Expand="HORIZONTAL"; };
lblNonDup; lstNonDup;
iup.hbox { Margin="0x0"; btnSelAll; btnSelNil; btnDelAll; btnDelSel; Homogeneous="YES"; Expand="HORIZONTAL"; };
};
}
-- Create the Set Preferences tab layout
local tblPref = {}
for intTab = 1, #tblSet do
tblTab = tblSet[intTab]
if tblTab == nil then break end
local strMargin = "40x1"
if intTab == intNames then strMargin = "2x1" end
tblPref[intTab] = iup.vbox { Gap="8"; Margin=strMargin; }
for intRow = 1, #tblTab do
if tblTab[intRow] == nil then break end
iup.Append( tblPref[intTab], tblTab[intRow].Overall )
end
end
local tabPref = iup.tabs {
tblPref[intInter]; TabTitle0=" User Interface ";
tblPref[intNames]; TabTitle1=" Names Matching ";
tblPref[intEvent]; TabTitle2=" Event Matching ";
tblPref[intChron]; TabTitle3=" Date Chronology ";
tblPref[intOther]; TabTitle4=" Family && Gender ";
}
local vboxPref = iup.vbox { Gap="10";
tabPref;
}
-- Create the Tab controls layout
local tabCont = iup.tabs {
vboxFind; TabTitle0=" Find Duplicates ";
vboxOmit; TabTitle1="Omit Non-Duplicates";
vboxPref; TabTitle2=" Set Preferences ";
}
-- Combine all the above controls
local allCont = iup.vbox {
tabCont;
iup.hbox { btnUpdates; btnGetHelp; btnDestroy; Homogeneous="YES"; Margin="90x10"; Gap="10"; Expand="HORIZONTAL"; } -- V3.9
}
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local dialogMain = iup.dialog { Title=iup_gui.Plugin..iup_gui.Version;
allCont;
}
-- Local Variables and Functions
local intTotal = 0 -- Total number of Idividual Records
local intPick = 0 -- Picked number of Individual Records
local tblIndi = { } -- User selection of Individual Records
local intDate = 0 -- Date threshold for last Updated value
local tblResults = { } -- Results List for Omit Non-Duplicates tab, and Show previous Result Set button
local intTabPosn = 0
local tblControls = { } -- GUI control attributes used by doSetFont
local function setMonospacedFont() -- Monospaced font "Consolas, " or "Lucida Console, " or "Lucida Sans Typewriter, " or "DejaVu Sans Mono, "
local strFont = iup_gui.FontBody:gsub(".-, ","Lucida Console, ")
lstResult.Font = strFont
lstNonDup.Font = strFont
end -- local function setMonospacedFont
local function setControls() -- Set control attributes mainly for Preference tab Font & FgColor & BgColor & Padding
local function setAttribs(iupControl,intRow) -- pcall(setAttribs,iupControl,intRow) prevents missing control handle errors propagating
if intRow == 1 then
if iupControl.Title:match("Default") then
iupControl.FgColor = iup_gui.Safe -- Set all Default button attributes
iupControl.Expand = "HORIZONTAL"
iupControl.Padding = "10x2"
else
iupControl.Expand = "YES" -- Set all Spin heading attributes
iupControl.Padding = "x3"
end
elseif iupControl.Spin == "YES" then
iupControl.Font = iup_gui.FontHead -- Set all Spin control attributes
iupControl.FgColor = iup_gui.Safe
iupControl.BgColor = iup_gui.Smoke
iupControl.Padding = "20"
iupControl.Size = "42"
end
end -- local function setAttribs()
if fhGetAppVersion() > 6 then -- FH V7 IUP 3.28 -- V3.8
tabPref.TabPadding = "8x4"
tabCont.TabPadding = "8x4"
else -- FH V6 IUP 3.11 -- V3.8
tabPref.Padding = "8x8"
tabCont.Padding = "8x8"
end
for i, iupControl in ipairs({ tblResultSetLim.Heading; tblNamesDefaults.Heading; tblEventDefault.Heading; tblChronDefault.Heading; tblOtherDefault.Heading; tblGenderMismatch.Heading; }) do
iupControl.Font = iup_gui.FontHead
iupControl.FgColor = iup_gui.Head -- Set top left Header attributes
iupControl.Expand = "YES"
iupControl.Padding = "x4"
end
for i, iupControl in ipairs({ tblPref[intInter]; tblPref[intNames]; tblPref[intEvent]; tblPref[intChron]; tblPref[intOther]; }) do
iupControl.Font = iup_gui.FontBody
iupControl.FgColor = iup_gui.Body -- Set each Preference tab default attributes
end
for intTab = 1, #tblSet do -- Reset the attributes of Defaults buttons and Spin headings/controls
tblTab = tblSet[intTab]
if tblTab == nil then break end
for intRow = 1, #tblTab do
tblRow = tblTab[intRow]
if tblRow == nil then break end
if intRow == 1 then
pcall(setAttribs,tblRow.Default,intRow) -- pcall for Defaults button prevents missing handle errors propagating
end
if intTab == intNames then
for intRel = IntIndivi, IntChild do
pcall(setAttribs,tblRow[intRel],intRow) -- pcall for Spin heading/control prevents missing handle errors propagating
end
else
pcall(setAttribs,tblRow.Current,intRow) -- pcall for Spin heading/control prevents missing handle errors propagating
end
end
end
setMonospacedFont()
end -- local function setControls
local function doResetRecords() -- Reset all Records to excluded
intTotal = 0
local ptrIndi = fhNewItemPtr()
ptrIndi:MoveToFirstRecord("INDI")
while ptrIndi:IsNotNull() do
local intRecId = fhGetRecordId(ptrIndi)
if not TblData[intRecId] then TblData[intRecId] = {} end -- Create table of Data entries per Record Id
TblData[intRecId].Chosen = false -- Flag all Records as excluded
intTotal = intTotal + 1 -- Count the Total number of Records
ptrIndi:MoveNext()
end
end -- local function doResetRecords
local function intChosenRecord(ptrIndi) -- Check if Record last Updated after chosen Date
local intRecId = fhGetRecordId(ptrIndi)
if ( fhCallBuiltInFunction("DayNumber",fhCallBuiltInFunction("LastUpdated",ptrIndi)) or 999999 ) >= intDate then -- 0 => 999999 to allow undated records -- V3.7
TblData[intRecId].Chosen = true -- Flag the chosen Records to include
return 1
end
TblData[intRecId].Chosen = false -- Flag the other Records to exclude
return 0
end -- local function intChosenRecord
local function setEstimatedTime() -- Set estimated run time based on chosen & total records
local strTime = "less than a few minutes"
local intScale = 60000000
if tglDiag.Value == "ON" then intScale = intScale / 20 end -- Lengthen estimate in Diagnostic Mode
local intMins = math.floor( intPick * intTotal / intScale )
if intMins <= 0 then strTime = "only a few seconds" end
if intMins >= 2 then strTime = "from "..intMins.." minutes to "..(intMins*4).." minutes" end
lblTime.Title = "Estimated run time to check for Duplicates is "..strTime
end -- local function setEstimatedTime
local function doPickRecords() -- Pick chosen Individual Records
intPick = 0
if #tblIndi == 0 then -- No selection
local ptrIndi = fhNewItemPtr()
ptrIndi:MoveToFirstRecord("INDI")
while ptrIndi:IsNotNull() do
intPick = intPick + intChosenRecord(ptrIndi) -- So check all the Records
ptrIndi:MoveNext()
end
else
for intIndi = 1, #tblIndi do
intPick = intPick + intChosenRecord(tblIndi[intIndi]) -- Check just selected Records
end
end
local strRecords = " Records Chosen"
if intPick == 1 then strRecords = strRecords:gsub("s "," ") end
lblPick.Title = intPick..strRecords
setEstimatedTime()
if intPick == 0 then btnFind.Active = "NO" else btnFind.Active = "YES" end
return intPick
end -- local function doPickRecords
local function setDateValue() -- Set Date Value and toggle ticks
local datDate = fhNewDate(9999)
if StrTick == "ON" and StrLast:match("^%d") then -- Use the Plugin Last run Date if it exists
tglLast.Value = "ON"
tglDate.Value = "OFF"
lblLast.Active = "YES" -- Set mode of toggles & Dates
lblDate.Active = "NO"
datDate:SetValueAsText(StrLast)
intDate = fhCallBuiltInFunction("DayNumber",datDate:GetDatePt1()) or 999999
else
StrTick = "OFF" -- Use the adjustable Date
tglLast.Value = "OFF"
tglDate.Value = "ON"
lblLast.Active = "NO" -- Set mode of toggles & Dates
lblDate.Active = "YES"
local strDate = lblDate.Title
datDate:SetValueAsText(strDate) -- Check that Date has valid format
intDate = general.GetDayNumber(datDate:GetDatePt1())
if intDate == 0 then
iup_gui.MemoDialogue("\n Unrecognised Date \n "..strDate.." \n")
intDate = 999999
else
StrDate = strDate
SaveSettings() -- Save the adjustable Date
end
--[==[
if datDate:SetValueAsText(strDate) then -- Check that Date has valid format
intDate = fhCallBuiltInFunction("DayNumber",datDate:GetDatePt1())
else
intDate = nil
end
if intDate then
StrDate = strDate
SaveSettings() -- Save the adjustable Date
else
iup_gui.MemoDialogue("\n Unrecognised Date \n "..strDate.." \n")
intDate = 999999
end
--]==]
end
end -- local function setDateValue
local function getEntry(intRecId) -- From Rec Id get Name & Format Size for Entry
local ptrName = fhNewItemPtr()
ptrName:MoveToRecordById("INDI",intRecId or 0)
local strName = fhGetDisplayText(ptrName)
local intSize = strName:length() -- Name length in characters
if string.encoding() == "UTF-8" then
while intSize > 30 do -- Remove trailing UTF-8 chars beyond 30th
intSize = intSize - 1
strName = strName:gsub("[%z\1-\127\194-\244][\128-\191]*$","")
end
end
return intRecId,strName,( 30 + strName:len() - intSize ) -- Format size adjusted for UTF-8 chars
end -- local function getEntry
local function strFormatResult(tblEntry) -- Format a pair of Duplicate Records
if not tblEntry then return " " end
local intRecIdA,strNameA,intSizeA = getEntry(tblEntry.RecordIdA)
local intRecIdB,strNameB,intSizeB = getEntry(tblEntry.RecordIdB)
local strFormat = ("%6d %-{A}.{A}s%6d %-.{B}s"):gsub("{A}",intSizeA):gsub("{B}",intSizeB)
return string.format(strFormat,intRecIdA,strNameA,intRecIdB,strNameB)
end -- local function strFormatResult
local function doDisplayTables() -- Display both Results List and Non-Duplicates tables in Omit Non-Duplicates tab
setMonospacedFont()
local intValue = lstResult.Value
lstResult.removeitem = nil
for intEntry = 1, #tblResults do
lstResult[intEntry] = strFormatResult(tblResults[intEntry]) -- Results List candidate pairs
end
lstResult.Value = intValue
local intValue = lstNonDup.Value
lstNonDup.removeitem = nil
for intEntry = 1, #TblNonDups do
lstNonDup[intEntry] = strFormatResult(TblNonDups[intEntry]) -- Non-Duplicates list of pairs
end
lstNonDup.Value = intValue
end -- local function doDisplayTables
local function doLoadLists() -- Load the Result List excluding missing Records and Non-Duplicate pairs
if general.FlgFileExists(StrResultsFile) then
local tblResultsFile, strErr = table.load(StrResultsFile) -- Retrieve saved previous Result Set file
local intEntry = 1
for i, tblResultsFile in ipairs( tblResultsFile ) do
local intDataA = tblResultsFile.RecordIdA -- Get RecordIds of each Result Set pair in turn
local intDataB = tblResultsFile.RecordIdB
local ptrIndiA = fhNewItemPtr()
local ptrIndiB = fhNewItemPtr()
ptrIndiA:MoveToRecordById('INDI',intDataA) -- Do both Individual Records exist i.e. have not been Merged
ptrIndiB:MoveToRecordById('INDI',intDataB)
if ptrIndiA:IsNotNull() and ptrIndiB:IsNotNull() then -- Both the Individual Records do exist in GEDCOM
tblResults[intEntry] = tblResultsFile
for i, tblNonDups in ipairs( TblNonDups ) do -- Search Non-Duplicates table
if intDataA == tblNonDups.RecordIdA and intDataB == tblNonDups.RecordIdB then
tblResults[intEntry] = nil
intEntry = intEntry - 1 -- Exclude Non-Duplicate pairs
break
end
end
intEntry = intEntry + 1 -- Step onto next internal Results List entry
end
end
doDisplayTables()
if #TblNonDups > 0 then btnDelAll.Active = "YES" end
end
end -- local function doLoadLists
local function doTick(tblArg,intMode) -- Action for Last run Date & adjustable Date toggles
local intState = tblArg[2]
if intState == intMode then StrTick = "ON" else StrTick = "OFF" end
setDateValue()
doPickRecords() -- Pick and count the Records
SaveSettings()
end -- local function doTick
function lblDate:button_cb(iupButton,intPressed,intPosX,intPosY,strStatus)
if iupButton == iup.BUTTON1 and intPressed == 1 then -- Left mouse button is pressed within Date label
local datDate = fhNewDate(0000)
local isOK = datDate:SetValueAsText(StrDate)
datDate = fhPromptUserForDate(datDate) -- Prompt user with adjustable Date as default
if datDate
and datDate:GetType() == "Simple"
and datDate:GetSubtype() == "" then
lblDate.Title = datDate:GetDisplayText() -- New adjusted Date is plain simple
setDateValue()
doPickRecords() -- Pick and count the Records
else
iup_gui.MemoDialogue("\n The adjustable Date must be a Simple date and \n not a Period, Phrase, Range, or Quarter Date. \n")
end
end
end -- function lblDate:button_cb
local function doPick() -- Action for Pick button
dialogMain.Active = "NO"
tblIndi = fhPromptUserForRecordSel('INDI')
if #tblIndi > 0 then doResetRecords() end
doPickRecords() -- Pick and count the Records
dialogMain.bringfront = "YES"
dialogMain.Active = "YES"
end -- local function doPick
local function doFind() -- Action for Find any Duplicates button
local tglSpanActive = tglSpan.Active
tglSpan.Active = "NO"
dialogMain.Active = "NO"
if intPick > 0 then -- If any Records chosen, then run Find Duplicates, which returns true if any found and not stopped
if FindDuplicateRecords(intTotal,tglDiag.Value=="ON",tglSpan.Value=="ON") then
local dateToday = fhNewDate(0000)
dateToday:SetSimpleDate(fhCallBuiltInFunction("Today"))
StrLast = dateToday:GetDisplayText() -- Set date Today as last run date
SaveSettings()
return iup.CLOSE
else
local ptrIndi = fhNewItemPtr()
ptrIndi:MoveToFirstRecord("INDI")
while ptrIndi:IsNotNull() do -- Loop through every Individual Record
TblData[fhGetRecordId(ptrIndi)].Names = nil -- Clear individual records
ptrIndi:MoveNext()
end
end
end
dialogMain.bringfront = "YES"
dialogMain.Active = "YES"
tglSpan.Active = tglSpanActive
end -- local function doFind
local function doShow() -- Action for Show previous Result Set button
local tglSpanActive = tglSpan.Active
tglSpan.Active = "NO"
dialogMain.Active = "NO"
if general.FlgFileExists(StrResultsFile) then
doLoadLists() -- Load the Results List and display as Result Set
if DisplayResultSet(tblResults,tglDiag.Value=="ON",tglSpan.Value=="ON") then
return iup.CLOSE
end
end
dialogMain.bringfront = "YES"
dialogMain.Active = "YES"
tglSpan.Active = tglSpanActive
end -- local function doShow
local function doDiag() -- Action for Diagnostic toggle
StrDiag = tglDiag.Value
if StrDiag == "ON" then tglSpan.Active = "YES" end
if StrDiag == "OFF" then tglSpan.Active = "NO" end
setEstimatedTime() -- Increase run time estimate
--? SaveSettings() -- Enable StrDiag in doDefault/Load/SaveSettings() to make it sticky
end -- local function doDiag
local function doSpan() -- Action for Timespan toggle
StrSpan = tglSpan.Value
--? SaveSettings() -- Enable StrSpan in doDefault/Load/SaveSettings() to make it sticky
end -- local function doSpan
local function setButtons() -- Set Non-Duplicates tab buttons active or not
local function setButton(isMatch,btnName)
if isMatch then btnName.Active = "YES" else btnName.Active = "NO" end
end -- local function setButton
local strMove = lstResult.Value
setButton(strMove:match("%-"),btnSetAll) -- Need some unselected to enable Select All
setButton(strMove:match("%+"),btnSetNil) -- Need some selected to enable Select None
setButton(strMove:match("^."),btnMovAll) -- Need some entries to enable Move All
setButton(strMove:match("%+"),btnMovSet) -- Need some selected to enable Move Selected
local strDrop = lstNonDup.Value
setButton(strDrop:match("%-"),btnSelAll) -- Need some unselected to enable Select All
setButton(strDrop:match("%+"),btnSelNil) -- Need some selected to enable Select None
setButton(strDrop:match("^."),btnDelAll) -- Need some entries to enable Erase List
setButton(strDrop:match("%+"),btnDelSel) -- Need some selected to enable Erase Selected
end -- local function setButtons
local function doSelect(iupList,strChar) -- Select All or None of List entries for btnSetAll, btnSetNil, btnSelAll, btnSelNil
iupList.Value = string.rep(strChar,iupList.Value:len())
setButtons()
end -- local function doSelect
local function doChosen(strMatch) -- Move chosen Result Set entries to Non-Duplicates for btnMovAll & btnMovSet
local strMove = lstResult.Value
local intMove = strMove:len()
for intEntry = intMove, 1, -1 do
strChar = strMove:sub(intEntry,intEntry)
if strChar:match(strMatch) then
table.insert(TblNonDups,1,tblResults[intEntry]) -- Add to Non-Duplicates and remove from Results Set
table.remove(tblResults,intEntry)
end
end
doDisplayTables()
lstResult.Value = string.rep("-",lstResult.Value:len()) -- Clear all selections
setButtons()
end -- local function doChosen
local function doDelete(strMatch) -- Delete chosen Non-Duplicates entries for btnDelAll & btnDelSel
local strMove = lstNonDup.Value
local intMove = strMove:len()
local intButton = 1
if strMatch == "^." or strMove == string.rep("+",intMove) then -- If btnDelAll or all entries selected then prompt for approval
intButton = iup_gui.MemoDialogue("\n Continue to ERASE the entire Non-Duplicates list ? \n","Yes, Erase","No, Cancel")
end
if intButton == 1 then
for intEntry = intMove, 1, -1 do -- If approved then delete all matching Non-Duplicates entries
strChar = strMove:sub(intEntry,intEntry)
if strChar:match(strMatch) then
table.remove(TblNonDups,intEntry)
end
end
doLoadLists()
lstNonDup.Value = string.rep("-",lstNonDup.Value:len()) -- Clear all selections
setButtons()
end
end -- local function doDelete
local function doDefault() -- Handle the Restore GUI Defaults button on Set Preferences tab
ResetDefaultSettings()
--? tglDiag.Value = StrDiag -- Reset controls
--? tglSpan.Value = StrSpan
lblDate.Title = StrDate
setDateValue()
tblIndi = {}
doPickRecords() -- Pick and count the Records
iup_gui.ShowDialogue("Main")
iup_gui.DefaultDialogue() -- V3.9
SaveSettings() -- Save sticky data settings
end -- local function doDefault
local function doSoundex() -- Handle the Erase Soundex Cache button on Set Preferences tab
TblSoundex = { } -- Soundex dictionary codes cache of previously coded Names & Places
table.save(TblSoundex,StrSoundexFile)
end -- local function doSoundex
local function doSetFont() -- Handle the Set Window Font button on Set Preferences tab
btnSetFont.Active = "NO"
iup_gui.FontDialogue(tblControls)
SaveSettings() -- Save sticky data settings
btnSetFont.Active = "YES"
end -- local function doSetFont
function btnSetFont:button_cb(intButton,intPress) -- Action for mouse right-click on Set Window Fonts button
if intButton == iup.BUTTON3 and intPress == 0 then
iup_gui.BalloonToggle() -- Toggle tooltips Balloon mode
end
end -- function btnSetFont:button_cb
function tblLimitResults.Current:spin_cb(intItem) -- Call back for Result Set Maximum Rows spin control on Set Preferences tab
IntLimitResults = intItem
tblPruneResults.Current.SpinMin = intItem
intItem = intItem * 2
if IntPruneResults < intItem then
IntPruneResults = intItem
tblPruneResults.Current.SpinValue = intItem
end
SaveSettings() -- Save sticky data settings
end -- function tblLimitResults.Current:spin_cb
local function setResultSet() -- Set the Result Set spin values
tblIndiScoreMin.Current.SpinValue = IntIndiScoreMin
tblLeastResults.Current.SpinValue = IntLeastResults
tblLimitResults.Current.SpinValue = IntLimitResults
tblPruneResults.Current.SpinMin = IntLimitResults
tblPruneResults.Current.SpinValue = IntPruneResults
end -- local function setResultSet
function tblResultSetLim.Default:action() -- Action for Result Set Limits Default Settings button on Set Preferences tab
SetUserInterfaceDefaults()
setResultSet()
SaveSettings() -- Save sticky data settings
end -- function tblResultSetLim.Default:action
local function setNamesValues() -- Set the Names Matching spin Values
for intRelation = IntIndivi, IntChild do
tblLastNameRight[intRelation].SpinValue = TblLastNameRight[intRelation]
tblForeNameRight[intRelation].SpinValue = TblForeNameRight[intRelation]
tblForeNameOther[intRelation].SpinValue = TblForeNameOther[intRelation]
tblNameSoundex [intRelation].SpinValue = TblNameSoundex [intRelation]
tblNameLastWrong[intRelation].SpinValue = TblNameLastWrong[intRelation]
tblNameMinimum [intRelation].SpinValue = TblNameMinimum [intRelation]
tblNameDeduction[intRelation].SpinValue = TblNameDeduction[intRelation]
tblNameMaximum [intRelation].SpinValue = TblNameMaximum [intRelation]
tblNameThreshold[intRelation].SpinValue = TblNameThreshold[intRelation]
end
end -- local function setNamesValues
function tblNamesDefaults.Default:action() -- Action for Names Matching Default Settings button on Set Preferences tab
SetNamesMatchDefaults()
setNamesValues()
SaveSettings() -- Save sticky data settings
end -- function tblNamesDefaults.Default:action
local function setEventValues() -- Set the Event Matching spin Values
tblDatesTolerance.Current.SpinValue = IntDatesTolerance
tblDatesMatched .Current.SpinValue = IntDatesMatched
tblDatesOverlap .Current.SpinValue = IntDatesOverlap
tblDatesMinimum .Current.SpinValue = IntDatesMinimum
tblDatesDeduction.Current.SpinValue = IntDatesDeduction
tblPlacePartRight.Current.SpinValue = IntPlacePartRight
tblPlacePartOther.Current.SpinValue = IntPlacePartOther
tblPlaceSoundex .Current.SpinValue = IntPlaceSoundex
tblEventMaximum .Current.SpinValue = IntEventMaximum
tblBoostedBirth .Current.SpinValue = IntBoostedBirth -- V3.8
tblBoostedBapCh .Current.SpinValue = IntBoostedBapCh -- V3.8
tblBoostedMarry .Current.SpinValue = IntBoostedMarry -- V3.8
tblBoostedDeath .Current.SpinValue = IntBoostedDeath -- V3.8
end -- local function setEventValues
function tblEventDefault.Default:action() -- Action for Event Matching Default Settings button on Set Preferences tab
SetEventMatchDefaults()
setEventValues()
SetEventPoints() -- Update and Save sticky data settings
end -- function tblEventDefault.Default:action
local intSpinLo = 1
local intSpinHi = 6
function tblChronMagnitude.Current:spin_cb(intItem) -- Call back for Chronology Magnitude spin control on Set Preferences tab
local intSpininc = tonumber(txtChMag.SpinInc) -- Magnitudes = 1, 2, 3, 4, 6, 12, 18 and increments of intSpinHi=6 to 120 Months
if intSpininc > intSpinLo and intItem < 6 then
txtChMag.SpinInc = intSpinLo -- When value decreases below 6 set decrement = intSpinLo=1 and adjust value
intItem = 4
txtChMag.SpinValue = intItem + intSpininc
elseif intSpininc < intSpinHi and intItem > 4 then
txtChMag.SpinInc = intSpinHi -- When value increases above 4 set increment = intSpinHi=6 and adjust value
intItem = math.ceil( intItem / intSpinHi ) * intSpinHi
txtChMag.SpinValue = intItem - intSpininc
end
if intItem == 6 then txtChMag.SpinInc = 2 end -- Needed to allow spin decrement below 6 to reduce to 1
IntChronMagnitude = intItem
SetChronology() -- Update and Save sticky data settings
end -- function tblChronMagnitude.Current:spin_cb
local function setChronValues() -- Set the Date Chronology spin Values
tblDatesTimespan .Current.SpinValue = IntDatesTimespan
tblDatesVariance .Current.SpinValue = IntDatesVariance
tblDatesPregnant .Current.SpinValue = IntDatesPregnant
tblDatesPuberty .Current.SpinValue = IntDatesPuberty
tblDatesMarriage .Current.SpinValue = IntDatesMarriage
tblDatesFertile .Current.SpinValue = IntDatesFertile
tblDatesLifespan .Current.SpinValue = IntDatesLifespan
tblChronMagnitude.Current.SpinValue = IntChronMagnitude
tblChronTolerance.Current.SpinValue = IntChronTolerance
local intSpininc = 2
if IntChronMagnitude < 6 then intSpininc = intSpinLo end
if IntChronMagnitude > 6 then intSpininc = intSpinHi end
tblChronMagnitude.Current.SpinInc = intSpininc
end -- local function setChronValues
function tblChronDefault.Default:action() -- Action for Date Chronology Default Settings button on Set Preferences tab
SetChronologyDefaults()
setChronValues()
SetChronology() -- Update and Save sticky data settings
end -- function tblChronDefault.Default:action
local function setOtherValues() -- Set Family & Gender spin Values
tblGenGapFamily .Current.SpinValue = IntGenGapFamily
tblGenGapRelative.Current.SpinValue = IntGenGapRelative
tblGenGapDeduct .Current.SpinValue = IntGenGapDeduct
tblGenderDeduct .Current.SpinValue = IntGenderDeduct
end -- local function setOtherValues
function tblOtherDefault.Default:action() -- Action for Date Chronology Default Settings button on Set Preferences tab
SetOtherMatchDefaults()
setOtherValues()
SetGenerations() -- Update and Save sticky data settings
end -- function tblOtherDefault.Default:action
function btnUpdates:action() -- Action for Check for Updates button -- V3.9
iup_gui.CheckVersionInStore() -- Notify if later Version
end -- function btnUpdates:action
local function doExecute(strExecutable, strParameter) -- Invoke FH Shell Execute API -- V3.8
local function ReportError(strMessage)
iup_gui.WarnDialogue( "Shell Execute Error",
"ERROR: "..strMessage.." :\n"..strExecutable.."\n"..strParameter.."\n\n",
"OK" )
end -- local function ReportError
return general.DoExecute(strExecutable, strParameter, ReportError)
end -- local function doExecute
local strHelp = "https://pluginstore.family-historian.co.uk/page/help/find-duplicate-individuals"
local arrHelp = { "-find-duplicates-tab"; "-omit-non-duplicates-tab"; "-set-preferences-tab"; }
function btnGetHelp:action() -- Action for Help and Advice button according to current tab -- V3.8
local strPage = arrHelp[intTabPosn] or ""
doExecute( strHelp..strPage )
fhSleep(2000,500)
dialogMain.BringFront="YES"
end -- function btnGetHelp:action
function tabCont:tabchangepos_cb(intNew,intOld) -- Call back when Main tab position is changed
intTabPosn = intNew + 1 -- 31 July 2013
if intNew == 1 then
doLoadLists() -- Load the Omit Non-Duplicates tab
end
setButtons()
end -- function tabCont:tabchangepos_cb
-- Set other GUI control attributes
local strMiddle = "ACENTER:ACENTER"
tblControls = { { "Font"; "FgColor"; "Alignment"; "Padding"; "Tip"; "action"; {"TipBalloon";"Balloon";}; {"Expand";"YES";}; {"help_cb";function() iup_gui.HelpDialogue(intTabPosn) end;}; setControls; };
-- Find Duplicates tab
[vboxFind] = { "FontBody"; "Body"; "ARIGHT" ; };
[tglLast] = { "FontBody"; "Safe"; false ; false; "Include Individuals updated after Plugin last run Date" ; function(...) doTick({...},1) end; };
[lblLast] = { "FontBody"; "Body"; strMiddle; "10" ; "Date the Plugn was last run successfully" ; };
[tglDate] = { "FontBody"; "Safe"; false ; false; "Include Individuals updated after this adjustable Date" ; function(...) doTick({...},0) end; };
[lblDate] = { "FontBody"; "Safe"; strMiddle; "10" ; "User adjustable Date threshold for Individuals" ; };
[btnPick] = { "FontBody"; "Safe"; false ; "10" ; "Select any subset of Individuals to include" ; function() return doPick() end; };
[lblPick] = { "FontBody"; "Body"; strMiddle; "10" ; "Number of Individual records included" ; };
[btnFind] = { "FontBody"; "Safe"; false ; "10" ; "Find Duplicates from Included subset of Individuals" ; function() return doFind() end; };
[lblTime] = { "FontBody"; "Body"; strMiddle; "10" ; "Estimated run time to find Duplicates" ; };
[btnShow] = { "FontBody"; "Safe"; false ; "10" ; "Show previous Result Set without using Find again" ; function() return doShow() end; };
[tglDiag] = { "FontBody"; "Safe"; false ; false; "Enable Diagnostic Mode showing details in Result Set" ; function(...) doDiag({...}) end; };
[tglSpan] = { "FontBody"; "Safe"; false ; false; "Include Timespan Dates in the Result Set details" ; function(...) doSpan({...}) end; };
-- Omit Non-Duplicates tab
[vboxOmit] = { "FontBody"; "Body"; "ARIGHT" ; };
[lblResult] = { "FontHead"; "Head"; strMiddle; "0" ; "Result Set of possible duplicates" ; };
[lstResult] = { "FontBody"; "Body"; "ALEFT" ; "0" ; "Use Leftclick and Shft+Leftclick and Ctrl+Leftclick" ; function() setButtons() end; };
[btnSetAll] = { "FontBody"; "Safe"; false ; "10" ; "Select all of the Result Set entries" ; function() doSelect(lstResult,"+") end; };
[btnSetNil] = { "FontBody"; "Safe"; false ; "10" ; "Select none of the Result Set entries" ; function() doSelect(lstResult,"-") end; };
[btnMovAll] = { "FontBody"; "Safe"; false ; "10" ; "Move all Result Set entries to Non-Duplicates" ; function() doChosen("^.") end; };
[btnMovSet] = { "FontBody"; "Safe"; false ; "10" ; "Move selected Result Set entries to Non-Duplicates" ; function() doChosen("%+") end; };
[lblNonDup] = { "FontHead"; "Head"; strMiddle; "0" ; "List of Non-Duplicates to ignore" ; };
[lstNonDup] = { "FontBody"; "Body"; "ALEFT" ; "0" ; "Use Leftclick and Shft+Leftclick and Ctrl+Leftclick" ; function() setButtons() end; };
[btnSelAll] = { "FontBody"; "Safe"; false ; "10" ; "Select all of the Non-Duplicates list" ; function() doSelect(lstNonDup,"+") end; };
[btnSelNil] = { "FontBody"; "Safe"; false ; "10" ; "Select none of the Non-Duplicates list" ; function() doSelect(lstNonDup,"-") end; };
[btnDelAll] = { "FontBody"; "Safe"; false ; "10" ; "Erase all of the Non-Duplicates list" ; function() doDelete("^.") end; };
[btnDelSel] = { "FontBody"; "Safe"; false ; "10" ; "Erase selected Non-Duplicates list entries" ; function() doDelete("%+") end; };
-- Preferences tab
[tabPref] = { "FontHead"; "Head"; false ; false; "Select preference settings to adjust scoring" ; }; -- V3.8
[vboxPref] = { "FontBody"; "Body"; "ARIGHT" ; };
[btnDefault] = { "FontBody"; "Safe"; false ; "10" ; "Restore GUI defaults and Window sizes and positions" ; function() doDefault() end; };
[btnSoundex] = { "FontBody"; "Safe"; false ; "10" ; "Erase the Soundex Cache history file" ; function() doSoundex() end; };
[btnSetFont] = { "FontBody"; "Safe"; false ; "10" ; "Choose user interface window font styles" ; function() doSetFont() end; };
-- Dialogue Common controls
[tabCont] = { "FontHead"; "Head"; false ; false; "Find Duplicates or Omit Non-Duplicates or Set Preferences" ; }; -- V3.8
[allCont] = { "FontBody"; "Body"; "ARIGHT" ; };
[btnUpdates] = { "FontBody"; "Safe"; false ; "4" ; "Check for a later plugin version in the Plugin Store"; }; -- V3.9
[btnGetHelp] = { "FontBody"; "Safe"; false ; "4" ; "Obtain online Help and Advice from the Plugin Store" ; };
[btnDestroy] = { "FontBody"; "Risk"; false ; "4" ; "Close the plugin and keep all edits" ; function() return iup.CLOSE end; };
}
iup_gui.AssignAttributes(tblControls) -- Assign GUI control attributes
doResetRecords()
lblDate.Title = StrDate
setDateValue()
doPickRecords() -- Pick and count the Records
if not general.FlgFileExists(StrResultsFile) then btnShow.Active = "NO" end
setResultSet()
setNamesValues()
setEventValues()
setChronValues()
setOtherValues()
iup_gui.ShowDialogue("Main",dialogMain,btnDestroy,"Map") -- Map Main GUI Dialogue
iup_gui.RefreshDialogue("Main") -- Adjust GUI size after adding Preferences
iup_gui.ShowDialogue("Main") -- Display Main GUI Dialogue
end -- function GUI_MainDialogue
function NewSoundex(tblSoundex) -- Prototype for Soundex Calculator
-- See http://en.wikipedia.org/wiki/Soundex and http://creativyst.com/Doc/Articles/SoundEx1/SoundEx1.htm#SoundExAndCensus
-- This Soundex variant converts all characters to unaccented upper case ASCII,
-- and encodes 1st letter into its code number e.g. Gill (G400) & Jill (J400) are now both 2400.
local tblSoundex = tblSoundex or { } -- Soundex dictionary cache of previously coded Names
tblSoundex[""] = "0000" -- Seed with null string special case
local tblCodeNum = { -- Soundex code number table
A=0;E=0;I=0;O=0;U=0;Y=0;H=0; -- H=0;W=0; -- H & W are ignored after first character
B=1;F=1;P=1;V=1;W=1; -- H=1;W=1; -- W treated as B,F,P,V so BILL=WILL=1400
C=2;G=2;J=2;K=2;Q=2;S=2;X=2;Z=2;
D=3;T=3;
L=4;
M=5;N=5;
R=6;
}
local function getSoundex(strAnyName) -- Get the Soundex code for any Name
local strNewName = encoder.StrEncode_ASCII(strAnyName):upper() -- Convert to ASCII upper case characters
local strSoundex = tblCodeNum[strNewName:sub(1,1)] or "" -- Soundex starts with leading letter code number
local strLastNum = strSoundex -- Set initial Soundex code number
tblCodeNum.H = nil -- Ignore H after first character
tblCodeNum.W = nil -- Ignore W after first character
for i = 2, string.len(strNewName) do
local strCodeNum = tblCodeNum[strNewName:sub(i,i)] -- Step through Soundex code of each subsequent letter
if strCodeNum then
if strCodeNum > 0 and strCodeNum ~= strLastNum then -- Not a vowel nor same as Soundex preceeding code
strSoundex = strSoundex..strCodeNum -- So append Soundex code until 4 chars long
if string.len(strSoundex) == 4 then break end
end
strLastNum = strCodeNum -- Save as Soundex preceeding code, unless H or W
end
end
tblCodeNum.H = 0 -- Reinstate initial H code number
tblCodeNum.W = 1 -- Reinstate initial W code number
strSoundex = string.sub(strSoundex.."0000",1,4) -- Pad code with zeroes to 4 chars long
tblSoundex[strAnyName] = strSoundex -- Save code in cache for future quick lookup
return strSoundex
end -- local function getSoundex
return function(strAnyName) -- Convert a Name to Soundex
return tblSoundex[strAnyName] or getSoundex(strAnyName) -- If already in cache then return previous code else get new code
end -- anonymous function
end -- function NewSoundex
function NewNamesData() -- Prototype to Make Names & Soundex Dictionary per Individual
local tblNameLastRight = TblNameLastRight -- Duplicate globals as locals
local tblNameForeRight = TblNameForeRight
local tblNameForeOther = TblNameForeOther
local tblNameSoundex = TblNameSoundex
local intIndivi = IntIndivi
local intChild = IntChild
return function(ptrInd) -- Make Names & Soundex Dictionary per Individual
local tblName = {}
local ptrName = fhGetItemPtr(ptrInd,"~.NAME")
while ptrName:IsNotNull() do -- Loop through every NAME tag instance replacing punctuation such as - , . [ ] ( ) with a space
local strSurname = fhGetItemText(ptrName,"~:SURNAME")
local strSURNAME = " "..strSurname:upper():gsub(StrS,""):gsub(StrP," ").." " -- Strip spaces in Surnames, so "Van Dyke" becomes "VANDYKE", but "Smith-Jones" becomes "SMITH JONES"
strSurname = " "..strSurname:lower():gsub(StrP," ").." "
for intRef, strRef in ipairs({ "~:ADORNED_FULL"; "~.NICK"; "~._USED"; "~.FONE"; "~.ROMN"; }) do -- V3.8
local strName = fhGetItemText(ptrName,strRef):lower():gsub(StrP," ").." " -- Remove unnamed Names, ensure Forenames are lowercase, and Surname is uppercase
--# strName = strName:gsub("%[unnamed person%]",""):gsub(strSurname,strSURNAME) -- gsub() is faster than replace() and no magic symbols in surname by now?
strName = strName:gsub("%[unnamed person%]",""):replace(strSurname,strSURNAME) -- gsub() is faster than replace() and no magic symbols in surname by now?
for intName, strName in ipairs(strName:split(" ")) do -- Extract Names separated by space, and more than 2 chars long
if string.len(strName) > 2 and not tblName[strName] then -- Ensure replicated Names are skipped
if strName:match("[A-Z]") then
tblName["0"..strName] = tblNameLastRight -- Lastname gets most points, has "0" upper case ( default 7 total = 5 + 2 for Soundex )
else
tblName[intName..strName] = tblNameForeRight -- Forename in right position, has leading 1 - 9 ( default 6 total = 3 + 1 below + 2 for Soundex )
tblName[strName] = tblNameForeOther -- Forename in other position, is all lower case ( default 3 total = 1 + 2 for Soundex )
end
local strSoundex = StrSoundex(strName)
if not tblName[strSoundex] then
tblName[strSoundex] = tblNameSoundex -- Soundex match only points, has capital + digits ( default 2 total )
else
local tblSoundex = {} -- Different Name part but replicated Soundex, so points must accumulate to achieve correct total
for intRelation = intIndivi, intChild do
tblSoundex[intRelation] = tblName[strSoundex][intRelation] + tblNameSoundex[intRelation]
end
tblName[strSoundex] = tblSoundex
end
end
end
end
ptrName:MoveNext("SAME_TAG")
end
return tblName
end -- anonymous function
end -- function NewNamesData
function NewEventData() -- Prototype to Make Event Date Timespan & Place Parts per Individual
local intPartRight = IntPartRight -- Duplicate globals as locals
local intPartOther = IntPartOther
local intPlaceSoundex = IntPlaceSoundex
local tblLower = { Before=IntTimespanDays; To=IntTimespanDays; Approximate0=IntVarianceDays; Calculated0=IntVarianceDays; Estimated0=IntVarianceDays; }
local tblUpper = { After=IntTimespanDays; From=IntTimespanDays; Approximate0=IntVarianceDays; Calculated0=IntVarianceDays; Estimated0=IntVarianceDays; }
return function(ptrEvent) -- Make Event Date Timespan & Place Parts per Individual
local dateDate = fhGetValueAsDate(fhGetItemPtr(ptrEvent,"~.DATE"))
if dateDate:IsNull() then return nil end -- If no Event Date, then return nil
local pntLower = dateDate:GetDatePt1() -- Lower Date whether Type is Simple or Period or Range
--# local intLower = fhCallBuiltInFunction("DayNumber",pntLower) or 0 -- Lower Date with Month missing uses Jan, and with Day missing uses 1st
local intLower = general.GetDayNumber(pntLower) or 0 -- Lower Date with Month missing uses Jan, and with Day missing uses 1st
local pntUpper = dateDate:GetDatePt2()
--# local intUpper = fhCallBuiltInFunction("DayNumber",pntUpper)
local intUpper = general.GetDayNumber(pntUpper) or 0
--# if intUpper then -- Upper Date for Period(From-To) or Range(Between)=Quarter Date
if intUpper > 0 then -- Upper Date for Period(From-To) or Range(Between)=Quarter Date
if pntUpper:GetMonth() == 0 then intUpper = intUpper + 364 -- Upper Date with Month missing, so extend to end of Year
elseif pntUpper:GetDay() == 0 then intUpper = intUpper + 30 end -- Upper Date with Day missing, so extend to end of Month (could adjust according to Month?)
else -- No Upper Date for Simple(Approximate,Calculated,Extimated) or Period(From,To) or Range(After,Before)
intUpper = intLower -- So Upper Date = Lower Date and extend as above
if pntLower:GetMonth() == 0 then intUpper = intUpper + 364
elseif pntLower:GetDay() == 0 then intUpper = intUpper + 30 end
local strSubType = dateDate:GetSubtype() -- Subtype is Approximate, Calculated, Estimated, From, To, After, Before, or ""
if string.len(strSubType) > 7 then
strSubType = strSubType..pntLower:GetMonth() -- Simple(Approximate,Calculated,Estimated) Year Date needs Upper/Lower = +/- half Timespan
end
intLower = intLower - ( tblLower[strSubType] or 0 ) -- Period(To) or Range(Before) Date needs Lower = Lower - Timespan
intUpper = intUpper + ( tblUpper[strSubType] or 0 ) -- Period(From) or Range(After) Date needs Upper = Upper + Timespan
end
local tblPlace = {}
for _, strRef in ipairs({ "~.PLAC"; "~.PLAC.FONE"; "~.PLAC.ROMN"; }) do -- Cater for multiple FONE & ROMN variants -- V3.8
local ptrPlace = fhGetItemPtr(ptrEvent,strRef)
while ptrPlace:IsNotNull() do
for intPlace, strPlace in ipairs(fhGetValueAsText(ptrPlace):split(",")) do
strPlace = strPlace:lower():gsub(StrSP,"") -- Remove all spaces and punctuation from Place part and ensure lowercase
if string.length(strPlace) > 1 and not tblPlace[strPlace] then -- Ensure replicated Places & Soundex are eliminated, and part > 1 char
tblPlace[strPlace] = intPartOther -- Place part in other position, is all lower case ( default 2 total = 1 + 1 for Soundex )
tblPlace[intPlace..strPlace] = intPartRight -- Place part in right position, has leading digit ( default 3 total = 1 + 1 above + 1 for Soundex )
tblPlace[StrSoundex(strPlace)] = intPlaceSoundex -- Similar sounding Place part, has capital+digits ( default 1 total )
end
end
ptrPlace:MoveNext("SAME_TAG")
end
end
return TblMakeEvent(intLower,intUpper,tblPlace) -- Save Lower & Upper Date Timespan and Place & Soundex for each comma separated Place part
end -- anonymous function
end -- function NewEventData
function NewPersonData() -- Prototype to Make Person Database for Individual
local tblFact = { BIRT="Birth"; BAPM="BapCh"; CHR="BapCh"; FAMS="Marry"; DEAT="Death"; BURI="Death"; CREM="Death"; }
local tblRel = { FAMCHUSB="Father"; FAMCWIFE="Mother"; FAMSHUSB="Spouse"; FAMSWIFE="Spouse"; FAMSCHIL="Child"; }
return function(ptrInd,intRid) -- Make Person Database for Individual
local tblInd = TblData[intRid]
tblInd.Indiv = ptrInd:Clone() -- Save Individual Record pointer
tblInd.Gender= fhGetItemText(ptrInd,"~.SEX") -- Save Individual Gender
tblInd.Names = TblNamesData(ptrInd) -- Save Individual Names data table
tblInd.Birth = {}
tblInd.BapCh = {} -- Save an Event table for each type of BMD Event in tblFact
tblInd.Marry = {}
tblInd.Death = {}
tblInd.Father= {} -- Save a Record Id table for each type of Relation in tblRel
tblInd.Mother= {}
tblInd.Spouse= {}
tblInd.Child = {}
local ptrItem = fhNewItemPtr()
ptrItem:MoveToFirstChildItem(ptrInd)
while ptrItem:IsNotNull() do -- Loop through all instances of each Item
local strTag = fhGetTag(ptrItem)
local strFact = tblFact[strTag] -- Lookup the Fact name for Item Tag (if any)
if strFact then
local ptrFact = ptrItem:Clone()
if strFact == "Marry" then ptrFact:MoveTo(fhGetValueAsLink(ptrFact),"~.MARR") end
table.insert(tblInd[strFact],TblEventData(ptrFact)) -- Save each Event table of Dates & Places
end
if strTag:match("FAM") then -- Family link Item
local ptrRel = fhNewItemPtr()
ptrRel:MoveToFirstChildItem(fhGetValueAsLink(ptrItem))
while ptrRel:IsNotNull() do -- Loop through all instances of each Item
local strRel = tblRel[strTag..fhGetTag(ptrRel)] -- Lookup the Relation name for Tags (if any)
if strRel then
local ptrLnk = fhGetValueAsLink(ptrRel) -- Ensure link pointer has a value
if ptrLnk:IsNotNull() then
local intRel = fhGetRecordId(ptrLnk) -- Obtain the Record Id of Relation
if intRel ~= intRid then -- Relation is not original Individual
table.insert(tblInd[strRel],intRel) -- Save their Record Id
TblData[intRel].Indiv = ptrLnk:Clone() -- Save their Record pointer
end
end
end
ptrRel:MoveNext("ANY")
end
end
ptrItem:MoveNext("ANY")
end
end -- anonymous function
end -- function NewPersonData
function TblMakeEvent(intLower,intUpper,tblPlace) -- Make Event from Date Timespan & Place Parts
local tblEvent = {}
tblEvent.Lower = intLower
tblEvent.Upper = intUpper
tblEvent.Place = tblPlace
return tblEvent
end -- function TblMakeEvent
function NewScoreNamesIndi() -- Prototype to Score Names of primary Individuals
local intLastWrongIndi = TblNameLastWrong[IntIndivi]
local intNameMaxIndi = TblNameMaximum[IntIndivi]
return function(tblListA,tblListB) -- Calculate the Score for Comparing two Individual Name Lists
local intScore = 0
local intLastWrong = intLastWrongIndi -- Deduction if no Lastname match (default is 0)
for strName in pairs(tblListA) do
local tblName = tblListB[strName]
if tblName then
intLastWrong = strName:match("^(0)") or intLastWrong -- Note if Lastname matches to inhibit deduction
intScore = intScore + tblName[IntIndivi] -- Increase score for any Name or Soundex matches
end
end
if tonumber(intLastWrong) < 0 then return intLastWrong end -- Reduce score if no Lastname match and deduction exists
return math.min(intScore,intNameMaxIndi) -- Limit prevents multiple Alternate Name matches overwhelming result
end -- anonymous function
end -- function NewScoreNamesIndi
function IntScoreNamesData(tblListA,tblListB,intRelation) -- Calculate the Score for Comparing two Name Lists of Relations
local intScore = 0
local intLastWrong = TblNameLastWrong[intRelation] -- Deduction if no Lastname match (default is 0)
for strName in pairs(tblListA) do
local tblName = tblListB[strName]
if tblName then
intLastWrong = strName:match("^(0)") or intLastWrong -- Note if Lastname matches to inhibit deduction
intScore = intScore + tblName[intRelation] -- Increase score for any Name or Soundex matches
end
end
if tonumber(intLastWrong) < 0 then return intLastWrong end -- Reduce score if no Lastname match and deduction exists
if intScore < TblNameMinimum[intRelation] then return TblNameDeduction[intRelation] end -- Reduce score for Name matches below minimum threshold
return math.min(intScore,TblNameMaximum[intRelation]) -- Limit prevents multiple Alternate Name matches overwhelming result
end -- function IntScoreNamesData
function IntScoreEventData(tblEventA,tblEventB) -- Calculate the Score for Comparing two Events
if tblEventA.Place and tblEventB.Place then -- Both the Event Dates exist and are Real not Synthetic
local intScore = 0
if math.abs(tblEventA.Lower-tblEventB.Lower) <= IntDatesTolerance then intScore = intScore + IntDatesMatched end
if math.abs(tblEventA.Upper-tblEventB.Upper) <= IntDatesTolerance then intScore = intScore + IntDatesMatched end
if math.max(tblEventA.Lower,tblEventB.Lower) <= math.min(tblEventA.Upper,tblEventB.Upper) then intScore = intScore + IntDatesOverlap end
if intScore < IntDatesMinimum then return IntDatesDeduction end -- Dates completely different, so reduce score
for strPlace in pairs(tblEventA.Place) do
intScore = intScore + ( tblEventB.Place[strPlace] or 0 ) -- Increase score for Place part or Soundex matches
end
return math.min(intScore,IntEventMaximum) -- Limit prevents multiple Place part matches overwhelming result
end
return 0
end -- function IntScoreEventData
function IntScoreBestEvents(tblEventsA,tblEventsB) -- Calculate best Score for Comparing all Events
local intScore = nil
for intEventA, tblEventA in ipairs(tblEventsA) do
for intEventB, tblEventB in ipairs(tblEventsB) do
intScore = math.max(intScore or -999,IntScoreEventData(tblEventA,tblEventB))
end
end
return intScore or 0
end -- function IntScoreBestEvents
function TblScoreRelatives(tblRidA,tblRidB,intRelation) -- Calculate best Score for Comparing all Relatives
local tblScore = { 0; 0; 0; 0; 0; 0; }
tblRidA.Best = tblRidA[1] -- Record Id of Best matching Relatives
tblRidB.Best = tblRidB[1]
for intRelA, intRidA in ipairs(tblRidA) do
for intRelB, intRidB in ipairs(tblRidB) do
if intRidA and intRidB then -- Both the Record Id exist
if intRidA == intRidB and IntGenGapFamily > 0 then -- Both the same person and immediate family excluded -- V3.7
return { 0; 0; 0; 0; 0; 0; } -- So return zero, as they are not duplicates to be scored, and negative score upsets close Family preferences
end
local tblIndA = TblData[intRidA]
local tblIndB = TblData[intRidB] -- Score Names data
if not tblIndA.Names then GetPersonData(tblIndA.Indiv,intRidA) end
if not tblIndB.Names then GetPersonData(tblIndB.Indiv,intRidB) end
local intNames = IntScoreNamesData(tblIndA.Names,tblIndB.Names,intRelation)
if intNames >= TblNameThreshold[intRelation] then -- Threshold has been reached, so score Event data
local intBirth = IntScoreBestEvents(tblIndA.Birth,tblIndB.Birth)
local intBapCh = IntScoreBestEvents(tblIndA.BapCh,tblIndB.BapCh)
local intMarry = IntScoreBestEvents(tblIndA.Marry,tblIndB.Marry)
local intDeath = IntScoreBestEvents(tblIndA.Death,tblIndB.Death)
local intScore = intNames+intBirth+intBapCh+intMarry+intDeath
if intScore > tblScore[1] then -- Save the best Scores and pair of Relatives Id
tblScore = { intScore; intNames; intBirth; intBapCh; intMarry; intDeath; }
tblRidA.Best = intRidA
tblRidB.Best = intRidB
end
else
if intNames > tblScore[1] then
tblScore = { intNames; intNames; }
tblRidA.Best = intRidA
tblRidB.Best = intRidB
end
end
end
end
end
return tblScore
end -- function TblScoreRelatives
function TblScoreGender(tblIndA,tblIndB) -- Calculate Score for Comparing Gender of Individuals and Best Children
local intIndivGend = 0
if tblIndA.Gender ~= tblIndB.Gender then intIndivGend = IntGenderDeduct end
local intChildGend = 0
local intChildA = tblIndA.Child.Best -- Retrieve the Best matching Child Record Id
local intChildB = tblIndB.Child.Best
if intChildA and intChildB then
if TblData[intChildA].Gender ~= TblData[intChildB].Gender then intChildGend = IntGenderDeduct end
end
return { intIndivGend+intChildGend; intIndivGend; intChildGend; }
end -- function TblScoreGender
function IntLower(tblEvent) -- Lower date range value
if tblEvent then return tblEvent.Lower end
return -99999
end -- function IntLower
function IntUpper(tblEvent) -- Upper date range value
if tblEvent then return tblEvent.Upper end
return 999999
end -- function IntUpper
function TblDataIndi(intRid) -- Create Person Database if not yet compiled
local tblInd = TblData[intRid]
if not tblInd.Names then GetPersonData(tblInd.Indiv,intRid) end
return tblInd
end -- function TblDataIndi(intRid)
function SynthesiseDates(tblInd) -- Synthesise missing Event Dates from other Event Dates
local tblBirth = tblInd.Birth[1] -- Only consider 1st Date for each type of Event
local tblBapCh = tblInd.BapCh[1]
local tblMarry = tblInd.Marry[1]
local tblDeath = tblInd.Death[1]
local intSpouse = tblInd.Spouse[1] -- Cannot cope with multiple Spouses, so only synthesise Marriage Date when one Spouse
if #tblInd.Spouse == 1 and not tblMarry then -- Single Spouse but no Marriage Date, so synthesise from own/spouse BMD events
local tblSpouse = TblDataIndi(intSpouse)
local intLower = math.max( IntLower(tblBirth), IntLower(tblBapCh), IntLower(tblSpouse.Birth[1]), IntLower(tblSpouse.BapCh[1]) )
local intUpper = math.min( IntUpper(tblDeath), IntUpper(tblSpouse.Death[1]) )
if intLower > 0 or intUpper < 999999 then -- Synthetic Lower or Upper date exists
if intLower <= 0 then intLower = intUpper - IntLifespanDays -- No Lower date so Lower = Upper - Lifespan
else intLower = intLower + IntMarriageDays end -- Lower Birth/Baptism date exists so add Age of Consent
if intUpper >= 999999 then intUpper = intLower + IntLifespanDays end -- No upper date so Upper = Lower + Lifespan
tblInd.Marry[1] = TblMakeEvent(intLower,intUpper) -- Synthesise Event with no Place
tblMarry = tblInd.Marry[1]
end
end
if not tblBirth then -- No Birth Date, so synthesise from own/child/parent BMD events
local intLower = -99999
local intUpper = math.min( IntUpper(tblDeath), IntUpper(tblMarry) - IntMarriageDays, IntUpper(tblBapCh) )
local intChild1 = tblInd.Child[1]
if intChild1 then
local tblChild1 = TblDataIndi(intChild1) -- Latest Birth is 1st Child's latest Birth/Baptism/Death - Age of Puberty, or Marriage - Age of Puberty - Age of Consent
intUpper = math.min( intUpper, IntUpper(tblChild1.Birth[1]) - IntPubertyDays, IntUpper(tblChild1.BapCh[1]) - IntPubertyDays, IntUpper(tblChild1.Death[1]) - IntPubertyDays, IntUpper(tblChild1.Marry[1]) - IntPubertyDays - IntMarriageDays )
end
local intMother = tblInd.Mother[1]
if intMother then
local tblMother = TblDataIndi(intMother)
intLower = math.max( intLower, IntLower(tblMother.Birth[1]) + IntPubertyDays ) -- Earliest Birth is 1st Mother's earliest Birth + Age of Puberty
intUpper = math.min( intUpper, IntUpper(tblMother.Birth[1]) + IntFertileDays, IntUpper(tblMother.Death[1]) )
end -- Latest Birth is earliest of 1st Mother's latest Birth + Age of Fertility, or 1st Mother's latest Death
local intFather = tblInd.Father[1]
if intFather then
local tblFather = TblDataIndi(intFather)
intLower = math.max( intLower, IntLower(tblFather.Birth[1]) + IntPubertyDays ) -- Earliest Birth is 1st Father's earliest Birth + Age of Puberty
intUpper = math.min( intUpper, IntUpper(tblFather.Death[1]) + IntPregnantDays ) -- Latest Birth is 1st Father's latest Death + Pregnancy
end
if intLower > 0 or intUpper < 999999 then -- Synthetic Lower or Upper date exists
if intLower <= 0 then intLower = intUpper - IntLifespanDays end -- No Lower date so Lower = Upper - Lifespan
if intUpper >= 999999 then intUpper = intLower + IntLifespanDays end -- No upper date so Upper = Lower + Lifespan
tblInd.Birth[1] = TblMakeEvent(intLower,intUpper) -- Synthesise Event with no Place
tblBirth = tblInd.Birth[1]
end
end
if tblBirth and not tblDeath then
tblInd.Death[1] = TblMakeEvent(tblBirth.Lower,tblBirth.Upper+IntLifespanDays) -- Birth Date but no Death Date, so synthesise from own Birth event
end
end -- function SynthesiseDates
function IntDateChronCheck(intDateA,intDateB) -- Check date chronology and return proportional points score
return math.floor( math.min( ( intDateA - intDateB ), 0 ) / IntChronMagDays )
end -- function IntDateChronCheck
function IntScoreDateChron(tblIndA,tblIndB) -- Calculate the Score for the Chronology of Event Dates i.e. Is Birth after Baptism after Marriage after Death ?
local intLowerBirthA = IntLower(tblIndA.Birth[1]) -- Lower Date for each Event for both Individuals
local intLowerBirthB = IntLower(tblIndB.Birth[1])
local intLowerBapChA = IntLower(tblIndA.BapCh[1])
local intLowerBapChB = IntLower(tblIndB.BapCh[1])
local intLowerMarryA = IntLower(tblIndA.Marry[1])
local intLowerMarryB = IntLower(tblIndB.Marry[1])
local intLowerDeathA = IntLower(tblIndA.Death[1])
local intLowerDeathB = IntLower(tblIndB.Death[1])
local intUpperBirthA = IntUpper(tblIndA.Birth[#tblIndA.Birth]) -- Upper Date for each Event for both Individuals
local intUpperBirthB = IntUpper(tblIndB.Birth[#tblIndB.Birth])
local intUpperBapChA = IntUpper(tblIndA.BapCh[#tblIndA.BapCh])
local intUpperBapChB = IntUpper(tblIndB.BapCh[#tblIndB.BapCh])
local intUpperMarryA = IntUpper(tblIndA.Marry[#tblIndA.Marry])
local intUpperMarryB = IntUpper(tblIndB.Marry[#tblIndB.Marry])
local intUpperDeathA = IntUpper(tblIndA.Death[#tblIndA.Death])
local intUpperDeathB = IntUpper(tblIndB.Death[#tblIndB.Death])
local intScore =
IntDateChronCheck( intUpperBirthA, intLowerBirthB ) + -- Individual A latest birth before Individual B earliest birth
IntDateChronCheck( intUpperBirthB, intLowerBirthA ) + -- Individual B latest birth before Individual A earliest birth
IntDateChronCheck( intUpperBapChA, intLowerBirthB ) + -- Individual A baptised before Individual B born
IntDateChronCheck( intUpperBapChB, intLowerBirthA ) + -- Individual B baptised before Individual A born
IntDateChronCheck( intUpperMarryA, intLowerBirthB ) + -- Individual A married before Individual B born
IntDateChronCheck( intUpperMarryB, intLowerBirthA ) + -- Individual B married before Individual A born
IntDateChronCheck( intUpperDeathA, intLowerBirthB ) + -- Individual A died before Individual B born
IntDateChronCheck( intUpperDeathB, intLowerBirthA ) + -- Individual B died before Individual A born
IntDateChronCheck( intUpperBapChA, intLowerBapChB ) + -- Individual A latest baptised before Individual B earliest baptised
IntDateChronCheck( intUpperBapChB, intLowerBapChA ) + -- Individual B latest baptised before Individual A earliest baptised
IntDateChronCheck( intUpperMarryA, intLowerBapChB ) + -- Individual A married before Individual B baptised
IntDateChronCheck( intUpperMarryB, intLowerBapChA ) + -- Individual B married before Individual A baptised
IntDateChronCheck( intUpperDeathA, intLowerBapChB ) + -- Individual A died before Individual B baptised
IntDateChronCheck( intUpperDeathB, intLowerBapChA ) + -- Individual B died before Individual A baptised
IntDateChronCheck( intUpperMarryA, intLowerMarryB ) + -- Individual A latest marriage before Individual B earliest marriage
IntDateChronCheck( intUpperMarryB, intLowerMarryA ) + -- Individual B latest marriage before Individual A earliest marriage
IntDateChronCheck( intUpperDeathA, intLowerMarryB ) + -- Individual A died before Individual B married
IntDateChronCheck( intUpperDeathB, intLowerMarryA ) + -- Individual B died before Individual A married
IntDateChronCheck( intUpperDeathA, intLowerDeathB ) + -- Individual A latest died before Individual B earliest died
IntDateChronCheck( intUpperDeathB, intLowerDeathA ) -- Individual B latest died before Individual A earliest died
if intScore > IntChronTolerance then
local intMother = tblIndB.Mother.Best
if intMother then
tblEvent = TblDataIndi(intMother)
intScore = intScore +
IntDateChronCheck( intUpperBirthA, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual A born before Mother of B mature
IntDateChronCheck( IntUpper(tblEvent.Birth[#tblEvent.Birth])+IntFertileDays, intLowerBirthA ) + -- Mother of B infertile before Individual A born
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthA ) -- Mother of B died before Individual A born
end
local intMother = tblIndA.Mother.Best
if intMother then
tblEvent = TblDataIndi(intMother)
intScore = intScore +
IntDateChronCheck( intUpperBirthB, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual B born before Mother of A mature
IntDateChronCheck( IntUpper(tblEvent.Birth[#tblEvent.Birth])+IntFertileDays, intLowerBirthA ) + -- Mother of A infertile before Individual B born
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthA ) -- Mother of A died before Individual B born
end
local intFather = tblIndB.Father.Best
if intFather then
tblEvent = TblDataIndi(intFather)
intScore = intScore +
IntDateChronCheck( intUpperBirthA, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual A born before Father of B mature
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthA-IntPregnantDays ) -- Father of B died before Individual A conceived
end
local intFather = tblIndA.Father.Best
if intFather then
tblEvent = TblDataIndi(intFather)
intScore = intScore +
IntDateChronCheck( intUpperBirthB, IntLower(tblEvent.Birth[1])+IntPubertyDays ) + -- Individual B born before Father of A mature
IntDateChronCheck( IntUpper(tblEvent.Death[#tblEvent.Death]), intLowerBirthB-IntPregnantDays ) -- Father of A died before Individual B conceived
end
local intChild = tblIndB.Child.Best
if intChild then
tblEvent = TblDataIndi(intChild)
intScore = intScore +
IntDateChronCheck( intUpperDeathA, IntLower(tblEvent.Birth[1])-IntPregnantDays ) -- Individual A died before Child of B conceived
end
local intChild = tblIndA.Child.Best
if intChild then
tblEvent = TblDataIndi(intChild)
intScore = intScore +
IntDateChronCheck( intUpperDeathB, IntLower(tblEvent.Birth[1])-IntPregnantDays ) -- Individual B died before Child of A conceived
end
end
return intScore
end -- function IntScoreDateChron
function TblScoreGenGap(ptrIndA,ptrIndB) -- Calculate the Score for Comparing Gender Gap
local intGensUp = fhCallBuiltInFunction("RelationCode",ptrIndA,ptrIndB,"GENS_UP",1) -- Always positive or nil if unrelated
local intGensDn = fhCallBuiltInFunction("RelationCode",ptrIndA,ptrIndB,"GENS_DOWN",1) -- Always positive or nil if unrelated
if intGensUp and intGensDn then
local intGenGap = intGensUp + intGensDn - IntGenGapRelative -- Spouse = -6, Parent:Child = -5, Sibling/Gparent:Gchild = -4, -3, -2, -1 , 0 , 1, etc
return { math.min( intGenGap * IntGenGapDeduction, 0 ); intGensUp; intGensDn; }
end
return { 0; intGensUp or ""; intGensDn or ""; }
end -- function TblScoreGenGap
function SortResults(tblResults) -- Sort the Results into Descending Order of Full Score and then by Individual Score
table.sort( tblResults, function(tblA,tblB) if tblA.FullScore ~= tblB.FullScore then return tblA.FullScore > tblB.FullScore else return tblA.IndiScore > tblB.IndiScore end end )
end -- function SortResults
-- Find Duplicate Records --
function FindDuplicateRecords(intTotal,flgDiag,flgSpan) -- Total records, Diagnostic mode, with Timespans
local tblData = TblData -- Data table of Record Id to include in candidate checks
local tblRecId = {} -- Individual Record Id pointers to Individual Record Data saved for comparisons
local tblResults = {} -- Results for Individual Record matches
local intBestScore = 0
local intMinimum = IntLeastResults -- Minimum initial score to retain in Results
local isResultOK = true -- Flag if completed run with Result Set
local intStartTime = os.time() -- Time search started for Result Set duration Message
local intComparisons = intTotal * ( intTotal-1 ) / 2 -- Number of Individual versus Individual comparisons
local intProgressStep = 0 -- Progress count of inner loop Individual comparisons
local intIndThreshold = TblNameThreshold[IntIndivi] -- Local copies of Global values
local intIndiScoreMin = IntIndiScoreMin
local intLeastResults = IntLeastResults
local intLimitResults = IntLimitResults
local intPruneResults = IntPruneResults
local intProgBarStart = IntProgBarStart
if flgDiag then
intIndThreshold = -100 -- points -- Adjusted values for Diagnostic Mode to retain many more Results
intIndiScoreMin = -100 -- points
intLeastResults = -100 -- points
intLimitResults = 200 -- rows
intPruneResults = intLimitResults * 2
intProgBarStart = intProgBarStart / 10
end
progbar.Setup(iup_gui.DialogueAttributes("Bars")) -- Create the Progress Bar functions
if intComparisons > intProgBarStart then -- Optionally start Progress Bar for Comparsions
progbar.Start("Finding Duplicates",intComparisons)
end
TblSoundex = { } -- Soundex dictionary codes cache of previously coded Names & Places
if general.FlgFileExists(StrSoundexFile) then
progbar.Focus()
progbar.Message("Loading Soundex Cache")
TblSoundex, StrErr = table.load(StrSoundexFile) -- Load Soundex dictionary codes cache table
if TblSoundex[""] == "Z000" then
TblSoundex = { } -- Erase old initial letter Soundex cache
end
end
StrSoundex = NewSoundex(TblSoundex) -- Prototype for Soundex Calculator using Soundex dictionary codes cache
TblNamesData = NewNamesData() -- Prototype to Make Names & Soundex Dictionary per Individual
TblEventData = NewEventData() -- Prototype to Make Event Date Timespan & Place Parts per Individual
GetPersonData = NewPersonData() -- Prototype to Make Personal Profile Database per Individual
IntScoreNamesIndi = NewScoreNamesIndi() -- Prototype to Score Names of primary Individuals
local ptrIndA = fhNewItemPtr()
ptrIndA:MoveToFirstRecord("INDI")
while ptrIndA:IsNotNull() do -- Loop through every Individual Record
local intRidA = fhGetRecordId(ptrIndA) -- Obtain the Record Id of Individual
local tblIndA = tblData[intRidA]
if not tblIndA.Names then GetPersonData(ptrIndA,intRidA) end -- Make the Person Database for Individual
table.insert(tblRecId,intRidA) -- Save the Data Record Id in consecutively indexed table
SynthesiseDates(tblIndA) -- Synthesise missing Event Dates from other Events
for intIndB = 1, #tblRecId - 1 do -- Loop through prior Individual Record entries
local intRidB = tblRecId[intIndB] -- Record Id from consecutively indexed table
local tblIndB = tblData[intRidB] -- Lookup Individual Record Data
if tblIndA.Chosen or tblIndB.Chosen then -- Only score Records that were Chosen in GUI
local intIndiNames = IntScoreNamesIndi(tblIndA.Names,tblIndB.Names)
if intIndiNames >= intIndThreshold then -- If enough Individual Names match, assess score for Individual BMD Events, etc
local intIndiBirth = IntScoreBestEvents(tblIndA.Birth,tblIndB.Birth) * IntBoostedBirth -- V3.8
local intIndiBapCh = IntScoreBestEvents(tblIndA.BapCh,tblIndB.BapCh) * IntBoostedBapCh -- V3.8
local intIndiMarry = IntScoreBestEvents(tblIndA.Marry,tblIndB.Marry) * IntBoostedMarry -- V3.8
local intIndiDeath = IntScoreBestEvents(tblIndA.Death,tblIndB.Death) * IntBoostedDeath -- V3.8
local intIndiScore = intIndiNames + intIndiBirth + intIndiBapCh + intIndiMarry + intIndiDeath
if intIndiScore >= intIndiScoreMin then -- If Individual Score exceeds minimum, assess score for Relatives, Gender, etc
local tblFather = TblScoreRelatives(tblIndA.Father,tblIndB.Father,IntFather)
local tblMother = TblScoreRelatives(tblIndA.Mother,tblIndB.Mother,IntMother)
local tblSpouse = TblScoreRelatives(tblIndA.Spouse,tblIndB.Spouse,IntSpouse)
local tblChild = TblScoreRelatives(tblIndA.Child ,tblIndB.Child ,IntChild )
local tblGender = TblScoreGender(tblIndA,tblIndB)
local intFullScore = intIndiScore + tblFather[1] + tblMother[1] + tblSpouse[1] + tblChild[1] + tblGender[1]
if intFullScore >= intMinimum then -- Continue if score is above lowest retained Results entry
local ptrIndB = tblIndB.Indiv -- Get other Individual Record pointer
local intDateChron = IntScoreDateChron(tblIndA,tblIndB) -- Check date chronology using Best match Relations
local tblGenGap = TblScoreGenGap(ptrIndA,ptrIndB) -- Only check generation gap now as it has a high run time overhead
if intDateChron > IntChronTolerance -- Exclude major chronology mismatch
and tblGenGap[1] > IntFamGenGapMax then -- Exclude spouse (including spouse's spouses), parent/child, sibling, gparent/gchild
intFullScore = intFullScore + intDateChron + tblGenGap[1]
if intFullScore >= intMinimum then
local isCandidate = true
for i, tblNonDups in ipairs( TblNonDups ) do
if intRidA == tblNonDups.RecordIdA and intRidB == tblNonDups.RecordIdB then
isCandidate = false -- Exclude Non-Duplicate pairs
break
end
end
if isCandidate then
table.insert( tblResults,
{
FullScore=intFullScore; RecordIdA=intRidA; RecordIdB=intRidB;
IndiScore=intIndiScore; IndiNames=intIndiNames; IndiBirth=intIndiBirth; IndiBapCh=intIndiBapCh; IndiMarry=intIndiMarry; IndiDeath=intIndiDeath;
FathScore=tblFather[1]; FathNames=tblFather[2]; FathBirth=tblFather[3]; FathBapCh=tblFather[4]; FathMarry=tblFather[5]; FathDeath=tblFather[6];
MothScore=tblMother[1]; MothNames=tblMother[2]; MothBirth=tblMother[3]; MothBapCh=tblMother[4]; MothMarry=tblMother[5]; MothDeath=tblMother[6];
SpouScore=tblSpouse[1]; SpouNames=tblSpouse[2]; SpouBirth=tblSpouse[3]; SpouBapCh=tblSpouse[4]; SpouMarry=tblSpouse[5]; SpouDeath=tblSpouse[6];
ChilScore=tblChild [1]; ChilNames=tblChild [2]; ChilBirth=tblChild [3]; ChilBapCh=tblChild [4]; ChilMarry=tblChild [5]; ChilDeath=tblChild [6];
FamGenGap=tblGenGap[1]; FamGensUp=tblGenGap[2]; FamGensDn=tblGenGap[3]; DateChron=intDateChron;
GendScore=tblGender[1]; IndivGend=tblGender[2]; ChildGend=tblGender[3];
} )
if #tblResults >= intPruneResults then -- Prune low scores from Results to avoid exceeding memory
SortResults(tblResults)
for intEntry = intLimitResults + 1, #tblResults do tblResults[intEntry] = nil end -- table.remove(tblResults) end
intMinimum = tblResults[#tblResults].FullScore
end
end
end
end
end
end
end
end
end
progbar.Message("Individual Record Id "..intRidA) -- Report progress
progbar.Step(intProgressStep)
intProgressStep = intProgressStep + 1 -- Each inner loop performs incrementing number of comparisons
if progbar.Stop() then -- Break out of outer loop
isResultOK = false
break
end
ptrIndA:MoveNext()
end
progbar.Focus()
progbar.Message("Saving Soundex Cache") -- Save Soundex dictionary codes cache table
table.save(TblSoundex,StrSoundexFile)
if isResultOK then -- Dislay Result Set unless Progress Bar was Stopped
progbar.Message("Composing Result Set")
SortResults(tblResults) -- Sort the final results
local dateOrigin = fhNewDate(0001,01,10):GetDatePt1() -- Date origin for Timespan Dates is 10-Jan-0001 = 1-Jan-0001 + 9 days for Gregorian calendar offset
local dateTimespan = fhNewDate(0000) -- Date Timespan pointer for Timespan Date entries
for intEntry = 1, #tblResults do -- Extract highest scoring entries & populate table with Timespan Dates
local tblEntry = tblResults[intEntry] -- Timespan Dates only added now to avoid slowing down main search loop above
if tblEntry then intFullScore = tblEntry.FullScore end
if intFullScore < intLeastResults
or intEntry > intLimitResults then
table.remove(tblResults) -- When low Score or Results limit reached, purge remainder of results
else
intRecordIdA = tblEntry.RecordIdA
intRecordIdB = tblEntry.RecordIdB
-- Populate this Entry in Results table with Event Date Timespans
for strEvent, strPrefix in pairs( { Birth="B_"; BapCh="C_"; Marry="M_"; Death="D_"; } ) do
for strRecId, intRecId in pairs( { A_=intRecordIdA; B_=intRecordIdB; } ) do
local strIndex = strPrefix..strRecId.."Span"
local tblIndi = tblData[intRecId]
local tblEvent = tblIndi[strEvent][1] -- Get earliest Lower/Upper Timespan Dates for each Event for both Records
if tblEvent then -- Event Timespan Date exists
local intLower = tblEvent.Lower
local intUpper = tblEvent.Upper
if intLower == intUpper then -- Lower = Upper so Simple Date
dateTimespan:SetSimpleDate(fhCallBuiltInFunction("CalcDate",dateOrigin,0,0,intLower))
else -- Lower < Upper so Date Range
dateTimespan:SetRange("between",fhCallBuiltInFunction("CalcDate",dateOrigin,0,0,intLower),fhCallBuiltInFunction("CalcDate",dateOrigin,0,0,intUpper))
end
local strType = " Synth"
if tblEvent.Place then strType = " Actual" end -- Date is Actual and not Synthetic
tblEntry[strIndex] = dateTimespan:GetDisplayText("COMPACT")..strType
else
tblEntry[strIndex] = " " -- Event Timespan Date missing
end
end
end
progbar.Step(intProgressStep)
end
end
table.save(tblResults,StrResultsFile) -- Save results so can show again later
isResultOK = DisplayResultSet(tblResults,flgDiag,flgSpan,intStartTime)
end
progbar.Close() -- Close the Progress Bar and retrieve its window position
return isResultOK
end -- function FindDuplicateRecords
function DisplayResultSet(tblResults,flgDiag,flgSpan,intStartTime) -- Display the Result Set in FH
if #tblResults == 0 then
iup_gui.MemoDialogue("\n No duplicate Individual Records found. \n","OK")
return false
end
local tblColumnKey =
{ -- Column Title ; Index ; Type ;Width; Alignment; -- Result Set Column parameters allow easy changes
{ "Individual" ; "IndiScore"; };
{ "I-Names" ; "IndiNames"; };
{ "I-Birth" ; "IndiBirth"; };
{ "I-Birth Timespan-A " ; "B_A_Span" ; "text" ;142 ; "align_right"; }; -- Birth Timespan Record A
{ "I-Birth Timespan-B " ; "B_B_Span" ; "text" ;142 ; "align_right"; }; -- Birth Timespan Record B
{ "I-BapCh" ; "IndiBapCh"; };
{ "I-BapCh Timespan-A " ; "C_A_Span" ; "text" ;142 ; "align_right"; }; -- Bap/Chr Timespan Record A
{ "I-BapCh Timespan-B " ; "C_B_Span" ; "text" ;142 ; "align_right"; }; -- Bap/Chr Timespan Record B
{ "I-Marry" ; "IndiMarry"; };
{ "I-Marry Timespan-A " ; "M_A_Span" ; "text" ;142 ; "align_right"; }; -- Marry Timespan Record A
{ "I-Marry Timespan-B " ; "M_B_Span" ; "text" ;142 ; "align_right"; }; -- Marry Timespan Record B
{ "I-Death" ; "IndiDeath"; };
{ "I-Death Timespan-A " ; "D_A_Span" ; "text" ;142 ; "align_right"; }; -- Death Timespan Record A
{ "I-Death Timespan-B " ; "D_B_Span" ; "text" ;142 ; "align_right"; }; -- Death Timespan Record B
{ "Father" ; "FathScore"; };
{ "F-Names" ; "FathNames"; };
{ "F-Birth" ; "FathBirth"; };
{ "F-BapCh" ; "FathBapCh"; };
{ "F-Marry" ; "FathMarry"; };
{ "F-Death" ; "FathDeath"; };
{ "Mother" ; "MothScore"; };
{ "M-Names" ; "MothNames"; };
{ "M-Birth" ; "MothBirth"; };
{ "M-BapCh" ; "MothBapCh"; };
{ "M-Marry" ; "MothMarry"; };
{ "M-Death" ; "MothDeath"; };
{ "Spouse" ; "SpouScore"; };
{ "S-Names" ; "SpouNames"; };
{ "S-Birth" ; "SpouBirth"; };
{ "S-BapCh" ; "SpouBapCh"; };
{ "S-Marry" ; "SpouMarry"; };
{ "S-Death" ; "SpouDeath"; };
{ "Child" ; "ChilScore"; };
{ "C-Names" ; "ChilNames"; };
{ "C-Birth" ; "ChilBirth"; };
{ "C-BapCh" ; "ChilBapCh"; };
{ "C-Marry" ; "ChilMarry"; };
{ "C-Death" ; "ChilDeath"; };
{ "Chrono" ; "DateChron"; };
{ "Gen.Gap" ; "FamGenGap"; };
{ "Gens-Up" ; "FamGensUp"; };
{ "Gens-Dn" ; "FamGensDn"; };
{ "Gender" ; "GendScore"; };
{ "I-Gend" ; "IndivGend"; };
{ "C-Gend" ; "ChildGend"; };
}
local intFullScore = 0 -- Full Score
local tblFullScore = {}
local tblPercentage = {}
local intRecordIdA = 0 -- Record Id A & Individual A
local tblRecordIdA = {}
local tblPossibleA = {}
local intRecordIdB = 0 -- Record Id B & Individual B
local tblRecordIdB = {}
local tblPossibleB = {}
local tblResultSet = {} -- Table of Result Set Columns for Diagnostic sub-scores
local intMaxScore = 0
local intBoosted = IntBoostedBirth + IntBoostedBapCh + IntBoostedMarry + IntBoostedDeath -- V3.8
for intRelation = IntIndivi, IntChild do -- Max score for Percentage calculation = Event*Boost & Name maxima for each relation
intMaxScore = intMaxScore + (IntEventMaximum * intBoosted) + TblNameMaximum[intRelation]
intBoosted = 4 -- V3.8
end
for intEntry = 1, #tblResults do -- Create Result Set Column tables
local tblEntry = tblResults[intEntry]
intRecordIdA = tblEntry.RecordIdA
local ptrPossibleA = fhNewItemPtr() -- Record Id and Pointer of Individual A
ptrPossibleA:MoveToRecordById("INDI",intRecordIdA)
intRecordIdB = tblEntry.RecordIdB
local ptrPossibleB = fhNewItemPtr() -- Record Id and Pointer of Individual B
ptrPossibleB:MoveToRecordById("INDI",intRecordIdB)
intFullScore = tblEntry.FullScore
table.insert(tblPercentage,math.floor(intFullScore*100/intMaxScore)) -- Percentage Full Score
table.insert(tblRecordIdA,intRecordIdA) -- Individual Rec Id A
table.insert(tblPossibleA,ptrPossibleA) -- Individual Record A
table.insert(tblRecordIdB,intRecordIdB) -- Individual Rec Id B
table.insert(tblPossibleB,ptrPossibleB) -- Individual Record B
table.insert(tblFullScore,intFullScore) -- Points Full Score
for i, tblColumn in ipairs( tblColumnKey ) do
local strIndex = tblColumn[2]
if intEntry==1 then tblResultSet[strIndex] = {} end -- Create Diagnostic sub-score Column tables
table.insert(tblResultSet[strIndex],tblEntry[strIndex])
end
end
local intSize = #tblFullScore -- Output primary Result Set Columns unconditionally
local strMessage = ""
if intStartTime then
local intTime = os.difftime(os.time(),intStartTime)
strMessage = " in "..intTime.." seconds"
end
strMessage = "found "..intSize.." candidates"..strMessage
if intSize == 1 then strMessage = strMessage:gsub("candidates","candidate") end
if intTime == 1 then strMessage = strMessage:gsub("seconds","second") end
fhOutputResultSetTitles(iup_gui.Plugin..iup_gui.Version..strMessage)
fhOutputResultSetColumn("Percent" , "integer" , tblPercentage, intSize, 31, "align_mid" )
fhOutputResultSetColumn("Rec Id A" , "integer" , tblRecordIdA , intSize, 31, "align_mid" )
fhOutputResultSetColumn("Record A" , "item" , tblPossibleA , intSize, 140, "align_left")
fhOutputResultSetColumn("Rec Id B" , "integer" , tblRecordIdB , intSize, 31, "align_mid" )
fhOutputResultSetColumn("Record B" , "item" , tblPossibleB , intSize, 140, "align_left")
fhOutputResultSetColumn(" Total " , "integer" , tblFullScore , intSize, 31, "align_mid" )
if intSize > 0 then
for i, tblColumn in ipairs( tblColumnKey ) do -- Output Diagnostic sub-score Columns only if they exist
local strTitle = tblColumn[1]
local strIndex = tblColumn[2]
local strType = tblColumn[3]
local intWidth = tblColumn[4]
local strAlign = tblColumn[5]
if flgDiag or not strTitle:match("%-") then -- Only output Diagnostic or Timespan columns if requested
if flgSpan or not strTitle:match("Timespan") then
if strType and intWidth and strAlign then
fhOutputResultSetColumn(strTitle, strType, tblResultSet[strIndex], intSize, intWidth, strAlign )
else
fhOutputResultSetColumn(strTitle, "integer", tblResultSet[strIndex], intSize, 31, "align_mid" )
end
end
end
end
end
return true
end -- function DisplayResultSet
-- Main Code Section Starts Here --
if fhGetContextInfo("CI_APP_MODE") ~= "Project Mode" then -- V3.9
fhMessageBox("\nThis plugin cannot operate on a Standalone GEDCOM File.\n")
return
end
fhInitialise(5,0,8,"save_recommended")
PresetGlobalData() -- Preset global data settings
ResetDefaultSettings() -- Preset default sticky settings
LoadSettings() -- Load sticky data settings
GUI_MainDialogue() -- Invoke graphical user interface
SaveSettings() -- Save sticky data settings
if IntFhVersion > 5 then fhSetConversionLossFlag(false) end -- Inhibit loss of accents message
--[[
@V3.9: Library 4.1; Check for Updates button; Centre window on Parent FH window; Exclude Standalone GEDCOM Files;
@V3.8: Updated library to Functions Prototypes v3.0; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; Boost event scores so for instance duplicate Marriages are given precedence; Add FONE & ROMN to NAME & PLAC; FH V7 Lua 3.5 IUP 3.28;
@V3.7: Fix "±" character in Set Preferences tab Event Matching & Date Chronology, include immediate family in TblScoreRelatives(...), include undated records in intChosenRecord(...).
@V3.6: Both ANSI FH V5 & UTF-8 FH V6 IUP 3.11.2 plus new Soundex all numeric coding for Unicode, etc.
@V3.5: Latest GUI Library, add F1 help_cb, add BalloonToggle().
@V3.4: Fix close relations problem, improve Find Duplicates date selection using toggles & prompt, allow multiple selection from lists in Omit Non-Duplicate tab.
@V3.4: Fix stack overflow problem, Version History help pages, and new GUI library, Library modules, iup_gui.Balloon = "YES", auto Plugin Title & Version, GUI attribute mixed case.
@V3.3: Better Progress Bar message while loading large family Relation Pool.
@V3.2: More efficient Record Pool Database construction and Individual comparison loop, detects all same sex parents, fix minor ProgressBar bug, include CREMation="Death".
@V3.1: Includes all multiple Relations and all multiple Events in assessments, and adds some function prototypes.
@V3.0: Update to Omit Non-Duplicates list, remove Named List option, add Erase Soundex Cache, finish Help & Advice, and first Plugin Store release.
@V2.6: Minor updates to the Soundex function and its file load/save, plus better Progress Bar presentation.
@V2.5: Minor updates to Name & Event checks and ProgressBar to improve performance, extra ProgressBar.Messages and moved ProgressBar.Stop() after output of Result Set.
@V2.4: Minor adjustment to Set Preferences, structured Help & Advice, small corrections to Name & Event checks, plus Soundex and ProgressBar function prototypes.
@V2.3: Add extra chronology date checks & remove obsolete checks, add CheckVersionInStore, strip all spaces & ignore case in Place parts, better Result Set sort, add complete sticky Set Preferences.
@V2.2: Bug fix for Non-Duplicates list, and some minor improvements.
@V2.1: Adjust Soundex scoring for similar Lastname & Forename case, treat space separated Surnames as one Name, ignore case when matching Fornames,
monospaced font for Non-Duplicates tab, added some Set Preferences sticky options and tabs.
@V2.0: Add Non-Duplicates list management tab & simple Set Preferences tab, deduction for Surname mismatch per relative, proportional Chronology in Months.
@V1.9: Fix bug in scoring values per relative.
@V1.8: Add name scoring values per relative, separate Period/Range and Approx/Calc/Est timespans, proportional Chronology scores, show previous Result Set.
@V1.7: Add Forename positional scores, increase score for Surname match, reduce Name mismatch to 0,
add Place positional scores, drop Child Count, reduce Generation Gap deductions, include Percentage result score,
new Date timespans for Approx/Calc/Est & Before/After & From/To, check 1st Child Gender, User Preference Settings,
synthesise missing Event Dates, extra Chronology checks, exclude multiple Chronology errors, exclude close Family,
sticky Settings and Soundex & NonDups tables, Date Timespans in diagnostic Results, GUI Last Run Date and bug fixes.
@V1.6: Fix bug with selecting subset, extra Date chronology checks.
@V1.5: 1st Child name check, corrected Gen Gap check, better Date chronology check, tweaked other scores, improved run time estimate, Diagnostic mode.
@V1.4: Fixed GUI size on Font change, fixed bug in last Update check, corrected run time estimate, added Burial Event, added Date chronology check.
@V1.3: Proportional Generation gap score, separate Names table, Father & Mother Name & Soundex check, Result Set diagnostic sub-scores, GUI with selection options.
@V1.2: Soundex checks runtime improvements, limit Name checks score, Event Date +/-50 days check, Place Name & Soundex check, Spouse Name & Soundex check, Generation gap check.
@V1.1: Added child count comparison. Prune low scores from results to prevent exceeding memory.
@V1.0: Initial version.
]]Source:Find-Duplicate-Individuals-2.fh_lua