Trim Date and Text Fields.fh_lua--[[
@Title: Trim Date and Text Fields
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.1
@Keywords:
@LastUpdated: 19 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: Reduce the details held in Date, Note, Address & Place fields.
@V1.1: FH V7 Lua 3.5 IUP 3.28; progbar 3.0;
@V1.0: First published version;
@V0.3: Simplified AddResults() function; Support Note field; Allow both standard Date formats; Inhibit Result Set option; Add progress bar.
@V0.2: Make options globally sticky; Allow fields to be deleted; Support Address field.
@V0.1: 1st prototype.
]]
require "iuplua" -- To access GUI window builder
require "lfs" -- To access LUA filing system
local strTitle = "Trim Date and Text Fields 1.1 "
if fhGetAppVersion() > 5 then -- Cater for Unicode UTF-8 from FH Version 6 onwards
fhSetStringEncoding("UTF-8")
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
end
if fhGetAppVersion() > 6 then unpack = table.unpack end -- V1.1
-- Split a string using "," or chosen separator --
function 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
-- Check if file exists --
function FlgFileExists(strFileName)
return lfs.attributes(strFileName,"mode") == "file"
end -- function FlgFileExists
-- Open File and return Handle --
function 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
-- Load Options
function LoadOptions(strFileName)
local dicOption = {}
strFileName = strFileName or "?"
if FlgFileExists(strFileName) then -- Read the file in table lines
for strLine in io.lines(strFileName) do
local arrFields = split(strLine,"=")
dicOption[arrFields[1]] = tonumber(arrFields[2])
end
end
return dicOption
end -- function LoadOptions
-- Save Options
function SaveOptions(dicOption,strFileName)
strFileName = strFileName or "?"
local fileHandle = OpenFile(strFileName,"w")
for strField, strValue in pairs(dicOption) do -- Write the file in table lines
fileHandle:write(strField.."="..strValue.."\n")
end
fileHandle:close()
end -- function SaveOptions
-- Result Set Tables used by function AddResults and Main
local arrAct = {} -- Trim Action Text
local arrRec = {} -- Record Name Text
local arrBud = {} -- Record Buddy Pointer
local arrTyp = {} -- Record Type Tag
local arrTid = {} -- Record Type Integer sort key
local arrRid = {} -- Record Id Integer sort key
local arrOld = {} -- Old Trim Field Text
local arrNew = {} -- New Trim Field Pointer
local intAct = 0 -- Count trims when Result Set inhibited
do -- Handle the Result Set table features
local dicAct = { -- Action descriptions
DATE = "1. Date Field Trim " ;
NOTE = "2. Note Field Trim " ;
ADDR = "3. Address Field Trim " ;
PLAC = "4. Place Field Trim " ;
Purge = "5. Place Record Purge " ;
}
local dicOK = {
[false] = "Failure" ;
[true] = "Success" ;
}
function CountTrims() -- Count trims when Result Set inhibited
intAct = intAct + 1
end -- function CountTrims
function AddResults(strAct,isOK,intTid,strVal,ptrNew,intRid) -- Update the Result Set tables
-- strAct Action tag index for dicAct
-- isOK Action true/false for dicOK
-- intTid Record type integer sort key
-- strVal Old text field value or purged record name
-- ptrNew New text field pointer or nil
-- intRid Purged record id or nil
local ptrRec = fhNewItemPtr()
local strRec = strVal -- Place record purge values
local strTyp = "_PLAC"
local strOld = ""
ptrNew = ptrNew or fhNewItemPtr()
if strAct ~= "Purge" then -- Date or text field frim values
ptrRec:MoveToRecordItem(ptrNew)
strRec = fhGetDisplayText(ptrRec)
strTyp = fhGetTag(ptrRec)
intRid = fhGetRecordId(ptrRec)
strOld = strVal
end
table.insert(arrAct,dicAct[strAct]..dicOK[isOK or false]) -- Save the Result Set values
table.insert(arrRec,strRec)
table.insert(arrBud,ptrRec:Clone())
table.insert(arrTyp,strTyp)
table.insert(arrTid,intTid)
table.insert(arrRid,intRid)
table.insert(arrOld,strOld)
table.insert(arrNew,ptrNew:Clone())
end -- function AddResults
end -- do
function HandleDate(intDate) -- Handle the Date field trimming features
local strWord = "( ?[abcft]?[feniro]?[tfdro]?[eowcm]?[rea]?e?n? ?)" -- Pattern to capture keywords: after before between and circa from to
local strMnth = "[JFMASOND]?[aepuco]?%l-,? ?" -- Pattern to match the months: Ja Fe Ma Ap Ju Au Se Oc No De letters comma space
local arrDate =
{ -- gsub parameters for Date trim options > 0
{ Pattern = "^.*$" ; Replace = ""; };
{ Pattern = "^.-(%d%d%d%d).*$" ; Replace = "%1"; };
{ Pattern = strWord.."Q?%d?%d? ?"..strMnth.."%d?%d?,? ?(%d%d%d%d)" ; Replace = "%1%2"; };
{ Pattern = strWord.."Q?%d?%d? ?("..strMnth..")%d?%d?,? ?(%d%d%d%d)" ; Replace = "%1%2%3"; };
}
local dicDate = arrDate[intDate] or { } -- Set the Date trim parameters
local strPatt = dicDate.Pattern
local strRepl = dicDate.Replace
function TrimDate(intTid,ptrItem) -- Trim old Date field if enabled
if intDate > 0 then
local isPhrase = false
local strPhr = ""
local datVal = fhGetValueAsDate(ptrItem)
local strOld = datVal:GetValueAsText()
if datVal:GetType() == "Phrase" then -- Cope with Date Phrase and Interpreted value
isPhrase = true
strOld,strPhr = strOld:match('^(.-)(%(?".+"%)?)$')
end
local strNew = strOld:gsub(strPatt,strRepl)
if #strNew < #strOld then -- Save new Date field if different
local isOK = true
if #strNew == 0 then
datVal:SetNull()
else
isOK = datVal:SetValueAsText(strNew..strPhr,isPhrase) -- Update the Date value
end
if isOK then
isOK = fhSetValueAsDate(ptrItem,datVal) -- Update the Date field
end
AddResults("DATE",isOK,intTid,strOld..strPhr,ptrItem) -- Update the Result Set
end
end
end -- local function TrimDate
end -- function HandleDate
function HandleNote(intNote) -- Handle the Note field trimming features
local arrNote =
{ -- gsub parameters for Note trim options > 0
{ Pattern = "^.*$" ; Replace = ""; };
{ Pattern = "%[%[.-%]%]" ; Replace = ""; };
}
local dicNote = arrNote[intNote] or { } -- Set the Note trim parameters
local strPatt = dicNote.Pattern
local strRepl = dicNote.Replace
function TrimNote(intTid,ptrItem) -- Trim old Note field if enabled
if intNote > 0 then
local strOld = fhGetValueAsText(ptrItem)
local strNew = strOld:gsub(strPatt,strRepl)
if #strNew < #strOld then -- Save new Note field if different
local isOK = fhSetValueAsText(ptrItem,strNew)
AddResults("NOTE",isOK,intTid,strOld,ptrItem) -- Update the Result Set
end
end
end -- local function TrimNote
end -- function HandleNote
function HandleAddr(intAddr) -- Handle the Address field trimming features
function TrimAddr(intTid,ptrItem) -- Trim old Address field if enabled
if intAddr > 0 then
local strOld = fhGetValueAsText(ptrItem)
local strNew = "" -- For intAddr == 1 then delete field
if intAddr > 1 then -- For intAddr > 1 then keep last intAddr-1 columns
strNew = fhCallBuiltInFunction("TextPart",strOld,-1,intAddr-1)
end
if #strNew < #strOld then -- Save new Address field if different
local isOK = fhSetValueAsText(ptrItem,strNew)
AddResults("ADDR",isOK,intTid,strOld,ptrItem) -- Update the Result Set
end
end
end -- local function TrimAddr
end -- function HandleAddr
function HandlePlac(intPlac) -- Handle the Place field trimming features
function TrimPlac(intTid,ptrItem) -- Trim old Place field if enabled
if intPlac > 0 then
local strOld = fhGetValueAsText(ptrItem)
local strNew = "" -- For intPlac == 1 then delete field
if intPlac > 1 then -- For intPlac > 1 then keep last intPlac-1 columns
strNew = fhCallBuiltInFunction("TextPart",strOld,-1,intPlac-1)
end
if #strNew < #strOld then -- Save new Place field if different
local isOK = fhSetValueAsText(ptrItem,strNew)
AddResults("PLAC",isOK,intTid,strOld,ptrItem) -- Update the Result Set
end
end
end -- local function TrimPlac
end -- function HandlePlac
function HandlePurge(intPurge) -- Handle the Place record purging features
function PurgePlac(intTid,ptrRec) -- Purge the Place record if enabled
if intPurge > 0 then
if fhCallBuiltInFunction("LinksTo",ptrRec) == 0 then
local strRec = fhGetDisplayText(ptrRec) -- Save record Name & Id before deletion
local intRid = fhGetRecordId(ptrRec)
local isOK = fhDeleteItem(ptrRec)
AddResults("Purge",isOK,intTid,strRec,nil,intRid) -- Update the Result Set
end
end
end -- function PurgePlac
end -- function HandlePurge
function TrimFields(dicOptions)
-- dicOptions["DATE"] Dates field trim mode : 0 = None, 1 = Delete, 2 = Year only, 3 = No Day/Month, 4 = No Day number
-- dicOptions["NOTE"] Notes field trim mode : 0 = None, 1 = Delete, 2 = Delete [[private]]
-- dicOptions["ADDR"] Addrs field trim mode : 0 = None, 1 = Delete, 2+ = One greater than number of trailing parts to keep
-- dicOptions["PLAC"] Place field trim mode : 0 = None, 1 = Delete, 2+ = One greater than number of trailing parts to keep
-- dicOptions["Purge"] Place record purge mode: 0 = No , 1 = Yes
-- dicOptions["ResSet"] Create Result Set mode : 0 = No , 1 = Yes
if dicOptions["ResSet"] == 0 then AddResults = CountTrims end -- When Result Set inhibited then just count trims
HandleDate ( dicOptions["DATE"] ) -- Initialise the field trimming functions
HandleNote ( dicOptions["NOTE"] )
HandleAddr ( dicOptions["ADDR"] )
HandlePlac ( dicOptions["PLAC"] )
HandlePurge( dicOptions["Purge"])
local function null() -- Do nothing function for other fields
end -- local function null
local dicTrim = { -- Trim handler dictionary per field tag (should be faster than multiple if then ifelse then...)
DATE = TrimDate ;
_DATE = TrimDate ;
NOTE2 = TrimNote ;
_NOTE2 = TrimNote ;
ADDR = TrimAddr ;
PLAC = TrimPlac ;
_PLAC = TrimPlac ;
}
local intMaxType = fhGetRecordTypeCount()
progbar.Start("Trimming Fields",intMaxType+1) -- Initiate progress bar
local ptrItem = fhNewItemPtr()
for intType = 1, intMaxType do -- Loop all record types
progbar.Step()
if progbar.Stop() then break end -- Step or Stop the progress bar
local intRec = 1
local strType = fhGetRecordTypeTag(intType)
ptrItem:MoveToFirstRecord(strType)
while ptrItem:IsNotNull() do -- Loop all fields
local ptrParent = fhNewItemPtr()
ptrParent:MoveToParentItem(ptrItem)
if ptrParent:IsNull() then -- No parent so must be next record
intRec = intRec - 1
if intRec == 0 then -- Only report some records to reduce run time
intRec = 31
progbar.Message("Searching record "..strType.." ["..tostring(fhGetRecordId(ptrItem)).."]")
if progbar.Stop() then break end
end
end
if strType == "_PLAC" then -- Purge Place records if enabled and no links
local ptrRec = ptrItem:Clone()
ptrItem:MoveNext() -- Move pointer to next record before deletion
PurgePlac(intType,ptrRec)
else
( dicTrim[fhGetTag(ptrItem)] or null )(intType,ptrItem) -- Trim any matching text field
ptrItem:MoveNextSpecial() -- Move pointer to next item
end
end
end
progbar.Close()
end -- function TrimFields
function GetOptions(dicOption) -- Prompt User to Select Options (derived from Clean Living Persons)
local tblName = {} -- Index names for dicOption settings
local tblData = {} -- Parameter values for iup.GetParam
local tblForm = {} -- Format strings for iup.GetParam
local function paramAction(iupDialog,intIndex) -- Display Help Page (derived from Clean Living Persons)
if intIndex == iup.GETPARAM_MAP then -- Correct button labels needed for IUP 3.28 bug -- V1.1
iupDialog.Button1.Title = " Trim Fields" -- Otherwise remain as OK and Cancel
iupDialog.Button2.Title = " Cancel Plugin"
end
if intIndex == (iup.GETPARAM_HELP or -4) then -- FH V5 needs -4
fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/trim-date-and-text-fields","","","open") -- V1.1
fhSleep(3000,500)
iupDialog.BringFront="YES"
end
return 1
end -- local function paramAction
local function setTables(strName,intData,strForm) -- Set those lookup tables to values below
table.insert(tblName,strName) -- 'nil' values insert nothing
table.insert(tblData,dicOption[strName] or intData) -- Use dicOption or set default value
table.insert(tblForm,strForm)
end -- local function setTables
-- Options , Data , Parameter Format for iup.GetParam(...)
setTables( "DATE" , 0 , "Select trim Date fields mode : %l| No Date field trim | Delete entire Date | Keep first/only Year | Omit Day & Month | Omit Day number |" )
setTables( "NOTE" , 0 , "Select trim Note fields mode : %l| No Note field trim | Delete entire Note | Delete [[private]] text|" )
setTables( "ADDR" , 0 , "Select trim Address fields mode : %l| No Address field trim| Delete all columns | Keep last column | Keep last 2 columns | Keep last 3 columns | Keep last 4 columns | Keep last 5 columns |" )
setTables( "PLAC" , 0 , "Select trim Place fields mode : %l| No Place field trim | Delete all columns | Keep last column | Keep last 2 columns | Keep last 3 columns | Keep last 4 columns | Keep last 5 columns |" )
setTables( "Purge" , 0 , "Purge unused Place records ? %b" )
setTables( "ResSet" , 1 , "Create Result Set of changes ? %b" )
setTables( nil , nil , "Button Names %u[ Trim Fields, Cancel Plugin, Help && Advice]" )
tblData = { iup.GetParam("Family Historian - "..strTitle,paramAction,table.concat(tblForm,"\n"),unpack(tblData)) }
if not tblData[1] then return false end
table.remove(tblData,1) -- Align data with tables above
for intName, strName in ipairs (tblName) do
dicOption[strName] = tblData[intName] -- Update options
end
return true
end -- function GetOptions
function CheckMode() -- Check for Project and Warn if in Project Mode (derived from Clean Living Persons)
if fhGetContextInfo("CI_APP_MODE") == "Project Mode" then
local strAns = fhMessageBox(
[[
Plugin Warning:
This is designed to delete data from the current file.
You have a Project open.
Please confirm you want to trim the data from
]]..fhGetContextInfo("CI_PROJECT_NAME"),
"MB_OKCANCEL",
"MB_ICONEXCLAMATION")
if strAns ~= "OK" then
return false
end
end
local strName = fhGetPluginDataFileName("LOCAL_MACHINE") -- Global sticky options file needs V5.0.8
return strName:gsub(" %- V%d.*%.dat$",".dat") -- Use same .dat file for all Plugin versions
end -- function CheckMode
function Main()
local fileOptions = CheckMode() -- Check Project/Gedcom mode and compose sticky options filename
if fileOptions then
local dicOption = LoadOptions(fileOptions) -- Load sticky options from file
if GetOptions(dicOption) then -- Get options choice from user
SaveOptions(dicOption,fileOptions) -- Save sticky options into file
TrimFields(dicOption) -- Trim fields as per options
if #arrAct > 0 then
fhOutputResultSetTitles(strTitle,strTitle) -- Output Result Set
fhOutputResultSetColumn("Action" , "text", arrAct, #arrAct, 104, "align_left", 1, true, "default", "show" )
fhOutputResultSetColumn("Record" , "text", arrRec, #arrAct, 200, "align_left")
fhOutputResultSetColumn("Buddy" , "item", arrBud, #arrAct, 200, "align_left", 0, true, "default", "buddy")
fhOutputResultSetColumn("Type" , "text", arrTyp, #arrAct, 30, "align_mid" )
fhOutputResultSetColumn("TypId","integer", arrTid, #arrAct, 30, "align_mid" , 2, true, "integer", "hide" )
fhOutputResultSetColumn("RecId","integer", arrRid, #arrAct, 30, "align_mid" , 3, true, "integer", "show" )
fhOutputResultSetColumn("Old Val", "text", arrOld, #arrAct, 200, "align_left")
fhOutputResultSetColumn("New Val", "item", arrNew, #arrAct, 200, "align_left")
else
fhMessageBox(tostring(intAct).." field trims performed.")
end
end
end
end -- function Main
--[[
@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
progbar = progbar_v3() -- To access FH progress bars module
Main() -- Invoke main function
--[[
@Title: Trim Date and Text Fields
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.1
@Keywords:
@LastUpdated: 19 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: Reduce the details held in Date, Note, Address & Place fields.
@V1.1: FH V7 Lua 3.5 IUP 3.28; progbar 3.0;
@V1.0: First published version;
@V0.3: Simplified AddResults() function; Support Note field; Allow both standard Date formats; Inhibit Result Set option; Add progress bar.
@V0.2: Make options globally sticky; Allow fields to be deleted; Support Address field.
@V0.1: 1st prototype.
]]
require "iuplua" -- To access GUI window builder
require "lfs" -- To access LUA filing system
local strTitle = "Trim Date and Text Fields 1.1 "
if fhGetAppVersion() > 5 then -- Cater for Unicode UTF-8 from FH Version 6 onwards
fhSetStringEncoding("UTF-8")
iup.SetGlobal("UTF8MODE","YES")
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
end
if fhGetAppVersion() > 6 then unpack = table.unpack end -- V1.1
-- Split a string using "," or chosen separator --
function 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
-- Check if file exists --
function FlgFileExists(strFileName)
return lfs.attributes(strFileName,"mode") == "file"
end -- function FlgFileExists
-- Open File and return Handle --
function 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
-- Load Options
function LoadOptions(strFileName)
local dicOption = {}
strFileName = strFileName or "?"
if FlgFileExists(strFileName) then -- Read the file in table lines
for strLine in io.lines(strFileName) do
local arrFields = split(strLine,"=")
dicOption[arrFields[1]] = tonumber(arrFields[2])
end
end
return dicOption
end -- function LoadOptions
-- Save Options
function SaveOptions(dicOption,strFileName)
strFileName = strFileName or "?"
local fileHandle = OpenFile(strFileName,"w")
for strField, strValue in pairs(dicOption) do -- Write the file in table lines
fileHandle:write(strField.."="..strValue.."\n")
end
fileHandle:close()
end -- function SaveOptions
-- Result Set Tables used by function AddResults and Main
local arrAct = {} -- Trim Action Text
local arrRec = {} -- Record Name Text
local arrBud = {} -- Record Buddy Pointer
local arrTyp = {} -- Record Type Tag
local arrTid = {} -- Record Type Integer sort key
local arrRid = {} -- Record Id Integer sort key
local arrOld = {} -- Old Trim Field Text
local arrNew = {} -- New Trim Field Pointer
local intAct = 0 -- Count trims when Result Set inhibited
do -- Handle the Result Set table features
local dicAct = { -- Action descriptions
DATE = "1. Date Field Trim " ;
NOTE = "2. Note Field Trim " ;
ADDR = "3. Address Field Trim " ;
PLAC = "4. Place Field Trim " ;
Purge = "5. Place Record Purge " ;
}
local dicOK = {
[false] = "Failure" ;
[true] = "Success" ;
}
function CountTrims() -- Count trims when Result Set inhibited
intAct = intAct + 1
end -- function CountTrims
function AddResults(strAct,isOK,intTid,strVal,ptrNew,intRid) -- Update the Result Set tables
-- strAct Action tag index for dicAct
-- isOK Action true/false for dicOK
-- intTid Record type integer sort key
-- strVal Old text field value or purged record name
-- ptrNew New text field pointer or nil
-- intRid Purged record id or nil
local ptrRec = fhNewItemPtr()
local strRec = strVal -- Place record purge values
local strTyp = "_PLAC"
local strOld = ""
ptrNew = ptrNew or fhNewItemPtr()
if strAct ~= "Purge" then -- Date or text field frim values
ptrRec:MoveToRecordItem(ptrNew)
strRec = fhGetDisplayText(ptrRec)
strTyp = fhGetTag(ptrRec)
intRid = fhGetRecordId(ptrRec)
strOld = strVal
end
table.insert(arrAct,dicAct[strAct]..dicOK[isOK or false]) -- Save the Result Set values
table.insert(arrRec,strRec)
table.insert(arrBud,ptrRec:Clone())
table.insert(arrTyp,strTyp)
table.insert(arrTid,intTid)
table.insert(arrRid,intRid)
table.insert(arrOld,strOld)
table.insert(arrNew,ptrNew:Clone())
end -- function AddResults
end -- do
function HandleDate(intDate) -- Handle the Date field trimming features
local strWord = "( ?[abcft]?[feniro]?[tfdro]?[eowcm]?[rea]?e?n? ?)" -- Pattern to capture keywords: after before between and circa from to
local strMnth = "[JFMASOND]?[aepuco]?%l-,? ?" -- Pattern to match the months: Ja Fe Ma Ap Ju Au Se Oc No De letters comma space
local arrDate =
{ -- gsub parameters for Date trim options > 0
{ Pattern = "^.*$" ; Replace = ""; };
{ Pattern = "^.-(%d%d%d%d).*$" ; Replace = "%1"; };
{ Pattern = strWord.."Q?%d?%d? ?"..strMnth.."%d?%d?,? ?(%d%d%d%d)" ; Replace = "%1%2"; };
{ Pattern = strWord.."Q?%d?%d? ?("..strMnth..")%d?%d?,? ?(%d%d%d%d)" ; Replace = "%1%2%3"; };
}
local dicDate = arrDate[intDate] or { } -- Set the Date trim parameters
local strPatt = dicDate.Pattern
local strRepl = dicDate.Replace
function TrimDate(intTid,ptrItem) -- Trim old Date field if enabled
if intDate > 0 then
local isPhrase = false
local strPhr = ""
local datVal = fhGetValueAsDate(ptrItem)
local strOld = datVal:GetValueAsText()
if datVal:GetType() == "Phrase" then -- Cope with Date Phrase and Interpreted value
isPhrase = true
strOld,strPhr = strOld:match('^(.-)(%(?".+"%)?)$')
end
local strNew = strOld:gsub(strPatt,strRepl)
if #strNew < #strOld then -- Save new Date field if different
local isOK = true
if #strNew == 0 then
datVal:SetNull()
else
isOK = datVal:SetValueAsText(strNew..strPhr,isPhrase) -- Update the Date value
end
if isOK then
isOK = fhSetValueAsDate(ptrItem,datVal) -- Update the Date field
end
AddResults("DATE",isOK,intTid,strOld..strPhr,ptrItem) -- Update the Result Set
end
end
end -- local function TrimDate
end -- function HandleDate
function HandleNote(intNote) -- Handle the Note field trimming features
local arrNote =
{ -- gsub parameters for Note trim options > 0
{ Pattern = "^.*$" ; Replace = ""; };
{ Pattern = "%[%[.-%]%]" ; Replace = ""; };
}
local dicNote = arrNote[intNote] or { } -- Set the Note trim parameters
local strPatt = dicNote.Pattern
local strRepl = dicNote.Replace
function TrimNote(intTid,ptrItem) -- Trim old Note field if enabled
if intNote > 0 then
local strOld = fhGetValueAsText(ptrItem)
local strNew = strOld:gsub(strPatt,strRepl)
if #strNew < #strOld then -- Save new Note field if different
local isOK = fhSetValueAsText(ptrItem,strNew)
AddResults("NOTE",isOK,intTid,strOld,ptrItem) -- Update the Result Set
end
end
end -- local function TrimNote
end -- function HandleNote
function HandleAddr(intAddr) -- Handle the Address field trimming features
function TrimAddr(intTid,ptrItem) -- Trim old Address field if enabled
if intAddr > 0 then
local strOld = fhGetValueAsText(ptrItem)
local strNew = "" -- For intAddr == 1 then delete field
if intAddr > 1 then -- For intAddr > 1 then keep last intAddr-1 columns
strNew = fhCallBuiltInFunction("TextPart",strOld,-1,intAddr-1)
end
if #strNew < #strOld then -- Save new Address field if different
local isOK = fhSetValueAsText(ptrItem,strNew)
AddResults("ADDR",isOK,intTid,strOld,ptrItem) -- Update the Result Set
end
end
end -- local function TrimAddr
end -- function HandleAddr
function HandlePlac(intPlac) -- Handle the Place field trimming features
function TrimPlac(intTid,ptrItem) -- Trim old Place field if enabled
if intPlac > 0 then
local strOld = fhGetValueAsText(ptrItem)
local strNew = "" -- For intPlac == 1 then delete field
if intPlac > 1 then -- For intPlac > 1 then keep last intPlac-1 columns
strNew = fhCallBuiltInFunction("TextPart",strOld,-1,intPlac-1)
end
if #strNew < #strOld then -- Save new Place field if different
local isOK = fhSetValueAsText(ptrItem,strNew)
AddResults("PLAC",isOK,intTid,strOld,ptrItem) -- Update the Result Set
end
end
end -- local function TrimPlac
end -- function HandlePlac
function HandlePurge(intPurge) -- Handle the Place record purging features
function PurgePlac(intTid,ptrRec) -- Purge the Place record if enabled
if intPurge > 0 then
if fhCallBuiltInFunction("LinksTo",ptrRec) == 0 then
local strRec = fhGetDisplayText(ptrRec) -- Save record Name & Id before deletion
local intRid = fhGetRecordId(ptrRec)
local isOK = fhDeleteItem(ptrRec)
AddResults("Purge",isOK,intTid,strRec,nil,intRid) -- Update the Result Set
end
end
end -- function PurgePlac
end -- function HandlePurge
function TrimFields(dicOptions)
-- dicOptions["DATE"] Dates field trim mode : 0 = None, 1 = Delete, 2 = Year only, 3 = No Day/Month, 4 = No Day number
-- dicOptions["NOTE"] Notes field trim mode : 0 = None, 1 = Delete, 2 = Delete [[private]]
-- dicOptions["ADDR"] Addrs field trim mode : 0 = None, 1 = Delete, 2+ = One greater than number of trailing parts to keep
-- dicOptions["PLAC"] Place field trim mode : 0 = None, 1 = Delete, 2+ = One greater than number of trailing parts to keep
-- dicOptions["Purge"] Place record purge mode: 0 = No , 1 = Yes
-- dicOptions["ResSet"] Create Result Set mode : 0 = No , 1 = Yes
if dicOptions["ResSet"] == 0 then AddResults = CountTrims end -- When Result Set inhibited then just count trims
HandleDate ( dicOptions["DATE"] ) -- Initialise the field trimming functions
HandleNote ( dicOptions["NOTE"] )
HandleAddr ( dicOptions["ADDR"] )
HandlePlac ( dicOptions["PLAC"] )
HandlePurge( dicOptions["Purge"])
local function null() -- Do nothing function for other fields
end -- local function null
local dicTrim = { -- Trim handler dictionary per field tag (should be faster than multiple if then ifelse then...)
DATE = TrimDate ;
_DATE = TrimDate ;
NOTE2 = TrimNote ;
_NOTE2 = TrimNote ;
ADDR = TrimAddr ;
PLAC = TrimPlac ;
_PLAC = TrimPlac ;
}
local intMaxType = fhGetRecordTypeCount()
progbar.Start("Trimming Fields",intMaxType+1) -- Initiate progress bar
local ptrItem = fhNewItemPtr()
for intType = 1, intMaxType do -- Loop all record types
progbar.Step()
if progbar.Stop() then break end -- Step or Stop the progress bar
local intRec = 1
local strType = fhGetRecordTypeTag(intType)
ptrItem:MoveToFirstRecord(strType)
while ptrItem:IsNotNull() do -- Loop all fields
local ptrParent = fhNewItemPtr()
ptrParent:MoveToParentItem(ptrItem)
if ptrParent:IsNull() then -- No parent so must be next record
intRec = intRec - 1
if intRec == 0 then -- Only report some records to reduce run time
intRec = 31
progbar.Message("Searching record "..strType.." ["..tostring(fhGetRecordId(ptrItem)).."]")
if progbar.Stop() then break end
end
end
if strType == "_PLAC" then -- Purge Place records if enabled and no links
local ptrRec = ptrItem:Clone()
ptrItem:MoveNext() -- Move pointer to next record before deletion
PurgePlac(intType,ptrRec)
else
( dicTrim[fhGetTag(ptrItem)] or null )(intType,ptrItem) -- Trim any matching text field
ptrItem:MoveNextSpecial() -- Move pointer to next item
end
end
end
progbar.Close()
end -- function TrimFields
function GetOptions(dicOption) -- Prompt User to Select Options (derived from Clean Living Persons)
local tblName = {} -- Index names for dicOption settings
local tblData = {} -- Parameter values for iup.GetParam
local tblForm = {} -- Format strings for iup.GetParam
local function paramAction(iupDialog,intIndex) -- Display Help Page (derived from Clean Living Persons)
if intIndex == iup.GETPARAM_MAP then -- Correct button labels needed for IUP 3.28 bug -- V1.1
iupDialog.Button1.Title = " Trim Fields" -- Otherwise remain as OK and Cancel
iupDialog.Button2.Title = " Cancel Plugin"
end
if intIndex == (iup.GETPARAM_HELP or -4) then -- FH V5 needs -4
fhShellExecute("https://pluginstore.family-historian.co.uk/page/help/trim-date-and-text-fields","","","open") -- V1.1
fhSleep(3000,500)
iupDialog.BringFront="YES"
end
return 1
end -- local function paramAction
local function setTables(strName,intData,strForm) -- Set those lookup tables to values below
table.insert(tblName,strName) -- 'nil' values insert nothing
table.insert(tblData,dicOption[strName] or intData) -- Use dicOption or set default value
table.insert(tblForm,strForm)
end -- local function setTables
-- Options , Data , Parameter Format for iup.GetParam(...)
setTables( "DATE" , 0 , "Select trim Date fields mode : %l| No Date field trim | Delete entire Date | Keep first/only Year | Omit Day & Month | Omit Day number |" )
setTables( "NOTE" , 0 , "Select trim Note fields mode : %l| No Note field trim | Delete entire Note | Delete [[private]] text|" )
setTables( "ADDR" , 0 , "Select trim Address fields mode : %l| No Address field trim| Delete all columns | Keep last column | Keep last 2 columns | Keep last 3 columns | Keep last 4 columns | Keep last 5 columns |" )
setTables( "PLAC" , 0 , "Select trim Place fields mode : %l| No Place field trim | Delete all columns | Keep last column | Keep last 2 columns | Keep last 3 columns | Keep last 4 columns | Keep last 5 columns |" )
setTables( "Purge" , 0 , "Purge unused Place records ? %b" )
setTables( "ResSet" , 1 , "Create Result Set of changes ? %b" )
setTables( nil , nil , "Button Names %u[ Trim Fields, Cancel Plugin, Help && Advice]" )
tblData = { iup.GetParam("Family Historian - "..strTitle,paramAction,table.concat(tblForm,"\n"),unpack(tblData)) }
if not tblData[1] then return false end
table.remove(tblData,1) -- Align data with tables above
for intName, strName in ipairs (tblName) do
dicOption[strName] = tblData[intName] -- Update options
end
return true
end -- function GetOptions
function CheckMode() -- Check for Project and Warn if in Project Mode (derived from Clean Living Persons)
if fhGetContextInfo("CI_APP_MODE") == "Project Mode" then
local strAns = fhMessageBox(
[[
Plugin Warning:
This is designed to delete data from the current file.
You have a Project open.
Please confirm you want to trim the data from
]]..fhGetContextInfo("CI_PROJECT_NAME"),
"MB_OKCANCEL",
"MB_ICONEXCLAMATION")
if strAns ~= "OK" then
return false
end
end
local strName = fhGetPluginDataFileName("LOCAL_MACHINE") -- Global sticky options file needs V5.0.8
return strName:gsub(" %- V%d.*%.dat$",".dat") -- Use same .dat file for all Plugin versions
end -- function CheckMode
function Main()
local fileOptions = CheckMode() -- Check Project/Gedcom mode and compose sticky options filename
if fileOptions then
local dicOption = LoadOptions(fileOptions) -- Load sticky options from file
if GetOptions(dicOption) then -- Get options choice from user
SaveOptions(dicOption,fileOptions) -- Save sticky options into file
TrimFields(dicOption) -- Trim fields as per options
if #arrAct > 0 then
fhOutputResultSetTitles(strTitle,strTitle) -- Output Result Set
fhOutputResultSetColumn("Action" , "text", arrAct, #arrAct, 104, "align_left", 1, true, "default", "show" )
fhOutputResultSetColumn("Record" , "text", arrRec, #arrAct, 200, "align_left")
fhOutputResultSetColumn("Buddy" , "item", arrBud, #arrAct, 200, "align_left", 0, true, "default", "buddy")
fhOutputResultSetColumn("Type" , "text", arrTyp, #arrAct, 30, "align_mid" )
fhOutputResultSetColumn("TypId","integer", arrTid, #arrAct, 30, "align_mid" , 2, true, "integer", "hide" )
fhOutputResultSetColumn("RecId","integer", arrRid, #arrAct, 30, "align_mid" , 3, true, "integer", "show" )
fhOutputResultSetColumn("Old Val", "text", arrOld, #arrAct, 200, "align_left")
fhOutputResultSetColumn("New Val", "item", arrNew, #arrAct, 200, "align_left")
else
fhMessageBox(tostring(intAct).." field trims performed.")
end
end
end
end -- function Main
--[[
@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
progbar = progbar_v3() -- To access FH progress bars module
Main() -- Invoke main function
Source:Trim-Date-and-Text-Fields-1.fh_lua