Module:Complex date

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
CodeDiscussionEditHistoryNo test API DocumentationSubpagesLinksTestsResultsSandbox All modules

This module is intended for processing of date strings. It is used by:

Complex date

This function is the engine behind {{Complex date}}. Please see that template for full documentation


should not be used directly but only through {{Other date}} and other templates



Parameters (simple syntax):

option. See {{Other date}}
date #1
date #2

Parameters (advanced syntax):

in case the link between the two dates: "-" (from-until), between, or, and (see {{Complex date}})
adj1, adj2
adjectives and prepositions used to describe single date. Possible values: early, mid, late, spring, summer, fall, winter, 1st half, 2nd half, 1st quarter, 2nd quarter, 3rd quarter, 4th quarter, etc. used for describing which part of date1 and date2 are involved (see {{Complex date}})
date1, date2
the dates involved. Format: Number or whatever {{ISOdate}} takes.
precision1, precision1
usually year or more precise (default), but can also be decade, century or millennium, then the dates involved are centuries or millennia rather than specific dates
era1, era2
the eras that those dates are from (see {{Complex date}}) If parameter era is present, it overrides these two parameters.

The internationalization of the date formats can be found at Module:I18n/complex date

See Also


This module is intended for creation of complex date phrases in variety of languages.
Once deployed, please do not modify this code without applying the changes first at Module:Complex date/sandbox and testing 
at Module:Complex date/sandbox/testcases.
Authors and maintainers:
* User:Sn1per - first draft of the original version 
* User:Jarekt - corrections and expansion of the original version 

-- List of external modules and functions
local p = {Error = nil}
local i18n       = require('Module:i18n/complex date')
local ISOdate    = require('Module:ISOdate')._ISOdate
local Date       = require('Module:Date')._Date
local formatnum  = require('Module:Formatnum').formatNum
local linguistic = require('Module:Linguistic')
local Calendar   = require('Module:Calendar')

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

local function langSwitch(list,lang)
	local langList = mw.language.getFallbacksFor(lang)
	for i,language in ipairs(langList) do
		if list[language] then
			return list[language]

local function formatnum1(numStr, lang)
-- mostly require('Module:Formatnum').formatNum function used to translate a number to use different numeral characters, 
-- except that it it does not call  that function unless the language is on the list "LList"
	local LList = {bn=1,bpy=1,kn=1,hi=1,mr=1,new=1,pa=1,gu=1,fa=1,glk=1,mzn=1,ur=1,ar=1,ckb=1,ks=1,lo=1,['or']=1,bo=1,['ml-old']=1,mn=1,te=1,th=1}
	if LList[lang] then -- call only when the language is on the list
		numStr = formatnum(numStr, lang, 1)
	return numStr

local function getISODate(datestr, datetype, lang, num, case)
-- translate dates in the format YYYY, YYYY-MM, and YYYY-MM-DD
	if  not case and i18n.Translations[datetype] then
		-- look up the grammatical case needed and call ISOdate module
		local rec = langSwitch(i18n.Translations[datetype], lang)
		if type(rec)=='table' then
			case =[num]
	return ISOdate(datestr, lang, case, '', 1)

local function translatePhrase(date1, date2, operation, lang, state)
-- use tables in Module:i18n/complex date to translate a phrase
	if not i18n.Translations[operation] then
		p.Error = string.format('<span style="background-color:red;">Error in [[Module:Complex date]]: input parameter "%s" is not recognized.</span>', operation or 'nil')
		return ''
	local dateStr = langSwitch(i18n.Translations[operation], lang)
	if type(dateStr)=='table' then
		dateStr = dateStr[1]
	if type(dateStr)=='function' then
		local success
		local nDates = i18n.Translations[operation]['nDates']
		if nDates==2 then -- 2 date phrase
			success, dateStr = pcall(dateStr, date1, date2, state)
		else  -- 1 date phrase
			success, dateStr = pcall(dateStr, date1, state)
	if type(dateStr)=='string' then
		-- replace parts of the string '$date1' and '$date2' with date1 and date2 strings
		dateStr = mw.ustring.gsub(dateStr, '$date1', date1)
		dateStr = mw.ustring.gsub(dateStr, '$date2', date2)
		-- Special case of more complex phrases that can be build out of simple phrases
		-- If complex case is not translated to "lang" than build it out of simpler ones
		local x = dateStr
		dateStr = p._complex_date(x.conj, x.adj1, date1, x.units1, x.era1, x.adj2, date2, x.units2, x.era2, lang, 2)
	return dateStr

local function oneDatePhrase(dateStr, adj, era, units, lang, num, case, state)
-- translate a single date phrase
	if num==2 then
		state.adj, state.era, state.units, state.precision = state.adj2, state.era2, state.units2, state.precision2 
	-- dateStr can have many forms: ISO date, year or a number for 
	-- decade, century or millennium
	if units == '' then -- unit is "year", "month", "day"
		dateStr = getISODate(dateStr, adj, lang, num, case)
	else -- units is "decade", "century", "millennium''
		dateStr = translatePhrase(dateStr, '', units, lang, state)
	-- add adjective ("early", "mid", etc.) or preposition ("before", "after", 
	-- "circa", etc.) to the date
	if adj ~= '' then
		dateStr = translatePhrase(dateStr, '', adj, lang, state)
	else -- only era?
		dateStr = formatnum1(dateStr, lang)
	-- add era
	if era ~= '' then
		dateStr = translatePhrase(dateStr, '', era, lang, state)
	return dateStr

local function twoDatePhrase(date1, date2, state, lang)
-- translate a double date phrase
	local dateStr, case
	local era=''
	if state.era1 == state.era2 then
		-- if both eras are the same than add it only once
		era = state.era1
		state.era1 = ''
		state.era2 = ''
	case = {nil, nil}
	if i18n.Translations[state.conj] then
		local rec = langSwitch(i18n.Translations[state.conj], lang)
		if type(rec)=='table' then
			case =
	date1   = oneDatePhrase(date1, state.adj1, state.era1, state.units1, lang, 1, case[1], state)
	date2   = oneDatePhrase(date2, state.adj2, state.era2, state.units2, lang, 2, case[2], state)
	dateStr = translatePhrase(date1, date2, state.conj, lang, state)
	if era ~= '' then
		dateStr = translatePhrase(dateStr, '', era, lang, state)
	return dateStr

local function otherPhrases(date1, date2, operation, era, lang, state)
-- translate specialized phrases
	local dateStr = ''
	if operation == 'islamic' then
		if date2=='' then date2 = mw.getCurrentFrame():callParserFunction('#time', 'xmY', date1) end
		date1 = getISODate(date1, operation, lang, 1, nil)
		date2 = getISODate(date2, operation, lang, 2, nil)
		if era == '' then era = 'ad' end
		dateStr = translatePhrase(date1, '', era, lang, state) .. ' (' .. translatePhrase(date2, '', 'ah', lang, state) .. ')'
		era = ''
	elseif operation == 'julian' then
		if not date2 and date1 then -- Convert from Julian to Gregorian calendar date
			local JDN = Calendar._date2jdn(date1, 0)
			if JDN then
				date2 = date1 -- first date is assumed to be Julian
				date1 = Calendar._jdn2date(JDN, 1)
		date1 = getISODate(date1, operation, lang, 1, nil)
		date2 = getISODate(date2, operation, lang, 2, nil)
		dateStr = translatePhrase(date1, date2, operation, lang, state)
		dateStr = mw.ustring.gsub(mw.ustring.gsub(dateStr, '%( ', '('), ' %)', ')') -- in case date2 is empty
	elseif operation == 'turn of the year' or operation == 'turn of the decade' or operation == 'turn of the century' then 
		if operation == 'turn of the decade' then dt=10 else dt=1 end
		if not date2 or date2=='' then date2=tostring(tonumber(date1)-dt) end
		if era~='bp' and era~='bc' then date1, date2 = date2, date1 end
		if operation == 'turn of the year' then
			date1 = ISOdate(date1, lang, '', '', 1)
			date2 = ISOdate(date2, lang, '', '', 1)
			date1 = formatnum1(date1, lang)
			date2 = formatnum1(date2, lang)
		dateStr = translatePhrase(date1, date2, operation, lang, state)
	elseif operation == 'year unknown' then
		dateStr = translatePhrase('', '', operation, lang, state)
	elseif operation == 'unknown' then
		dateStr = tostring( "exif-unknowndate" ):inLanguage( lang ))
	-- add era
	if era ~= '' then
		dateStr = translatePhrase(dateStr, '', era, lang, state)
	return dateStr

local function checkAliases(str1, str2, sType)
-- some inputs have many aliases - reconcile them and ensure string is playing a proper role	
local out = ''
if str1 and str1~='' then
	a = i18n.Synonyms[str1] -- look up synonyms of "str1"
	if a then
		out = a[1]
		p.Error = string.format('<span style="background-color:red;">Error in [[Module:Complex date]]: %s is not recognized.</span>', str1)
elseif str2 and str2~='' then -- if "str1" of type "sType" is empty than maybe ...
	a = i18n.Synonyms[str2]   -- ..."str2" is of the same type and is not empty
	if a and a[2]==sType then
		out  = a[1]
		str2 = ''
return out, str2

local function datePrecision(dateStr, units)	
-- "in this module "Units" is a string like millennium, century, or decade
--	"precision" is wikibase compatible date precision number: 6=millennium, 7=century, 8=decade, 9=year, 10=month, 11=day
-- based on string or numeric input calculate "Units" and "precision"
	local dateNum = tonumber(dateStr);
	if type(units)=='number' then
		precision = units
		if precision>11 then precision=11 end -- clip the range of precision values
		if     precision==6 then units='millennium' 		
		elseif precision==7 then units='century'
		elseif precision==8 then units='decade'
		else units = ''
	elseif type(units)=='string' then
		units = string.lower(units);
		if     units=='millennium' then precision=6
		elseif units=='century'    then precision=7
		elseif units=='decade'     then precision=8
		else precision=9
	if units=='' or precision==9 then
		local sLen = mw.ustring.len(dateStr)
		if     sLen<= 4 then precision=9
		elseif sLen== 7 then precision=10
		elseif sLen>=10 then precision=11
	if precision==6 and dateStr.match( dateStr, '%d000' )~=nil then 
		dateStr = tostring(math.floor(tonumber(dateStr)/1000) +1)
	elseif precision==7 and mw.ustring.match( dateStr, '%d%d00' )~=nil then
		dateStr = tostring(math.floor(tonumber(dateStr)/100) +1)

	return dateStr, units, precision

local function isodate2timestamp(dateStr, precision, era)
	local tStamp = nil
	if era == 'ah' or precision<6 then
		return nil
	elseif era ~= '' then
		eraLUT = {ad='+', bc='-', bp='-' }
		era = eraLUT[era]

-- convert isodate to timestamp used by quick statements
	if precision>=9 then 
		if string.match(dateStr,"^%d%d%d%d$") then               -- if YYYY  format 
			tStamp = era .. dateStr .. '-00-00T00:00:00Z/9'
		elseif string.match(dateStr,"^%d%d%d%d%-%d%d$") then      -- if YYYY-MM format 
			tStamp = era .. dateStr .. '-00T00:00:00Z/10'
		elseif string.match(dateStr,"^%d%d%d%d%-%d%d%-%d%d$") then  -- if YYYY-MM-DD format 
			tStamp = era .. dateStr .. 'T00:00:00Z/11'
	elseif precision==8 then -- decade
		tStamp = era .. dateStr .. '-00-00T00:00:00Z/8'
	elseif precision==7 then -- century
		local d = tostring(tonumber(dateStr)-1)
		tStamp = era .. d .. '50-00-00T00:00:00Z/7'
	elseif precision==6 then
		local d = tostring(tonumber(dateStr)-1)
		tStamp = era .. d .. '500-00-00T00:00:00Z/6'
	return tStamp

local function oneDateQScode(dateStr, adj, era, precision)
	local outputStr = ''

	local d = isodate2timestamp(dateStr, precision, era)
	if not d then
		return ''
	local rLUT = {            early='Q40719727'     , mid='Q40719748',      late='Q40719766',
		['1quarter']='Q40690303' , ['2quarter']='Q40719649'  , ['3quarter']='Q40719662', ['4quarter']='Q40719674',
		spring='Q40720559'   , summer='Q40720564'    , autumn='Q40720568'  , winter='Q40720553',
		firsthalf='Q40719687', secondhalf='Q40719707' }
	local qLUT = {['from']='P580', ['until']='P582', ['after']='P1319', ['before']='P1326'}

	local refine = rLUT[adj]
	local qualitier = qLUT[adj]

	if adj=='' then
		outputStr = d
	elseif adj=='circa' then
		outputStr = d..",P1480,Q5727902"
	elseif refine then
		outputStr = d..",P4241,"..refine
	elseif precision>7 and qualitier then
		local century = string.gsub(d, 'Z%/%d+', 'Z/7')
		outputStr = century ..",".. qualitier ..","..d
	return outputStr 

-- ==================================================
-- === External functions ===========================
-- ==================================================

function p.Era(frame)
    -- process inputs
	local dateStr
	local args    = frame.args
	if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then 
		args.lang = frame:callParserFunction( "int", "lang" ) -- get user's chosen language  
	local lang    = args['lang']
	local dateStr = args['date'] or ''
	local eraType = string.lower(args['era']  or '')

	dateStr = ISOdate(dateStr, lang, '', '', 1)
	if eraType then 
		eraType = checkAliases(eraType ,'','e')
		dateStr = translatePhrase(dateStr, '', eraType, lang, {}) 
	return dateStr
function p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)
	local Output=''

    -- process inputs and save date in state array
	local state  = {} 
	state.conj   = string.lower(conj   or '')
	state.adj1   = string.lower(adj1   or '')
	state.adj2   = string.lower(adj2   or '')
	state.era1   = string.lower(era1   or '')
	state.era2   = string.lower(era2   or '')
	state.units1 = string.lower(units1 or '')
	state.units2 = string.lower(units2 or '')
	-- if date 1 is missing but date 2 is provided than swap them
	if date1 == '' and date2 ~= '' then
		date1 = date2
		date2 = ''
		state = {adj1 = state.adj2, era1 = state.era2, units1 = state.units2, 
		         adj2 = '',         era2 = '',         units2 = '',  conj=state.conj, num=1}
	if     date2 ~= '' then state.nDates = 2 
	elseif date1 ~= '' then state.nDates = 1 
	else	                state.nDates = 0
	-- reconcile alternative names for text inputs
	local conj         = checkAliases(state.conj ,''  ,'j')
	state.adj1 ,conj   = checkAliases(state.adj1 ,conj,'a')
	state.units1,conj  = checkAliases(state.units1,conj,'p')
	state.era1 ,conj   = checkAliases(state.era1 ,conj,'e')
	state.special,conj = checkAliases('',conj,'c')
	state.adj2         = checkAliases(state.adj2 ,'','a')
	state.units2       = checkAliases(state.units2,'','p')
	state.era2         = checkAliases(state.era2 ,'','e')
	state.conj         = conj
	state.lang         = lang
	if p.Error~=nil then
		return nil
	-- calculate date precision value
	date1, state.units1, state.precision1 = datePrecision(date1, state.units1)
	date2, state.units2, state.precision2 = datePrecision(date2, state.units2)

	-- Handle special cases 
	-- Some complex phrases can be created out of simpler ones. Therefore on pass # 1 we try to create 
	-- the phrase using complex phrase and if that is not found than on the second pass we try to build
	-- the phrase out of the simpler ones
	if passNr==1 then
		if state.adj1=='circa' and state.nDates == 2 then
			state.conj = 'circa2'
			state.adj1 = ''
			state.adj2 = ''
		if state.nDates == 2 and state.adj1=='late' and state.adj2=='early' and state.conj=='and' 
		and state.units1==state.units2 and state.era1==state.era2 then
			if state.units1=='century' then
				state.conj='turn of the century'
			elseif state.units1=='decade' then
				state.conj='turn of the decade'
			elseif state.units1=='' then
				state.conj='turn of the year'
			state.adj1 = ''
			state.adj2 = ''
			state.units1 = ''
			state.units2 = ''

	local errorStr = string.format(
	 '\n*conj=%s, adj1=%s, era1=%s, unit1=%s, prec1=%i, adj2=%s, era2=%s, unit2=%s, prec2=%i, special=%s', 
	  state.conj, state.adj1, state.era1, state.units1, state.precision1,
	  state.adj2, state.era2, state.units2, state.precision2, state.special)  
	state.adj, state.era, state.units, state.precision = state.adj1, state.era1, state.units1, state.precision1 

	-- call specialized functions
	local QScode = ''
	if state.special~='' then
		Output = otherPhrases(date1, date2, state.special, state.era1, lang, state)	
	elseif state.conj~='' then
		Output = twoDatePhrase(date1, date2, state, lang)
	elseif state.adj1~='' or state.era1~='' or state.units1~='' then
		Output = oneDatePhrase(date1, state.adj1, state.era1, state.units1, lang, 1, nil, state)
		QScode = oneDateQScode(date1, state.adj1, state.era1, state.precision1)
	elseif date1~='' then
		Output = ISOdate(date1, lang, '', 'dtstart', '100-999')
	if p.Error~=nil then
		return errorStr

	-- if there is any wikicode in the string than execute it
	if mw.ustring.find(Output, '{') then
		Output = mw.getCurrentFrame():preprocess(Output)
	if QScode and #QScode>0 then
		QScode = ' <div style="display: none;">date QS:P,' .. QScode .. '</div>'

	return Output .. QScode

function p.complex_date(frame)
    -- process inputs
	local dateStr, Error
	local args   = frame.args
	if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then 
		args.lang = frame:callParserFunction( "int", "lang" ) -- get user's chosen language  
	local date1  = args['date1'] or args['2'] or args['date'] or ''
	local date2  = args['date2'] or args['3'] or ''
	local conj   = args['conj']  or args['1'] or ''
	local adj1   = args['adj1']  or args['adj'] or ''
	local adj2   = args['adj2'] or ''
	local units1 = args['precision1'] or args['precision'] or ''
	local units2 = args['precision2'] or args['precision'] or ''
	local era1   = args['era1'] or args['era'] or ''
	local era2   = args['era2'] or args['era'] or ''
	local lang   = args['lang']

	dateStr = p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, 1)
	if p.Error~=nil then
		dateStr = p.Error .. '[[Category:Pages using Complex date template with incorrect parameter]]'
	return dateStr

return p