Fix Date Fields.fh_lua

--[[
@Title:			Fix Date Fields
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			1.3
@Keywords:		
@LastUpdated:	25 Mar 2021
@Licence:			This plugin is copyright (c) 2021 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.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 strPluginName = "Fix Date Fields 1.3"

--[[
@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

local progbar = progbar_v3()										-- To access FH progress bars module

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
	-- 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",
										action=function() intButton=intArgNum return iup.CLOSE end
							}
		iup.Append(iupBox,btnName)
	end
	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local dialogue = iup.dialog{Title=strTitle,iup.vbox{lblMessage,lblLineSep,iupBox},DialogFrame="YES",Background="250 250 250",Gap="8",Margin="8x8"}
	dialogue:show()
	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 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 --
		{ '^february (%d%d%d%d) %(approx%.%)$'			; 'Q1 %1' 					};	-- Fix Quarter Dates (Pedigree format) -- V1.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
		{ '^""$'												; ''							}; -- 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' 					};
		{ '^"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
	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

	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")
	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
		local strType = fhGetRecordTypeTag(intType)
		ptrTag:MoveToFirstRecord(strType)
		while ptrTag:IsNotNull() do													-- Scan all child tags
			local strClass = fhGetDataClass(ptrTag)
			if strClass == "record" then												-- Obtain current record details
				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 strClass == "date" then											-- Date field that can be deleted
				ptrDat = ptrTag:Clone()
				ptrTag:MoveNextSpecial()												-- Get next child before it's deleted
				strOld = fhGetItemText(ptrDat,"~:LONG")
				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-2.fh_lua