Find Duplicate Individuals.fh_lua

--[[
@Title:			Find Duplicate Individuals
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			3.8
@Keywords:		
@LastUpdated:	16 Dec 2020
@Licence:			This plugin is copyright (c) 2020 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:			3.0
@LastUpdated:	20 Sep 2020
@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+tablex_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	25 Aug 2020
@Description:	A Table Load Save Module.
@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,_ 					-- V1.2 -- Added _
   -- 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
      file,err = io.open( filename, "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()
      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
      tables,err = loadfile( sfile )
   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+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.0
@LastUpdated:	25 Aug 2020
@Description:	A general functions module to supplement LUA functions, where all filenames are in ANSI.
@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 "lfs"													-- To access LUA filing system

	if fhGetAppVersion() > 6 then
		unpack = table.unpack
	end

	-- Check if file exists --
	function fh.FlgFileExists(strFileName)
		return lfs.attributes(strFileName,"mode") == "file"
		--[=[
		if lfs.attributes(strFileName,"mode") == "file" then
			return true
		else
			return false
		end
		--]=]
	end -- function FlgFileExists

	-- Check if folder exists --
	function fh.FlgFolderExists(strFolderName)
		return lfs.attributes(strFolderName:gsub("\\$",""),"mode") == "directory"
		--[=[
		if lfs.attributes(strFolderName:gsub("\\$",""),"mode") == "directory" then
			return true
		else
			return false
		end
		--]=]
	end -- function FlgFolderExists

	-- Check if folder writable --
	function fh.FlgFolderWrite(strFolderName)
		if fh.FlgFolderExists(strFolderName) then
			local fileHandle, strError = io.open(strFolderName.."\\xyz.xyz","w")
			if fileHandle ~= nil then
				fileHandle:close()
				os.remove(strFolderName.."\\xyz.xyz")
				return true
			end
		end
		return false
	end -- function FlgFolderWrite

	-- Open File and return Handle --
	function fh.OpenFile(strFileName,strMode)
		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(strString,strFileName)
		local fileHandle = fh.OpenFile(strFileName,"w")
		fileHandle:write(strString)
		assert(fileHandle:close())
	end -- function SaveStringToFile

	-- Load string from file --
	function fh.StrLoadFromFile(strFileName)
		local fileHandle = fh.OpenFile(strFileName,"r")
		local strString = fileHandle:read("*all")
		assert(fileHandle:close())
		return strString
	end -- function StrLoadFromFile

	-- Returns the Path, Filename, and Extension as 3 values
	function fh.SplitFilename(strFilename)
		if lfs.attributes(strFilename,"mode") == "directory" then
			local strPath = strFilename:gsub("[\\/]$","")
			return strPath.."\\","",""
		end
		strFilename = strFilename.."."
		return strFilename:match("^(.-)([^\\/]-%.([^\\/%.]-))%.?$")
	end -- function SplitFilename

	-- Return a Directory Tree entry & attributes on each iteration --
	function fh.DirTree(strDir,...)
		local arg = {...}
		assert(strDir and strDir ~= "", "directory parameter is missing or empty")
		if strDir:sub(-1) == "/"
		or strDir:sub(-1) == "\\" then
			strDir = strDir:sub(1,-2)								-- Remove trailing "/" or "\"
		end
    
		local function doYieldTree(strDir)
			for strEntry in lfs.dir(strDir) do
				if strEntry ~= "." and strEntry ~= ".." then
					strEntry = strDir.."\\"..strEntry
					local tblAttr, strError = lfs.attributes(strEntry)
					if not tblAttr then tblAttr = { mode="attrfail"; error=strError; } end 
					coroutine.yield(strEntry,tblAttr)
					if tblAttr.mode == "directory" then
						local isOK = true
						for intOmit, strOmit in ipairs (arg) do
							if strEntry:matches(strOmit) then	-- Omit tree branch
								isOK = false
								break
							end
						end
						if isOK then doYieldTree(strEntry) end
					end
				end
			end
		end -- local function doYieldTree

		return coroutine.wrap(function() doYieldTree(strDir) end)
	end -- function DirTree

	local function strErrorText(strError,strFileName,intRepeat)
		return strError:gsub(strFileName:match("(.+\\).+"),"Del#"..tostring(intRepeat)..":")
	end -- local function strErrorText

	-- Delete file if it exists --
	function fh.DeleteFile(strFileName,errFunction)
		if fh.FlgFileExists(strFileName) then
			local fileHandle, strError = os.remove(strFileName)
			if fileHandle == nil then
				local intRepeat = 1
				repeat
					if intRepeat > 1 and type(errFunction) == "function" then
						errFunction(strErrorText(strError,strFileName,intRepeat))
					end
					fhSleep(300,100)
					if fh.FlgFileExists(strFileName) then
						fileHandle, strError = os.remove(strFileName)
					end
					intRepeat = intRepeat + 1
				until fileHandle ~= nil or intRepeat > 10
				if intRepeat > 10 then error(strErrorText(strError,strFileName,intRepeat)) end
			end
		end
	end -- function DeleteFile

	-- Make subfolder if does not exist --
	function fh.MakeFolder(strFolder,errFunction)
		if not fh.FlgFolderExists(strFolder) then
			local isOK, strError = lfs.mkdir(strFolder)
			if not isOK then
				local strMessage = "Cannot Make Folder: "..tostring(strError)..".\n"..strFolder.."\n"
				if type(errFunction) == "function" then
					errFunction(strMessage)
				else
					error(strMessage)
				end
				return false
			end
		end
		return true
	end -- function MakeFolder

	-- Invoke FH Shell Execute API --
	function fh.DoExecute(strExecutable,...)
		local arg = {...}
		local errFunction = fhMessageBox
		if type(arg[#arg]) == 'function' then
			errFunction = arg[#arg]
			table.remove(arg)
		end
		if fhGetAppVersion() > 5 and fhGetStringEncoding() == "UTF-8" then
			strExecutable = fhConvertANSItoUTF8(strExecutable)
		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)
		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)
		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
		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
		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
		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)
		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)
		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+encoder_v3
@Author:			Mike Tate
@Version:			3.5
@LastUpdated:	25 Aug 2020
@Description:	Text encoder module for HTML XHTML XML URI UTF8 UTF16 ISO CP1252/ANSI character codings.
@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") 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) -- Read first lump from 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" then -- Define ANSI conversion to current encoding 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.0 @LastUpdated: 27 Aug 2020 @Description: Progress Bar library module. @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.CENTER -- Show window default position is central local intPosY = iup.CENTER 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 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: 3.8 @LastUpdated: 13 Sep 2020 @Description: Graphical User Interface Library Module @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, Help, 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 Help dialogue Window attributes :- GUI.Help.GetHelp Parent dialogue GetHelp button GUI.Help.RootURL Wiki Help & Advice root URL GUI.Help.TblAttr Table of button attributes GUI.Help[n] Help dialogue nth button :- GUI.Help[n].Name Name for title attribute GUI.Help[n].Tip Tooltip for tip attribute GUI.Help[n].URL Page URL to append to root URL GUI.Help[n].Page Page order for intTabPosn --]] -- 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 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") function fh.SetUtf8Mode() -- Set IUP into UTF-8 display mode if iupVersion == "3.5" or stringx.encoding() == "ANSI" then return false end iup.SetGlobal("UTF8MODE","YES") iup.SetGlobal("UTF8MODE_FILE","NO") return true end -- function SetUtf8Mode local function tblOfNames(...) -- Get table of dialogue Names including "Font","Help","Main" by default local arg = {...} local tblNames = {"Font";"Help";"Main";} for intName, strName in ipairs(arg) do if type(strName) == "string" and strName ~= "Font" and strName ~= "Help" 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.CENTER tblName.CoordY = iup.CENTER 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(0,intX,intMaxW-intWide) -- Ensure X co-ordinate positions window on screen end if intY and intY < iup.CENTERPARENT then intY = intDimension(0,intY,intMaxH-intHigh) -- Ensure Y co-ordinate positions window on screen end tblName.Raster = strRas or "x" tblName.CoordX = tonumber(intX) or iup.CENTER tblName.CoordY = tonumber(intY) or iup.CENTER 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","Help","Main" by default for intName, strName in ipairs(tblOfNames(...)) do local tblName = tblNameFor(strName) --# tblName.Raster = tostring(fh.LoadLocal(strName.."S",tblName.Raster)) -- Legacy of "S" becomes "R" 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 = {} for strLine in io.lines(strFileName) do if #tblField == 0 and strLine == "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 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.FontSet = tonumber(fh.LoadGlobal("Fonts" ,fh.FontSet)) -- Legacy only fh.FontSet = tonumber(fh.LoadGlobal("FontSet" ,fh.FontSet)) -- Legacy only fh.History = tostring(fh.LoadGlobal("History" ,fh.History)) fh.Balloon = tostring(fh.LoadGlobal("Balloon" ,fh.Balloon, "Machine")) fh.LoadDialogue(...) if fh.FontSet > 0 then fh.FontAssignment(fh.FontSet) end -- Legacy only 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 --# doSaveData(fh.ComputerName.."-"..strParam,strFolder,strScope) -- Uses relative paths to let Paths change 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","Help","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 type(table.save) == "function" then -- Save entire Settings File table per Project/User/Machine table.save(tblClipData,strFileName) else local fileHandle = general.OpenFile(strFileName,"w") -- Else save Settings File lines with key & val fields for strKey,strVal in pairs(tblClipData) do fileHandle:write(strKey.."="..strVal.."\n") end fileHandle:close() end break end end end -- function SaveSettings function fh.CheckWindowPosition(tblName) -- Ensure dialogue window coordinates are on Screen if tonumber(tblName.CoordX) == nil or tonumber(tblName.CoordX) < 0 -- V3.5 or tonumber(tblName.CoordX) > intMaxW then tblName.CoordX = iup.CENTER end if tonumber(tblName.CoordY) == nil or tonumber(tblName.CoordY) < 0 -- V3.5 or tonumber(tblName.CoordY) > intMaxH then tblName.CoordY = iup.CENTER end end -- function CheckWindowPosition function fh.IsNormalWindow(iupDialog) -- Check dialogue window is not Maximised or Minimised (now redundant) -- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height local tblPosn = stringx.splitnumbers(iupDialog.ScreenPosition) local intPosX = tblPosn[1] local intPosY = tblPosn[2] 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 return true end -- function IsNormalWindow 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] local intPosY = tblPosn[2] 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 iupDialog.MaxSize = intMaxW.."x"..intMaxH -- Maximum size is screen size iupDialog.MinSize = "x" -- Minimum size (default "x" becomes nil) iupDialog.RasterSize = tblName.Raster or "x" -- Raster size (default "x" becomes nil) 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) fh.SetWindowCoord(tblName) end -- V3.5 iupDialog.resize_cb = iupDialog.resize_cb or function(self) if fh.SetWindowCoord(tblName) then tblName.Raster=self.RasterSize end end -- V3.5 if strFrame:match("map") then -- Only dialogue mapping is required iupDialog:map() --# tblName.Frame = strFrame:gsub("map","") -- Remove "map" from frame mode ready for subsequent call tblName.Frame = strFrame:gsub("%s-%a-map%a*[%s%p]*","") -- Remove "map" from frame mode ready for subsequent call return end fh.RefreshDialogue(strName) -- Refresh to set Natural Size as Minimum Size 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 fh.History ~= fh.Version then -- Initially show new Version History Help if type(fh.HelpDialogue) == "function" then fh.History = fh.Version fh.HelpDialogue(fh.Version) -- But only after Help dialogue exists iupDialog.BringFront = "YES" end end if not ( strName == "Help" or strFrame:match("dialog") ) -- Inhibit MainLoop if Help dialogue or "dialog" mode 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 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.CENTER tblName.CoordY = nil -- iup.CENTER else 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 ") ---[=[ local intFontPlain = 1 -- Font Face & Style values for legacy FontSet setting local intFontBold = 2 local intArialPlain = 3 local intArialBold = 4 local intTahomaPlain= 5 local intTahomaBold = 6 local strFontFace = fh.FontBody:gsub(",.*","") local tblFontSet = {} -- Lookup table for FontHead and FontBody tblFontSet[intFontPlain] = { Head=strFontFace.."; Bold -16"; Body=strFontFace.."; -15"; } tblFontSet[intFontBold] = { Head=strFontFace.."; Bold -16"; Body=strFontFace.."; Bold -15"; } tblFontSet[intArialPlain] = { Head="Arial; Bold -16"; Body="Arial; -16"; } tblFontSet[intArialBold] = { Head="Arial; Bold -16"; Body="Arial; Bold -15"; } tblFontSet[intTahomaPlain] = { Head="Tahoma; Bold -15"; Body="Tahoma; -16"; } tblFontSet[intTahomaBold] = { Head="Tahoma; Bold -15"; Body="Tahoma; Bold -14"; } function fh.FontAssignment(intFontSet) -- Assign Font Face & Style GUI values for legacy FontSet setting if intFontSet then intFontSet = math.max(intFontSet,1) intFontSet = math.min(intFontSet,#tblFontSet) fh.FontHead = tblFontSet[intFontSet]["Head"] -- Legacy Font for all GUI dialog header text fh.FontBody = tblFontSet[intFontSet]["Body"] -- Legacy Font for all GUI dialog body text end end -- function FontAssignment --]=] 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 and Help dialogues local tblHelp = tblNameFor("Help") if type(tblHelp.Dialog) == "userdata" then -- Help dialogue exists fh.AssignAttributes(tblHelp.TblAttr) -- Assign the Help dialogue attributes fh.RefreshDialogue("Help") -- Refresh the Help window size & position end fh.AssignAttributes(tblAttr) -- Assign parent dialogue attributes fh.RefreshDialogue(strName) -- Refresh parent window size & position and bring infront of Help 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 -- Help Dialogue Attributes and Functions fh.HelpDialogue = "" -- HelpDialogue must be declared for ShowDialogue local strHelpButtonActive = nil -- defaults to "YES" -- Help button active attribute mode used only in NewHelpDialogue function fh.NewHelpDialogue(btnGetHelp,strRootURL) -- Prototype for GUI Help Dialogue, with parent Help button, and web page root/namespace URL local tblHelp = tblNameFor("Help") local oleControl, btnDestroy, hboxHelp, dialogHelp, tblAttr -- Dialogue component upvalues if type(btnGetHelp) == "userdata" then btnGetHelp.Active = strHelpButtonActive if btnGetHelp.Active == "NO" then -- Help button inactive, so Help dialogue exists, so just update parent button tblHelp.GetHelp = btnGetHelp -- Allows successive parent GUI to share one Help dialogue return end end tblHelp.GetHelp = btnGetHelp strRootURL = strRootURL or fh.Plugin:gsub(" ","_"):lower() -- Default to Plugin name as Wiki namespace if strRootURL:match("^[%w_]+$") then -- Append Wiki namespace to Wiki root URL strRootURL = "http://www.fhug.org.uk/wiki/doku.php?id=plugins:help:"..strRootURL..":" end tblHelp.RootURL = strRootURL local intURL = 1 -- Index to Version History help page URL local tblURL = { } -- List of help page URL local tblAttr = { } -- Attribute table primarily for FontDialogue() tblHelp.TblAttr = tblAttr local function doCommonAction() -- Common action when creating/destroying Help dialogue local strMode = "NO" if tblHelp.Dialog then tblHelp.Dialog = nil -- Clear dialog handle strMode = nil -- Defaults to "YES" but more efficient to test else tblAttr = { {"Font";"FgColor";}; } -- Reset attribute table primarily for FontDialogue() tblHelp.TblAttr = tblAttr end if type(tblHelp.GetHelp) == "userdata" then -- Set parent dialogue Help button active mode tblHelp.GetHelp.Active = strMode end strHelpButtonActive = strMode end -- local function doCommonAction -- Save global Help button active mode function fh.HelpDialogue(anyPage) -- GUI Help Dialogue for chosen web page --[=[ Parameter anyPage can be one of several values: 1. Page number from 0 to index tblURL, often equal to intTabPosn. 2. Version to display Version History page for version chosen. 3. String with " "="_" and lowercase substring of a page name in tblURL. --]=] if not fh.GetRegKey("HKLM\\SOFTWARE\\Microsoft\\Internet Explorer\\MAIN\\FeatureControl\\FEATURE_BROWSER_EMULATION\\fh.exe") then fhMessageBox("\n The 'Help and Advice' web page has encountered a problem. \n\n The FH IE Shell version is undefined in the Windows Registry. \n\n So please run the 'Write Reg IE Shell Version' Plugin to rectify. \n") return end if not tblHelp.Dialog then doCommonAction() -- Create the WebBrowser based on its ProgID and connect it to LuaCOM oleControl = iup.olecontrol{ "Shell.Explorer.1"; designmode="NO"; } oleControl:CreateLuaCOM() btnDestroy = iup.button { Title="Close Window"; Tip="Close this Help and Advice window"; TipBalloon=fh.Balloon; Expand="HORIZONTAL"; Size="x10"; FgColor=fh.Risk; action=function() dialogHelp:destroy() doCommonAction() end; } hboxHelp = iup.hbox { Margin=fh.Margin; Homogeneous="NO"; } -- Create each GUI button with title, tooltip, color, action, etc, and table of web page URL for intButton, tblButton in ipairs(tblHelp) do local intPage = tblButton.Page or intButton local strURL = tblButton.URL if strURL:match("ver.-hist") then intURL = intPage end tblURL[intPage] = strURL local btnName = iup.button { Title=tblButton.Name; Tip=tblButton.Tip; TipBalloon=fh.Balloon; Expand=btnDestroy.Expand; Size=btnDestroy.Size; FgColor=fh.Safe; action=function() oleControl.com:Navigate(tblHelp.RootURL..strURL) end; } iup.Append(hboxHelp,btnName) tblAttr[btnName] = { "FontBody"; "Safe"; } end iup.Append(hboxHelp,btnDestroy) tblAttr[btnDestroy] = { "FontBody"; "Risk"; } local strExpChild = "NO" if iupVersion == "3.5" then strExpChild = "YES" end -- V3.1 for IUP 3.11.2 dialogHelp = iup.dialog { Title=fh.Plugin.." Help & Advice"; Font=fh.FontBody; iup.vbox { Alignment="ACENTER"; Gap=fh.Gap; Margin=fh.Border; ExpandChildren=strExpChild; -- V3.1 for IUP 3.11.2 oleControl; hboxHelp; }; close_cb = function() doCommonAction() end; } fh.ShowDialogue("Help",dialogHelp,btnDestroy) -- Show Help dialogue window and set tblHelp.Dialog = dialogHelp end anyPage = anyPage or 0 if type(anyPage) == "number" then -- Select page by Tab = Button = Help page index anyPage = math.max(1,math.min(#tblURL,anyPage+1)) anyPage = tblURL[anyPage] or "" elseif anyPage == fh.Version then -- Select the Version History features section anyPage = anyPage:gsub("[%s%p]","") anyPage = anyPage:gsub("^(%d)","V%1") anyPage = tblURL[intURL].."#features_of_"..anyPage elseif type(anyPage) == "string" then -- Select page by matching name text local strPage = anyPage:gsub(" ","_"):lower() anyPage = tblURL[1] or "" -- Default to first web page for intURL = 1, #tblURL do local strURL = tblURL[intURL] if strURL:match(strPage) then anyPage = strURL break end end else anyPage = tblURL[1] or "" -- Default to first web page end oleControl.com:Navigate(tblHelp.RootURL..anyPage) -- Navigate to chosen web page end -- function HelpDialogue end -- function NewHelpDialogue function fh.AddHelpButton(strName,strTip,strURL,intPage) -- Add button to GUI Help Dialogue local tblHelp = tblNameFor("Help") if tblHelp and not strHelpButtonActive then for intHelp, tblHelp in ipairs(tblHelp) do -- Check button does not already exist if tblHelp.Name == strName then return end end if tonumber(intPage) then intPage = intPage + 1 end -- Optional external intPage number matches intTabPosn table.insert( tblHelp, { Name=strName or "?"; Tip=strTip or "?"; URL=strURL or ""; Page=intPage; } ) end end -- function AddHelpButton 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 end -- Return label control so message can be changed 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(anyPlugin) -- Obtain the Version in Plugin Store by Name or Id local strType = "name=" if type(anyPlugin) == "number" or tonumber(anyPlugin) then strType = "id=" end if anyPlugin then local strFile = fh.MachinePath.."\\VersionInStoreInternetError.dat" local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?"..strType..anyPlugin local isOK, strReturn = pcall(httpRequest,strRequest) if not isOK then -- Problem with Internet access local intTime = os.time() - 36000 -- Time in seconds 10 hours ago local tblAttr, strError = lfs.attributes(strFile) -- Obtain file attributes if not tblAttr or tblAttr.modification < intTime then -- File does not exist or was modified long ago fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ") end general.SaveStringToFile(strFile,strFile) -- Update file modified time return "0" end general.DeleteFile(strFile) -- Delete file if Internet is OK local strVersion = "0" if strReturn ~= nil then strVersion = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits end return strVersion or "0" else return "0" end end -- function VersionInStore local function intVersion(strVersion) -- Convert version string to comparable integer local intVersion = 0 local tblVersion = stringx.split(strVersion,".") for i=1,5 do intVersion = intVersion * 1000 + tonumber(tblVersion[i] or 0) end return intVersion end -- local function intVersion function fh.CheckVersionInStore() -- Check if later Version available in Plugin Store 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 Family Historian '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 strToANSI(strFileName) if stringx.encoding() == "ANSI" then return strFileName end return fhConvertUTF8toANSI(strFileName) end -- local function strToANSI 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 strToANSI(strDataFile) end -- local function getPluginDataFileName local function getDataFiles(strScope) -- Compose the Plugin Data file & path & root names local strPluginName = strToANSI(fh.Plugin) local strPluginPlain = stringx.plain(strPluginName) 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 == "" and strScope == "CURRENT_PROJECT" then -- Use standalone GEDCOM path & filename..".fh_data\Plugin Data\" as the folder + the Plugin Filename..".dat" strDataFile = strToANSI(fhGetContextInfo("CI_GEDCOM_FILE")) strDataFile = strDataFile:gsub("%.[G,g][E,e][D,d]",".fh_data") --# lfs.mkdir(strDataFile) general.MakeFolder(strDataFile) -- V3.4 strDataFile = strDataFile.."\\Plugin Data" --# lfs.mkdir(strDataFile) general.MakeFolder(strDataFile) -- V3.4 strDataFile = strDataFile.."\\"..strPluginName..".dat" end local strDataPath = strDataFile:gsub("\\"..strPluginPlain.."%.[D,d][A,a][T,t]$","") -- Plugin data folder path name local strDataRoot = strDataPath.."\\"..strPluginName -- Plugin data file root name general.MakeFolder(strDataPath) -- V3.4 return strDataFile, strDataPath, strDataRoot end -- local function getDataFiles function fh.Initialise(strVersion,strPlugin) -- Initialise the GUI module with optional Version & Plugin name local strAppData = strToANSI(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 "")) end else fhMessageBox("\nPlugin has not been saved!") end end fh.History = fh.Version -- Version History fh.Plugin = strPlugin or fh.Plugin -- Plugin Name from argument or default from file fh.CustomDialogue("Help","1020x730") -- Custom "Help" dialogue sizes fh.DefaultDialogue() -- Default "Font","Help","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 = strToANSI(fhGetContextInfo("CI_PROJECT_DATA_FOLDER")) -- Paths used by Load/SaveFolder for relative folders fh.PublicPath = strToANSI(fhGetContextInfo("CI_PROJECT_PUBLIC_FOLDER")) -- Public data folder path name 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 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 { btnGetHelp; btnDestroy; Homogeneous="YES"; Gap="100"; Margin="90x10"; Expand="HORIZONTAL"; } } -- 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") 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 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" ; }; [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 -- fhInitialise(5,0,8,"save_recommended") PresetGlobalData() -- Preset global data settings ResetDefaultSettings() -- Preset default sticky settings LoadSettings() -- Load sticky data settings iup_gui.CheckVersionInStore() -- Notify if later Version GUI_MainDialogue() -- Invoke graphical user interface SaveSettings() -- Save sticky data settings if IntFhVersion > 5 then fhSetConversionLossFlag(false) end -- Inhibit loss of accents message --[[ @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.fh_lua