Documentation for this module may be created at Modul:DateUtils/doc

local p = {}
local maxDaysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local roman = require('Modul:Roman')
local getArgs = require('Modul:Arguments').getArgs
local defaultPostfixYear = { bc = 'î.Hr.', ad = 'd.Hr.' }
local linkingPostfixYear = { bc = 'î.Hr.', ad = 'd.Hr.' }
local months = mw.loadData('Modul:DateUtils/data').months

local suffixFormatYear = function(y, postFixYear)
	local postFixYear = postFixYear or defaultPostfixYear
	return (y < 0 and (' ' .. postFixYear.bc) or (y < 1000 and (' ' .. postFixYear.ad) or ''))
end

p.formatYear = function(y, link)
	local out = ''
	local yearLink = tostring(math.abs(y)) .. (y < 0 and suffixFormatYear(y, linkingPostfixYear) or '')
	local yearLabel = tostring(math.abs(y)) .. suffixFormatYear(y, defaultPostfixYear)
	if link then
		out = out .. '[['
		if yearLabel == yearLink then out = out .. yearLink
		else out = out .. yearLink .. '|' .. yearLabel end
		out = out .. ']]'
	else
		out = yearLabel
	end
	return out
end

p.surroundWithTimeTag = function(out, tagDate)
	local timeTag = mw.html.create('time')
	if tagDate.year > 0 then 
		local datetimeFormat = 'Y-m-d'
		if tagDate.precision == 10 then
			datetimeFormat = 'Y-m'
			tagDate.day = 1
		end
		local intermediateFormatDate =  os.date('%d %B %Y', os.time(tagDate))
		timeTag:attr('datetime', mw.language.getContentLanguage():formatDate(datetimeFormat, intermediateFormatDate))
	end
	timeTag:wikitext(out)
	
	return tostring(timeTag)
end

p.formatDate = function(indate, link, notimetag, dateFormat)
	if not indate then return nil end
	if indate.precision == 6 then
		local out = 'mileniul '
		if indate.year >= 2000 or indate.year <= -2000 then out = out .. 'al ' end
		out = out .. roman.main({tostring(1 + math.floor((math.abs(indate.year) - 1) / 1000))})
		if indate.year >= 2000 or indate.year <= -2000 then out = out .. '-lea' end
		out = out .. suffixFormatYear(indate.year)
		return out
	end
	if indate.precision == 7 then
		local out = 'secolul '
		if indate.year >= 200 or indate.year <= -200 then out = out .. 'al ' end
		out = out .. roman.main({tostring(1 + math.floor((math.abs(indate.year) - 1) / 100))})
		if indate.year >= 200 or indate.year <= -200 then out = out .. '-lea' end
		out = out .. suffixFormatYear(indate.year)
		return out
	end
	if indate.precision == 8 then
		return 'anii ' .. tostring(math.floor(math.abs(indate.year) / 10) * 10) .. suffixFormatYear(indate.year)
	end
	if indate.precision == 9 then
		if notimetag then return p.formatYear(indate.year, link) end
		local timeTag = mw.html.create('time')
		if indate.year > 0 then timeTag:attr('datetime', tostring(indate.year)) end
		timeTag:wikitext(p.formatYear(indate.year, link))
		return tostring(timeTag)
	end
	if indate.precision and indate.precision > 9  then
		local d1 = {}
		d1.day = indate.day
		if not d1.day or d1.day == 0 then d1.day = 1 end
		d1.month = indate.month
		if not d1.month or d1.month == 0 then d1.month = 1 end
		d1.year = math.abs(indate.year)
		local out = ''
		local intermediateFormatDate = os.date('%d %B %Y', os.time(d1))
		if dateFormat then
			out = out .. mw.language.getContentLanguage():formatDate(dateFormat, intermediateFormatDate)
		else
			out = out .. mw.language.getContentLanguage():formatDate((indate.precision >= 11) and (link and '[[j F]]' or 'j F') or (link and '[[F]]' or 'F'), intermediateFormatDate)
			out = out .. ' ' .. p.formatYear(indate.year, link)
		end
		
		if notimetag or indate.precision < 11 then return out end
		
		return p.surroundWithTimeTag(out, indate)
	end
end

p.formatDateFromFrame = function(frame)
	local args = getArgs(frame)
	local dateObj = {}
	if args[1] then
		dateObj = p.parseDate(args[1])
	end
	if args[2] then
		dateObj.month = tonumber(months[args[2]] or args[2])
		if args[3] then dateObj.day = tonumber(args[3]) end
	end
	dateObj.calendar = 'gregorian'
	if not dateObj.precision then
		dateObj.precision = 9
	end
	if args[2] ~= nil then
		dateObj.precision = 10
		if args[3] ~= nil then
			dateObj.precision = 11
		end
	end
	return p.formatDate(dateObj, args['link'] ~= nil, args['notimetag'] ~= nil)
end

p.isDateGregorian = function(indate)
	return indate.calendarmodel == 'http://www.wikidata.org/entity/Q1985727' or indate.calendarmodel == 'http://www.wikidata.org/entity/Q12138' or indate.calendar == 'gregorian'
end

p.isDateJulian = function(indate)
	return indate.calendarmodel == 'http://www.wikidata.org/entity/Q1985786' or indate.calendarmodel == 'http://www.wikidata.org/entity/Q11184' or indate.calendar == 'julian'
end

p.isLeapYearGregorian = function(year)
	if (year % 4 ~= 0) then return false
	elseif (year % 100 ~= 0) then return true
	elseif (year % 400 ~= 0) then return false
	end
	return true
end

p.isDateInLeapYear = function(indate)
	if p.isDateJulian(indate) then
		return 0 == indate.year % 4
	end
	return p.isLeapYearGregorian(indate.year)
end

p.addDaysToDate = function(indate, days)
	local outdate = mw.clone(indate)

	outdate.day = outdate.day + days
	local lastDayOfMonth = maxDaysInMonth[math.fmod(outdate.month-1, 12)+1]
	while outdate.day > lastDayOfMonth do
	    mw.logObject(outdate, "outdate")
		lastDayOfMonth = maxDaysInMonth[math.fmod(outdate.month-1, 12)+1]
		if outdate.month == 2 and p.isDateInLeapYear(outdate) then lastDayOfMonth = 29 end
		outdate.month = outdate.month + 1
		outdate.day = outdate.day - lastDayOfMonth
	end
	while outdate.month > 12 do
		outdate.year = outdate.year + 1
		outdate.month = outdate.month - 12
	end
	return outdate
end

p.parseCentury = function(datetext)
	if datetext and mw.ustring.len(datetext) < 9 then return nil end
	local centuryPrefixExpected = mw.ustring.sub(mw.ustring.lower(datetext), 1, 7)
	if centuryPrefixExpected == 'secolul' then
		local alLeaMatcherFunction = mw.ustring.gmatch(mw.ustring.lower(datetext), '%s+al%s+([xivlcm]+)%-lea')
		local centNum = nil
		local centStr = nil
		if alLeaMatcherFunction then
			centStr = alLeaMatcherFunction()
		end
		if not centStr then
			local nonAlLEaMatcherFunction = mw.ustring.gmatch(mw.ustring.lower(datetext), '%s+([xivlcm]+)%s*')
			if nonAlLEaMatcherFunction then
				centStr = nonAlLEaMatcherFunction()
			end
		end
		if not centStr then return nil end
		local romanTestIdx = 1
		while romanTestIdx < 30 do
			if mw.ustring.lower(roman.main({tostring(romanTestIdx)})) == mw.ustring.lower(centStr) then
				centNum = romanTestIdx
				break
			end
			romanTestIdx = romanTestIdx + 1
		end
		if not centNum then return nil end
		local bcPatterns = {}
		table.insert(bcPatterns, 'î%.e%.n%.?')
		table.insert(bcPatterns, 'î%.%s*Hr%.')
		for _,eachBCPattern in ipairs(bcPatterns) do
			local eraMatchFunction = mw.ustring.gmatch(datetext, eachBCPattern)
			if eraMatchFunction then
				local eraMatch = eraMatchFunction()
				if eraMatch and eraMatch ~= '' then
					if centNum > 0 then centNum = -centNum end
				end
			end
		end
		return centNum
	end
	return nil
end
p.parseYear = function(datetxt)
	if (not mw.ustring.gmatch(datetxt, '^[%d%sîd%.HrenADBC]+$')) then
		return nil
	end
	local yearPattern = '%d+'
	local bcPatterns = {'î%.e%.n%.?', 'î%.%s*Hr%.', 'BC'}
	
	local yearMatchFunction = mw.ustring.gmatch(datetxt, yearPattern)
	if not yearMatchFunction then return nil end
	local d = {}
	local yearMatch = yearMatchFunction()
	if not yearMatch or yearMatch == '' then return nil end
	d.year = tonumber(yearMatch)
	d.precision = 9
	for _,eachBCPattern in ipairs(bcPatterns) do
		local eraMatchFunction = mw.ustring.gmatch(datetxt, eachBCPattern)
		if eraMatchFunction then
			local eraMatch = eraMatchFunction()
			if eraMatch and eraMatch ~= '' then
				if d.year > 0 then d.year = -d.year end
			end
		end
	end
	return d
end

p.parseDate = function(datetxt)
	if not datetxt then return nil end
	
	local parsers = {}
	local stdDateParser = {
			pattern = '((%d%d%d%d)-(%d%d?)-(%d%d?))',
			patternIsMatched = function(matchArray)
				return tonumber(matchArray[2]) < 13 and tonumber(matchArray[3]) < 32
			end,
			extractDateFromText = function(matchArray)
				local d = {}
				d.day = tonumber(matchArray[3])
				d.month = tonumber(matchArray[2])
				d.year = tonumber(matchArray[1])
				d.precision = 11
				return d
			end
	}
	table.insert(parsers, stdDateParser)
	
	local noHyphensStdDateParser = {
		pattern = '((%d%d%d%d)(%d%d)(%d%d))',
		patternIsMatched = stdDateParser.patternIsMatched,
		extractDateFromText = stdDateParser.extractDateFromText
	}
	table.insert(parsers, noHyphensStdDateParser)
	
	local roDateParser = {
			pattern = '((%d+)%s+(%a+)%s+(%d+))',
			patternIsMatched = function(matchArray)
				return matchArray[1] and mw.ustring.len(mw.text.trim(matchArray[1])) > 0 and matchArray[2] and months[matchArray[2]] ~= nil
			end,
			extractDateFromText = function(matchArray)
				local d = {}
				d.day = tonumber(matchArray[1])
				d.month = tonumber(months[matchArray[2]])
				d.year = tonumber(matchArray[3])
				d.precision = 11
				return d
			end
	}
	table.insert(parsers, roDateParser)

	local slashedDateParser = {
		pattern = '((%d%d)/(%d%d)/(%d%d%d%d))',
		patternIsMatched = function(matchArray)
			return matchArray[1] and tonumber(matchArray[1]) < 32 and matchArray[2] and tonumber(matchArray[2]) < 13 
		end,
		extractDateFromText = function(matchArray)
				local d = {}
				d.day = tonumber(matchArray[1])
				d.month = tonumber(matchArray[2])
				d.year = tonumber(matchArray[3])
				d.precision = 11
				return d
			end
	}
	table.insert(parsers, slashedDateParser)
	

	local enDateParser = {
			pattern = '((%a+)%s+(%d+),%s+(%d+))',
			patternIsMatched = function(matchArray)
				return matchArray[2] and mw.ustring.len(mw.text.trim(matchArray[2])) > 0 and matchArray[1] and months[matchArray[1]] ~= nil
			end,
			extractDateFromText = function(matchArray)
				local d = {}
				d.day = tonumber(matchArray[2])
				d.month = tonumber(months[matchArray[1]])
				d.year = tonumber(matchArray[3])
				d.precision = 11
				return d
			end
	}
	table.insert(parsers, enDateParser)

	local monthOnlyParser = {	
			pattern = '((%a+)%s+(%d+))',
			patternIsMatched = function(matchArray)
				return matchArray[1] and months[matchArray[1]] ~= nil
			end,
			extractDateFromText = function(matchArray)
				local d = {}
				d.month = tonumber(months[matchArray[1]])
				d.year = tonumber(matchArray[2])
				d.precision = 10
				return d
			end
	}
	table.insert(parsers, monthOnlyParser)
	
	
	local monthOnlyNumericParser = {	
			pattern = '((%d+)-(%d+))',
			patternIsMatched = function(matchArray)
				return matchArray[1] and matchArray[2]
			end,
			extractDateFromText = function(matchArray)
				local d = {}
				d.month = tonumber(matchArray[2])
				d.year = tonumber(matchArray[1])
				d.precision = 10
				return d
			end
	}
	table.insert(parsers, monthOnlyNumericParser)
	
	for _,eachParser in ipairs(parsers) do
		local eachMatchSet = {mw.ustring.gmatch(datetxt, eachParser.pattern)()}
		if eachMatchSet[1] == datetxt then
			table.remove(eachMatchSet, 1)
			if eachParser.patternIsMatched(eachMatchSet) then
				local d = eachParser.extractDateFromText(eachMatchSet)
				if d ~= nil then
					return d
				end
			end
		end
	end
	
	local yr = p.parseYear(datetxt)
	if yr ~= nil then
		return yr
	end
	local cent = p.parseCentury(datetxt)
	if cent ~= nil then
		local d = {}
		d.year = tonumber(cent * 100)
		d.precision = 7
		return d
	end
	return nil
end

p.parseWikidataDate = function(datetxt, precision)
	if not datetxt then return nil end
	if precision == nil then precision = 11 end
	local iSOTimeSign = mw.ustring.sub(datetxt, 1, 1)
	local datePattern = '(%d+)-(%d+)-(%d+)'
	local matchesIterator = mw.ustring.gmatch(mw.ustring.sub(datetxt, 2), datePattern)
	local yearSign = 1
	--local timePattern = '(%d+):(%d+):(%d+)'
	--local matchestimeIterator = mw.ustring.gmatch(datetxt, timePattern)
	
	local yearStr, monthStr, dayStr = matchesIterator()
	if dayStr and tonumber(dayStr) == 0 then dayStr = "01" end
	if monthStr and tonumber(monthStr) == 0 then monthStr = "01" end
	if iSOTimeSign == "-" then yearSign = -1 end

	if precision >= 11 and dayStr and monthStr and yearStr then
		local d = {}
		d.day = tonumber(dayStr)
		d.month = tonumber(monthStr)
		d.year = tonumber(yearStr) * yearSign
		d.precision = precision
		return d
	end
	if precision == 10 and monthStr and yearStr then
		local d = {}
		d.day = 1 --this is a "valid 0"
		d.month = tonumber(monthStr)
		d.year = tonumber(yearStr) * yearSign
		d.precision = precision
		return d
	end
	if precision <= 9 and yearStr then
		local d = {}
		d.day = 1 --this is a "valid 0"
		d.month = 1 --this is a "valid 0"
		d.year = tonumber(yearStr) * yearSign
		d.precision = precision
		return d
	end
	return nil
end

p.compare = function(d1, d2)
	if not d1.year and d2.year then return 1 end
	if not d2.year and d1.year then return -1 end
	if not d1.month and d2.month then
		if d1.year == d2.year then
			return 1
		else 
			return d1.year - d2.year
		end
	end
	if not d2.month and d1.month then
		if d1.year == d2.year then
			return -1
		else 
			return d1.year - d2.year
		end
	end
	if not d1.day and d2.day then 
		if d1.year == d2.year then
			if d1.month == d2.month then
				return 1
			else 
				return d1.month - d2.month
			end
		else 
			return d1.year - d2.year
		end
	end
	if not d2.day and d1.day then
		if d1.year == d2.year then
			if d1.month == d2.month then
				return -1
			else 
				return d1.month - d2.month
			end
		else 
			return d1.year - d2.year
		end
	 end
	if d1.year == d2.year then
		if d1.month == d2.month then
			if d1.day == d2.day then
				return 0
			else return (d1.day - d2.day) / math.abs(d1.day - d2.day)
			end
		else return (d1.month - d2.month) / math.abs(d1.month - d2.month)
		end
	else return (d1.year - d2.year) / math.abs(d1.year - d2.year)
	end
end

p.extractDateFromWikidataSnak = function(snak)
	if snak.snaktype ~= 'value' or not snak.datavalue then return nil end
	local timestamp = snak.datavalue.value.time
	local precision = snak.datavalue.value.precision
	return p.parseWikidataDate(timestamp, precision)
end

p.toISO8601 = function(s)
	if not s then return nil end
	local d = type(s) == 'string' and p.parseDate(s) or s
	return mw.language.getContentLanguage():formatDate('Y-m-d', os.date('%d %B %Y', os.time(d)))
end

p.addDaysToDateFromFrame = function(frame)
	local args = getArgs(frame)
	local days = args.delta and tonumber(args.delta) or 1
	local today
	if args.referenceDate then
		today = p.parseDate(args.referenceDate)
	else
		today = os.date("*t")
		today.precision = 11
	end
	local tomorrow = p.addDaysToDate(today, days)
	return p.formatDate(tomorrow, false, args.notimetag ~= nil, args.dateFormat)
end

function p.daysBetween(d1, d2)
	if d1.precision < 10 then
		d1.month = 1
	end
	if d1.precision < 11 then
		d1.day = 1
	end
	if d2.precision < 10 then
		d2.month = 12
	end
	if d2.precision < 11 then
		d2.day = maxDaysInMonth[d1.month]
	end
	
	local time1 = os.time(d1)
	local time2 = os.time(d2)
	local secsdiff = os.difftime(time2, time1)
	return secsdiff / 3600 / 24
end
return p