Module:Sandbox/Ajuanca/Dates

From English Wikipedia @ Freddythechick
-- Task 9 of GCI 2019
local p = {}

-- Main function
function p.formatDates(frame)
	local inputDate = tostring(frame.args.name)
	local inputFormat=tostring(frame.args.format)
	local inputTable = p.divideString(inputDate)
	local str=""
	local myDate = p.getDate(inputTable)
	local dateResults = p.checkDate(myDate)
	if(not(dateResults.isDateCorrect))then
		return dateResults.errorMessage
	else
		local outputFormat=p.chooseFormat(myDate, inputFormat)
		local completeMonths={"January", "February", "March", "April", "May", "June", "July", "August",
			"September", "October", "November", "December"}
		if(myDate.isAprox)then
			str="circa "
		end
		if(outputFormat=="dmy")then
			str = str .. myDate.day .. " " .. completeMonths[myDate.month] .. " " .. myDate.year
		elseif(outputFormat=="mdy")then
			str = str .. completeMonths[myDate.month] .. " " .. myDate.day .. ", " .. myDate.year
		elseif(outputFormat=="iso")then
			str = str .. myDate.year .. "-" .. myDate.month .. "-" .. myDate.day
		elseif(outputFormat=="y")then
			str=str..myDate.year
		elseif(outputFormat=="my")then
			str=str.. completeMonths[myDate.month] .. " " .. myDate.year
		end
		if(myDate.specifiedEra~= nil)then
			str = str .." "..myDate.specifiedEra
		end
	end
	return str
end

-- Selects correct output format.
-- Returns a String
function p.chooseFormat(dateObject, prefference)
	local correctFormat=""
	if(dateObject.dateFormat~= nil)then
		correctFormat=dateObject.dateFormat
	else
		correctFormat="#error"
	end
	local compatibleFormats = {"dmy", "mdy", "iso", "y", "my"}
	for n, actualFormat in ipairs(compatibleFormats) do
		if (prefference:lower() == actualFormat) then
			correctFormat=prefference:lower()
			break
		end
	end
	return correctFormat
end

-- Truncates the input by spaces.
-- Returns a table
function p.divideString(stringSentence)
	local nameTable = {}
	stringSentence = string.gsub(stringSentence, "-", " ")
	stringSentence = string.gsub(stringSentence, "/", " ")
	for m in string.gmatch(stringSentence, ("%S+")) do 
		table.insert(nameTable, m)
	end
	return nameTable
end

-- Detects input date.
-- Returns a Object
function p.getDate(stringTable)
	local comparison = ""
	local finalDate = {
		day = 0,
		month = 0,
		year = 0,
		isAprox=false,
		specifiedEra=nil,
		dateFormat=nil
	}
	local dateIsStored = false
	-- Matchs string month with correspoding number.
	-- Returns number
	function getMonth(strInput)
		local months = {"jan", "feb", "mar", "apr", "may", "june", "july", "aug", "sept", 
			"oct", "nov", "dec"}
		if(strInput:find("(%a+)"))then
			for w, month in ipairs(months)do
				if(strInput:lower():find(month)~= nil)then
					return tonumber(w)
				end
			end
		end
		return 0
	end
	-- Match given ordinal number to its cardinal value.
	-- Returns a number
	function getCardinal(inputStr)
		if(inputStr:find("(%d+)(%a+)"))then
			local number, termination=inputStr:lower():match("(%d+)(%a+)")
			suffixes = {"st", "nd", "rd", "th"}
			for g, suffix in ipairs(suffixes)do
				if(termination:find(suffix))then
					return tonumber(number)
				end
			end
		end
		return 0
	end
	-- Start comparison
	for z, number in ipairs(stringTable) do
		-- Creates a string of three followed items.
		if(z<=#stringTable-2)then
			local comparison = stringTable[z] .. " " .. stringTable[z+1] .. " " .. stringTable[z+2]
			-- Search for the format 'typedMonth Day, Year'.
			if(comparison:lower():find("(%a+) (%d+), (%d+)"))then
				local m, d, y = comparison:lower():match("(%a+) (%d+), (%d+)")
				finalDate.month=tonumber(getMonth(m))
				finalDate.year=tonumber(y)
				finalDate.day=tonumber(d)
				finalDate.dateFormat="mdy"
				dateIsStored=true
			end
			-- Search for the format 'Day typedMonth Year'
			if(comparison:lower():find("(%d+) (%a+) (%d+)") and not(dateIsStored))then
				local d, m, y = comparison:lower():match("(%d+) (%a+) (%d+)")
				finalDate.month=getMonth(m:lower())
				finalDate.year=tonumber(y)
				finalDate.day=tonumber(d)
				finalDate.dateFormat="dmy"
				if(finalDate.month==0)then
					dateIsStored=false
				else
					dateIsStored=true
				end
			end
			-- Search for the format 'Day Month Year' or 'Year Month Day'
			if(comparison:lower():find("(%d+) (%d+) (%d+)") and not(dateIsStored))then
				local d, m, y= comparison:lower():match("(%d+) (%d+) (%d+)")
				-- If day and year are both less than 31, it'll be considered d-m-y
				if(tonumber(d)>31)then
					local temporalDay=y
					y=d
					d=temporalDay
				end
				finalDate.month=tonumber(m)
				finalDate.year=tonumber(y)
				finalDate.day=tonumber(d)
				-- This format isn't allowed. The most similar is ISO.
				finalDate.dateFormat="iso"
				if(finalDate.month==0)then
					dateIsStored=false
				else 
					dateIsStored=true
				end
			end
			-- Search for format 'ordinalDay typedMonth Year'
			if(comparison:lower():find("(%w+) (%a+) (%d+)") and not(dateIsStored))then
				local d, m, y=comparison:lower():match("(%w+) (%a+) (%d+)")
				finalDate.day=getCardinal(d)
				finalDate.month=getMonth(m)
				finalDate.year=y
				-- This format isn't allowed. The most similar is ISO.
				finalDate.dateFormat="iso"
				if(finalDate.month==0)then
					dateIsStored=false
				else
					dateIsStored=true
				end
			end
		end
		-- Searchs for aproximations indicators
		local aproxIndicators = {"circa", "approx", "around", "uncertain"}
		for z, indicator in ipairs(aproxIndicators)do
			if(number:lower():match(indicator))then
				finalDate.isAprox=true
			end
		end
		-- Searchs for era indicators. Note that, althought it isn't used,
		-- the value g gives us the calendar used.
		local eraIndicators={{"bce", "bc"}, {"ce", "ad"}}
		for d, era in ipairs(eraIndicators)do
			for g, specific in ipairs(era)do
				if(specific==number:lower())then
					finalDate.specifiedEra=specific:upper()
				end
			end
		end
	end
	if(not(dateIsStored))then
		for k, number in ipairs(stringTable) do
			-- Creates a string of two followed elements.
			if(k<=#stringTable-1)then
				local comparison = stringTable[k] .. " " .. stringTable[k+1]
				-- Search for a date in format 'Day typedMonth'
				if(comparison:lower():find("(%d+) (%a+)"))then
					local d, m=comparison:lower():match("(%d+) (%a+)")
					finalDate.day=tonumber(d)
					finalDate.month=tonumber(getMonth(m))
					-- This format isn't allowed. The most similar is ISO.
					finalDate.dateFormat="iso"
					if(finalDate.month==0)then
						dateIsStored=false
					else
						dateIsStored=true
					end
				-- Search for a date in format 'ordinalDay typedMonth'
				elseif(comparison:lower():find("(%w+) (%a+)"))then
					local d, m=comparison:lower():match("(%w+) (%a+)")
					finalDate.day=getCardinal(d)
					finalDate.month=getMonth(m)
					-- This format isn't allowed. The most similar is ISO.
					finalDate.dateFormat="iso"
					if(finalDate.day==0)then
						dateIsStored=false
					else
						dateIsStored=true
					end
				end
				-- Search for a date in format 'typedMonth Year'
				if(comparison:lower():find("(%a+) (%d+)"))then
					local m, y=comparison:lower():match("(%a+) (%d+)")
					finalDate.month=getMonth(m)
					finalDate.year=tonumber(y)
					finalDate.dateFormat="my"
					if(finalDate.month==0)then
						dateIsStored=false
					else
						dateIsStored=true
					end
				end
			end
		end
	end
	-- Search for a result if nothing is still found.
	if(not(dateIsStored))then
		local numbersFound = {}
		for n, number in ipairs(stringTable)do
			-- Search for possible given values in an non-regulated order. 
			if(number:find("(%a+)")~=nil)then
				if(finalDate.day==0 and number:find("(%d+)"))then
					-- Search for ordinal day
					finalDate.day=getCardinal(number)
					if(not(finalDate.day==0))then
						finalDate.dateFormat="dmy"
					end
				elseif(finalDate.month==0)then
					-- Search for months
					finalDate.month=tonumber(getMonth(number))
					if(not(finalDate.month==0))then
						finalDate.dateFormat="my"
					end
				end
			else
				table.insert(numbersFound, tonumber(number))
			end
			if(#numbersFound==1 and #stringTable==n)then
				finalDate.year=numbersFound[1]
				if((finalDate.month~=0) and (finalDate.day~=0))then
					finalDate.dateFormat="dmy"
				elseif(finalDate.month~=0)then
					finalDate.dateFormat="my"
				else
					finalDate.dateFormat="y"
				end
			elseif(#numbersFound==2 and #stringTable==n)then
				if(numbersFound[1]<=31) then
					finalDate.day=numbersFound[1]
					finalDate.year=numbersFound[2]
				else
					finalDate.year=numberFound[1]
					finalDate.day=numberFound[2]
				end
				finalDate.dateFormat="y"
			end
		end
	end
	return finalDate
end

-- Checks that given date
-- Returns a object
function p.checkDate(givenDate)
	-- Detects is the given year is leap.
	-- Returns a boolean
	function isLeap(yearInput)
		local yearIsLeap = (tonumber(yearInput)%4)==0
		if((tonumber(yearInput)%100)==0 )then
			yearIsLeap = (tonumber(yearInput)%400)==0
		end
		return yearIsLeap
	end
	local finalObject = {
		isDateCorrect=true,
		errorMessage=""
	}
	-- Check if day is correct
	if(not(givenDate.day==nil))then
		if(givenDate.day>31)then
			finalObject.isDateCorrect=false
			finalObject.errorMessage="That day doesn't exists!"
			return finalObject
		end
	end
	if(not(givenDate.month==nil))then
		if(givenDate.month==2)then
			if(isLeap(givenDate.year))then
				if(givenDate.day>29)then
					finalObject.isDateCorrect=false
					finalObject.errorMessage="That day doesn't exists!"
				end
			else 
				if(givenDate.day>=29)then
					finalObject.isDateCorrect=false
					finalObject.errorMessage="That day doesn't exists!"
				end
			end
		end
	end
	-- Check if month is correct
	if(not(givenDate.month==nil))then
		if(givenDate.month>12)then
			finalObject.isDateCorrect=false
			finalObject.errorMessage="That month doesn't exist!"
		end
	end
	-- Check if date is nil
	if(givenDate.year==0 and givenDate.day==0 and givenDate.month==0)then
		finalObject.isDateCorrect=false
		finalObject.errorMessage="No date was detected."
	end
	return finalObject
end
return p