Format UniqueID.fh_lua--[[
@Title: Format UniqueID
@Type: Standard
@Author: Mark Draper
@Version: 1.2
@LastUpdated: 13 Jan 2026
@Licence: This plugin is copyright (c) 2026 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Converts UniqueID tags between GEDCOM 5.5.1 and GEDCOM 7.0 format to aid import and
export with other family history applications that do not understand both formats. This ensures that
the identifier is preserved in order to enable unambiguous record identification between Family Historian
and other applicaions.
]]
--[[
Version 1.0 (Sep 2021)
- Initial Store version
Version 1.1 (Nov 2023)
- Improved user interface, but no change in function
Version 1.2 (Jan 2026)
- Multi-monitor support added
]]
require('iuplua')
fh = require('fhUtils')
fh.setIupDefaults()
fhInitialise(7,0,15, 'save_recommended')
-- *********************************************************************************** --
function main()
local pI = fhNewItemPtr()
-- check for missing UID values
pI:MoveToFirstRecord('INDI')
while pI:IsNotNull() do
if fhGetItemPtr(pI, '~._UID'):IsNull() then
local Msg = 'Not all Individual Records in this Project have a UniqueID. Select "Tools > ' ..
'Record Identifiers..." from the main menu to calculate missing values before ' ..
're-running this plugin.'
fhMessageBox(Msg, 'MB_OK', 'MB_ICONSTOP')
return
end
pI:MoveNext('SAME_TAG')
end
-- present main menu for option selection
ShowMenu()
end
-- *********************************************************************************** --
function ShowMenu()
local option1 = iup.toggle{title='Hyphenated, no checksum (GEDCOM 7 preferred format)',
tip='Family Historian format'}
local option2 = iup.toggle{title='Non-hyphenated, no checksum (32 characters)'}
local option3 = iup.toggle{title='Non-hyphenated, with checksum (36 characters)',
tip='e.g. PAF and RootsMagic format'}
local exclusive = iup.radio{iup.vbox{option1, option2, option3}; value=option1, gap=10}
local frame = iup.frame{exclusive; title='Select Preferred UID Format', size='250x70', margin='20x20'}
local btnUpdate = iup.button{title='Update', tip='Process changes', padding='10x3',
action = function(self) ProcessUIDs(exclusive.Value.Title) end}
local btnHelp = iup.button{title='Help', tip='Show help',
action = function(self) fhShellExecute('https://pluginstore.family-historian.co.uk/help/format-uniqueid') end}
local btnClose = iup.button{title='Close', tip='Close plugin', action = function(self) return iup.CLOSE end}
local buttons = iup.hbox{iup.fill{}, btnUpdate, btnHelp, btnClose, iup.fill{};
margin='0x10', normalizesize='BOTH', padding=10, gap=20}
local vbox = iup.vbox{iup.fill{}, frame, buttons, iup.fill{};
alignment='Acenter', gap=15}
local dialog = iup.dialog{vbox; title='Format UniqueID (1.2)', margin='20x20',
resize='No', minbox='No', maxbox='No'}
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
dialog:popup()
end
-- *********************************************************************************** --
function ProcessUIDs(Format)
local pI = fhNewItemPtr()
pI:MoveToFirstRecord('INDI')
while pI:IsNotNull() do
local pU = fhGetItemPtr(pI, '~._UID')
local UID = fhGetValueAsText(pU)
local NewUID = ProcessUID(UID, Format)
if NewUID ~= UID then fhSetValueAsText(pU, NewUID) end
pI:MoveNext('SAME_TAG')
end
fhUpdateDisplay()
end
-- *********************************************************************************** --
function ProcessUID(UID, Format)
-- determines whether passed UID is valid (return same value if not)
local S = UID:gsub('-', '')
if not tonumber('0x' .. S) then return UID end -- not hexadecimal
if S:len() ~= 32 and S:len() ~= 36 then return UID end -- invalid length
if S:len() == 36 then -- checksum error
if S:sub(1, 32) .. CalculateCheckSum(S:sub(1, 32)) ~= UID then return UID end
end
-- process valid values according to the Format option
if Format == 'Hyphenated, no checksum (GEDCOM 7 preferred format)' then
local NewUID = S:sub(1, 8) .. '-' .. S:sub(9, 12) .. '-' .. S:sub(13, 16) ..
'-' .. S:sub(17, 20) .. '-' .. S:sub(21, 32)
return NewUID
elseif Format == 'Non-hyphenated, no checksum (32 characters)' then
return S:sub(1, 32)
elseif Format == 'Non-hyphenated, with checksum (36 characters)' then
if S:len() == 32 then -- no checksum
return S .. CalculateCheckSum(S)
elseif S:len() == 36 then -- checksum present, so don't recalculate
return S
end
end
end
-- *********************************************************************************** --
function CalculateCheckSum(UID)
-- calculates checksum using published method
local a = 0
local b = 0
for i = 1, 31, 2 do
local byte = UID:sub(i, i + 1)
local value = tonumber('0x' .. byte)
a = a + value
b = b + a
end
local cs1 = string.format('%x', a)
local cs2 = string.format('%x', b)
local checksum = cs1:sub(-2) .. cs2:sub(-2)
-- use same case for checksum as for main string
if UID:upper() == UID then checksum = checksum:upper() end
return checksum
end
-- *********************************************************************************** --
main()
--[[
@Title: Format UniqueID
@Type: Standard
@Author: Mark Draper
@Version: 1.2
@LastUpdated: 13 Jan 2026
@Licence: This plugin is copyright (c) 2026 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Converts UniqueID tags between GEDCOM 5.5.1 and GEDCOM 7.0 format to aid import and
export with other family history applications that do not understand both formats. This ensures that
the identifier is preserved in order to enable unambiguous record identification between Family Historian
and other applicaions.
]]
--[[
Version 1.0 (Sep 2021)
- Initial Store version
Version 1.1 (Nov 2023)
- Improved user interface, but no change in function
Version 1.2 (Jan 2026)
- Multi-monitor support added
]]
require('iuplua')
fh = require('fhUtils')
fh.setIupDefaults()
fhInitialise(7,0,15, 'save_recommended')
-- *********************************************************************************** --
function main()
local pI = fhNewItemPtr()
-- check for missing UID values
pI:MoveToFirstRecord('INDI')
while pI:IsNotNull() do
if fhGetItemPtr(pI, '~._UID'):IsNull() then
local Msg = 'Not all Individual Records in this Project have a UniqueID. Select "Tools > ' ..
'Record Identifiers..." from the main menu to calculate missing values before ' ..
're-running this plugin.'
fhMessageBox(Msg, 'MB_OK', 'MB_ICONSTOP')
return
end
pI:MoveNext('SAME_TAG')
end
-- present main menu for option selection
ShowMenu()
end
-- *********************************************************************************** --
function ShowMenu()
local option1 = iup.toggle{title='Hyphenated, no checksum (GEDCOM 7 preferred format)',
tip='Family Historian format'}
local option2 = iup.toggle{title='Non-hyphenated, no checksum (32 characters)'}
local option3 = iup.toggle{title='Non-hyphenated, with checksum (36 characters)',
tip='e.g. PAF and RootsMagic format'}
local exclusive = iup.radio{iup.vbox{option1, option2, option3}; value=option1, gap=10}
local frame = iup.frame{exclusive; title='Select Preferred UID Format', size='250x70', margin='20x20'}
local btnUpdate = iup.button{title='Update', tip='Process changes', padding='10x3',
action = function(self) ProcessUIDs(exclusive.Value.Title) end}
local btnHelp = iup.button{title='Help', tip='Show help',
action = function(self) fhShellExecute('https://pluginstore.family-historian.co.uk/help/format-uniqueid') end}
local btnClose = iup.button{title='Close', tip='Close plugin', action = function(self) return iup.CLOSE end}
local buttons = iup.hbox{iup.fill{}, btnUpdate, btnHelp, btnClose, iup.fill{};
margin='0x10', normalizesize='BOTH', padding=10, gap=20}
local vbox = iup.vbox{iup.fill{}, frame, buttons, iup.fill{};
alignment='Acenter', gap=15}
local dialog = iup.dialog{vbox; title='Format UniqueID (1.2)', margin='20x20',
resize='No', minbox='No', maxbox='No'}
iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
dialog:popup()
end
-- *********************************************************************************** --
function ProcessUIDs(Format)
local pI = fhNewItemPtr()
pI:MoveToFirstRecord('INDI')
while pI:IsNotNull() do
local pU = fhGetItemPtr(pI, '~._UID')
local UID = fhGetValueAsText(pU)
local NewUID = ProcessUID(UID, Format)
if NewUID ~= UID then fhSetValueAsText(pU, NewUID) end
pI:MoveNext('SAME_TAG')
end
fhUpdateDisplay()
end
-- *********************************************************************************** --
function ProcessUID(UID, Format)
-- determines whether passed UID is valid (return same value if not)
local S = UID:gsub('-', '')
if not tonumber('0x' .. S) then return UID end -- not hexadecimal
if S:len() ~= 32 and S:len() ~= 36 then return UID end -- invalid length
if S:len() == 36 then -- checksum error
if S:sub(1, 32) .. CalculateCheckSum(S:sub(1, 32)) ~= UID then return UID end
end
-- process valid values according to the Format option
if Format == 'Hyphenated, no checksum (GEDCOM 7 preferred format)' then
local NewUID = S:sub(1, 8) .. '-' .. S:sub(9, 12) .. '-' .. S:sub(13, 16) ..
'-' .. S:sub(17, 20) .. '-' .. S:sub(21, 32)
return NewUID
elseif Format == 'Non-hyphenated, no checksum (32 characters)' then
return S:sub(1, 32)
elseif Format == 'Non-hyphenated, with checksum (36 characters)' then
if S:len() == 32 then -- no checksum
return S .. CalculateCheckSum(S)
elseif S:len() == 36 then -- checksum present, so don't recalculate
return S
end
end
end
-- *********************************************************************************** --
function CalculateCheckSum(UID)
-- calculates checksum using published method
local a = 0
local b = 0
for i = 1, 31, 2 do
local byte = UID:sub(i, i + 1)
local value = tonumber('0x' .. byte)
a = a + value
b = b + a
end
local cs1 = string.format('%x', a)
local cs2 = string.format('%x', b)
local checksum = cs1:sub(-2) .. cs2:sub(-2)
-- use same case for checksum as for main string
if UID:upper() == UID then checksum = checksum:upper() end
return checksum
end
-- *********************************************************************************** --
main()Source:Format-UniqueID-2.fh_lua