Difference between revisions of "Module:Complex date"

From blackwiki
Jump to navigation Jump to search
blackwiki>Johnuniq
(fix one more global problem; couple of minor cleanups; this fixes errors in a few articles such as Alexey Verstovsky)
m (14 revisions imported)
 
(One intermediate revision by one other user not shown)
Line 1: Line 1:
 
--[[
 
--[[
   __  __          _      _        ____                      _                _      _
+
   __  __          _      _        ____                      _                _
  |  \/  | ___  __| |_  _| | ___ _ / ___|___  _ __ ___  _ __ | | _____  __  __| | __ _| |_ ___
+
  |  \/  | ___  __| |_  _| | ___ _ / ___|___  _ __ ___  _ __ | | _____  __  __| | __ _| |_ ___  
 
  | |\/| |/ _ \ / _` | | | | |/ _ (_) |  / _ \| '_ ` _ \| '_ \| |/ _ \ \/ /  / _` |/ _` | __/ _ \
 
  | |\/| |/ _ \ / _` | | | | |/ _ (_) |  / _ \| '_ ` _ \| '_ \| |/ _ \ \/ /  / _` |/ _` | __/ _ \
 
  | |  | | (_) | (_| | |_| | |  __/_| |__| (_) | | | | | | |_) | |  __/>  <  | (_| | (_| | ||  __/
 
  | |  | | (_) | (_| | |_| | |  __/_| |__| (_) | | | | | | |_) | |  __/>  <  | (_| | (_| | ||  __/
Line 15: Line 15:
 
* User:Sn1per - first draft of the original version
 
* User:Sn1per - first draft of the original version
 
* User:Jarekt - corrections and expansion of the original version
 
* User:Jarekt - corrections and expansion of the original version
 
 
]]
 
]]
  
 
-- List of external modules and functions
 
-- List of external modules and functions
 
local p = {Error = nil}
 
local p = {Error = nil}
local i18n      = require('Module:i18n/complex date')
+
local i18n      = require('Module:i18n/complex date')   -- used for translations of date related phrases
local ISOdate    = require('Module:ISOdate')._ISOdate
+
local ISOdate    = require('Module:ISOdate')._ISOdate   -- used for parsing dates in YYYY-MM-DD and related formats
local Calendar -- loaded lazily
+
local Calendar   -- loaded lazily
  
 
-- ==================================================
 
-- ==================================================
Line 39: Line 38:
 
end
 
end
  
 +
-- ==================================================
 
local function formatnum1(numStr, lang)
 
local function formatnum1(numStr, lang)
 
-- mostly require('Module:Formatnum').formatNum function used to translate a number to use different numeral characters,
 
-- mostly require('Module:Formatnum').formatNum function used to translate a number to use different numeral characters,
Line 49: Line 49:
 
end
 
end
  
 +
-- ==================================================
 
local function getISODate(datestr, datetype, lang, num, case)
 
local function getISODate(datestr, datetype, lang, num, case)
 
-- translate dates in the format YYYY, YYYY-MM, and YYYY-MM-DD
 
-- translate dates in the format YYYY, YYYY-MM, and YYYY-MM-DD
Line 61: Line 62:
 
end
 
end
  
 +
-- =======================================================================
 
local function translatePhrase(date1, date2, operation, lang, state)
 
local function translatePhrase(date1, date2, operation, lang, state)
 
-- use tables in Module:i18n/complex date to translate a phrase
 
-- use tables in Module:i18n/complex date to translate a phrase
Line 72: Line 74:
 
end
 
end
 
if type(dateStr)=='function' then
 
if type(dateStr)=='function' then
local success
+
local dateFunc = dateStr
 
local nDates = i18n.Translations[operation]['nDates']
 
local nDates = i18n.Translations[operation]['nDates']
 
if nDates==2 then -- 2 date phrase
 
if nDates==2 then -- 2 date phrase
success, dateStr = pcall(dateStr, date1, date2, state)
+
dateStr = dateFunc(date1, date2, state)
 
else  -- 1 date phrase
 
else  -- 1 date phrase
success, dateStr = pcall(dateStr, date1, state)
+
dateStr = dateFunc(date1, state)
 
end
 
end
 
end
 
end
 
+
 
if type(dateStr)=='string' then
 
if type(dateStr)=='string' then
 
-- replace parts of the string '$date1' and '$date2' with date1 and date2 strings
 
-- replace parts of the string '$date1' and '$date2' with date1 and date2 strings
Line 94: Line 96:
 
end
 
end
  
 +
-- =======================================================================
 
local function oneDatePhrase(dateStr, adj, era, units, lang, num, case, state)
 
local function oneDatePhrase(dateStr, adj, era, units, lang, num, case, state)
 
-- translate a single date phrase
 
-- translate a single date phrase
Line 99: Line 102:
 
state.adj, state.era, state.units, state.precision = state.adj2, state.era2, state.units2, state.precision2
 
state.adj, state.era, state.units, state.precision = state.adj2, state.era2, state.units2, state.precision2
 
end
 
end
 
+
 
-- dateStr can have many forms: ISO date, year or a number for
 
-- dateStr can have many forms: ISO date, year or a number for
 
-- decade, century or millennium
 
-- decade, century or millennium
Line 107: Line 110:
 
dateStr = translatePhrase(dateStr, '', units, lang, state)
 
dateStr = translatePhrase(dateStr, '', units, lang, state)
 
end
 
end
 
+
 
-- add adjective ("early", "mid", etc.) or preposition ("before", "after",
 
-- add adjective ("early", "mid", etc.) or preposition ("before", "after",
 
-- "circa", etc.) to the date
 
-- "circa", etc.) to the date
Line 115: Line 118:
 
dateStr = formatnum1(dateStr, lang)
 
dateStr = formatnum1(dateStr, lang)
 
end
 
end
 
+
 
-- add era
 
-- add era
 
if era ~= '' then
 
if era ~= '' then
Line 123: Line 126:
 
end
 
end
  
 +
-- =======================================================================
 
local function twoDatePhrase(date1, date2, state, lang)
 
local function twoDatePhrase(date1, date2, state, lang)
 
-- translate a double date phrase
 
-- translate a double date phrase
Line 149: Line 153:
 
end
 
end
  
 +
-- =======================================================================
 
local function otherPhrases(date1, date2, operation, era, lang, state)
 
local function otherPhrases(date1, date2, operation, era, lang, state)
 
-- translate specialized phrases
 
-- translate specialized phrases
 
local dateStr = ''
 
local dateStr = ''
 
+
 
if operation == 'islamic' then
 
if operation == 'islamic' then
 
if date2=='' then date2 = mw.getCurrentFrame():callParserFunction('#time', 'xmY', date1) end
 
if date2=='' then date2 = mw.getCurrentFrame():callParserFunction('#time', 'xmY', date1) end
Line 163: Line 168:
 
if not date2 and date1 then -- Convert from Julian to Gregorian calendar date
 
if not date2 and date1 then -- Convert from Julian to Gregorian calendar date
 
if Calendar == nil then
 
if Calendar == nil then
Calendar = require("Module:Calendar")
+
Calendar = require("Module:Calendar") -- lazy loding (only if needed)
 
end
 
end
 
local JDN = Calendar._date2jdn(date1, 0)
 
local JDN = Calendar._date2jdn(date1, 0)
Line 189: Line 194:
 
dateStr = translatePhrase(date1, date2, operation, lang, state)
 
dateStr = translatePhrase(date1, date2, operation, lang, state)
 
elseif operation == 'year unknown' then
 
elseif operation == 'year unknown' then
dateStr = translatePhrase('', '', operation, lang, state)
+
dateStr = translatePhrase('', '', operation, lang, state) .. '<div style="display: none;">Unknown date</div>'
 
elseif operation == 'unknown' then
 
elseif operation == 'unknown' then
dateStr = tostring(mw.message.new( "exif-unknowndate" ):inLanguage( lang ))
+
dateStr = tostring(mw.message.new( "exif-unknowndate" ):inLanguage( lang )) .. '<div style="display: none;">Unknown date</div>'
 
end
 
end
 
+
 
-- add era
 
-- add era
 
if era ~= '' then
 
if era ~= '' then
Line 201: Line 206:
 
end
 
end
  
 +
-- =======================================================================
 
local function checkAliases(str1, str2, sType)
 
local function checkAliases(str1, str2, sType)
-- some inputs have many aliases - reconcile them and ensure string is playing a proper role
+
-- some inputs have many aliases - reconcile them and ensure string is playing a proper role
local out = ''
+
local out = ''
if str1 and str1~='' then
+
if str1 and str1~='' then
local a = i18n.Synonyms[str1] -- look up synonyms of "str1"
+
local a = i18n.Synonyms[str1] -- look up synonyms of "str1"
if a then
+
if a then
out = a[1]
+
out = a[1]
else
+
else
p.Error = string.format('<span style="background-color:red;">Error in [[Module:Complex date]]: %s is not recognized.</span>', str1)
+
p.Error = string.format('<span style="background-color:red;">Error in [[Module:Complex date]]: %s is not recognized.</span>', str1)
end
+
end
elseif str2 and str2~='' then -- if "str1" of type "sType" is empty than maybe ...
+
elseif str2 and str2~='' then -- if "str1" of type "sType" is empty than maybe ...
local a = i18n.Synonyms[str2]  -- ..."str2" is of the same type and is not empty
+
local a = i18n.Synonyms[str2]  -- ..."str2" is of the same type and is not empty
if a and a[2]==sType then
+
if a and a[2]==sType then
out  = a[1]
+
out  = a[1]
str2 = ''
+
str2 = ''
 +
end
 
end
 
end
end
+
return out, str2
return out, str2
 
 
end
 
end
  
 +
-- =======================================================================
 
local function datePrecision(dateStr, units)
 
local function datePrecision(dateStr, units)
 
-- "in this module "Units" is a string like millennium, century, or decade
 
-- "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
+
-- "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"
 
-- based on string or numeric input calculate "Units" and "precision"
local dateNum = tonumber(dateStr)  -- TODO dateNum is not used: should remove?
 
 
local precision
 
local precision
 
if type(units)=='number' then
 
if type(units)=='number' then
Line 236: Line 242:
 
end
 
end
 
elseif type(units)=='string' then
 
elseif type(units)=='string' then
units = string.lower(units);
+
units = string.lower(units)
 
if    units=='millennium' then precision=6
 
if    units=='millennium' then precision=6
 
elseif units=='century'    then precision=7
 
elseif units=='century'    then precision=7
Line 260: Line 266:
 
end
 
end
  
 
+
-- =======================================================================
 
local function isodate2timestamp(dateStr, precision, era)
 
local function isodate2timestamp(dateStr, precision, era)
 
-- convert date string to timestamps used by Quick Statements
 
-- convert date string to timestamps used by Quick Statements
Line 291: Line 297:
 
tStamp = era .. d .. '500-00-00T00:00:00Z/6'
 
tStamp = era .. d .. '500-00-00T00:00:00Z/6'
 
end
 
end
 
+
 
return tStamp
 
return tStamp
 
end
 
end
  
 +
-- =======================================================================
 
local function oneDateQScode(dateStr, adj, era, precision)
 
local function oneDateQScode(dateStr, adj, era, precision)
 
-- create QuickStatements string for "one date" dates
 
-- create QuickStatements string for "one date" dates
Line 307: Line 314:
 
spring='Q40720559'  , summer='Q40720564'    , autumn='Q40720568'  , winter='Q40720553',
 
spring='Q40720559'  , summer='Q40720564'    , autumn='Q40720568'  , winter='Q40720553',
 
firsthalf='Q40719687', secondhalf='Q40719707' }
 
firsthalf='Q40719687', secondhalf='Q40719707' }
local qLUT = {['from']='P580', ['until']='P582', ['after']='P1319', ['before']='P1326'}
+
local qLUT = {['from']='P580', ['until']='P582', ['after']='P1319', ['before']='P1326', ['by']='P1326'}
  
 
local refine = rLUT[adj]
 
local refine = rLUT[adj]
Line 325: Line 332:
 
end
 
end
  
 +
-- =======================================================================
 
local function twoDateQScode(date1, date2, state)
 
local function twoDateQScode(date1, date2, state)
 
-- create QuickStatements string for "two date" dates
 
-- create QuickStatements string for "two date" dates
 
if state.adj1~='' or state.adj2~='' or state.era1~=state.era2 then
 
if state.adj1~='' or state.adj2~='' or state.era1~=state.era2 then
return ''
+
return '' -- QuickStatements string are not generated for two date phrases with adjectives
 
end
 
end
 
local outputStr = ''
 
local outputStr = ''
Line 338: Line 346:
 
-- find date with lower precision in common to both dates
 
-- find date with lower precision in common to both dates
 
local cd
 
local cd
local year1 = string.sub(d1,2,5)
+
local year1 = tonumber(string.sub(d1,2,5))
 +
local year2 = tonumber(string.sub(d2,2,5))
 
local k = 0
 
local k = 0
 
for i = 1,10,1 do
 
for i = 1,10,1 do
Line 348: Line 357:
 
cd = isodate2timestamp(string.sub(d1,2,8), 10, state.era1)
 
cd = isodate2timestamp(string.sub(d1,2,8), 10, state.era1)
 
elseif k>=6 and k<9 then  -- same year, since "+YYYY-" is in common
 
elseif k>=6 and k<9 then  -- same year, since "+YYYY-" is in common
cd = isodate2timestamp(year1, 9, state.era1)
+
cd = isodate2timestamp(tostring(year1), 9, state.era1)
 
elseif k==4 then          -- same decade(k=4, precision=8),  since "+YYY" is in common
 
elseif k==4 then          -- same decade(k=4, precision=8),  since "+YYY" is in common
cd = isodate2timestamp(year1, 8, state.era1)
+
cd = isodate2timestamp(tostring(year1), 8, state.era1)
 
elseif k==3 then          -- same century(k=3, precision=7) since "+YY" is in common
 
elseif k==3 then          -- same century(k=3, precision=7) since "+YY" is in common
local d = tostring(math.floor(tonumber(year1)/100) +1) -- convert 1999 -> 20
+
  local d = tostring(math.floor(year1/100) +1) -- convert 1999 -> 20
 
cd = isodate2timestamp( d, 7, state.era1)
 
cd = isodate2timestamp( d, 7, state.era1)
 
elseif k==2 then          -- same millennium (k=2, precision=6),  since "+Y" is in common
 
elseif k==2 then          -- same millennium (k=2, precision=6),  since "+Y" is in common
local d = tostring(math.floor(tonumber(year1)/1000) +1) -- convert 1999 -> 2
+
local d = tostring(math.floor(year1/1000) +1) -- convert 1999 -> 2
 
cd = isodate2timestamp( d, 6, state.era1)
 
cd = isodate2timestamp( d, 6, state.era1)
else
+
end
 +
if not cd then
 
return ''
 
return ''
 
end
 
end
Line 365: Line 375:
  
 
--
 
--
if state.conj=='from-until' then
+
if (state.conj=='from-until') or (state.conj=='and' and year1==year2-1) then
 
outputStr = cd ..",P580,".. d1 ..",P582,".. d2
 
outputStr = cd ..",P580,".. d1 ..",P582,".. d2
elseif state.conj=='between' then
+
elseif (state.conj=='between') or (state.conj=='or' and year1==year2-1) then
 
outputStr = cd ..",P1319,".. d1 ..",P1326,".. d2
 
outputStr = cd ..",P1319,".. d1 ..",P1326,".. d2
 
elseif state.conj=='circa2' then
 
elseif state.conj=='circa2' then
Line 376: Line 386:
 
end
 
end
  
-- ==================================================
+
-- =======================================================================
-- === External functions ===========================
+
local function processInputParams(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)
-- ==================================================
 
 
 
function p.Era(frame)
 
-- process inputs
 
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
 
end
 
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, {})
 
end
 
return dateStr
 
end
 
 
 
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
 
-- process inputs and save date in state array
Line 415: Line 403:
 
date1 = date2
 
date1 = date2
 
date2 = ''
 
date2 = ''
state = {adj1 = state.adj2, era1 = state.era2, units1 = state.units2,
+
state = {adj1 = state.adj2, era1 = state.era2, units1 = state.units2,  
adj2 = '',        era2 = '',        units2 = '',  conj=state.conj, num=1}
+
        adj2 = '',        era2 = '',        units2 = '',  conj=state.conj, num=1}
 
end
 
end
 
if    date2 ~= '' then state.nDates = 2
 
if    date2 ~= '' then state.nDates = 2
 
elseif date1 ~= '' then state.nDates = 1
 
elseif date1 ~= '' then state.nDates = 1
else                   state.nDates = 0
+
else                   state.nDates = 0
 
end
 
end
  
Line 466: Line 454:
 
state.units2 = ''
 
state.units2 = ''
 
end
 
end
 +
end
 +
 +
state.adj, state.era, state.units, state.precision = state.adj1, state.era1, state.units1, state.precision1
 +
return date1, date2, state
 +
end
 +
 +
-- ==================================================
 +
-- === 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
 +
end
 +
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, {})
 +
end
 +
return dateStr
 +
end
 +
 +
-- =======================================================================
 +
function p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)
 +
local Output=''
 +
local state
 +
 +
-- process inputs and save date in state array
 +
date1, date2, state  = processInputParams(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)
 +
if p.Error~=nil then
 +
return nil
 
end
 
end
  
 
local errorStr = string.format(
 
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',
+
  '\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.conj, state.adj1, state.era1, state.units1, state.precision1,
 
  state.adj2, state.era2, state.units2, state.precision2, state.special)
 
  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
 
-- call specialized functions
Line 502: Line 527:
 
end
 
end
  
 +
-- =======================================================================
 +
function p._complex_date_cer(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, certainty, lang)
 +
-- same as p._complex_date but with extra parameter for certainty: probably, possibly, presumably, etc.
 +
local dateStr = p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, 1)
 +
certainty = checkAliases(certainty, conj, 'r')
 +
local LUT = {probably='Q56644435',  presumably='Q18122778', possibly='Q30230067', circa='Q5727902' }
 +
if certainty and LUT[certainty] then
 +
local state  = {}
 +
date1, date2, state  = processInputParams(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, 1)
 +
dateStr = translatePhrase(dateStr, '', certainty, lang, state)
 +
dateStr = string.gsub(dateStr, '(%<div style="display: none;"%>date QS:P,[^%<]+)(%</div%>)', '%1,P1480,' .. LUT[certainty] .. '%2' )
 +
end
 +
return dateStr
 +
end
 +
 +
-- =======================================================================
 
function p.complex_date(frame)
 
function p.complex_date(frame)
 
-- process inputs
 
-- process inputs
Line 518: Line 559:
 
local era1  = args['era1'] or args['era'] or ''
 
local era1  = args['era1'] or args['era'] or ''
 
local era2  = args['era2'] or args['era'] or ''
 
local era2  = args['era2'] or args['era'] or ''
 +
local certainty = args['certainty']
 
local lang  = args['lang']
 
local lang  = args['lang']
  
dateStr = p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, 1)
+
dateStr = p._complex_date_cer(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, certainty, lang)
 
if p.Error~=nil then
 
if p.Error~=nil then
 
dateStr = p.Error .. '[[Category:Pages using Complex date template with incorrect parameter]]'
 
dateStr = p.Error .. '[[Category:Pages using Complex date template with incorrect parameter]]'

Latest revision as of 14:37, 25 September 2020

Usage

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

Complex date

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

Usage:

should not be used directly but only through c:Template:Other date and other templates
{{#invoke:Complex_date|complex_date|adj1=|date1=|era1=|precision1=|conj=-|adj2=|date2=|era2=|precision2=|lang=}}

or

{{#invoke:Complex_date|complex_date|1=|2=|3=|era=|lang=}}

Parameters (simple syntax):

1
option. See c:Template:Other date
2
date #1
3
date #2

Parameters (advanced syntax):

conj
in case the link between the two dates: "-" (from-until), between, or, and (see c:template: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 c:template: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 c:template: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



--[[
  __  __           _       _         ____                      _                 _
 |  \/  | ___   __| |_   _| | ___ _ / ___|___  _ __ ___  _ __ | | _____  __   __| | __ _| |_ ___ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) |   / _ \| '_ ` _ \| '_ \| |/ _ \ \/ /  / _` |/ _` | __/ _ \
 | |  | | (_) | (_| | |_| | |  __/_| |__| (_) | | | | | | |_) | |  __/>  <  | (_| | (_| | ||  __/
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/|_| |_| |_| .__/|_|\___/_/\_\  \__,_|\__,_|\__\___|
                                                        |_|

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')   -- used for translations of date related phrases
local ISOdate    = require('Module:ISOdate')._ISOdate    -- used for parsing dates in YYYY-MM-DD and related formats
local Calendar   -- loaded lazily

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

local function langSwitch(list,lang)
	local langList = mw.language.getFallbacksFor(lang)
	table.insert(langList,1,lang)
	table.insert(langList,math.max(#langList,2),'default')
	for i,language in ipairs(langList) do
		if list[language] then
			return list[language]
		end
	end
end

-- ==================================================
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 = require('Module:Formatnum').formatNum(numStr, lang, 1)
	end
	return numStr
end

-- ==================================================
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 = rec.case[num]
		end
	end
	return ISOdate(datestr, lang, case, '', 1)
end

-- =======================================================================
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 ''
	end
	local dateStr = langSwitch(i18n.Translations[operation], lang)
	if type(dateStr)=='table' then
		dateStr = dateStr[1]
	end
	if type(dateStr)=='function' then
		local dateFunc = dateStr
		local nDates = i18n.Translations[operation]['nDates']
		if nDates==2 then -- 2 date phrase
			dateStr = dateFunc(date1, date2, state)
		else  -- 1 date phrase
			dateStr = dateFunc(date1, state)
		end
	end
	
	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)
	else
		-- 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)
	end
	return dateStr
end

-- =======================================================================
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
	end
	
	-- 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)
	end
	
	-- 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)
	end
	
	-- add era
	if era ~= '' then
		dateStr = translatePhrase(dateStr, '', era, lang, state)
	end
	return dateStr
end

-- =======================================================================
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 = ''
	end
	case = {nil, nil}
	if i18n.Translations[state.conj] then
		local rec = langSwitch(i18n.Translations[state.conj], lang)
		if type(rec)=='table' then
			case = rec.case
		end
	end
	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)
	end
	return dateStr
end

-- =======================================================================
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
			if Calendar == nil then
				Calendar = require("Module:Calendar") -- lazy loding (only if needed)
			end
			local JDN = Calendar._date2jdn(date1, 0)
			if JDN then
				date2 = date1 -- first date is assumed to be Julian
				date1 = Calendar._jdn2date(JDN, 1)
			end
		end
		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
		local dt = 1
		if operation == 'turn of the decade' then dt=10 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)
		else
			date1 = formatnum1(date1, lang)
			date2 = formatnum1(date2, lang)
		end
		dateStr = translatePhrase(date1, date2, operation, lang, state)
	elseif operation == 'year unknown' then
		dateStr = translatePhrase('', '', operation, lang, state) .. '<div style="display: none;">Unknown date</div>'
	elseif operation == 'unknown' then
		dateStr = tostring(mw.message.new( "exif-unknowndate" ):inLanguage( lang )) .. '<div style="display: none;">Unknown date</div>'
	end
	
	-- add era
	if era ~= '' then
		dateStr = translatePhrase(dateStr, '', era, lang, state)
	end
	return dateStr
end

-- =======================================================================
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
		local a = i18n.Synonyms[str1] -- look up synonyms of "str1"
		if a then
			out = a[1]
		else
			p.Error = string.format('<span style="background-color:red;">Error in [[Module:Complex date]]: %s is not recognized.</span>', str1)
		end
	elseif str2 and str2~='' then -- if "str1" of type "sType" is empty than maybe ...
		local 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 = ''
		end
	end
	return out, str2
end

-- =======================================================================
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 precision
	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 = ''
		end
	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
		end
	end
	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
		end
		units=''
	end
	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)
	end

	return dateStr, units, precision
end

-- =======================================================================
local function isodate2timestamp(dateStr, precision, era)
-- convert date string to timestamps used by Quick Statements
	local tStamp = nil
	if era == 'ah' or precision<6 then
		return nil
	elseif era ~= '' then
		local eraLUT = {ad='+', bc='-', bp='-' }
		era = eraLUT[era]
	else
		era='+'
	end

-- 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'
		end
	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'
	end
	
	return tStamp
end

-- =======================================================================
local function oneDateQScode(dateStr, adj, era, precision)
-- create QuickStatements string for "one date" dates
	local outputStr = ''

	local d = isodate2timestamp(dateStr, precision, era)
	if not d then
		return ''
	end
	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', ['by']='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
	end
	return outputStr
end

-- =======================================================================
local function twoDateQScode(date1, date2, state)
-- create QuickStatements string for "two date" dates
	if state.adj1~='' or state.adj2~='' or state.era1~=state.era2 then
		return '' -- QuickStatements string are not generated for two date phrases with adjectives
	end
	local outputStr = ''
	local d1 = isodate2timestamp(date1, state.precision1, state.era1)
	local d2 = isodate2timestamp(date2, state.precision2, state.era2)
	if (not d1) or (not d2) then
		return ''
	end
	-- find date with lower precision in common to both dates
	local cd
	local year1 = tonumber(string.sub(d1,2,5))
	local year2 = tonumber(string.sub(d2,2,5))
	local k = 0
	for i = 1,10,1 do
		if string.sub(d1,1,i)==string.sub(d2,1,i) then
			k = i -- find last matching letter
		end
	end
	if k>=9 then              -- same month, since "+YYYY-MM-" is in common
		cd = isodate2timestamp(string.sub(d1,2,8), 10, state.era1)
	elseif k>=6 and k<9 then  -- same year, since "+YYYY-" is in common
		cd = isodate2timestamp(tostring(year1), 9, state.era1)
	elseif k==4 then          -- same decade(k=4, precision=8),  since "+YYY" is in common
		cd = isodate2timestamp(tostring(year1), 8, state.era1)
	elseif k==3 then          -- same century(k=3, precision=7) since "+YY" is in common
	  local d = tostring(math.floor(year1/100) +1) -- convert 1999 -> 20
		cd = isodate2timestamp( d, 7, state.era1)
	elseif k==2 then          -- same millennium (k=2, precision=6),  since "+Y" is in common
		local d = tostring(math.floor(year1/1000) +1) -- convert 1999 -> 2
		cd = isodate2timestamp( d, 6, state.era1)
	end
	if not cd then
		return ''
	end
	--if not cd then
	--	return ' <br/>error: ' .. d1.." / " .. d2.." / ".. (cd or '') .." / ".. string.sub(d1,2,5).." / " .. string.sub(d2,2,5).." / " .. tostring(k)
	--end

	--
	if (state.conj=='from-until') or (state.conj=='and' and year1==year2-1) then
		outputStr = cd ..",P580,".. d1 ..",P582,".. d2
	elseif (state.conj=='between') or (state.conj=='or' and year1==year2-1) then
		outputStr = cd ..",P1319,".. d1 ..",P1326,".. d2
	elseif state.conj=='circa2' then
		outputStr = cd ..",P1319,".. d1 ..",P1326,".. d2 ..",P1480,Q5727902"
	end

	return outputStr
end

-- =======================================================================
local function processInputParams(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)

	-- 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}
	end
	if     date2 ~= '' then state.nDates = 2
	elseif date1 ~= '' then state.nDates = 1
	else                    state.nDates = 0
	end

	-- 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
	end

	-- 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 = ''
		end
		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'
			end
			state.adj1 = ''
			state.adj2 = ''
			state.units1 = ''
			state.units2 = ''
		end
	end

	state.adj, state.era, state.units, state.precision = state.adj1, state.era1, state.units1, state.precision1
	return date1, date2, state
end

-- ==================================================
-- === 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
	end
	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, {})
	end
	return dateStr
end

-- =======================================================================
function p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)
	local Output=''
	local state

	-- process inputs and save date in state array
	date1, date2, state  = processInputParams(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, passNr)
	if p.Error~=nil then
		return nil
	end

	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)

	-- call specialized functions
	local QScode = ''
	if state.special~='' then
		Output = otherPhrases(date1, date2, state.special, state.era1, lang, state)
	elseif state.conj~='' then
		QScode = twoDateQScode(date1, date2, state)
		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')
	end
	if p.Error~=nil then
		return errorStr
	end

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

	return Output .. QScode
end

-- =======================================================================
function p._complex_date_cer(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, certainty, lang)
-- same as p._complex_date but with extra parameter for certainty: probably, possibly, presumably, etc.
	local dateStr = p._complex_date(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, 1)
	certainty = checkAliases(certainty, conj, 'r')
	local LUT = {probably='Q56644435',  presumably='Q18122778', possibly='Q30230067', circa='Q5727902' }
	if certainty and LUT[certainty] then
		local state  = {} 
		date1, date2, state  = processInputParams(conj, adj1, date1, units1, era1, adj2, date2, units2, era2, lang, 1)
		dateStr = translatePhrase(dateStr, '', certainty, lang, state)
		dateStr = string.gsub(dateStr, '(%<div style="display: none;"%>date QS:P,[^%<]+)(%</div%>)', '%1,P1480,' .. LUT[certainty] .. '%2' )
	end
	return dateStr
end

-- =======================================================================
function p.complex_date(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
	end
	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 certainty = args['certainty']
	local lang   = args['lang']

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

return p