Fix Date Fields.fh_lua--[[
@Title: Fix Date Fields
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.5
@Keywords:
@LastUpdated: 13 Feb 2026
@Licence: This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: List or Mend Date Phrases and unusual Date fields imported from other products.
@V1.5: Centre windows on FH window; Check for Updates button; Ch-ViS 1.4; progbar 3.1; Pedigree Quarter Dates conditional;
@V1.4: Add fix for BC dates; Support templated Date fields; Convert USA "mm/dd/yyyy" and similar Date Phrase to a valid Date; Check Version in Store;
@V1.3: Add RootsMagic date qualifiers including en dash & em dash hyphens; Cater for other unusual formats;
@V1.2: FH V7 Lua 3.5 IUP 3.28 compatible; progbar 3.0; Check validity of dates such as 39/02/1777, 20/13/1777, 10 Sep 1752, 10 Oct 1582;
@V1.1: Add Pedigree & PediTree Quarter Dates, extend double dates to 1927, and allow Date Phrase with valid date.
@V1.0: First Plugin Store Version.
@V0.1-0.4: Preliminary prototypes.
]]
require "iuplua"
local strEnDash = "–" -- \150 CP1252 en dash -- V1.3
local strEmDash = "—" -- \151 CP1252 en dash -- V1.3
if fhGetAppVersion() > 5 then
fhSetStringEncoding("UTF-8")
strEnDash = fhConvertANSItoUTF8(strEnDash) -- U+2013 UTF8 en dash -- V1.3
strEmDash = fhConvertANSItoUTF8(strEmDash) -- U+2014 UTF8 em dash -- V1.3
end
local strVersion = "1.5"
local strPluginName = "Fix Date Fields "..strVersion
--[[
@Module: +fh+progbar_v3
@Author: Mike Tate
@Version: 3.1
@LastUpdated: 23 Jan 2026
@Description: Progress Bar library module.
@V3.1: Use NATIVEPARENT amd CENTERPARENT.
@V3.0: Function Prototype Closure version.
@V1.0: Initial version.
]]
local function progbar_v3()
local fh = {} -- Local environment table
require "iuplua" -- To access GUI window builder
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local tblBars = {} -- Table for optional external attributes
local strBack = "255 255 255" -- Background colour default is white
local strBody = "0 0 0" -- Body text colour default is black
local strFont = nil -- Font dialogue default is current font
local strStop = "255 0 0" -- Stop button colour default is red
local intPosX = iup.CENTERPARENT -- Show window default position is central -- V3.1
local intPosY = iup.CENTERPARENT
local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
local lblText, barGauge, lblDelta, btnStop, dlgGauge
local function doFocus() -- Bring the Progress Bar window into Focus
dlgGauge.BringFront="YES" -- If used too often, inhibits other windows scroll bars, etc
end -- local function doFocus
local function doUpdate() -- Update the Progress Gauge and the Delta % with clock
barGauge.Value = intVal
lblDelta.Title = string.format("%4d %% %s ",math.floor(intPercent),strClock)
end -- local function doUpdate
local function doReset() -- Reset all dialogue variables and Update display
intVal = 0 -- Current value of Progress Bar
intPercent= 0.01 -- Percentage of progress
intStart = os.time() -- Start time of progress
intDelta = 0 -- Delta time of progress
intScale = math.ceil( intMax / 1000 ) -- Scale of percentage per second of progress (initial guess is corrected in Step function)
strClock = "00 : 00 : 00" -- Clock delta time display
isBarStop = false -- Stop button pressed signal
doUpdate()
doFocus()
end -- local function doReset
function fh.Start(strTitle,intMaximum) -- Create & start Progress Bar window
if not dlgGauge then
strTitle = strTitle or "" -- Dialogue and button title
intMax = intMaximum or 100 -- Maximun range of Progress Bar, default is 100
local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30" -- Adjust Stop button size to Title
lblText = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
barGauge = iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
lblDelta = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
btnStop = iup.button { Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
dlgGauge = iup.dialog { Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES"; -- Remove Windows minimize/maximize menu
iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
lblText;
barGauge;
lblDelta;
btnStop;
};
move_cb = function(self,x,y) tblBars.X = x tblBars.Y = y end;
close_cb = btnStop.action; -- Windows Close button = Stop button
}
if type(tblBars.GUI) == "table"
and type(tblBars.GUI.ShowDialogue) == "function" then
dlgGauge.move_cb = nil -- Use GUI library to show & move window
tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
else
if fhGetAppVersion() > 6 then -- Window centres on FH parent -- V3.1
iup.SetAttribute(dlgGauge,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
dlgGauge:showxy(intPosX,intPosY) -- Show the Progress Bar window
end
doReset() -- Reset the Progress Bar display
end
end -- function Start
function fh.Message(strText) -- Show the Progress Bar message
if dlgGauge then lblText.Title = strText end
end -- function Message
function fh.Step(intStep) -- Step the Progress Bar forward
if dlgGauge then
intVal = intVal + ( intStep or 1 ) -- Default step is 1
local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
if intPercent ~= intNew then -- Update progress once per percent or per second, whichever is smaller
intPercent = math.max( 0.1, intNew ) -- Ensure percentage is greater than zero
if intVal > intMax then intVal = intMax intPercent = 100 end -- Ensure values do not exceed maximum
intNew = os.difftime(os.time(),intStart)
if intDelta < intNew then -- Update clock of elapsed time
intDelta = intNew
intScale = math.ceil( intDelta / intPercent ) -- Scale of seconds per percentage step
local intHour = math.floor( intDelta / 3600 )
local intMins = math.floor( intDelta / 60 - intHour * 60 )
local intSecs = intDelta - intMins * 60 - intHour * 3600
strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
end
doUpdate() -- Update the Progress Bar display
end
iup.LoopStep()
end
end -- function Step
function fh.Focus() -- Bring the Progress Bar window to front
if dlgGauge then doFocus() end
end -- function Focus
function fh.Reset() -- Reset the Progress Bar display
if dlgGauge then doReset() end
end -- function Reset
function fh.Stop() -- Check if Stop button pressed
iup.LoopStep()
return isBarStop
end -- function Stop
function fh.Close() -- Close the Progress Bar window
isBarStop = false
if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
end -- function Close
function fh.Setup(tblSetup) -- Setup optional table of external attributes
if tblSetup then
tblBars = tblSetup
strBack = tblBars.Back or strBack -- Background colour
strBody = tblBars.Body or strBody -- Body text colour
strFont = tblBars.Font or strFont -- Font dialogue
strStop = tblBars.Stop or strStop -- Stop button colour
intPosX = tblBars.X or intPosX -- Window position
intPosY = tblBars.Y or intPosY
end
end -- function Setup
return fh
end -- local function progbar_v3
local progbar = progbar_v3() -- To access FH progress bars module
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.4
@LastUpdated: 20 Jan 2026
@Description: Check plugin version against version in Plugin Store
@Parameter: Plugin name and version
@Returns: None
@Requires: luacom
@V1.4: Dispense with files and assume called via IUP button;
@V1.3: Save and retrieve latest version in file;
@V1.2: Ensure the Plugin Data folder exists;
@V1.1: Monthly interval between checks; Report if Internet is inaccessible;
@V1.0: Initial version;
]]
function CheckVersionInStore(strPlugin,strVersion) -- Check if later Version available in Plugin Store
require "luacom"
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
local function intVersion(strVersion) -- Convert version string to comparable integer
local intVersion = 0
local arrNumbers = {}
strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)
for i = 1, 5 do
intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
end
return intVersion
end -- local function intVersion
local strLatest = "0"
if strPlugin then
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..tostring(strPlugin)
local isOK, strReturn = pcall(httpRequest,strRequest)
if not isOK then -- Problem with Internet access
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
elseif strReturn then
strLatest = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
end
end
local strMessage = "No later Version"
if intVersion(strLatest) > intVersion(strVersion or "0") then
strMessage = "Later Version "..strLatest
end
fhMessageBox(strMessage.." of this Plugin is available from the 'Plugin Store'.")
end -- function CheckVersionInStore
function iupButtons(strTitle,strMessage,strBoxType,...)
-- strTitle is dialogue title
-- strMessage is dialogue message
-- strBoxType is V for iup.vbox else use iup.hbox
-- {...} is list of button labels
local arg = {...}
local intButton = 0 -- Returned value if X Close button is used
local dialogue
-- Create the GUI labels and buttons
local lblMessage = iup.label{Title=strMessage;Expand="YES";Alignment="ACENTER";}
local lblLineSep = iup.label{Separator="HORIZONTAL";}
local iupBox = iup.hbox{Homogeneous="YES";}
if strBoxType == "V" then
iupBox = iup.vbox{Homogeneous="YES";}
end
for intArgNum, strButton in ipairs(arg) do
local btnName = iup.button{Title=strButton;Expand="YES";Padding="4";}
if strButton:match("update") then
function btnName:action() -- V1.5
dialogue.Active = "NO"
CheckVersionInStore( strTitle:match("^(.+) +(%d%.%d.*)$") )
dialogue.Active = "YES"
dialogue.BringFront = "YES"
end
else
function btnName:action()
intButton = intArgNum -- V1.5
return iup.CLOSE
end
end
iup.Append(iupBox,btnName)
end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
dialogue = iup.dialog{Title=strTitle;iup.vbox{lblMessage;lblLineSep;iupBox;};DialogFrame="YES";Background="250 250 250";Gap="8";Margin="8x8";}
if fhGetAppVersion() > 6 then -- Window centres on FH parent -- V1.5
iup.SetAttribute(dialogue,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
dialogue:showxy(iup.CENTERPARENT,iup.CENTERPARENT) -- V1.5
if (iup.MainLoopLevel()==0) then iup.MainLoop() end
dialogue:destroy()
return intButton
end -- function iupButtons
function FindDates() -- Loop through Date fields and List or Mend
local ptrRec = fhNewItemPtr() -- Pointer to each Record
local intRid = 0 -- Current Record Id
local ptrTag = fhNewItemPtr() -- Pointer to each child tag
local ptrDat = fhNewItemPtr() -- Pointer to each Date field
local ptrNew = fhNewItemPtr() -- Pointer to new Status or Age field
local strOld = "" -- Date field old text
local datDat = fhNewDate() -- Date field value
local intFixed = 0 -- Date mended counter
local intNoFix = 0 -- Date Phrase counter
local dicPhrase = {} -- Dictionary of recurrent Date Phrases to skip
local isCancel = false -- Cancel Plugin flag set in ReportError()
local function ReportError(strError) -- Report error message and continue or cancel Plugin
isCancel = fhMessageBox(strError,"MB_OKCANCEL","MB_ICONSTOP") == "Cancel"
end -- local function ReportError
local function StrReportInfo(ptrRec,strText) -- Report information details
return "\n\n["..fhGetRecordId(ptrRec).."] "..fhGetDisplayText(ptrRec).."\n\n"..strText
end -- function StrReportInfo
local arrRid = {} -- Result Set tables
local arrRec = {}
local arrOld = {}
local arrNew = {}
local arrEnd = {} -- V1.2
local function PutResultSet(strOld,ptrNew,strEnd) -- Make a Result Set table entry
-- strOld is old date phrase
-- ptrNew is pointer to new data
-- strEnd is end advice
if strEnd:match("NOT FIXED") then
intNoFix = intNoFix + 1 -- Count unfixed dates -- V0.4
local intRepeat = dicPhrase[strOld] or 0
dicPhrase[strOld] = intRepeat + 1 -- Only list first 10 examples of replicas -- V0.4
if intRepeat == 9 then
strEnd = strEnd.." ... more not listed" -- V1.2
elseif intRepeat > 9 then
return
end
else
intFixed = intFixed + 1 -- Count fixed dates -- V0.4
end
table.insert(arrRid,intRid) -- Parent Record Id
table.insert(arrRec,ptrRec:Clone()) -- Parent Record pointer
table.insert(arrOld,strOld) -- Old Date text
table.insert(arrNew,ptrNew:Clone()) -- New item data
table.insert(arrEnd,strEnd) -- End advice -- V1.2
end -- local function PutResultSet
local function setField(strNew,strVal) -- Create new Marriage Status or Age field
-- strNew is title of new field
-- strVal is value of new field
-- strOld and ptrDat and ptrNew inherited from above
if ptrNew:IsNotNull() then
if fhSetValueAsText(ptrNew,strVal) then -- Set the new field value
PutResultSet(strOld,ptrNew,"Used "..strNew) -- Add details to Result Set -- V1.2
else
ReportError("ERROR:\n"..strNew.." NOT set for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
else
ReportError("ERROR:\n"..strNew.." NOT created for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
end -- local function setField
local function setUnmarried() -- Fix Date Phrase "Not Married"
ptrNew:MoveToRecordItem(ptrDat)
if fhGetTag(ptrNew) == "FAM" then -- Family "Not Married" Date Phrase
ptrNew = fhCreateItem("_STAT",ptrNew,true) -- Set new Marriage Status field
setField("Marriage Status","Never Married")
return "" -- Remove Date Phrase
end
return nil -- Retain Date Phrase
end -- local function setUnmarried
local dicAge = { c="Child", i="Infant", s="Stillborn" }
local function saveAge(strAge) -- Save (Age) from Date Phrase
ptrNew:MoveToParentItem(ptrDat)
ptrNew = fhCreateItem("AGE",ptrNew,true) -- Set new Age field
setField("Age Field",dicAge[strAge] or strAge)
return "" -- Remove (Age) from Date Phrase
end -- local function saveAge
local function fixAltYrs(strPref,strYearA,strYearB) -- Fix double date years
-- strPref is prefix
-- strYearA is 1st year
-- strYearB is 2nd year
if #strYearB == 1 then strYearB = strYearA:gsub("(%d)$",strYearB) end
local intYearA = tonumber(strYearA)
local intYearB = tonumber(strYearB)
if intYearA < intYearB then
if intYearA+1 == intYearB and intYearB < 1927 then -- Gregorian year modifier for adjacent years before 1927 (Turkey) -- V1.1
return strPref:upper()..strYearA.."/"..strYearB:sub(3,4)
end
return "Btw "..strPref..strYearA.." and "..strPref..strYearB -- Otherwise create Date Range
end
return nil -- Retain Date Phrase
end -- local function fixAltYrs
local dicMonth = { " Jan "; " Feb "; " Mar "; " Apr "; " May "; " Jun "; " Jul "; " Aug "; " Sep "; " Oct "; " Nov "; " Dec "; }
local function fixUSAform(strMth,strDay,strYear) -- Fix Date Phrase "mm/dd/yyyy" etc. -- V1.4
-- strMth is month digits
-- strDay is day digits
-- strYear is year digits
if tonumber(strMth) <= 12 and tonumber(strDay) > 12 then -- Swap month and day
return strDay..dicMonth[tonumber(strMth)]..strYear
end
return strMth.."/"..strDay.."/"..strYear -- Keep original date
end -- local function fixUSAform
local arrDayNo = {31;28;31;30;31;30;31;31;30;31;30;31;} -- Last day number per month number -- V1.3
local function doValidate(dptDat) -- Validate the day, month, year numbers, etc -- V1.2
local strError = ""
if not ( dptDat:IsNull() or fhCallBuiltInFunction("DayNumber",dptDat) ) then
local intDayNo = dptDat:GetDay()
local intMonth = dptDat:GetMonth()
local intYear = dptDat:GetYear()
local strClndr = dptDat:GetCalendar() -- Gregorian, Julian, Hebrew, French
local isBC = dptDat:GetBC()
if intYear == 1582 and intMonth == 10 and intDayNo > 4 and intDayNo < 15 and strClndr == "Gregorian" and not isBC then
strError = "Date skipped 5-14 Oct 1582 !! "
end
if intYear == 1752 and intMonth == 9 and intDayNo > 2 and intDayNo < 14 and strClndr == "Gregorian" and not isBC then
strError = "Date skipped 3-13 Sep 1752 !! "
end
if intYear > 3761 and strClndr == "Hebrew" then
strError = "Hebrew year > 3761 !! "
end
if intDayNo < 0 or intDayNo > (arrDayNo[intMonth] or 31) then -- V1.3
strError = strError.."Day "..tostring(intDayNo).." Invalid!! "
end
if intMonth < 0 or intMonth > 12 then
strError = strError.."Month "..tostring(intMonth).." Invalid!! "
end
end
return strError
end -- local function doValidate
local dicMend = { -- Dictionary of Date fixes
-- Normal Date field fixes must come first --
{ '^between 00([0-3]%d) and (%d+) (%l+ %d+)$' ; 'Btw %1 %3 and %2 %3' }; -- Fix BET Day AND Day Month Year
{ '^from 00([0-3]%d) to (%d+) (%l+ %d+)$' ; 'Frm %1 %3 to %2 %3' }; -- Fix FRM Day TO Day Month Year
-- Pedigree Quarter Date fixes --
{ '^circa february (%d%d%d%d)$' ; 'Q1 %1' }; -- Fix Quarter Dates (Pedigree format) -- V1.1 -- V1.5
{ '^circa may (%d%d%d%d)$' ; 'Q2 %1' }; -- But these may also be genuine dates!
{ '^circa august (%d%d%d%d)$' ; 'Q3 %1' };
{ '^circa november (%d%d%d%d)$' ; 'Q4 %1' };
{ '^february (%d%d%d%d) %(approx%.%)$' ; 'Q1 %1' };
{ '^may (%d%d%d%d) %(approx%.%)$' ; 'Q2 %1' };
{ '^august (%d%d%d%d) %(approx%.%)$' ; 'Q3 %1' };
{ '^november (%d%d%d%d) %(approx%.%)$' ; 'Q4 %1' };
{ '^([^"]*)' ; string.upper }; -- Escape if not Date Phrase to save run time ? ?
-- Date Phrase field fixes come next, but order not too important --
{ '%(([0-9]+)%)' ; saveAge }; -- Move any Age in brackets to Age field
{ '^"(c)hil?d?"$' ; saveAge }; -- Move special Age words to Age field
{ '^"(i)nfa?n?t?"$' ; saveAge };
{ '^"(s)til?l?b?o?r?n?"$' ; saveAge };
{ '^"not married"$' ; setUnmarried }; -- Move marriage status
{ '^"(%d%d?)[ ,-/](%d%d?)[ ,-/](%d%d%d%d)"$' ; fixUSAform }; -- Fix USA mm/dd/yyyy & etc -- V1.4
{ '^""$' ; '' }; -- Eliminate blank Phrase -- V0.4
-- Mostly Ancestry/FTM Date Phrase fixes --
{ '^"bet (%d- ?%a+) and (.-) (%d[%d/]+)"$' ; 'Btw %1 %3 and %2 %3' }; -- Fix "BET Day Month AND Date"
{ '^"bet (.-%d[%d/]+) and (.-%d[%d/]+)"$' ; 'Btw %1 and %2' }; -- Fix "BET Date AND Date"
{ '^"(%d%d?) ?%- ?(%d%d?) (%a+ %d[%d/]+)"$' ; 'Frm %1 %3 to %2 %3' }; -- Fix "Day - Day Month Year"
{ '^"(.*%d[%d/]+) ?%- ?(.*%d[%d/]+)"$' ; 'Frm %1 to %2' }; -- Fix "Date - Date"
{ '^"(%d%d?) ?/ ?(%d%d?) (%a+ %d[%d/]+)"$' ; 'Btw %1 %3 and %2 %3' }; -- Fix "Day / Day Month Year"
{ '^"(.-)(%d%d%d%d)/(%d%d%d%d)"$' ; fixAltYrs }; -- Fix double date Years
{ '^"(.-)(%d%d%d%d)/(%d)"$' ; fixAltYrs }; -- V0.4
{ '^"a?b?o?u?t? q([1-4]) (%d%d%d%d*)"$' ; 'Q%1 %2' }; -- Fix Quarter Dates (PediTree format) -- V1.1
{ '^"a?f?t?e?r? q([1-4]) (%d%d%d%d*)"$' ; 'Q%1 %2' };
{ '^"b?e?f?o?r?e? q([1-4]) (%d%d%d%d*)"$' ; 'Q%1 %2' };
{ '^"pre (.*)"$' ; 'Bef %1' }; -- Fix "Pre Date" -- V0.4
{ '^"afte?r? (.*)"$' ; 'Frm %1' }; -- Fix "After Date" -- V0.4
{ '^"b?e?t?w?e?e?n? ?jan.-mar%l- (.*)"$' ; 'Q1 %1' }; -- Fix Quarter Dates (FTM format)
{ '^"b?e?t?w?e?e?n? ?apr.-jun%l- (.*)"$' ; 'Q2 %1' };
{ '^"b?e?t?w?e?e?n? ?jul.-sep%l- (.*)"$' ; 'Q3 %1' };
{ '^"b?e?t?w?e?e?n? ?oct.-dec%l- (.*)"$' ; 'Q4 %1' };
{ '^"(%d%d%d%d) 1st qu?a?r?t?e?r?"$' ; 'Q1 %1' }; -- V0.4
{ '^"(%d%d%d%d) 2nd qu?a?r?t?e?r?"$' ; 'Q2 %1' };
{ '^"(%d%d%d%d) 3rd qu?a?r?t?e?r?"$' ; 'Q3 %1' };
{ '^"(%d%d%d%d) 4th qu?a?r?t?e?r?"$' ; 'Q4 %1' };
-- Mostly RootsMagic 7 Date Phrase fixes -- -- V1.3
{ '^"by (.*)"$' ; 'Bef %1' }; -- Fix "By Date" -- V1.3
{ '^"unti?l? (.*)"$' ; 'To %1' }; -- Fix "Until Date" -- V1.3
{ '^"sinc?e? (.*)"$' ; 'Frm %1' }; -- Fix "Since Date" -- V1.3
{ '^"ca (.*)"$' ; 'C. %1' }; -- Fix "Circa Date" -- V1.3
{ '^"say? (.*)"$' ; '%1 Est' }; -- Fix "Say Date" -- V1.3
{ '^"cert?a?i?n?l?y? (.*)"$' ; ' %1 ("certainly")' }; -- Fix "Certainly Date" -- V1.3
{ '^"prob?a?b?l?y? (.*)"$' ; ' %1 ("probably")' }; -- Fix "Probably Date" -- V1.3
{ '^"poss?i?b?l?y? (.*)"$' ; ' %1 ("possibly")' }; -- Fix "Possibly Date" -- V1.3
{ '^"li?ke?ly? (.*)"$' ; ' %1 ("likely")' }; -- Fix "Likely Date" -- V1.3
{ '^"appa?r?e?n?t?l?y? (.*)"$' ; ' %1 ("apparently")' }; -- Fix "Apparently Date" -- V1.3
{ '^"pe?rha?p?s? (.*)"$' ; ' %1 ("perhaps")' }; -- Fix "Perhaps Date" -- V1.3
{ '^"mayb?e? (.*)"$' ; ' %1 ("maybe")' }; -- Fix "Maybe Date" -- V1.3
-- Mostly PAF Date Phrase fixes --
{ '^"marc?h? qu?a?r?t?e?r? (.*)"$' ; 'Q1 %1' }; -- Fix Quarter Dates (PAF format)
{ '^"june? qu?a?r?t?e?r? (.*)"$' ; 'Q2 %1' };
{ '^"sept?e?m?b?e?r? qu?a?r?t?e?r? (.*)"$' ; 'Q3 %1' };
{ '^"dece?m?b?e?r? qu?a?r?t?e?r? (.*)"$' ; 'Q4 %1' };
{ '^"(.+) b.?c.?"$' ; '%1 B.C.' }; -- Fix dates B.C. -- V1.4
{ '^"abo?u?t?%.? (.*)"$' ; 'C. %1' }; -- Fix Approximate Dates
{ '^"(.-)[\t-\r ]ci?r?c?a?%.?"$' ; 'C. %1' };
{ '^"esti?m?a?t?e?d? (.*)"$' ; '%1 Est' }; -- Fix Estimated Dates
{ '^"calcu?l?a?t?e?d? (.*)"$' ; '%1 Cal' }; -- Fix Calculated Dates
{ '^"<(.-)>"$' ; '%1 Cal' };
-- Special cases --
{ '^"(%d%d?) +(%d%d?) +(%d%d%d%d)"$' ; '%1/%2/%3' }; -- Fix dd mm yyyy -- V1.3
{ '^"(%d+).-day.-(%d+).-month.-(%d%d%d%d)"$' ; '%1/%2/%3' }; -- Fix ddth day mmth month yyyy -- V1.3
-- These catch-all fixes must be last --
{ '^"(.*%d%d%d%d) ?(.+)"$' ; ' %1 ("%2")' }; -- Fix Date prefix on other text -- V0.4
{ '^"(.*%d%d%d%d) ?"$' ; ' %1 ' }; -- Fix Date Phrase with valid date -- V1.1
}
local function doMendDate(strOrig) -- Apply dictionary of Date fixes
local strDate = strOrig:gsub('&','and'):gsub('<','<'):gsub('>','>'):gsub('[\t-\r ]+',' '):gsub(strEnDash,'-'):gsub(strEmDash,'-') -- V1.3
for intMend, arrMend in ipairs (dicMend) do
strDate = strDate:gsub(arrMend[1],arrMend[2])
if strDate:match('^[^"%l]') then -- Fix has been applied, or unfixed normal Date
if strDate:lower() ~= strOrig then -- Revalidate new changed Date -- V0.4
if datDat:SetValueAsText(strDate,true) and not datDat:IsNull() then -- V1.2
if not fhSetValueAsDate(ptrDat,datDat) then
ReportError("ERROR:\nDate NOT set for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
local strEnd = doValidate(datDat:GetDatePt1())..doValidate(datDat:GetDatePt2()) -- V1.2
PutResultSet(strOld,ptrDat,strEnd) -- Add fixed details to Result Set
else
PutResultSet(strOld,ptrDat,"NOT FIXED!!")
end
end
break
elseif #strDate == 0 then -- Delete empty Date field moved elsewhere
if not fhDeleteItem(ptrDat) then
ReportError("ERROR:\nDate Phrase NOT deleted for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
break
end
end
if strDate:match('^"') then -- Date Phrase not fixed
PutResultSet(strOld,ptrDat,"NOT FIXED!!")
end
end -- local function doMendDate
local dicList = { '^"' } -- Dictionary of Date formats to List
local function doListDate(strDate) -- Search dictionary of Date formats
for intList, strList in ipairs (dicList) do
if strDate:match(strList) then
PutResultSet(strOld,ptrDat,"") -- Add fixable details to Result Set
break
end
end
end -- local function doListDate
local arrAction = { { Name="List"; Func=doListDate; }; { Name="Mend"; Func=doMendDate; }; }
local intButton = iupButtons(strPluginName,"Ensure you have a BACKUP of your Project,\nBEFORE using the Mend Dates option.\n\nPlease select one of the following options:","V","List unusual Dates and Phrases","Mend unusual Dates and Phrases","Check for plugin updates") -- V1.5
local dicAction = arrAction[intButton]
if not dicAction then return end
local strAction = dicAction.Name
local fncAction = dicAction.Func
local intRec = 0
for intType = 1, fhGetRecordTypeCount() do -- Search each record type
ptrTag:MoveToFirstRecord(fhGetRecordTypeTag(intType))
while ptrTag:IsNotNull() do -- Count all records for ProgressBar
intRec = intRec + 1
ptrTag:MoveNext()
end
end
if intRec > 100 then progbar.Start(strAction.."ing Date Fields",intRec) end
intRec = 0
for intType = 1, fhGetRecordTypeCount() do -- Search each record type -- V1.5
local strType = fhGetRecordTypeTag(intType) -- To decide to cancel Pedigree Quarter Date fixes
ptrTag:MoveToFirstRecord(strType)
while ptrTag:IsNotNull() do -- Scan all child tags
if fhGetValueType(ptrTag) == "date" then -- Date field to be checked -- V1.5
local strDate = fhGetItemText(ptrTag,"~:LONG"):lower()
local strMnth = strDate:match('^circa (%l+) %d%d%d%d$') -- Match circa/approx month & year
or strDate:match('^(%l+) %d%d%d%d %(approx%.%)$')
if strMnth
and strMnth ~= 'february'
and strMnth ~= 'may'
and strMnth ~= 'august'
and strMnth ~= 'november' then -- Month is not a Pedigree Quarter Date
local intMend = 1
while intMend <= #dicMend do -- Purge their entries from the table
local arrMend = dicMend[intMend]
if arrMend[1]:match('%^circa %l+ %(%%d%%d%%d%%d%)%$')
or arrMend[1]:match('%^%l+ %(%%d%%d%%d%%d%) %%%(approx%%%.%%%)%$') then
table.remove(dicMend,intMend)
else
intMend = intMend + 1
end
end
intRec = intRec + 1 -- Abort the loops
break
end
end
ptrTag:MoveNextSpecial() -- Not a Date so move on
end
if intRec > 0 then break end
end
for intMend, arrMend in ipairs (dicMend) do
if not arrMend[1]:match('^%^%l+') then break end
table.insert(dicList,arrMend[1]) -- Add unusual Date patterns to Date Phrase pattern
end
for intType = 1, fhGetRecordTypeCount() do -- Search each record type
local strType = fhGetRecordTypeTag(intType)
ptrTag:MoveToFirstRecord(strType)
while ptrTag:IsNotNull() do -- Scan all child tags
if fhGetDataClass(ptrTag) == "record" then -- Obtain current record details -- V1.4
intRid = fhGetRecordId(ptrTag)
ptrRec = ptrTag:Clone()
intRec = intRec + 1
if ( intRec % 97 ) == 0 then -- Update progress bar occasionally
progbar.Message("Record "..strType.." ["..intRid.."]")
progbar.Step(97)
if progbar.Stop() then break end
end
ptrTag:MoveNextSpecial()
elseif fhGetValueType(ptrTag) == "date" then -- Date field that can be deleted -- V1.4
ptrDat = ptrTag:Clone()
ptrTag:MoveNextSpecial() -- Get next child before it's deleted
strOld = fhGetItemText(ptrDat,"~:LONG")
if #strOld == 0 then strOld = fhGetItemText(ptrDat,"~") end -- V1.4
fncAction(strOld:lower()) -- List or Mend the old Date
if isCancel then return end
else
ptrTag:MoveNextSpecial() -- Not a Date so move on
end
end
end
local strFixed = "No"
local strNoFix = "No"
local strInSet = "None"
local strButton = "Continue"
if #arrRid > 1000 then
strButton = "Continue and wait for large Result Set"
end
if strAction == "List" then
if #arrRid > 0 then -- Output List Result Set
fhOutputResultSetTitles(strPluginName.." ~ List Option")
fhOutputResultSetColumn("RecId","integer",arrRid,#arrRid, 24,"align_mid" )
fhOutputResultSetColumn("Record" ,"item",arrRec,#arrRid,100,"align_left")
fhOutputResultSetColumn("Date" ,"text",arrOld,#arrRid,180,"align_left")
fhOutputResultSetColumn("Buddy" ,"item",arrNew,#arrRid,180,"align_left",0,true,"default","buddy")
strNoFix = tostring(intNoFix)
strInSet = tostring(#arrRid)
end
progbar.Close()
if #arrRid == 0 or #arrRid > 1000 then
iupButtons(strPluginName.." ~ List Option","\n "..strNoFix.." unusual Date or Date Phrase fields detected. \n "..strInSet.." listed in the Result Set as examples. \n","V",strButton)
end
elseif strAction == "Mend" then
if #arrRid > 0 then -- Output Mend Result Set
fhOutputResultSetTitles(strPluginName.." ~ Mend Option")
fhOutputResultSetColumn("RecId","integer",arrRid,#arrRid, 24,"align_mid" )
fhOutputResultSetColumn("Record" ,"item",arrRec,#arrRid,100,"align_left")
fhOutputResultSetColumn("Old Date","text",arrOld,#arrRid,180,"align_left")
fhOutputResultSetColumn("New Data","item",arrNew,#arrRid,180,"align_left")
fhOutputResultSetColumn("Advice" ,"text",arrEnd,#arrRid,180,"align_left") -- V1.2
if intFixed > 0 then strFixed = tostring(intFixed) end
if intNoFix > 0 then strNoFix = tostring(intNoFix) end
strInSet = tostring(#arrRid-intFixed)
end
progbar.Close()
iupButtons(strPluginName.." ~ Mend Option","\n "..strFixed.." unusual Date or Date Phrase fields corrected. \n\n "..strNoFix.." Date Phrase fields remain uncorrected. \n "..strInSet.." listed in the Result Set as examples. \n","V",strButton)
end
end -- function FindDates
-- Main code starts here --
fhInitialise(5,0,8,"save_required") -- 5.0.8 for Project/User/Machine Plugin Data
FindDates() -- Invoke main function
--[[
@Title: Fix Date Fields
@Type: Standard
@Author: Mike Tate
@Contributors:
@Version: 1.5
@Keywords:
@LastUpdated: 13 Feb 2026
@Licence: This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: List or Mend Date Phrases and unusual Date fields imported from other products.
@V1.5: Centre windows on FH window; Check for Updates button; Ch-ViS 1.4; progbar 3.1; Pedigree Quarter Dates conditional;
@V1.4: Add fix for BC dates; Support templated Date fields; Convert USA "mm/dd/yyyy" and similar Date Phrase to a valid Date; Check Version in Store;
@V1.3: Add RootsMagic date qualifiers including en dash & em dash hyphens; Cater for other unusual formats;
@V1.2: FH V7 Lua 3.5 IUP 3.28 compatible; progbar 3.0; Check validity of dates such as 39/02/1777, 20/13/1777, 10 Sep 1752, 10 Oct 1582;
@V1.1: Add Pedigree & PediTree Quarter Dates, extend double dates to 1927, and allow Date Phrase with valid date.
@V1.0: First Plugin Store Version.
@V0.1-0.4: Preliminary prototypes.
]]
require "iuplua"
local strEnDash = "–" -- \150 CP1252 en dash -- V1.3
local strEmDash = "—" -- \151 CP1252 en dash -- V1.3
if fhGetAppVersion() > 5 then
fhSetStringEncoding("UTF-8")
strEnDash = fhConvertANSItoUTF8(strEnDash) -- U+2013 UTF8 en dash -- V1.3
strEmDash = fhConvertANSItoUTF8(strEmDash) -- U+2014 UTF8 em dash -- V1.3
end
local strVersion = "1.5"
local strPluginName = "Fix Date Fields "..strVersion
--[[
@Module: +fh+progbar_v3
@Author: Mike Tate
@Version: 3.1
@LastUpdated: 23 Jan 2026
@Description: Progress Bar library module.
@V3.1: Use NATIVEPARENT amd CENTERPARENT.
@V3.0: Function Prototype Closure version.
@V1.0: Initial version.
]]
local function progbar_v3()
local fh = {} -- Local environment table
require "iuplua" -- To access GUI window builder
iup.SetGlobal("CUSTOMQUITMESSAGE","YES") -- Needed for IUP 3.28
local tblBars = {} -- Table for optional external attributes
local strBack = "255 255 255" -- Background colour default is white
local strBody = "0 0 0" -- Body text colour default is black
local strFont = nil -- Font dialogue default is current font
local strStop = "255 0 0" -- Stop button colour default is red
local intPosX = iup.CENTERPARENT -- Show window default position is central -- V3.1
local intPosY = iup.CENTERPARENT
local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
local lblText, barGauge, lblDelta, btnStop, dlgGauge
local function doFocus() -- Bring the Progress Bar window into Focus
dlgGauge.BringFront="YES" -- If used too often, inhibits other windows scroll bars, etc
end -- local function doFocus
local function doUpdate() -- Update the Progress Gauge and the Delta % with clock
barGauge.Value = intVal
lblDelta.Title = string.format("%4d %% %s ",math.floor(intPercent),strClock)
end -- local function doUpdate
local function doReset() -- Reset all dialogue variables and Update display
intVal = 0 -- Current value of Progress Bar
intPercent= 0.01 -- Percentage of progress
intStart = os.time() -- Start time of progress
intDelta = 0 -- Delta time of progress
intScale = math.ceil( intMax / 1000 ) -- Scale of percentage per second of progress (initial guess is corrected in Step function)
strClock = "00 : 00 : 00" -- Clock delta time display
isBarStop = false -- Stop button pressed signal
doUpdate()
doFocus()
end -- local function doReset
function fh.Start(strTitle,intMaximum) -- Create & start Progress Bar window
if not dlgGauge then
strTitle = strTitle or "" -- Dialogue and button title
intMax = intMaximum or 100 -- Maximun range of Progress Bar, default is 100
local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30" -- Adjust Stop button size to Title
lblText = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
barGauge = iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
lblDelta = iup.label { Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
btnStop = iup.button { Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
dlgGauge = iup.dialog { Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES"; -- Remove Windows minimize/maximize menu
iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
lblText;
barGauge;
lblDelta;
btnStop;
};
move_cb = function(self,x,y) tblBars.X = x tblBars.Y = y end;
close_cb = btnStop.action; -- Windows Close button = Stop button
}
if type(tblBars.GUI) == "table"
and type(tblBars.GUI.ShowDialogue) == "function" then
dlgGauge.move_cb = nil -- Use GUI library to show & move window
tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
else
if fhGetAppVersion() > 6 then -- Window centres on FH parent -- V3.1
iup.SetAttribute(dlgGauge,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
dlgGauge:showxy(intPosX,intPosY) -- Show the Progress Bar window
end
doReset() -- Reset the Progress Bar display
end
end -- function Start
function fh.Message(strText) -- Show the Progress Bar message
if dlgGauge then lblText.Title = strText end
end -- function Message
function fh.Step(intStep) -- Step the Progress Bar forward
if dlgGauge then
intVal = intVal + ( intStep or 1 ) -- Default step is 1
local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
if intPercent ~= intNew then -- Update progress once per percent or per second, whichever is smaller
intPercent = math.max( 0.1, intNew ) -- Ensure percentage is greater than zero
if intVal > intMax then intVal = intMax intPercent = 100 end -- Ensure values do not exceed maximum
intNew = os.difftime(os.time(),intStart)
if intDelta < intNew then -- Update clock of elapsed time
intDelta = intNew
intScale = math.ceil( intDelta / intPercent ) -- Scale of seconds per percentage step
local intHour = math.floor( intDelta / 3600 )
local intMins = math.floor( intDelta / 60 - intHour * 60 )
local intSecs = intDelta - intMins * 60 - intHour * 3600
strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
end
doUpdate() -- Update the Progress Bar display
end
iup.LoopStep()
end
end -- function Step
function fh.Focus() -- Bring the Progress Bar window to front
if dlgGauge then doFocus() end
end -- function Focus
function fh.Reset() -- Reset the Progress Bar display
if dlgGauge then doReset() end
end -- function Reset
function fh.Stop() -- Check if Stop button pressed
iup.LoopStep()
return isBarStop
end -- function Stop
function fh.Close() -- Close the Progress Bar window
isBarStop = false
if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
end -- function Close
function fh.Setup(tblSetup) -- Setup optional table of external attributes
if tblSetup then
tblBars = tblSetup
strBack = tblBars.Back or strBack -- Background colour
strBody = tblBars.Body or strBody -- Body text colour
strFont = tblBars.Font or strFont -- Font dialogue
strStop = tblBars.Stop or strStop -- Stop button colour
intPosX = tblBars.X or intPosX -- Window position
intPosY = tblBars.Y or intPosY
end
end -- function Setup
return fh
end -- local function progbar_v3
local progbar = progbar_v3() -- To access FH progress bars module
--[[
@Function: CheckVersionInStore
@Author: Mike Tate
@Version: 1.4
@LastUpdated: 20 Jan 2026
@Description: Check plugin version against version in Plugin Store
@Parameter: Plugin name and version
@Returns: None
@Requires: luacom
@V1.4: Dispense with files and assume called via IUP button;
@V1.3: Save and retrieve latest version in file;
@V1.2: Ensure the Plugin Data folder exists;
@V1.1: Monthly interval between checks; Report if Internet is inaccessible;
@V1.0: Initial version;
]]
function CheckVersionInStore(strPlugin,strVersion) -- Check if later Version available in Plugin Store
require "luacom"
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
local function intVersion(strVersion) -- Convert version string to comparable integer
local intVersion = 0
local arrNumbers = {}
strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)
for i = 1, 5 do
intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
end
return intVersion
end -- local function intVersion
local strLatest = "0"
if strPlugin then
local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..tostring(strPlugin)
local isOK, strReturn = pcall(httpRequest,strRequest)
if not isOK then -- Problem with Internet access
fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
elseif strReturn then
strLatest = strReturn:match("([%d%.]*),%d*") -- Version digits & dots then comma and Id digits
end
end
local strMessage = "No later Version"
if intVersion(strLatest) > intVersion(strVersion or "0") then
strMessage = "Later Version "..strLatest
end
fhMessageBox(strMessage.." of this Plugin is available from the 'Plugin Store'.")
end -- function CheckVersionInStore
function iupButtons(strTitle,strMessage,strBoxType,...)
-- strTitle is dialogue title
-- strMessage is dialogue message
-- strBoxType is V for iup.vbox else use iup.hbox
-- {...} is list of button labels
local arg = {...}
local intButton = 0 -- Returned value if X Close button is used
local dialogue
-- Create the GUI labels and buttons
local lblMessage = iup.label{Title=strMessage;Expand="YES";Alignment="ACENTER";}
local lblLineSep = iup.label{Separator="HORIZONTAL";}
local iupBox = iup.hbox{Homogeneous="YES";}
if strBoxType == "V" then
iupBox = iup.vbox{Homogeneous="YES";}
end
for intArgNum, strButton in ipairs(arg) do
local btnName = iup.button{Title=strButton;Expand="YES";Padding="4";}
if strButton:match("update") then
function btnName:action() -- V1.5
dialogue.Active = "NO"
CheckVersionInStore( strTitle:match("^(.+) +(%d%.%d.*)$") )
dialogue.Active = "YES"
dialogue.BringFront = "YES"
end
else
function btnName:action()
intButton = intArgNum -- V1.5
return iup.CLOSE
end
end
iup.Append(iupBox,btnName)
end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
dialogue = iup.dialog{Title=strTitle;iup.vbox{lblMessage;lblLineSep;iupBox;};DialogFrame="YES";Background="250 250 250";Gap="8";Margin="8x8";}
if fhGetAppVersion() > 6 then -- Window centres on FH parent -- V1.5
iup.SetAttribute(dialogue,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
end
dialogue:showxy(iup.CENTERPARENT,iup.CENTERPARENT) -- V1.5
if (iup.MainLoopLevel()==0) then iup.MainLoop() end
dialogue:destroy()
return intButton
end -- function iupButtons
function FindDates() -- Loop through Date fields and List or Mend
local ptrRec = fhNewItemPtr() -- Pointer to each Record
local intRid = 0 -- Current Record Id
local ptrTag = fhNewItemPtr() -- Pointer to each child tag
local ptrDat = fhNewItemPtr() -- Pointer to each Date field
local ptrNew = fhNewItemPtr() -- Pointer to new Status or Age field
local strOld = "" -- Date field old text
local datDat = fhNewDate() -- Date field value
local intFixed = 0 -- Date mended counter
local intNoFix = 0 -- Date Phrase counter
local dicPhrase = {} -- Dictionary of recurrent Date Phrases to skip
local isCancel = false -- Cancel Plugin flag set in ReportError()
local function ReportError(strError) -- Report error message and continue or cancel Plugin
isCancel = fhMessageBox(strError,"MB_OKCANCEL","MB_ICONSTOP") == "Cancel"
end -- local function ReportError
local function StrReportInfo(ptrRec,strText) -- Report information details
return "\n\n["..fhGetRecordId(ptrRec).."] "..fhGetDisplayText(ptrRec).."\n\n"..strText
end -- function StrReportInfo
local arrRid = {} -- Result Set tables
local arrRec = {}
local arrOld = {}
local arrNew = {}
local arrEnd = {} -- V1.2
local function PutResultSet(strOld,ptrNew,strEnd) -- Make a Result Set table entry
-- strOld is old date phrase
-- ptrNew is pointer to new data
-- strEnd is end advice
if strEnd:match("NOT FIXED") then
intNoFix = intNoFix + 1 -- Count unfixed dates -- V0.4
local intRepeat = dicPhrase[strOld] or 0
dicPhrase[strOld] = intRepeat + 1 -- Only list first 10 examples of replicas -- V0.4
if intRepeat == 9 then
strEnd = strEnd.." ... more not listed" -- V1.2
elseif intRepeat > 9 then
return
end
else
intFixed = intFixed + 1 -- Count fixed dates -- V0.4
end
table.insert(arrRid,intRid) -- Parent Record Id
table.insert(arrRec,ptrRec:Clone()) -- Parent Record pointer
table.insert(arrOld,strOld) -- Old Date text
table.insert(arrNew,ptrNew:Clone()) -- New item data
table.insert(arrEnd,strEnd) -- End advice -- V1.2
end -- local function PutResultSet
local function setField(strNew,strVal) -- Create new Marriage Status or Age field
-- strNew is title of new field
-- strVal is value of new field
-- strOld and ptrDat and ptrNew inherited from above
if ptrNew:IsNotNull() then
if fhSetValueAsText(ptrNew,strVal) then -- Set the new field value
PutResultSet(strOld,ptrNew,"Used "..strNew) -- Add details to Result Set -- V1.2
else
ReportError("ERROR:\n"..strNew.." NOT set for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
else
ReportError("ERROR:\n"..strNew.." NOT created for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
end -- local function setField
local function setUnmarried() -- Fix Date Phrase "Not Married"
ptrNew:MoveToRecordItem(ptrDat)
if fhGetTag(ptrNew) == "FAM" then -- Family "Not Married" Date Phrase
ptrNew = fhCreateItem("_STAT",ptrNew,true) -- Set new Marriage Status field
setField("Marriage Status","Never Married")
return "" -- Remove Date Phrase
end
return nil -- Retain Date Phrase
end -- local function setUnmarried
local dicAge = { c="Child", i="Infant", s="Stillborn" }
local function saveAge(strAge) -- Save (Age) from Date Phrase
ptrNew:MoveToParentItem(ptrDat)
ptrNew = fhCreateItem("AGE",ptrNew,true) -- Set new Age field
setField("Age Field",dicAge[strAge] or strAge)
return "" -- Remove (Age) from Date Phrase
end -- local function saveAge
local function fixAltYrs(strPref,strYearA,strYearB) -- Fix double date years
-- strPref is prefix
-- strYearA is 1st year
-- strYearB is 2nd year
if #strYearB == 1 then strYearB = strYearA:gsub("(%d)$",strYearB) end
local intYearA = tonumber(strYearA)
local intYearB = tonumber(strYearB)
if intYearA < intYearB then
if intYearA+1 == intYearB and intYearB < 1927 then -- Gregorian year modifier for adjacent years before 1927 (Turkey) -- V1.1
return strPref:upper()..strYearA.."/"..strYearB:sub(3,4)
end
return "Btw "..strPref..strYearA.." and "..strPref..strYearB -- Otherwise create Date Range
end
return nil -- Retain Date Phrase
end -- local function fixAltYrs
local dicMonth = { " Jan "; " Feb "; " Mar "; " Apr "; " May "; " Jun "; " Jul "; " Aug "; " Sep "; " Oct "; " Nov "; " Dec "; }
local function fixUSAform(strMth,strDay,strYear) -- Fix Date Phrase "mm/dd/yyyy" etc. -- V1.4
-- strMth is month digits
-- strDay is day digits
-- strYear is year digits
if tonumber(strMth) <= 12 and tonumber(strDay) > 12 then -- Swap month and day
return strDay..dicMonth[tonumber(strMth)]..strYear
end
return strMth.."/"..strDay.."/"..strYear -- Keep original date
end -- local function fixUSAform
local arrDayNo = {31;28;31;30;31;30;31;31;30;31;30;31;} -- Last day number per month number -- V1.3
local function doValidate(dptDat) -- Validate the day, month, year numbers, etc -- V1.2
local strError = ""
if not ( dptDat:IsNull() or fhCallBuiltInFunction("DayNumber",dptDat) ) then
local intDayNo = dptDat:GetDay()
local intMonth = dptDat:GetMonth()
local intYear = dptDat:GetYear()
local strClndr = dptDat:GetCalendar() -- Gregorian, Julian, Hebrew, French
local isBC = dptDat:GetBC()
if intYear == 1582 and intMonth == 10 and intDayNo > 4 and intDayNo < 15 and strClndr == "Gregorian" and not isBC then
strError = "Date skipped 5-14 Oct 1582 !! "
end
if intYear == 1752 and intMonth == 9 and intDayNo > 2 and intDayNo < 14 and strClndr == "Gregorian" and not isBC then
strError = "Date skipped 3-13 Sep 1752 !! "
end
if intYear > 3761 and strClndr == "Hebrew" then
strError = "Hebrew year > 3761 !! "
end
if intDayNo < 0 or intDayNo > (arrDayNo[intMonth] or 31) then -- V1.3
strError = strError.."Day "..tostring(intDayNo).." Invalid!! "
end
if intMonth < 0 or intMonth > 12 then
strError = strError.."Month "..tostring(intMonth).." Invalid!! "
end
end
return strError
end -- local function doValidate
local dicMend = { -- Dictionary of Date fixes
-- Normal Date field fixes must come first --
{ '^between 00([0-3]%d) and (%d+) (%l+ %d+)$' ; 'Btw %1 %3 and %2 %3' }; -- Fix BET Day AND Day Month Year
{ '^from 00([0-3]%d) to (%d+) (%l+ %d+)$' ; 'Frm %1 %3 to %2 %3' }; -- Fix FRM Day TO Day Month Year
-- Pedigree Quarter Date fixes --
{ '^circa february (%d%d%d%d)$' ; 'Q1 %1' }; -- Fix Quarter Dates (Pedigree format) -- V1.1 -- V1.5
{ '^circa may (%d%d%d%d)$' ; 'Q2 %1' }; -- But these may also be genuine dates!
{ '^circa august (%d%d%d%d)$' ; 'Q3 %1' };
{ '^circa november (%d%d%d%d)$' ; 'Q4 %1' };
{ '^february (%d%d%d%d) %(approx%.%)$' ; 'Q1 %1' };
{ '^may (%d%d%d%d) %(approx%.%)$' ; 'Q2 %1' };
{ '^august (%d%d%d%d) %(approx%.%)$' ; 'Q3 %1' };
{ '^november (%d%d%d%d) %(approx%.%)$' ; 'Q4 %1' };
{ '^([^"]*)' ; string.upper }; -- Escape if not Date Phrase to save run time ? ?
-- Date Phrase field fixes come next, but order not too important --
{ '%(([0-9]+)%)' ; saveAge }; -- Move any Age in brackets to Age field
{ '^"(c)hil?d?"$' ; saveAge }; -- Move special Age words to Age field
{ '^"(i)nfa?n?t?"$' ; saveAge };
{ '^"(s)til?l?b?o?r?n?"$' ; saveAge };
{ '^"not married"$' ; setUnmarried }; -- Move marriage status
{ '^"(%d%d?)[ ,-/](%d%d?)[ ,-/](%d%d%d%d)"$' ; fixUSAform }; -- Fix USA mm/dd/yyyy & etc -- V1.4
{ '^""$' ; '' }; -- Eliminate blank Phrase -- V0.4
-- Mostly Ancestry/FTM Date Phrase fixes --
{ '^"bet (%d- ?%a+) and (.-) (%d[%d/]+)"$' ; 'Btw %1 %3 and %2 %3' }; -- Fix "BET Day Month AND Date"
{ '^"bet (.-%d[%d/]+) and (.-%d[%d/]+)"$' ; 'Btw %1 and %2' }; -- Fix "BET Date AND Date"
{ '^"(%d%d?) ?%- ?(%d%d?) (%a+ %d[%d/]+)"$' ; 'Frm %1 %3 to %2 %3' }; -- Fix "Day - Day Month Year"
{ '^"(.*%d[%d/]+) ?%- ?(.*%d[%d/]+)"$' ; 'Frm %1 to %2' }; -- Fix "Date - Date"
{ '^"(%d%d?) ?/ ?(%d%d?) (%a+ %d[%d/]+)"$' ; 'Btw %1 %3 and %2 %3' }; -- Fix "Day / Day Month Year"
{ '^"(.-)(%d%d%d%d)/(%d%d%d%d)"$' ; fixAltYrs }; -- Fix double date Years
{ '^"(.-)(%d%d%d%d)/(%d)"$' ; fixAltYrs }; -- V0.4
{ '^"a?b?o?u?t? q([1-4]) (%d%d%d%d*)"$' ; 'Q%1 %2' }; -- Fix Quarter Dates (PediTree format) -- V1.1
{ '^"a?f?t?e?r? q([1-4]) (%d%d%d%d*)"$' ; 'Q%1 %2' };
{ '^"b?e?f?o?r?e? q([1-4]) (%d%d%d%d*)"$' ; 'Q%1 %2' };
{ '^"pre (.*)"$' ; 'Bef %1' }; -- Fix "Pre Date" -- V0.4
{ '^"afte?r? (.*)"$' ; 'Frm %1' }; -- Fix "After Date" -- V0.4
{ '^"b?e?t?w?e?e?n? ?jan.-mar%l- (.*)"$' ; 'Q1 %1' }; -- Fix Quarter Dates (FTM format)
{ '^"b?e?t?w?e?e?n? ?apr.-jun%l- (.*)"$' ; 'Q2 %1' };
{ '^"b?e?t?w?e?e?n? ?jul.-sep%l- (.*)"$' ; 'Q3 %1' };
{ '^"b?e?t?w?e?e?n? ?oct.-dec%l- (.*)"$' ; 'Q4 %1' };
{ '^"(%d%d%d%d) 1st qu?a?r?t?e?r?"$' ; 'Q1 %1' }; -- V0.4
{ '^"(%d%d%d%d) 2nd qu?a?r?t?e?r?"$' ; 'Q2 %1' };
{ '^"(%d%d%d%d) 3rd qu?a?r?t?e?r?"$' ; 'Q3 %1' };
{ '^"(%d%d%d%d) 4th qu?a?r?t?e?r?"$' ; 'Q4 %1' };
-- Mostly RootsMagic 7 Date Phrase fixes -- -- V1.3
{ '^"by (.*)"$' ; 'Bef %1' }; -- Fix "By Date" -- V1.3
{ '^"unti?l? (.*)"$' ; 'To %1' }; -- Fix "Until Date" -- V1.3
{ '^"sinc?e? (.*)"$' ; 'Frm %1' }; -- Fix "Since Date" -- V1.3
{ '^"ca (.*)"$' ; 'C. %1' }; -- Fix "Circa Date" -- V1.3
{ '^"say? (.*)"$' ; '%1 Est' }; -- Fix "Say Date" -- V1.3
{ '^"cert?a?i?n?l?y? (.*)"$' ; ' %1 ("certainly")' }; -- Fix "Certainly Date" -- V1.3
{ '^"prob?a?b?l?y? (.*)"$' ; ' %1 ("probably")' }; -- Fix "Probably Date" -- V1.3
{ '^"poss?i?b?l?y? (.*)"$' ; ' %1 ("possibly")' }; -- Fix "Possibly Date" -- V1.3
{ '^"li?ke?ly? (.*)"$' ; ' %1 ("likely")' }; -- Fix "Likely Date" -- V1.3
{ '^"appa?r?e?n?t?l?y? (.*)"$' ; ' %1 ("apparently")' }; -- Fix "Apparently Date" -- V1.3
{ '^"pe?rha?p?s? (.*)"$' ; ' %1 ("perhaps")' }; -- Fix "Perhaps Date" -- V1.3
{ '^"mayb?e? (.*)"$' ; ' %1 ("maybe")' }; -- Fix "Maybe Date" -- V1.3
-- Mostly PAF Date Phrase fixes --
{ '^"marc?h? qu?a?r?t?e?r? (.*)"$' ; 'Q1 %1' }; -- Fix Quarter Dates (PAF format)
{ '^"june? qu?a?r?t?e?r? (.*)"$' ; 'Q2 %1' };
{ '^"sept?e?m?b?e?r? qu?a?r?t?e?r? (.*)"$' ; 'Q3 %1' };
{ '^"dece?m?b?e?r? qu?a?r?t?e?r? (.*)"$' ; 'Q4 %1' };
{ '^"(.+) b.?c.?"$' ; '%1 B.C.' }; -- Fix dates B.C. -- V1.4
{ '^"abo?u?t?%.? (.*)"$' ; 'C. %1' }; -- Fix Approximate Dates
{ '^"(.-)[\t-\r ]ci?r?c?a?%.?"$' ; 'C. %1' };
{ '^"esti?m?a?t?e?d? (.*)"$' ; '%1 Est' }; -- Fix Estimated Dates
{ '^"calcu?l?a?t?e?d? (.*)"$' ; '%1 Cal' }; -- Fix Calculated Dates
{ '^"<(.-)>"$' ; '%1 Cal' };
-- Special cases --
{ '^"(%d%d?) +(%d%d?) +(%d%d%d%d)"$' ; '%1/%2/%3' }; -- Fix dd mm yyyy -- V1.3
{ '^"(%d+).-day.-(%d+).-month.-(%d%d%d%d)"$' ; '%1/%2/%3' }; -- Fix ddth day mmth month yyyy -- V1.3
-- These catch-all fixes must be last --
{ '^"(.*%d%d%d%d) ?(.+)"$' ; ' %1 ("%2")' }; -- Fix Date prefix on other text -- V0.4
{ '^"(.*%d%d%d%d) ?"$' ; ' %1 ' }; -- Fix Date Phrase with valid date -- V1.1
}
local function doMendDate(strOrig) -- Apply dictionary of Date fixes
local strDate = strOrig:gsub('&','and'):gsub('<','<'):gsub('>','>'):gsub('[\t-\r ]+',' '):gsub(strEnDash,'-'):gsub(strEmDash,'-') -- V1.3
for intMend, arrMend in ipairs (dicMend) do
strDate = strDate:gsub(arrMend[1],arrMend[2])
if strDate:match('^[^"%l]') then -- Fix has been applied, or unfixed normal Date
if strDate:lower() ~= strOrig then -- Revalidate new changed Date -- V0.4
if datDat:SetValueAsText(strDate,true) and not datDat:IsNull() then -- V1.2
if not fhSetValueAsDate(ptrDat,datDat) then
ReportError("ERROR:\nDate NOT set for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
local strEnd = doValidate(datDat:GetDatePt1())..doValidate(datDat:GetDatePt2()) -- V1.2
PutResultSet(strOld,ptrDat,strEnd) -- Add fixed details to Result Set
else
PutResultSet(strOld,ptrDat,"NOT FIXED!!")
end
end
break
elseif #strDate == 0 then -- Delete empty Date field moved elsewhere
if not fhDeleteItem(ptrDat) then
ReportError("ERROR:\nDate Phrase NOT deleted for:"..StrReportInfo(ptrRec,strOld))
if isCancel then return end
end
break
end
end
if strDate:match('^"') then -- Date Phrase not fixed
PutResultSet(strOld,ptrDat,"NOT FIXED!!")
end
end -- local function doMendDate
local dicList = { '^"' } -- Dictionary of Date formats to List
local function doListDate(strDate) -- Search dictionary of Date formats
for intList, strList in ipairs (dicList) do
if strDate:match(strList) then
PutResultSet(strOld,ptrDat,"") -- Add fixable details to Result Set
break
end
end
end -- local function doListDate
local arrAction = { { Name="List"; Func=doListDate; }; { Name="Mend"; Func=doMendDate; }; }
local intButton = iupButtons(strPluginName,"Ensure you have a BACKUP of your Project,\nBEFORE using the Mend Dates option.\n\nPlease select one of the following options:","V","List unusual Dates and Phrases","Mend unusual Dates and Phrases","Check for plugin updates") -- V1.5
local dicAction = arrAction[intButton]
if not dicAction then return end
local strAction = dicAction.Name
local fncAction = dicAction.Func
local intRec = 0
for intType = 1, fhGetRecordTypeCount() do -- Search each record type
ptrTag:MoveToFirstRecord(fhGetRecordTypeTag(intType))
while ptrTag:IsNotNull() do -- Count all records for ProgressBar
intRec = intRec + 1
ptrTag:MoveNext()
end
end
if intRec > 100 then progbar.Start(strAction.."ing Date Fields",intRec) end
intRec = 0
for intType = 1, fhGetRecordTypeCount() do -- Search each record type -- V1.5
local strType = fhGetRecordTypeTag(intType) -- To decide to cancel Pedigree Quarter Date fixes
ptrTag:MoveToFirstRecord(strType)
while ptrTag:IsNotNull() do -- Scan all child tags
if fhGetValueType(ptrTag) == "date" then -- Date field to be checked -- V1.5
local strDate = fhGetItemText(ptrTag,"~:LONG"):lower()
local strMnth = strDate:match('^circa (%l+) %d%d%d%d$') -- Match circa/approx month & year
or strDate:match('^(%l+) %d%d%d%d %(approx%.%)$')
if strMnth
and strMnth ~= 'february'
and strMnth ~= 'may'
and strMnth ~= 'august'
and strMnth ~= 'november' then -- Month is not a Pedigree Quarter Date
local intMend = 1
while intMend <= #dicMend do -- Purge their entries from the table
local arrMend = dicMend[intMend]
if arrMend[1]:match('%^circa %l+ %(%%d%%d%%d%%d%)%$')
or arrMend[1]:match('%^%l+ %(%%d%%d%%d%%d%) %%%(approx%%%.%%%)%$') then
table.remove(dicMend,intMend)
else
intMend = intMend + 1
end
end
intRec = intRec + 1 -- Abort the loops
break
end
end
ptrTag:MoveNextSpecial() -- Not a Date so move on
end
if intRec > 0 then break end
end
for intMend, arrMend in ipairs (dicMend) do
if not arrMend[1]:match('^%^%l+') then break end
table.insert(dicList,arrMend[1]) -- Add unusual Date patterns to Date Phrase pattern
end
for intType = 1, fhGetRecordTypeCount() do -- Search each record type
local strType = fhGetRecordTypeTag(intType)
ptrTag:MoveToFirstRecord(strType)
while ptrTag:IsNotNull() do -- Scan all child tags
if fhGetDataClass(ptrTag) == "record" then -- Obtain current record details -- V1.4
intRid = fhGetRecordId(ptrTag)
ptrRec = ptrTag:Clone()
intRec = intRec + 1
if ( intRec % 97 ) == 0 then -- Update progress bar occasionally
progbar.Message("Record "..strType.." ["..intRid.."]")
progbar.Step(97)
if progbar.Stop() then break end
end
ptrTag:MoveNextSpecial()
elseif fhGetValueType(ptrTag) == "date" then -- Date field that can be deleted -- V1.4
ptrDat = ptrTag:Clone()
ptrTag:MoveNextSpecial() -- Get next child before it's deleted
strOld = fhGetItemText(ptrDat,"~:LONG")
if #strOld == 0 then strOld = fhGetItemText(ptrDat,"~") end -- V1.4
fncAction(strOld:lower()) -- List or Mend the old Date
if isCancel then return end
else
ptrTag:MoveNextSpecial() -- Not a Date so move on
end
end
end
local strFixed = "No"
local strNoFix = "No"
local strInSet = "None"
local strButton = "Continue"
if #arrRid > 1000 then
strButton = "Continue and wait for large Result Set"
end
if strAction == "List" then
if #arrRid > 0 then -- Output List Result Set
fhOutputResultSetTitles(strPluginName.." ~ List Option")
fhOutputResultSetColumn("RecId","integer",arrRid,#arrRid, 24,"align_mid" )
fhOutputResultSetColumn("Record" ,"item",arrRec,#arrRid,100,"align_left")
fhOutputResultSetColumn("Date" ,"text",arrOld,#arrRid,180,"align_left")
fhOutputResultSetColumn("Buddy" ,"item",arrNew,#arrRid,180,"align_left",0,true,"default","buddy")
strNoFix = tostring(intNoFix)
strInSet = tostring(#arrRid)
end
progbar.Close()
if #arrRid == 0 or #arrRid > 1000 then
iupButtons(strPluginName.." ~ List Option","\n "..strNoFix.." unusual Date or Date Phrase fields detected. \n "..strInSet.." listed in the Result Set as examples. \n","V",strButton)
end
elseif strAction == "Mend" then
if #arrRid > 0 then -- Output Mend Result Set
fhOutputResultSetTitles(strPluginName.." ~ Mend Option")
fhOutputResultSetColumn("RecId","integer",arrRid,#arrRid, 24,"align_mid" )
fhOutputResultSetColumn("Record" ,"item",arrRec,#arrRid,100,"align_left")
fhOutputResultSetColumn("Old Date","text",arrOld,#arrRid,180,"align_left")
fhOutputResultSetColumn("New Data","item",arrNew,#arrRid,180,"align_left")
fhOutputResultSetColumn("Advice" ,"text",arrEnd,#arrRid,180,"align_left") -- V1.2
if intFixed > 0 then strFixed = tostring(intFixed) end
if intNoFix > 0 then strNoFix = tostring(intNoFix) end
strInSet = tostring(#arrRid-intFixed)
end
progbar.Close()
iupButtons(strPluginName.." ~ Mend Option","\n "..strFixed.." unusual Date or Date Phrase fields corrected. \n\n "..strNoFix.." Date Phrase fields remain uncorrected. \n "..strInSet.." listed in the Result Set as examples. \n","V",strButton)
end
end -- function FindDates
-- Main code starts here --
fhInitialise(5,0,8,"save_required") -- 5.0.8 for Project/User/Machine Plugin Data
FindDates() -- Invoke main function
Source:Fix-Date-Fields-4.fh_lua