__ __ _ _ ____ _ ___ _ ___ 
 
This module is intended for processing of date strings.

Please do not modify this code without applying the changes first at Module:Date/sandbox and testing 
at Module:Date/sandbox/testcases and Module talk:Date/sandbox/testcases.

Authors and maintainers:
]]
require('strict')

-- ==================================================
-- === Internal functions ===========================
-- ==================================================

-- Function allowing for consistent treatment of boolean-like wikitext input.
-- It works similarly to Module:Yesno
local function yesno
	if type(val) == 'boolean' then
		return val
	elseif type(val) == 'number' then
		if val==1 then 
			return true
		elseif val==0 then
			return false
		end
	elseif type(val) == 'string' then
	 val = mw.ustring.lower(val) -- put in lower case
	 if val == 'no' or val == 'n' or val == 'false' or tonumber(val) == 0 then
	 return false
	 elseif val == 'yes' or val == 'y' or val == 'true' or tonumber(val) == 1 then
	 return true
	 end
 end
 return default
end

-- String replacement that ignores part of the string in "..."
local function strReplace
	if String:find('"') then
		local T={}
		for i, str in ipairs(mw.text.split) do
			if i%2==1 then
				str = str:gsub
			end
			table.insert
		end
		return table.concat
	else
		return String:gsub
	end
end

-- process datevec
-- INPUT:
-- * datevec - Array of {year,month,day,hour,minute,second, tzhour, tzmin} containing broken 
-- down date-time component strings or numbers
-- OUTPUT:
-- * datecode - a code specifying content of the array where Y' is year, 'M' is month, 
-- 'D' is day, 'H' is hour, 'M' minute, 'S' is second. output has to be one of YMDHMS, YMDHM, YMD, YM, MD, Y
-- * datenum - same array but holding only numbers or nuls
local function parserDatevec(datevec)
	-- if month is not a number than check if it is a month name in project's language
	local month = datevec[2]
	if month and month~='' and not tonumber(month) then
		datevec[2] = mw.getContentLanguage:formatDate
	end

	-- create datecode based on which variables are provided and check for out-of-bound values
	local maxval = {nil, 12, 31, 23, 59, 59, 23, 59} -- max values for year, month, ...
	local minval = {nil, 1, 1, 0, 0, 0, -23, 0} -- min values for year, month, ...
	local c = {'Y', 'M', 'D', 'H', 'M', 'S', '', ''}
	local datecode = '' -- a string signifying which combination of variables was provided
	local datenum = {} -- date-time encoded as a vector = [year, month, ... , second]
	for i = 1,8 do
		datenum[i] = tonumber(datevec[i])
		if datenum[i] and (i==1 or (datenum[i]>=minval[i] and datenum[i]<=maxval[i])) then
			datecode = datecode .. c[i]
		end
	end
	return datecode, datenum
end
	
-- process datevec
-- INPUT:
-- * datecode - a code specifying content of the array where Y' is year, 'M' is month, 
-- 'D' is day, 'H' is hour, 'M' minute, 'S' is second. output has to be one of YMDHMS, YMDHM, YMD, YM, MD, Y
-- * datenum - Array of {year,month,day,hour,minute,second, tzhour, tzmin} as numbers or nuls
-- OUTPUT:
-- * timeStamp - date string in the format taken by mw.language:formatDate lua function and perser function
-- https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate
-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
-- * datecode - with possible corrections
local function getTimestamp
	-- create time stamp string (for example 2000-02-20 02:20:20) based on which variables were provided
	local timeStamp
	if datecode == 'YMDHMS' then
		timeStamp = string.format
	elseif datecode == 'YMDHM' then
		timeStamp = string.format
	elseif datecode:sub=='YMD' then
		timeStamp = string.format
		datecode = 'YMD' -- 'YMD', 'YMDHMS' and 'YMDHM' are the only supported format starting with 'YMD'. All others will be converted to 'YMD'
	elseif datecode == 'YM' then
		timeStamp = string.format
	elseif datecode:sub=='Y' then
		timeStamp = string.format
		datecode = 'Y' 
	elseif datecode == 'M' then
		timeStamp = string.format
	elseif datecode == 'MD' then
		timeStamp = string.format
	else
		timeStamp = nil -- format not supported
	end
	return timeStamp, datecode
end

-- trim leading zeros in years prior to year 1000
-- INPUT:
-- * datestr - translated date string 
-- * lang - language of translation
-- OUTPUT:
-- * datestr - updated date string 

local function trimYear
	local yearStr0, yearStr1, yearStr2, zeroStr
	yearStr0 = string.format -- 4 digit year in standard form "0123"
	yearStr1 = mw.language.new(lang):formatDate -- same as calling parser function
	--yearStr1 = mw.getCurrentFrame:callParserFunction -- translate to a language 
	if yearStr0==yearStr1 then -- most of languages use standard form of year 
		yearStr2 = tostring(year)
	else -- some languages use different characters for numbers
		yearStr2 = yearStr1
		zeroStr = mw.ustring.sub -- get "0" in whatever language
		for i=1,3 do -- trim leading zeros
			if mw.ustring.sub==zeroStr then
				yearStr2 = mw.ustring.sub
			else
				break
			end
		end
	end
	return string.gsub -- in datestr replace long year with trimmed one
end

-- Look up proper format string to be passed to parser function
-- INPUTS:
-- * datecode: YMDHMS, YMDHM, YMD, YM, MD, Y, or M
-- * day : Number between 1 and 31 (not needed for most languages)
-- * lang : language
-- OUTPUT:
-- * dFormat : input to function
local function getDateFormat
	local function parseFormat
		if dFormat:find('default') and #dFormat>10 then
			-- special (and messy) case of dFormat code depending on a day number
			-- then json contains a string with more json containing "default" field and 2 digit day keys
			-- if desired day is not in that json than use "default" case
			dFormat = dFormat:gsub -- change fancy double quote to a straight one, used for json marking
			local D = mw.text.jsonDecode( dFormat )		--com = mw.dumpObject(D)
			day = string.format -- create day key
			dFormat = D[day] or D.default
			dFormat = dFormat:gsub -- change single quote to a double quote, used for marking
		end
		return dFormat
	end
	
	local T = {}
	local tab = mw.ext.data.get
	for _, row in pairs(tab.data) do -- convert the output into a dictionary table
		local id, _, msg = unpack(row)
		T[id] = msg
	end
	local dFormat = T[datecode]
	if dFormat=='default' and (datecode=='YMDHMS' or datecode=='YMDHM') then 
		-- for most languages adding hour:minute:second is done by adding ", HH:MM:SS to the 
		-- day precission date, those languages are skipped in DateI18n.tab and default to 
		-- English which stores word "default"
		dFormat = parseFormat.. ', H:i'
		if datecode=='YMDHMS' then
			dFormat = dFormat .. ':s'
		end
	else
		dFormat = parseFormat
	end
	return dFormat
end

-- Look up proper format string to be passed to parser function
-- INPUTS:
-- * month : month number
-- * case : gramatic case abbriviation, like "ins", "loc"
-- * lang : language
-- OUTPUT:
-- * dFormat : input to function
local function MonthCase
	local T = {{},{},{},{},{},{},{},{},{},{},{},{}}
	local tab = mw.ext.data.get
	for _, row in pairs(tab.data) do
		local mth, cs, msg = unpack(row)
		T[mth][cs] = msg
	end
	return T[month][case]
end

-- ==================================================
-- === External functions ===========================
-- ==================================================
local p = {}

-- ========================================================================================
Date
 
This function is the core part of the ISOdate template. 
 
Usage:
 local Date = require('Module:DateI18n')._Date
 local dateStr = Date
 
Parameters:
		tzhour, tzmin are timezone offsets from UTC, hours and minutes

function p._Date	
	-- make sure inputs are in the right format
	if not lang or not mw.language.isValidCode( lang ) then
		lang = mw.getCurrentFrame:callParserFunction -- get user's chosen language
	end
	if lang == 'be-tarsk' then
		lang = 'be-x-old'
	end
	
	-- process datevec and extract timeStamp and datecode strings as well as numeric datenum array
	local datecode, datenum = parserDatevec(datevec)
	local year, month, day = datenum[1], datenum[2], datenum[3]
	local timeStamp, datecode = getTimestamp
	if not timeStamp then -- something went wrong in parserDatevec
		return ''
	end
	-- Commons Data:DateI18n.tab page stores prefered formats for diferent 
	-- languages and datecodes 
	-- Look up country specific format input to function
	local dFormat = getDateFormat

	-- By default the gramatical case is not specified (case=='') allowing the format to be specified 
	-- in Data:DateI18n.tab. You can overwrite the default grammatical case of the month by 
	-- specifying "case" variable. This is needed mostly by Slavic languages to create more complex 
	-- phrases as it is done in c:Module:Complex date
	case = case or ''
	if (lang=='qu' or lang=='qug') and (case=='nom') then
		-- Special case related to Quechua and Kichwa languages. The form in the I18n is
		-- Genitive case with suffix "pi" added to month names provided by {#time}}
		-- in Nominative case that "pi" should be removed
		-- see https://commons.wikimedia.org/wiki/Template_talk:Date#Quechua from 2014
		dFormat = dFormat:gsub
	elseif (case=='gen') then
		dFormat = strReplace
	elseif (case=='nom') then
		dFormat = strReplace
	elseif (case ~= '') then
		-- see is page Data:I18n/MonthCases.tab on Commons have name of the month 
		-- in specific gramatic case in desired language. If we have it than replace 
		-- "F" and xg" in dFormat
		local monthMsg = MonthCase
		if monthMsg and monthMsg ~= '' then -- make sure it exists
			dFormat = strReplace -- replace default month with month name we already looked up
			dFormat = strReplace
		end
	end

 -- Translate the date using specified format
	-- See https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate and 
	-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time for explanation of the format
	local datestr = mw.language.new(lang):formatDate -- same as using parser function
	
	-- Special case related to Thai solar calendar: prior to 1940 new-year was at different 
	-- time of year, so just year (datecode=='Y') is ambiguous and is replaced by "YYYY or YYYY" phrase
	if lang=='th' and datecode=='Y' and year<=1940 then
		datestr = string.format 
	end
	
	-- If year<1000 than either keep the date padded to the length of 4 digits or trim it
	-- decide if the year will stay padded with zeros (for years in 0-999 range)
	if year and year<1000 then
		if type(trim_year)=='nil' then 
			trim_year = '100-999'
		end
		local trim = yesno -- convert to boolean
		if trim==nil and type(trim_year)=='string' then
			-- if "trim_year" not a simple True/False than it is range of dates
			-- for example '100-999' means to pad one and 2 digit years to be 4 digit long, while keeping 3 digit years as is
			local YMin, YMax = trim_year:match( '(%d+)-(%d+)' )
			trim = (YMin~=nil and year>=tonumber(YMin) and year<=tonumber(YMax)) 
		end
		if trim==true then
			datestr = trimYear -- in datestr replace long year with trimmed one
		end
	end

	-- append timezone if present
	if datenum[7] and (datecode == 'YMDHMS' or datecode == 'YMDHM') then
		-- use parser function to create timezone string, so that we use correct character set
		local sign = (datenum[7]<0) and '−' or '+'
		timeStamp = string.format("2000-01-01 %02i:%02i:00", math.abs(datenum[7]), datenum[8] or 0)
		local timezone = mw.language.new(lang):formatDate -- same as using parser function
		datestr = string.format
	end

	-- html formating and tagging of date string
	if class and class ~= '' and datecode~='M' and datecode~='MD'then
		local DateHtmlTags = ''
		datestr = DateHtmlTags:format
	end
	return datestr
end

-- ========================================================================================
Date
 
This function is the core part of the ISOdate template. 
 
Usage:
 
Parameters:

function p.Date(frame)
	local args = {}
	for name, value in pairs( frame.args ) do 
		name = string.gsub( string.lower(name), ' ', '_')
		args[name] = value
	end
	return p._Date	
end