-- Implement various "age of" and other date-related templates.localmtext={-- Message and other text that should be localized.-- Also need to localize text in table names in function dateDifference.['mt-bad-param2']='Parameter $1=$2 is invalid',['mt-bad-show']='Parameter show=$1 is not supported here',['mt-cannot-add']='Cannot add "$1"',['mt-conflicting-show']='Parameter show=$1 conflicts with round=$2',['mt-date-wrong-order']='The second date must be later in time than the first date',['mt-dd-future']='Death date (first date) must not be in the future',['mt-dd-wrong-order']='Death date (first date) must be later in time than the birth date (second date)',['mt-invalid-bd-age']='Invalid birth date for calculating age',['mt-invalid-dates-age']='Invalid dates for calculating age',['mt-invalid-end']='Invalid end date in second parameter',['mt-invalid-start']='Invalid start date in first parameter',['mt-need-jdn']='Need valid Julian date number',['mt-need-valid-bd']='Need valid birth date: year, month, day',['mt-need-valid-bd2']='Need valid birth date (second date): year, month, day',['mt-need-valid-date']='Need valid date',['mt-need-valid-dd']='Need valid death date (first date): year, month, day',['mt-need-valid-ymd']='Need valid year, month, day',['mt-need-valid-ymd-current']='Need valid year|month|day or "currentdate"',['mt-need-valid-ymd2']='Second date should be year, month, day',['mt-template-bad-name']='The specified template name is not valid',['mt-template-x']='The template invoking this must have "|template=x" where x is the wanted operation',['mt-warn-param1']='Invalid parameter $1',['mt-warn-param2']='Parameter $1=$2 is invalid',['txt-affirmative']={y=true,yes=true,Y=true,Yes=true,YES=true},-- valid values for df + mf parameters['txt-yes']={y=true,yes=true,on=true},-- valid values for parameters introduced with this module['txt-and']=' and ',['txt-or']=' or ',['txt-category']='Category:Pages using age template with invalid date',['txt-comma-and']=', and ',['txt-error']='Error: ',['txt-format-default']='mf',-- 'df' (day first = dmy) or 'mf' (month first = mdy)['txt-module-convertnumeric']='Module:ConvertNumeric',['txt-module-date']='Module:Date',['txt-sandbox']='sandbox',['txt-bda']='<span style="display:none"> (<span class="bday">$1</span>) </span>$2<span class="noprint ForceAgeToShow"> (age $3)</span>',['txt-dda']='$2<span style="display:none">($1)</span> (aged $3)',['txt-bda-disp']='disp_raw',-- disp_raw → age is a number only; disp_age → age is a number and unit (normally years but months or days if very young)['txt-dda-disp']='disp_raw',['txt-dmy']='%-d %B %-Y',['txt-mdy']='%B %-d, %-Y',}localisWarning={['mt-warn-param1']=true,['mt-warn-param2']=true,}-- yes[parameter] is true if parameter should be interpreted as "yes".-- Do not want to accept mixed upper/lowercase unless done by previously used templates.-- Need to accept "on" because "round=on" is wanted.localyes=mtext['txt-yes']-- Max valid age.localMAX_AGE=130localtranslate,from_en,to_en,isZeroiftranslatethen-- Functions to translate from en to local language and reverse go here.-- See example at [[:bn:Module:বয়স]].elsefrom_en=function(text)returntextendisZero=function(text)returntonumber(text)==0endendlocal_Date,_currentDatelocalfunctiongetExports(frame)-- Return objects exported from the date module or its sandbox.ifnot_Datethenlocalsandbox=frame:getTitle():find(mtext['txt-sandbox'],1,true)and('/'..mtext['txt-sandbox'])or''localdatemod=require(mtext['txt-module-date']..sandbox)localrealDate=datemod._Date_currentDate=datemod._currentifto_enthen_Date=function(...)localargs={}fori,vinipairs({...})doargs[i]=to_en(v)endreturnrealDate(unpack(args))endelse_Date=realDateendendreturn_Date,_currentDateendlocalCollection-- a table to hold itemsCollection={add=function(self,item)ifitem~=nilthenself.n=self.n+1self[self.n]=itemendend,join=function(self,sep)returntable.concat(self,sep)end,remove=function(self,pos)ifself.n>0and(pos==nilor(0<posandpos<=self.n))thenself.n=self.n-1returntable.remove(self,pos)endend,sort=function(self,comp)table.sort(self,comp)end,new=function()returnsetmetatable({n=0},Collection)end}Collection.__index=CollectionlocalfunctionstripToNil(text)-- If text is a string, return its trimmed content, or nil if empty.-- Otherwise return text (which may, for example, be nil).iftype(text)=='string'thentext=text:match('(%S.-)%s*$')endreturntextendlocalfunctionsubstituteParameters(text,...)-- Return text after substituting any given parameters for $1, $2, etc.returnmw.message.newRawMessage(text,...):plain()endlocalfunctionmessage(msg,...)-- Return formatted message text for an error or warning.localfunctiongetText(msg)returnmtext[msg]orerror('Bug: message "'..tostring(msg)..'" not defined')endlocalcategories={error=mtext['txt-category'],warning=mtext['txt-category'],}locala,b,k,categorylocaltext=substituteParameters(getText(msg),...)ifisWarning[msg]thena='<sup>[<i>'b='</i>]</sup>'k='warning'elsea='<strong class="error">'..getText('txt-error')b='</strong>'k='error'endifmw.title.getCurrentTitle():inNamespaces(0)then-- Category only in namespaces: 0=article.category='[['..categories[k]..']]'endreturna..mw.text.nowiki(text)..b..(categoryor'')endlocalfunctiondateFormat(args,options)-- Return-- nil, fif parameter is valid-- m, f otherwise-- where-- m = string for warning message with category-- f = string for wanted date formatlocalwantedifoptions.suppliedformat=='dmy'thenwanted='df'elseifoptions.suppliedformat=='mdy'thenwanted='mf'elsewanted=mtext['txt-format-default']endlocalproblemlocalother=wanted=='df'and'mf'or'df'localparm=args[other]or''ifmtext['txt-affirmative'][parm]thenwanted=otherelseifparm~=''thenproblem=message('mt-warn-param2',other,parm)endreturnproblem,wanted=='df'andmtext['txt-dmy']ormtext['txt-mdy']endlocalfunctionformatNumber(number)-- Return the given number formatted with commas as group separators,-- given that the number is an integer.localnumstr=tostring(number)locallength=#numstrlocalplaces=Collection.new()localpos=0repeatplaces:add(pos)pos=pos+3untilpos>=lengthplaces:add(length)localgroups=Collection.new()fori=places.n,2,-1dolocalp1=length-places[i]+1localp2=length-places[i-1]groups:add(numstr:sub(p1,p2))endreturngroups:join(',')endlocalfunctionspellNumber(number,options,i)-- Return result of spelling number, or-- return number (as a string) if cannot spell it.-- i == 1 for the first number which can optionally start with an uppercase letter.number=tostring(number)returnrequire(mtext['txt-module-convertnumeric']).spell_number(number,nil,-- fraction numeratornil,-- fraction denominatori==1andoptions.upper,-- true: 'One' instead of 'one'notoptions.us,-- true: use 'and' between tens/ones etcoptions.adj,-- true: hyphenatedoptions.ordinal-- true: 'first' instead of 'one')ornumberendlocalfunctionmakeExtra(args,flagCurrent)-- Return extra text that will be inserted before the visible result-- but after any sort key.localextra=args.prefixor''ifmw.ustring.len(extra)>1then-- Parameter "~" gives "~3" whereas "over" gives "over 3".ifextra:sub(-6,-1)~=' 'thenextra=extra..' 'endendifflagCurrentthenextra='<span class="currentage"></span>'..extraendreturnextraendlocalfunctionmakeSort(value,sortable)-- Return a sort key if requested.-- Assume value is a valid number which has not overflowed.ifsortable=='sortable_table'orsortable=='sortable_on'orsortable=='sortable_debug'thenlocalsortKeyifvalue==0thensortKey='5000000000000000000'elselocalmag=math.floor(math.log10(math.abs(value))+1e-14)ifvalue>0thensortKey=7000+magelsesortKey=2999-magvalue=value+10^(mag+1)endsortKey=string.format('%d',sortKey)..string.format('%015.0f',math.floor(value*10^(14-mag)))endlocalresultifsortable=='sortable_table'thenresult='data-sort-value="_SORTKEY_"|'elseifsortable=='sortable_debug'thenresult='<span data-sort-value="_SORTKEY_♠"><span style="border:1px solid">_SORTKEY_♠</span></span>'elseresult='<span data-sort-value="_SORTKEY_♠"></span>'endreturn(result:gsub('_SORTKEY_',sortKey))endendlocaltranslateParameters={abbr={off='abbr_off',on='abbr_on',},disp={age='disp_age',raw='disp_raw',},format={raw='format_raw',commas='format_commas',},round={on='on',yes='on',months='ym',weeks='ymw',days='ymd',hours='ymdh',},sep={comma='sep_comma',[',']='sep_comma',serialcomma='sep_serialcomma',space='sep_space',},show={hide={id='hide'},y={'y',id='y'},ym={'y','m',id='ym'},ymd={'y','m','d',id='ymd'},ymw={'y','m','w',id='ymw'},ymwd={'y','m','w','d',id='ymwd'},yd={'y','d',id='yd',keepZero=true},m={'m',id='m'},md={'m','d',id='md'},w={'w',id='w'},wd={'w','d',id='wd'},h={'H',id='h'},hm={'H','M',id='hm'},hms={'H','M','S',id='hms'},M={'M',id='M'},s={'S',id='s'},d={'d',id='d'},dh={'d','H',id='dh'},dhm={'d','H','M',id='dhm'},dhms={'d','H','M','S',id='dhms'},ymdh={'y','m','d','H',id='ymdh'},ymdhm={'y','m','d','H','M',id='ymdhm'},ymwdh={'y','m','w','d','H',id='ymwdh'},ymwdhm={'y','m','w','d','H','M',id='ymwdhm'},},sortable={off=false,on='sortable_on',table='sortable_table',debug='sortable_debug',},}localspellOptions={cardinal={},Cardinal={upper=true},cardinal_us={us=true},Cardinal_us={us=true,upper=true},ordinal={ordinal=true},Ordinal={ordinal=true,upper=true},ordinal_us={ordinal=true,us=true},Ordinal_us={ordinal=true,us=true,upper=true},}localfunctiondateExtract(frame)-- Return part of a date after performing an optional operation.localDate=getExports(frame)localargs=frame:getParent().argslocalparms={}fori,vinipairs(args)doparms[i]=vendifyes[args.fix]thentable.insert(parms,'fix')endifyes[args.partial]thentable.insert(parms,'partial')endlocalshow=stripToNil(args.show)or'dmy'localdate=Date(unpack(parms))ifnotdatethenifshow=='format'thenreturn'error'endreturnmessage('mt-need-valid-date')endlocaladd=stripToNil(args.add)ifaddthenforiteminadd:gmatch('%S+')dodate=date+itemifnotdatethenreturnmessage('mt-cannot-add',item)endendendlocalsortKey,resultlocalsortable=translateParameters.sortable[args.sortable]ifsortablethenlocalvalue=(date.partialanddate.partial.firstordate).jdzsortKey=makeSort(value,sortable)endifshow~='hide'thenresult=date[show]ifresult==nilthenresult=from_en(date:text(show))elseiftype(result)=='boolean'thenresult=resultand'1'or'0'elseresult=from_en(tostring(result))endendreturn(sortKeyor'')..makeExtra(args)..(resultor'')endlocalfunctionrangeJoin(range)-- Return text to be used between a range of ages.returnrange=='dash'and'–'ormtext['txt-or']endlocalfunctionmakeText(values,components,names,options,noUpper)-- Return wikitext representing an age or duration.localtext=Collection.new()localcount=#valueslocalsep=names.sepor''fori,vinipairs(values)do-- v is a number (say 4 for 4 years), or a table ({4,5} for 4 or 5 years).localislist=type(v)=='table'if(islistorv>0)or(text.n==0andi==count)or(text.n>0andcomponents.keepZero)thenlocalfmt,vstrifoptions.spellthenfmt=function(number)returnspellNumber(number,options.spell,noUpperori)endelseifi==1andoptions.format=='format_commas'then-- Numbers after the first should be small and not need formatting.fmt=formatNumberelsefmt=tostringendifislistthenvstr=fmt(v[1])..rangeJoin(options.range)noUpper=truevstr=vstr..fmt(v[2])elsevstr=fmt(v)endlocalname=names[components[i]]ifnametheniftype(name)=='table'thenname=mw.getContentLanguage():plural(islistandv[2]orv,name)endtext:add(vstr..sep..name)elsetext:add(vstr)endendendlocalfirst,lastifoptions.join=='sep_space'thenfirst=' 'last=' 'elseifoptions.join=='sep_comma'thenfirst=', 'last=', 'elseifoptions.join=='sep_serialcomma'andtext.n>2thenfirst=', 'last=mtext['txt-comma-and']elsefirst=', 'last=mtext['txt-and']endfori,vinipairs(text)doifi<text.nthentext[i]=v..(i+1<text.nandfirstorlast)endendlocalsign=''ifoptions.isnegativethen-- Do not display negative zero.iftext.n>1or(text.n==1andtext[1]:sub(1,1)~='0')thenifoptions.format=='format_raw'thensign='-'-- plain hyphen so result can be used in a calculationelsesign='−'-- Unicode U+2212 MINUS SIGNendendendreturn(options.sortKeyor'')..(options.extraor'')..sign..text:join()..(options.suffixor'')endlocalfunctiondateDifference(parms)-- Return a formatted date difference using the given parameters-- which have been validated.localnames={-- Each name is:-- * a string if no plural form of the name is used; or-- * a table of strings, one of which is selected using the rules at-- https://translatewiki.net/wiki/Plural/Mediawiki_plural_rulesabbr_off={sep=' ',y={'year','years'},m={'month','months'},w={'week','weeks'},d={'day','days'},H={'hour','hours'},M={'minute','minutes'},S={'second','seconds'},},abbr_on={y='y',m='m',w='w',d='d',H='h',M='m',S='s',},abbr_infant={-- for {{age for infant}}sep=' ',y={'yr','yrs'},m={'mo','mos'},w={'wk','wks'},d={'day','days'},H={'hr','hrs'},M={'min','mins'},S={'sec','secs'},},abbr_raw={},}localdiff=parms.diff-- must be a valid date differencelocalshow=parms.show-- may be nil; default is set belowlocalabbr=parms.abbror'abbr_off'localdefaultJoinifabbr~='abbr_off'thendefaultJoin='sep_space'endifnotshowthenshow='ymd'ifparms.disp=='disp_age'thenifdiff.years<3thendefaultJoin='sep_space'ifdiff.years>=1thenshow='ym'elseshow='md'endelseshow='y'endendendiftype(show)~='table'thenshow=translateParameters.show[show]endifparms.disp=='disp_raw'thendefaultJoin='sep_space'abbr='abbr_raw'elseifparms.wantScthendefaultJoin='sep_serialcomma'endlocaldiffOptions={round=parms.round,duration=parms.wantDuration,range=parms.rangeandtrueornil,}localsortKeyifparms.sortablethenlocalvalue=diff.age_days+(parms.wantDurationand1or0)-- days and fraction of a dayifdiff.isnegativethenvalue=-valueendsortKey=makeSort(value,parms.sortable)endlocaltextOptions={extra=parms.extra,format=parms.format,join=parms.sepordefaultJoin,isnegative=diff.isnegative,range=parms.range,sortKey=sortKey,spell=parms.spell,suffix=parms.suffix,-- not currently used}ifshow.id=='hide'thenreturnsortKeyor''endlocalvalues={diff:age(show.id,diffOptions)}ifvalues[1]thenreturnmakeText(values,show,names[abbr],textOptions)endifdiff.partialthen-- Handle a more complex range such as-- {{age_yd|20 Dec 2001|2003|range=yes}} → 1 year, 12 days or 2 years, 11 dayslocalopt={format=textOptions.format,join=textOptions.join,isnegative=textOptions.isnegative,spell=textOptions.spell,}return(textOptions.sortKeyor'')..makeText({diff.partial.mindiff:age(show.id,diffOptions)},show,names[abbr],opt)..rangeJoin(textOptions.range)..makeText({diff.partial.maxdiff:age(show.id,diffOptions)},show,names[abbr],opt,true)..(textOptions.suffixor'')endreturnmessage('mt-bad-show',show.id)endlocalfunctiongetDates(frame,getopt)-- Parse template parameters and return one of:-- * date (a date table, if single)-- * date1, date2 (two date tables, if not single)-- * text (a string error message)-- A missing date is optionally replaced with the current date.-- If wantMixture is true, a missing date component is replaced-- from the current date, so can get a bizarre mixture of-- specified/current y/m/d as has been done by some "age" templates.-- Some results may be placed in table getopt.localDate,currentDate=getExports(frame)getopt=getoptor{}localfunctionflagCurrent(text)-- This allows the calling template to detect if the current date has been used,-- that is, whether both dates have been entered in a template expecting two.-- For example, an infobox may want the age when an event occurred, not the current age.-- Don't bother detecting if wantMixture is used because not needed and it is a poor option.ifnottextthenifgetopt.noMissingthenreturnnil-- this gives a nil date which gives an errorendtext='currentdate'ifgetopt.flag=='usesCurrent'thengetopt.usesCurrent=trueendendreturntextendlocalargs=frame:getParent().argslocalfields={}localisNamed=args.yearorargs.year1orargs.year2orargs.monthorargs.month1orargs.month2orargs.dayorargs.day1orargs.day2ifisNamedthenfields[1]=args.year1orargs.yearfields[2]=args.month1orargs.monthfields[3]=args.day1orargs.dayfields[4]=args.year2fields[5]=args.month2fields[6]=args.day2elsefori=1,6dofields[i]=args[i]endendlocalimax=0fori=1,6dofields[i]=stripToNil(fields[i])iffields[i]thenimax=iendifgetopt.omitZeroandi%3~=1then-- omit zero months and days as unknown values but keep year 0 which is 1 BCEifisZero(fields[i])thenfields[i]=nilgetopt.partial=trueendendendlocalfix=getopt.fixand'fix'or''localpartialText=getopt.partialand'partial'or''localdates={}ifisNamedorimax>=3thenlocalnrDates=getopt.singleand1or2ifgetopt.wantMixturethen-- Cannot be partial since empty fields are set from current.localcomponents={'year','month','day'}fori=1,nrDates*3dofields[i]=fields[i]orcurrentDate[components[i>3andi-3ori]]endfori=1,nrDatesdolocalindex=i==1and1or4localy,m,d=fields[index],fields[index+1],fields[index+2]if(m==2orm=='2')and(d==29ord=='29')then-- Workaround error with following which attempt to use invalid date 2001-02-29.-- {{age_ymwd|year1=2001|year2=2004|month2=2|day2=29}}-- {{age_ymwd|year1=2001|month1=2|year2=2004|month2=1|day2=29}}-- TODO Get rid of wantMixture because even this ugly code does not handle-- 'Feb' or 'February' or 'feb' or 'february'.ifnot((y%4==0andy%100~=0)ory%400==0)thend=28endenddates[i]=Date(y,m,d)endelse-- If partial dates are allowed, accept-- year only, or-- year and month only-- Do not accept year and day without a month because that makes no sense-- (and because, for example, Date('partial', 2001, nil, 12) sets day = nil, not 12).fori=1,nrDatesdolocalindex=i==1and1or4localy,m,d=fields[index],fields[index+1],fields[index+2]if(getopt.partialandyand(mornotd))or(yandmandd)thendates[i]=Date(fix,partialText,y,m,d)elseifnotyandnotmandnotdthendates[i]=Date(flagCurrent())endendendelsegetopt.textdates=true-- have parsed each date from a single text fielddates[1]=Date(fix,partialText,flagCurrent(fields[1]))ifnotgetopt.singlethendates[2]=Date(fix,partialText,flagCurrent(fields[2]))endendifnotdates[1]thenreturnmessage(getopt.missing1or'mt-need-valid-ymd')endifgetopt.textdatesthengetopt.suppliedformat=dates[1].formatendifgetopt.singlethenreturndates[1]endifnotdates[2]thenreturnmessage(getopt.missing2or'mt-need-valid-ymd2')endreturndates[1],dates[2]endlocalfunctionageGeneric(frame)-- Return the result required by the specified template.-- Can use sortable=x where x = on/table/off/debug in any supported template.-- Some templates default to sortable=on but can be overridden.localname=frame.args.templateifnotnamethenreturnmessage('mt-template-x')endlocalargs=frame:getParent().argslocalspecs={age_days={-- {{age in days}}show='d',disp='disp_raw',},age_days_nts={-- {{age in days nts}}show='d',disp='disp_raw',format='format_commas',sortable='on',},duration_days={-- {{duration in days}}show='d',disp='disp_raw',duration=true,},duration_days_nts={-- {{duration in days nts}}show='d',disp='disp_raw',format='format_commas',sortable='on',duration=true,},age_full_years={-- {{age}}show='y',abbr='abbr_raw',flag='usesCurrent',omitZero=true,range='dash',},age_full_years_nts={-- {{age nts}}show='y',abbr='abbr_raw',format='format_commas',sortable='on',},age_in_years={-- {{age in years}}show='y',abbr='abbr_raw',negative='error',range='dash',},age_in_years_nts={-- {{age in years nts}}show='y',abbr='abbr_raw',negative='error',range='dash',format='format_commas',sortable='on',},age_infant={-- {{age for infant}}-- Do not set show because special processing is done later.abbr=yes[args.abbr]and'abbr_infant'or'abbr_off',disp='disp_age',sep='sep_space',sortable='on',},age_m={-- {{age in months}}show='m',disp='disp_raw',},age_w={-- {{age in weeks}}show='w',disp='disp_raw',},age_wd={-- {{age in weeks and days}}show='wd',},age_yd={-- {{age in years and days}}show='yd',format='format_commas',sep=args.sep~='and'and'sep_comma'ornil,},age_yd_nts={-- {{age in years and days nts}}show='yd',format='format_commas',sep=args.sep~='and'and'sep_comma'ornil,sortable='on',},age_ym={-- {{age in years and months}}show='ym',sep='sep_comma',},age_ymd={-- {{age in years, months and days}}show='ymd',range=true,},age_ymwd={-- {{age in years, months, weeks and days}}show='ymwd',wantMixture=true,},}localspec=specs[name]ifnotspecthenreturnmessage('mt-template-bad-name')endifname=='age_days'thenlocalsu=stripToNil(args['show unit'])ifsuthenifsu=='abbr'orsu=='full'thenspec.disp=nilspec.abbr=su=='abbr'and'abbr_on'ornilendendendlocalpartial,autofilllocalrange=stripToNil(args.range)orspec.rangeifrangethen-- Suppose partial dates are used and age could be 11 or 12 years.-- "|range=" (empty value) has no effect (spec is used).-- "|range=yes" or spec.range == true sets range = true (gives "11 or 12")-- "|range=dash" or spec.range == 'dash' sets range = 'dash' (gives "11–12").-- "|range=no" or spec.range == 'no' sets range = nil and fills each date in the diff (gives "12").-- ("on" is equivalent to "yes", and "off" is equivalent to "no").-- "|range=OTHER" sets range = nil and rejects partial dates.range=({dash='dash',off='no',no='no',[true]=true})[range]oryes[range]ifrangethenpartial=true-- accept partial dates with a possible age range for the resultifrange=='no'thenautofill=true-- missing month/day in first or second date are filled from other date or 1range=nilendendendlocalgetopt={fix=yes[args.fix],flag=stripToNil(args.flag)orspec.flag,omitZero=spec.omitZero,partial=partial,wantMixture=spec.wantMixture,}localdate1,date2=getDates(frame,getopt)iftype(date1)=='string'thenreturndate1endlocalformat=stripToNil(args.format)localspell=spellOptions[format]ifformatthenformat='format_'..formatelseifname=='age_days'andgetopt.textdatesthenformat='format_commas'endlocalparms={diff=date2:subtract(date1,{fill=autofill}),wantDuration=spec.durationoryes[args.duration],range=range,wantSc=yes[args.sc],show=args.show=='hide'and'hide'orspec.show,abbr=spec.abbr,disp=spec.disp,extra=makeExtra(args,getopt.usesCurrentandformat~='format_raw'),format=formatorspec.format,round=yes[args.round],sep=spec.sep,sortable=translateParameters.sortable[args.sortableorspec.sortable],spell=spell,}if(spec.negativeorframe.args.negative)=='error'andparms.diff.isnegativethenreturnmessage('mt-date-wrong-order')endreturnfrom_en(dateDifference(parms))endlocalfunctionisFake(args)-- Some templates have TemplateData with an auto value like "{{Birth date and age|YYYY|MM|DD}}".-- Return true if that appears to be the case so the caller can output nothing rather than an error.returnargs[1]=='YYYY'endlocalfunctionbda(frame)-- Implement [[Template:Birth date and age]].localargs=frame:getParent().argsifisFake(args)thenreturn''endlocaloptions={missing1='mt-need-valid-bd',noMissing=true,single=true,}localdate=getDates(frame,options)iftype(date)=='string'thenreturndate-- error textendlocalDate=getExports(frame)localdiff=Date('currentdate')-dateifdiff.isnegativeordiff.years>MAX_AGEthenreturnmessage('mt-invalid-bd-age')endlocaldisp=mtext['txt-bda-disp']localshow='y'ifdiff.years<2thendisp='disp_age'ifdiff.years==0anddiff.months==0thenshow='d'elseshow='m'endendlocalproblem,format=dateFormat(args,options)localresult=substituteParameters(mtext['txt-bda'],date:text('%-Y-%m-%d'),from_en(date:text(format)),from_en(dateDifference({diff=diff,show=show,abbr='abbr_off',disp=disp,sep='sep_space',})))..(problemor'')localwarnings=tonumber(frame.args.warnings)ifwarningsandwarnings>0thenlocalgood={df=true,mf=true,day=true,day1=true,month=true,month1=true,year=true,year1=true,}localinvalidlocalimax=options.textdatesand1or3fork,_inpairs(args)doiftype(k)=='number'thenifk>imaxtheninvalid=tostring(k)breakendelseifnotgood[k]theninvalid=kbreakendendendifinvalidthenresult=result..message('mt-warn-param1',invalid)endendreturnresultendlocalfunctiondda(frame)-- Implement [[Template:Death date and age]].localargs=frame:getParent().argsifisFake(args)thenreturn''endlocaloptions={missing1='mt-need-valid-dd',missing2='mt-need-valid-bd2',noMissing=true,partial=true,}localdate1,date2=getDates(frame,options)iftype(date1)=='string'thenreturndate1endlocaldiff=date1-date2ifdiff.isnegativethenreturnmessage('mt-dd-wrong-order')endlocalDate=getExports(frame)localtoday=Date('currentdate')+1-- one day in future allows for timezonesifdate1>todaythenreturnmessage('mt-dd-future')endlocalyearsifdiff.partialthenyears=diff.partial.yearsyears=type(years)=='table'andyears[2]oryearselseyears=diff.yearsendifyears>MAX_AGEthenreturnmessage('mt-invalid-dates-age')endlocalfmt_date,fmt_ymd,problemifdate1.daythen-- y, m, d knownproblem,fmt_date=dateFormat(args,options)fmt_ymd='%-Y-%m-%d'elseifdate1.monththen-- y, m known; d unknownfmt_date='%B %-Y'fmt_ymd='%-Y-%m-00'else-- y known; m, d unknownfmt_date='%-Y'fmt_ymd='%-Y-00-00'endlocalsortKeylocalsortable=translateParameters.sortable[args.sortable]ifsortablethenlocalvalue=(date1.partialanddate1.partial.firstordate1).jdzsortKey=makeSort(value,sortable)endlocalresult=(sortKeyor'')..substituteParameters(mtext['txt-dda'],date1:text(fmt_ymd),from_en(date1:text(fmt_date)),from_en(dateDifference({diff=diff,show='y',abbr='abbr_off',disp=mtext['txt-dda-disp'],range='dash',sep='sep_space',})))..(problemor'')localwarnings=tonumber(frame.args.warnings)ifwarningsandwarnings>0thenlocalgood={df=true,mf=true,}localinvalidlocalimax=options.textdatesand2or6fork,_inpairs(args)doiftype(k)=='number'thenifk>imaxtheninvalid=tostring(k)breakendelseifnotgood[k]theninvalid=kbreakendendendifinvalidthenresult=result..message('mt-warn-param1',invalid)endendreturnresultendlocalfunctiondateToGsd(frame)-- Implement [[Template:Gregorian serial date]].-- Return Gregorian serial date of the given date, or the current date.-- The returned value is negative for dates before 1 January 1 AD-- despite the fact that GSD is not defined for such dates.localdate=getDates(frame,{wantMixture=true,single=true})iftype(date)=='string'thenreturndateendreturntostring(date.gsd)endlocalfunctionjdToDate(frame)-- Return formatted date from a Julian date.-- The result includes a time if the input includes a fraction.-- The word 'Julian' is accepted for the Julian calendar.localDate=getExports(frame)localargs=frame:getParent().argslocaldate=Date('juliandate',args[1],args[2])ifdatethenreturnfrom_en(date:text())endreturnmessage('mt-need-jdn')endlocalfunctiondateToJd(frame)-- Return Julian date (a number) from a date which may include a time,-- or the current date ('currentdate') or current date and time ('currentdatetime').-- The word 'Julian' is accepted for the Julian calendar.localDate=getExports(frame)localargs=frame:getParent().argslocaldate=Date(args[1],args[2],args[3],args[4],args[5],args[6],args[7])ifdatethenreturntostring(date.jd)endreturnmessage('mt-need-valid-ymd-current')endlocalfunctiontimeInterval(frame)-- Implement [[Template:Time interval]].-- There are two positional arguments: date1, date2.-- The default for each is the current date and time.-- Result is date2 - date1 formatted.localDate=getExports(frame)localargs=frame:getParent().argslocalparms={extra=makeExtra(args),wantDuration=yes[args.duration],range=yes[args.range]or(args.range=='dash'and'dash'ornil),wantSc=yes[args.sc],}localfix=yes[args.fix]and'fix'or''localdate1=Date(fix,'partial',stripToNil(args[1])or'currentdatetime')ifnotdate1thenreturnmessage('mt-invalid-start')endlocaldate2=Date(fix,'partial',stripToNil(args[2])or'currentdatetime')ifnotdate2thenreturnmessage('mt-invalid-end')endparms.diff=date2-date1forargname,translateinpairs(translateParameters)dolocalparm=stripToNil(args[argname])ifparmthenparm=translate[parm]ifparm==nilthen-- test for nil because false is a valid settingreturnmessage('mt-bad-param2',argname,args[argname])endparms[argname]=parmendendifparms.roundthenlocalround=parms.roundlocalshow=parms.showifround~='on'thenifshowthenifshow.id~=roundthenreturnmessage('mt-conflicting-show',args.show,args.round)endelseparms.show=translateParameters.show[round]endendparms.round=trueendreturnfrom_en(dateDifference(parms))endlocalfunctiontemplateGeneric(frame)-- Example: {{#invoke:age||template=age_days|1 Apr 1980|1 Apr 2080|format=raw}} → 36525localname=frame.args.templateifnotnamethenreturnmessage('mt-template-x')endreturnageGeneric(frame:newChild({title=mw.title.new(name,10),args=frame.args}))endreturn{age_generic=ageGeneric,-- can emulate several age templatesbirth_date_and_age=bda,-- Template:Birth_date_and_agedeath_date_and_age=dda,-- Template:Death_date_and_agegsd=dateToGsd,-- Template:Gregorian_serial_dateextract=dateExtract,-- Template:Extractjd_to_date=jdToDate,-- Template:?JULIANDAY=dateToJd,-- Template:JULIANDAYtime_interval=timeInterval,-- Template:Time_interval['']=templateGeneric,-- same as age_generic, but can be invoked directly}