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

Source:Fix-Date-Fields-4.fh_lua