Documentation for this module may be created at Module:Sandbox/Takidelfin/Dates/doc

-- Task 7 for Google Code-in | Date formatting)
testList = {
	"31 august 2013",
	"31 August 2013",
	"31 August 2013 (uncertain)",
	"August 27, 2013",
	"29 February 2004 (uncertain)",
	"29 February 2005 (uncertain)",
	"27/08/2013",
	"04/27/2013",
	"2013-08-27",
	"2013 (uncertain)",
	"27",
	"27 December",
	"27 2017",
	"sometime around 27th August 2013",
	"on the 16th of December in the year of our Lord 1770",
	"on the 16th of December in the year skjdgfgjksdgjkheg hgj32g gh 11 of our Lord 1770",
	"99 red balloons",
	"sometime around 3rd August 2013",
	"31 August 103 AD",
	"31 August 2013 BC",
	"31 August 2013 BCE",
	"31 August 103 CE",
	"31 August 13 BC",
	"31 August 13",
	"31 August 213",
	"213",
	"30 BCE",
	"3 may 2017",
	"3 Jan 2017",
	"31 February 2013",
	"the quick brown fox",
	"4 and 20 blackbirds ...",
	"3 jan 9 AD"
}
possiblePatterns = {
	{ pattern = "(%d+) (%w+) (%d+)", format = "dmy" },
	{ pattern = "(%d+)st (%w+), (%d+)", format = "dmy" },
	{ pattern = "(%d+)nd (%w+), (%d+)", format = "dmy" },
	{ pattern = "(%d+)rd (%w+), (%d+)", format = "dmy" },
	{ pattern = "(%d+)th (%w+), (%d+)", format = "dmy" },
	{ pattern = "(%d+)st (%w+) (%d+)", format = "dmy" },
	{ pattern = "(%d+)nd (%w+) (%d+)", format = "dmy" },
	{ pattern = "(%d+)rd (%w+) (%d+)", format = "dmy" },
	{ pattern = "(%d+)th (%w+) (%d+)", format = "dmy" },
	{ pattern = "(%d+)th of (%w+) .+ (%d+)", format = "dmy" },
	{ pattern = "(%w+) (%d+)th (%d+)", format = "mdy" },
  { pattern = "(%w+) (%d+), (%d+)",  format = "mdy" },
	{ pattern = "(%d+)/(%d+)/(%d+)",  format = "dmy" },
	{ pattern = "(%d+)/(%d+)/(%d+)",  format = "mdy" },
	{ pattern = "(%d+)/(%d+)", format = "my" },
	{ pattern = "(%d+)-(%d+)-(%d+)", format = "ymd" },
	{ pattern = "(%d+) (%w+)", format = "ym" },
	{ pattern = "(%d%d%d%d%d)", format = "y" },
	{ pattern = "(%d%d%d%d)", format = "y" },
	{ pattern = "(%d%d%d)", format = "y" },
	{ pattern = "year (%d+)", format = "y" },
	{ pattern = "(%d+)", format = "y" }
}

local allowedFormats = {
	["dmy"] = "$D$ $M$ $y$ $E$",
	["mdy"] = "$M$ $D$ $y$ $E$",
	["iso"] = "$Y$-$m$-$d$",
	["year"] = "$y$ $E$",
	["y"] = "$y$ $E$",
	["my"] = "$M$ $y$ $E$"
}

-- First object in the array is prefix, second is a suffx
local circa = {
	{ "sometime around", "around" },
	{ "%(uncertain%)" }
}
local eraSuffix = {
	{ "AD", "CE" }, --
	{ "BC", "BCE" }
}

local monthsDays = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
local monthsNames = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }
-- local months = { ["January"] = 31, ["February"] = 28, ["March"] = 31, ["April"] = 30, ["May"] = 31, ["June"] = 30, ["July"] = 31, ["August"] = 31, ["September"]= 30, ["October"]= 31, ["November"] = 30, ["December"] = 31 }

local p = {}

isLeapYear = function ( yearRaw )
	year = tonumber(yearRaw)
	return (((year % 4 == 0) and (year % 100 ~= 0)) or (year % 400 == 0))
end

extractData = function ( dateRaw )
	msg = ""
	matched = false
	-- print ('-- ' .. dateRaw .. ' --')
	for key, value in pairs(possiblePatterns) do
		if matched ~= true then
			local date = {}
			date.circa = false
			--[[
				Expected date look:
					date.y = Integer
					date.m = Object, {
						length = Integer, <= 31
						id = Integer, <= 12
						name = String, vaild month name starting with capital letter
					}
					date.d = Integer, <= date.m.length
					date.era = String, vaild era shorthand
					date.circa = boolean
			]]

			local match = {}
			match.match1, match.match2, match.match3 = dateRaw:match( value.pattern )

			for ci, ciValue in pairs(circa) do
				for cmi,cmiValue in pairs(ciValue) do
					if ci == 1 then
						local patternDropped = value.pattern:gsub( "%)", "" ):gsub( "%(", "" )
						local patternFinal = "(" .. cmiValue .. ") " .. patternDropped
						local circaPossibleMatch = dateRaw:match( patternFinal )
						-- print(patternFinal)
						if circaPossibleMatch ~= nil then
							if string.lower( circaPossibleMatch ) == string.lower( cmiValue ) then
								date.circa = true
							end
						end
					else
						local patternDropped = value.pattern:gsub( "%)", "" ):gsub( "%(", "" )
						local patternFinal = patternDropped .. " (" .. cmiValue .. ")"
						local circaPossibleMatch = dateRaw:match( patternFinal )
						if circaPossibleMatch ~= nil then
							if string.lower( circaPossibleMatch ) == string.lower( cmiValue:gsub("%%%(", "("):gsub("%%%)", ")") ) then
								date.circa = true
							end
						end
					end
				end
				-- match.circa = dateRaw:match( "%d+ (AB)" )
			end

			for ei=1,2 do
				-- suffix or preffix
				local era = eraSuffix[ei]
				for emi, emiPattern in pairs(eraSuffix[ei]) do
					local patternDropped = value.pattern:gsub( "%)", "" ):gsub( "%(", "" )
					local patternFinal = patternDropped .. " (" .. emiPattern .. ")"
					local circaPossibleMatch = dateRaw:match( patternFinal )
					if circaPossibleMatch ~= nil then
						if string.lower( circaPossibleMatch ) == string.lower( emiPattern ) then
							date.era = emiPattern
						end
					end
				end
			end
			-- Each value.pattern (pattern) has got an value.format (date format) associated with it
			-- this loops iterates thorough them and assings variables to proper value.formats

			for i=1,string.len( value.format ) + 1 do
				date[string.sub( value.format, i, i)] = match["match" .. i]
			end

			if tonumber(date.y) ~= nil then
				date.y = tonumber(date.y)
				matched = true
			end

			if date.m ~= nil and date.m ~= "" then
				matched = false
				local month = {}
				if tonumber(date.m) ~= nil then
					if (tonumber( date.m ) or 13) <= 12 then
						month.name = monthsNames[tonumber( date.m )]
						month.length = monthsDays[tonumber( date.m )]
						month.id = tonumber( date.m )
						matched = true
					else
						if value.format == 'mdy' then
							-- print('INVALID ENTRY\n\n')
							return "Invalid entry"
						end
					end
				elseif #date.m > 1 then
					for mi=1,#monthsNames do
						local monthPrefixPossible = string.upper( string.sub( monthsNames[mi], 1, 3 ) )
						local monthPrefix = string.upper( string.sub( date.m, 1, 3 ) )
						if monthPrefix == monthPrefixPossible then
							-- print(monthPrefix, monthPrefixPossible)
							month.name = monthsNames[mi]
							month.length = monthsDays[mi]
							month.id = mi
							matched = true
						end
					end
				else
					date.m = nil
				end

				if month.name ~= nil and month.length ~= nil then
					matched = true
					date.m = month
					if date.d ~= nil and date.d ~= "" then
						matched = false
						date.d = tonumber( date.d )
						if date.m.length == 29 and date.y ~= nil then
							if isLeapYear(date.y) == true and date.d <= 29 then
								matched = true
							else
								-- print('INVALID ENTRY\n\n')
								matched = true
								return "Invalid entry"
							end
						elseif not (date.d <= month.length) then
							date.d = nil
						else
							matched = true
						end
					end
				end
			end
			if matched == true then
				-- local dateMsg = ""
				-- if date.y then
					-- dateMsg = dateMsg .. date.y
				-- end
				-- if date.m then
				-- 	dateMsg = dateMsg .. " " ..  date.m.name
				-- end
				-- if date.d then
				-- 	dateMsg = dateMsg .. " " ..  date.d
				-- end
				-- if date.circa then
				-- 	dateMsg = "circa " .. dateMsg
				-- end
				-- if date.era then
				-- 	dateMsg = dateMsg .. " " .. date.era
				-- end
				-- print( 'FORMAT: ' .. value.format )
				-- print( 'PATTERN: ' .. value.pattern )
				-- print( 'CIRCA: ' .. tostring(date.circa) )
				-- print( 'ERA: ' .. tostring(date.era) )
				-- print( '--- Result ---' )
				-- print(dateMsg)
				-- print('\n\n')
				return date
			end
		end
	end
end

formatDate = function ( date, formatRaw )
	if (date == nil) or (date == 'Invalid entry') then
		return 'Invalid entry'
	elseif formatRaw ~= nil then
		local format = allowedFormats[formatRaw] or 'invalidFormat'
		if format == 'invalidFormat' then
			local returnValue = ""
			if date.d then
				returnValue = returnValue .. date.d .. " "
			end
			if date.m then
				returnValue = returnValue .. date.m.name .. " "
			end
			if date.y then
				returnValue = returnValue .. date.y .. " "
			end
			if date.circa then
				returnValue = "circa " .. returnValue
			end
			if date.era then
				returnValue = returnValue .. date.era .. " "
			end
			return returnValue
		end

		local finalFormat = format
		local formatIterator = format:gsub("%$", "")
		for l=1,#formatIterator do
			local char = formatIterator:sub(l,l)
			-- Day
			if char == "d" then
				if date.d == nil then
					if formatRaw == "iso" then return "Invalid entry" end
					finalFormat = finalFormat:gsub("%$d%$", "")
				elseif #tostring(date.d) < 2 then
					finalFormat = finalFormat:gsub("%$d%$", 0 .. date.d)
				else
					finalFormat = finalFormat:gsub("%$d%$", date.d)
				end
			elseif char == "D" then
				if date.d == nil then
					if formatRaw == "iso" then return "Invalid entry" end
					finalFormat = finalFormat:gsub("%$D%$", "")
				else
					finalFormat = finalFormat:gsub("%$D%$", date.d)
				end
			end
			-- Year
			if char == "y" then
				if date.y == nil then
					finalFormat = finalFormat:gsub("%$y%$", "")
				else
					finalFormat = finalFormat:gsub("%$y%$", date.y)
				end
			elseif char == "Y" then
				if date.y == nil then
					if formatRaw == "iso" then return "Invalid entry" end
					finalFormat = finalFormat:gsub("%$Y%$", "")
				elseif 0 <= date.y and date.y <= 9999 then
					local zero = "0"
					finalFormat = finalFormat:gsub("%$Y%$", zero:rep(4 - #tostring(date.y)) .. date.y)
				else
					return "Invalid entry"
				end
			end
			-- Era
			if char == "E" then
				if date.era == nil then
					if formatRaw == "iso" then
						return "Invalid entry"
					end
					finalFormat = finalFormat:gsub("%$E%$", "")
				else
					finalFormat = finalFormat:gsub("%$E%$", date.era)
				end
			end
			-- Month
			if char == "m" then
				if date.m == nil then
					if formatRaw == "iso" then return "Invalid entry" end
					finalFormat = finalFormat:gsub("%$m%$", "")
				else
					local zero = "0"
					finalFormat = finalFormat:gsub("%$m%$", zero:rep(2 - #tostring(date.m.id)) .. date.m.id)
				end
			elseif char == "M" then
				if date.m == nil then
					if formatRaw == "iso" then return "Invalid entry" end
					finalFormat = finalFormat:gsub("%$M%$", "")
				else
					finalFormat = finalFormat:gsub("%$M%$", date.m.name)
				end
			end
		end
		-- Circa
		if date.circa then
			if formatRaw == "iso" then
				return "ISO format does not allow uncertain dates"
			end
			finalFormat = "circa " .. finalFormat
		end

		if date.era and formatRaw == "iso" then
			return "ISO format does not allow embedding era into date"
		end

		finalFormat = finalFormat:gsub("^%s*(.-)%s*$", "%1")
		finalFormat = finalFormat:gsub("%s+", " ")
		return finalFormat
	else
		local returnValue = ""
		if date.d then
			returnValue = returnValue .. date.d .. " "
		end
		if date.m then
			returnValue = returnValue .. date.m.name .. " "
		end
		if date.y then
			returnValue = returnValue .. date.y .. " "
		end
		if date.circa then
			returnValue = "circa " .. returnValue
		end
		if date.era then
			returnValue = returnValue .. date.era .. " "
		end
		return returnValue
	end
end

p.convertTest = function ( frame )
  local msg = ""
  for i=1, #testList do
		local testDate = testList[i]
		msg = msg .. "<br/> <ul>Raw date: " .. testDate
		for key,value in pairs(allowedFormats) do
			msg = msg .. "<li> <b>Format:</b> " ..  key .. " | <b>Result:</b> " .. formatDate( extractData( testDate ), key ) .. "</li>"
		end
		msg = msg .. "</ul>"
  end
	return msg
end

p.convertDate = function ( frame )
  local date = frame.args.date
	local format = frame.args.format
	if date == nil or date == "" then return "Invalid entry" end
	return formatDate( extractData( date ), format )
end

return p