--[[ @Title: Format UniqueID @Type: Standard @Author: Mark Draper @Version: 1.1 @LastUpdated: 13 Nov 2023 @Licence: This plugin is copyright (c) 2023 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 ]] 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 (Version 1.1)', margin='20x20', resize='No', minbox='No', maxbox='No'} 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()