Module:Age/sandbox: Difference between revisions

Content deleted Content added
require Module:Date/sandbox when invoked from a sandbox template
strict checking of df=xxx parameter value to reduce wikitext junk; see Module talk:Age#df and mf with bad values
 
(62 intermediate revisions by 8 users not shown)
Line 1:
-- Implement various "age of" and other date-related templates.
--[[ TODO
Check the "This implements" functions to see what parameters are accepted by the
existing template. The code here might not be emulating what the template does.
Some functions have not been tested yet.
These features have not yet been implemented:
* Some templates accept an ambiguous date with a missing component.
* Some templates provide a hidden sort key.
* Some templates have extra parameters not yet implemented.
* {{age in years}} Age as a range (month/day unknown).
* {{age in months}}
* {{age in weeks and days}}
* {{age in weeks}}
]]
 
local _Date,mtext _current_date= {
-- Message and other text that should be localized.
local function get_exports(frame)
-- 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:Age error',
['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&nbsp;$3)</span>',
['txt-dda'] = '$2<span style="display:none">($1)</span> (aged&nbsp;$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',
}
 
local isWarning = {
['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.
local yes = mtext['txt-yes']
 
local translate, from_en, to_en, isZero
if translate then
-- Functions to translate from en to local language and reverse go here.
-- See example at [[:bn:Module:বয়স]].
else
from_en = function (text)
return text
end
isZero = function (text)
return tonumber(text) == 0
end
end
 
local _Date, _currentDate
local function getExports(frame)
-- Return objects exported from the date module or its sandbox.
if not _Date then
local sandbox = frame:getTitle():find(mtext['txt-sandbox'], 1, true) and ('/' .. mtext['txt-sandbox']) or ''
local datemod = require(mtext['Module:Datetxt-module-date'] .. sandbox)
_Datelocal realDate = datemod._Date
_current_date_currentDate = datemod._current
if to_en then
_Date = function (...)
local args = {}
for i, v in ipairs({...}) do
args[i] = to_en(v)
end
return realDate(unpack(args))
end
else
_Date = realDate
end
end
return _Date, _current_date_currentDate
end
 
local Collection -- a table to hold items
local function collection()
Collection = {
-- Return a table to hold items.
add = function (self, item)
return {
nif item ~= 0,nil then
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
end,
join = function (self, sep)
return join = function table.concat(self, sep)
return table.concat(self, sep)
end,
end,
}
remove = function (self, pos)
end
if self.n > 0 and (pos == nil or (0 < pos and pos <= self.n)) then
self.n = self.n - 1
return table.remove(self, pos)
end
end,
sort = function (self, comp)
table.sort(self, comp)
end,
new = function ()
return setmetatable({n = 0}, Collection)
end
}
Collection.__index = Collection
 
local function strip_to_nilstripToNil(text)
-- If text is a string, return its trimmed content, or nil if empty.
-- Otherwise return text (which may, for example, be nil).
Line 49 ⟶ 127:
end
 
local function yessubstituteParameters(parametertext, ...)
-- Return truetext ifafter parametersubstituting shouldany begiven interpretedparameters asfor "yes"$1, $2, etc.
return mw.message.newRawMessage(text, ...):plain()
-- Do not want to accept mixed upper/lowercase unless done by current templates.
-- Need to accept "on" because "round=on" is wanted.
return ({ y = true, yes = true, on = true })[parameter]
end
 
local function message(msg, nocat...)
-- Return formatted message text for an error or warning.
local function getText(msg)
-- Can append "#FormattingError" to URL of a page with a problem to find it.
return mtext[msg] or error('Bug: message "' .. tostring(msg) .. '" not defined')
local anchor = '<span id="FormattingError" />'
end
local category
local categories = {
if not nocat and mw.title.getCurrentTitle():inNamespaces(0, 10) then
error = mtext['txt-category'],
-- Category only in namespaces: 0=article, 10=template.
warning = mtext['txt-category'],
category = '[[Category:Age error]]'
}
local a, b, k, category
local text = substituteParameters(getText(msg), ...)
if isWarning[msg] then
a = '<sup>[<i>'
b = '</i>]</sup>'
k = 'warning'
else
a = '<strong class="error">' .. getText('txt-error')
category = ''
b = '</strong>'
k = 'error'
end
if mw.title.getCurrentTitle():inNamespaces(0) then
-- Category only in namespaces: 0=article.
category = '[[' .. categories[k] .. ']]'
end
return anchor ..
a ..
'<strong class="error">Error: ' ..
mw.text.nowiki(msgtext) ..
'</strong>'b ..
(category or '')
end
 
local function make_textdateFormat(values, components, names, optionsargs)
-- Return
-- nil, f if parameter is valid
-- m, f otherwise
-- where
-- m = string for warning message with category
-- f = string for wanted date format
local problem
local wanted = mtext['txt-format-default']
local other = wanted == 'df' and 'mf' or 'df'
local parm = args[other] or ''
if mtext['txt-affirmative'][parm] then
wanted = other
elseif parm ~= '' then
problem = message('mt-warn-param2', other, parm)
end
return problem, wanted == 'df' and mtext['txt-dmy'] or mtext['txt-mdy']
end
 
local function formatNumber(number)
-- Return the given number formatted with commas as group separators,
-- given that the number is an integer.
local numstr = tostring(number)
local length = #numstr
local places = Collection.new()
local pos = 0
repeat
places:add(pos)
pos = pos + 3
until pos >= length
places:add(length)
local groups = Collection.new()
for i = places.n, 2, -1 do
local p1 = length - places[i] + 1
local p2 = length - places[i - 1]
groups:add(numstr:sub(p1, p2))
end
return groups:join(',')
end
 
local function spellNumber(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)
return require(mtext['txt-module-convertnumeric']).spell_number(
number,
nil, -- fraction numerator
nil, -- fraction denominator
i == 1 and options.upper, -- true: 'One' instead of 'one'
not options.us, -- true: use 'and' between tens/ones etc
options.adj, -- true: hyphenated
options.ordinal -- true: 'first' instead of 'one'
) or number
end
 
local function makeExtra(args, flagCurrent)
-- Return extra text that will be inserted before the visible result
-- but after any sort key.
local extra = args.prefix or ''
if mw.ustring.len(extra) > 1 then
-- Parameter "~" gives "~3" whereas "over" gives "over 3".
if extra:sub(-6, -1) ~= '&nbsp;' then
extra = extra .. ' '
end
end
if flagCurrent then
extra = '<span class="currentage"></span>' .. extra
end
return extra
end
 
local function makeSort(value, sortable)
-- Return a sort key if requested.
-- Assume value is a valid number which has not overflowed.
if sortable == 'sortable_table' or sortable == 'sortable_on' or sortable == 'sortable_debug' then
local sortKey
if value == 0 then
sortKey = '5000000000000000000'
else
local mag = math.floor(math.log10(math.abs(value)) + 1e-14)
if value > 0 then
sortKey = 7000 + mag
else
sortKey = 2999 - mag
value = value + 10^(mag+1)
end
sortKey = string.format('%d', sortKey) .. string.format('%015.0f', math.floor(value * 10^(14-mag)))
end
local result
if sortable == 'sortable_table' then
result = 'data-sort-value="_SORTKEY_"|'
elseif sortable == 'sortable_debug' then
result = '<span data-sort-value="_SORTKEY_♠"><span style="border:1px solid">_SORTKEY_♠</span></span>'
else
result = '<span data-sort-value="_SORTKEY_♠"></span>'
end
return (result:gsub('_SORTKEY_', sortKey))
end
end
 
local translateParameters = {
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',
},
}
 
local spellOptions = {
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 },
}
 
local function dateExtract(frame)
-- Return part of a date after performing an optional operation.
local Date = getExports(frame)
local args = frame:getParent().args
local parms = {}
for i, v in ipairs(args) do
parms[i] = v
end
if yes[args.fix] then
table.insert(parms, 'fix')
end
if yes[args.partial] then
table.insert(parms, 'partial')
end
local show = stripToNil(args.show) or 'dmy'
local date = Date(unpack(parms))
if not date then
if show == 'format' then
return 'error'
end
return message('mt-need-valid-date')
end
local add = stripToNil(args.add)
if add then
for item in add:gmatch('%S+') do
date = date + item
if not date then
return message('mt-cannot-add', item)
end
end
end
local sortKey, result
local sortable = translateParameters.sortable[args.sortable]
if sortable then
local value = (date.partial and date.partial.first or date).jdz
sortKey = makeSort(value, sortable)
end
if show ~= 'hide' then
result = date[show]
if result == nil then
result = from_en(date:text(show))
elseif type(result) == 'boolean' then
result = result and '1' or '0'
else
result = from_en(tostring(result))
end
end
return (sortKey or '') .. makeExtra(args) .. (result or '')
end
 
local function rangeJoin(range)
-- Return text to be used between a range of ages.
return range == 'dash' and '–' or mtext['txt-or']
end
 
local function makeText(values, components, names, options, noUpper)
-- Return wikitext representing an age or duration.
local text = collectionCollection.new()
local count = #values
local plural = names.plural or ''
local sep = names.sep or ''
for i, v in ipairs(values) do
if-- v >is 0a ornumber (text.nsay ==4 0for and4 i == countyears), or a table (text.n{4,5} >for 04 andor 5 componentsyears).keepzero) then
local islist = type(v) == 'table'
if (islist or v > 0) or (text.n == 0 and i == count) or (text.n > 0 and components.keepZero) then
local fmt, vstr
if options.spell then
fmt = function(number)
return spellNumber(number, options.spell, noUpper or i)
end
elseif i == 1 and options.format == 'format_commas' then
-- Numbers after the first should be small and not need formatting.
fmt = formatNumber
else
fmt = tostring
end
if islist then
vstr = fmt(v[1]) .. rangeJoin(options.range)
noUpper = true
vstr = vstr .. fmt(v[2])
else
vstr = fmt(v)
end
local name = names[components[i]]
if name then
text:addif type(v .. sep .. name .. (v) == 1 and 'table' or plural))then
name = mw.getContentLanguage():plural(islist and v[2] or v, name)
end
text:add(vstr .. sep .. name)
else
text:add(tostring(v)vstr)
end
end
Line 99 ⟶ 438:
elseif options.join == 'sep_serialcomma' and text.n > 2 then
first = ', '
last = mtext[', txt-comma-and ']
else
first = ', '
last = mtext[' txt-and ']
end
for i, v in ipairs(text) do
Line 111 ⟶ 450:
local sign = ''
if options.isnegative then
-- Do not showdisplay negative zero.
if text.n > 1 or (text.n == 1 and text[1]:sub(1, 1) ~= '0' ) then
if options.format == 'format_raw' then
sign = '−' -- Unicode U+2212 MINUS SIGN
sign = '-' -- plain hyphen so result can be used in a calculation
else
sign = '−' -- Unicode U+2212 MINUS SIGN
end
end
end
return
return sign .. text:join() .. (options.append or '')
(options.sortKey or '') ..
(options.extra or '') ..
sign ..
text:join() ..
(options.suffix or '')
end
 
local function dateDifference(parms)
local show_components = {
y = { 'y' },
ym = { 'y', 'm' },
ymd = { 'y', 'm', 'd' },
ymw = { 'y', 'm', 'w' },
ymwd = { 'y', 'm', 'w', 'd' },
yd = { 'y', 'd', keepzero = true },
m = { 'm' },
md = { 'm', 'd' },
w = { 'w' },
wd = { 'w', 'd' },
d = { 'd' },
dh = { 'd', 'H' },
dhm = { 'd', 'H', 'M' },
ymdh = { 'y', 'm', 'd', 'H' },
ymdhm = { 'y', 'm', 'd', 'H', 'M' },
ymwdh = { 'y', 'm', 'w', 'd', 'H' },
ymwdhm = { 'y', 'm', 'w', 'd', 'H', 'M' },
}
 
local function age_generic(parms)
-- Return a formatted date difference using the given parameters
-- which have been validated.
-- TODO Handle 'partial' dates, for example, a year only.
local names = {
-- 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_rules
abbr_off = {
plural = 's',
sep = '&nbsp;',
y = {'year', 'years'},
m = {'month', 'months'},
w = {'week', 'weeks'},
d = {'day', 'days'},
H = {'hour', 'hours'},
M = {'minute', 'minutes'},
S = {'second', 'seconds'},
},
abbr_on = {
plural = 's',
sep = '&nbsp;',
y = 'yr',
m = 'mo',
w = 'wk',
d = 'day',
H = 'hr',
M = 'min',
},
abbr_letter = {
y = 'y',
m = 'm',
Line 171 ⟶ 492:
H = 'h',
M = 'm',
S = 's',
},
abbr_infant = { -- for {{age for infant}}
sep = '&nbsp;',
y = {'yr', 'yrs'},
m = {'mo', 'mos'},
w = {'wk', 'wks'},
d = {'day', 'days'},
H = {'hr', 'hrs'},
M = {'min', 'mins'},
S = {'sec', 'secs'},
},
abbr_raw = {},
Line 176 ⟶ 508:
local diff = parms.diff -- must be a valid date difference
local show = parms.show -- may be nil; default is set below
local xabbrabbr = parms.xabbrabbr or 'abbr_off'
local append, default_joindefaultJoin
if xabbrabbr ~= 'abbr_off' then
default_joindefaultJoin = 'sep_space'
end
if not show then
show = 'ymd'
if parms.xdispdisp == 'disp_age' then
if diff.years < 3 then
default_joindefaultJoin = 'sep_space'
if diff.years >= 1 then
show = 'ym'
Line 192 ⟶ 524:
end
else
-- TODO Have commented out following as templates do not seem to do this.
-- append = ' old'
show = 'y'
end
end
end
if parms.xdisptype(show) =~= 'disp_rawtable' then
show = translateParameters.show[show]
default_join = 'sep_space'
xabbr = 'abbr_raw'
elseif parms.want_sc then
default_join = 'sep_serialcomma'
end
if parms.disp == 'disp_raw' then
local options = {
defaultJoin = 'sep_space'
append = append,
abbr = 'abbr_raw'
join = parms.xsep or default_join,
elseif parms.wantSc then
defaultJoin = 'sep_serialcomma'
end
local diffOptions = {
round = parms.round,
duration = parms.wantDuration,
range = parms.range and true or nil,
}
local sortKey
if parms.sortable then
local value = diff.age_days + (parms.wantDuration and 1 or 0) -- days and fraction of a day
if diff.isnegative then
value = -value
end
sortKey = makeSort(value, parms.sortable)
end
local textOptions = {
extra = parms.extra,
format = parms.format,
join = parms.sep or defaultJoin,
isnegative = diff.isnegative,
range = parms.range,
sortKey = sortKey,
spell = parms.spell,
suffix = parms.suffix, -- not currently used
}
if show.id == 'hide' then
local values = { diff:age(show, parms.want_round, parms.want_duration) }
return sortKey or ''
return make_text(values, show_components[show], names[xabbr], options)
end
local values = { diff:age(show.id, diffOptions) }
if values[1] then
return makeText(values, show, names[abbr], textOptions)
end
if diff.partial then
-- Handle a more complex range such as
-- {{age_yd|20 Dec 2001|2003|range=yes}} → 1 year, 12 days or 2 years, 11 days
local opt = {
format = textOptions.format,
join = textOptions.join,
isnegative = textOptions.isnegative,
spell = textOptions.spell,
}
return
(textOptions.sortKey or '') ..
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.suffix or '')
end
return message('mt-bad-show', show.id)
end
 
local function get_datesgetDates(frame, want_mixture, singlegetopt)
-- 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 want_mixturewantMixture 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.
local Date, current_date = get_exports(frame)
local Date, currentDate = getExports(frame)
getopt = getopt or {}
local function flagCurrent(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.
if not text then
if getopt.noMissing then
return nil -- this gives a nil date which gives an error
end
text = 'currentdate'
if getopt.flag == 'usesCurrent' then
getopt.usesCurrent = true
end
end
return text
end
local args = frame:getParent().args
local fields = {}
local is_namedisNamed = args.year or args.year1 or args.year2 or
args.month or args.month1 or args.month2 or
if is_named then
args.day or args.day1 or args.day2
if isNamed then
fields[1] = args.year1 or args.year
fields[2] = args.month1 or args.month
Line 240 ⟶ 633:
local imax = 0
for i = 1, 6 do
fields[i] = strip_to_nilstripToNil(fields[i])
if fields[i] then
imax = i
end
if getopt.omitZero and i % 3 ~= 1 then -- omit zero months and days as unknown values but keep year 0 which is 1 BCE
if isZero(fields[i]) then
fields[i] = nil
getopt.partial = true
end
end
end
local fix = getopt.fix and 'fix' or ''
local partialText = getopt.partial and 'partial' or ''
local dates = {}
if is_namedisNamed or imax >= 23 then
local nr_datesnrDates = getopt.single and 1 or 2
if want_mixturegetopt.wantMixture then
-- Cannot be partial since empty fields are set from current.
local components = { 'year', 'month', 'day' }
for i = 1, nr_datesnrDates * 3 do
fields[i] = fields[i] or current_datecurrentDate[components[i > 3 and i - 3 or i]]
end
for i = 1, nr_datesnrDates do
local index = i == 1 and 1 or 4
dates[i]local y, m, d = Date(fields[index], fields[index+1], fields[index+2])
if (m == 2 or m == '2') and (d == 29 or d == '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'.
if not ((y % 4 == 0 and y % 100 ~= 0) or y % 400 == 0) then
d = 28
end
end
dates[i] = Date(y, m, d)
end
else
-- If partial dates are allowed, accept
for i = 1, nr_dates do
-- 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).
for i = 1, nrDates do
local index = i == 1 and 1 or 4
local y, m, d = fields[index], fields[index+1], fields[index+2]
if (getopt.partial and y and (m or not d)) or (y and m and d) then
dates[i] = Date(fix, partialText, y, m, d)
elseif not (y orand not m orand not d) then
dates[i] = Date('currentdate'flagCurrent())
end
end
end
else
getopt.textdates = true -- have parsed each date from a single text field
dates[1] = Date(fields[1] or 'currentdate')
dates[1] = Date(fix, partialText, flagCurrent(fields[1]))
if not single then
if not getopt.single then
dates[2] = Date(fields[2] or 'currentdate')
dates[2] = Date(fix, partialText, flagCurrent(fields[2]))
end
end
if not dates[1] then
return message(getopt.missing1 or 'Need mt-need-valid year, month, day-ymd')
end
if getopt.single then
return dates[1]
end
if not dates[2] then
return message('Secondgetopt.missing2 dateor should be year, month, day'mt-need-valid-ymd2')
end
return dates[1], dates[2]
end
 
local function ageGeneric(frame)
-- LATER: If decide to use call_generic, probably do not need all the functions
-- Return the result required by the specified template.
-- which call it as a template such as {{age}} could invoke call_generic with
-- Can use sortable=x where x = on/table/off/debug in any supported template.
-- a parameter like "template=age_full_years".
-- Some templates default to sortable=on but can be overridden.
local function call_generic(frame, name)
local name = frame.args.template
-- Return the result required by the template identified by name.
if not name then
return message('mt-template-x')
end
local args = frame:getParent().args
local specs = {
age_days = { -- {{age in days}}
show = 'd',
xdispdisp = 'disp_raw',
},
age_days_nts = { -- {{age in days nts}}
age_full_years = {
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',
xabbrabbr = '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}}
age_infant = {
show = 'wd',
-- Do not set show as special processing occurs later.
xabbr = yes(args.abbr) and 'abbr_on' or 'abbr_off',
xdisp = 'disp_age',
xsep = 'sep_space',
},
age_yd = { -- {{age in years and days}}
show = 'yd',
format = 'format_commas',
xsep = args.sep ~= 'and' and 'sep_comma' or nil,
sep = args.sep ~= 'and' and 'sep_comma' or nil,
},
age_yd_nts = { -- {{age in years and days nts}}
age_ym = {
show = 'yd',
format = 'format_commas',
sep = args.sep ~= 'and' and 'sep_comma' or nil,
sortable = 'on',
},
age_ym = { -- {{age in years and months}}
show = 'ym',
xsepsep = 'sep_comma',
},
age_ymd = { -- {{age in years, months and days}}
age_ymd = {
show = 'ymd',
want_mixturerange = true,
},
age_ymwd = { -- {{age in years, months, weeks and days}}
age_ymwd = {
show = 'ymwd',
wantMixture = true,
},
}
local spec = specs[name]
if not spec then
local date1, date2 = get_dates(frame, spec.want_mixture)
return message('mt-template-bad-name')
end
if name == 'age_days' then
local su = stripToNil(args['show unit'])
if su then
if su == 'abbr' or su == 'full' then
spec.disp = nil
spec.abbr = su == 'abbr' and 'abbr_on' or nil
end
end
end
local partial, autofill
local range = stripToNil(args.range) or spec.range
if range then
-- 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] or yes[range]
if range then
partial = true -- accept partial dates with a possible age range for the result
if range == 'no' then
autofill = true -- missing month/day in first or second date are filled from other date or 1
range = nil
end
end
end
local getopt = {
fix = yes[args.fix],
flag = stripToNil(args.flag) or spec.flag,
omitZero = spec.omitZero,
partial = partial,
wantMixture = spec.wantMixture,
}
local date1, date2 = getDates(frame, getopt)
if type(date1) == 'string' then
return date1
end
local format = stripToNil(args.format)
local spell = spellOptions[format]
if format then
format = 'format_' .. format
elseif name == 'age_days' and getopt.textdates then
format = 'format_commas'
end
local parms = {
diff = date2:subtract(date1, -{ date1fill = autofill }),
want_durationwantDuration = spec.duration or yes([args.duration)],
range = range,
want_round = yes(args.round),
want_scwantSc = yes([args.sc)],
show = args.show == 'hide' and 'hide' or spec.show,
xabbrabbr = spec.xabbrabbr,
xdispdisp = spec.xdispdisp,
extra = makeExtra(args, getopt.usesCurrent and format ~= 'format_raw'),
xsep = spec.xsep,
format = format or spec.format,
round = yes[args.round],
sep = spec.sep,
sortable = translateParameters.sortable[args.sortable or spec.sortable],
spell = spell,
}
if (spec.negative or frame.args.negative) == 'error' and parms.diff.isnegative then
return age_generic(parms)
return message('mt-date-wrong-order')
end
return from_en(dateDifference(parms))
end
 
local function age_daysisFake(frameargs)
-- Some templates have TemplateData with an auto value like "{{Birth date and age|YYYY|MM|DD}}".
-- This implements {{age in days}}.
-- Return true if that appears to be the case so the caller can output nothing rather than an error.
return call_generic(frame, 'age_days')
return args[1] == 'YYYY'
end
 
local function age_full_yearsbda(frame)
-- ThisImplement implements[[Template:Birth date and {{age}}]].
local args = frame:getParent().args
return call_generic(frame, 'age_full_years')
if isFake(args) then
end
return ''
 
end
local function age_infant(frame)
local options = {
-- This implements {{age for infant}}.
missing1 = 'mt-need-valid-bd',
return call_generic(frame, 'age_infant')
noMissing = true,
single = true,
}
local date = getDates(frame, options)
if type(date) == 'string' then
return date -- error text
end
local Date = getExports(frame)
local diff = Date('currentdate') - date
if diff.isnegative or diff.years > 150 then
return message('mt-invalid-bd-age')
end
local disp = mtext['txt-bda-disp']
local show = 'y'
if diff.years < 2 then
disp = 'disp_age'
if diff.years == 0 and diff.months == 0 then
show = 'd'
else
show = 'm'
end
end
local problem, format = dateFormat(args)
local result = 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',
}))
) .. (problem or '')
local warnings = tonumber(frame.args.warnings)
if warnings and warnings > 0 then
local good = {
df = true,
mf = true,
day = true,
day1 = true,
month = true,
month1 = true,
year = true,
year1 = true,
}
local invalid
local imax = options.textdates and 1 or 3
for k, _ in pairs(args) do
if type(k) == 'number' then
if k > imax then
invalid = tostring(k)
break
end
else
if not good[k] then
invalid = k
break
end
end
end
if invalid then
result = result .. message('mt-warn-param1', invalid)
end
end
return result
end
 
local function age_yddda(frame)
-- ThisImplement implements[[Template:Death {{age in yearsdate and days}}age]].
local args = frame:getParent().args
return call_generic(frame, 'age_yd')
if isFake(args) then
end
return ''
 
end
local function age_ym(frame)
local options = {
-- This implements {{age in years and months}}.
missing1 = 'mt-need-valid-dd',
return call_generic(frame, 'age_ym')
missing2 = 'mt-need-valid-bd2',
end
noMissing = true,
 
partial = true,
local function age_ymd(frame)
}
-- This implements {{age in years, months and days}}.
returnlocal call_genericdate1, date2 = getDates(frame, 'age_ymd'options)
if type(date1) == 'string' then
end
return date1
 
end
local function age_ymwd(frame)
local diff = date1 - date2
-- This implements {{age in years, months, weeks and days}}.
if diff.isnegative then
return call_generic(frame, 'age_ymwd')
return message('mt-dd-wrong-order')
end
local Date = getExports(frame)
local today = Date('currentdate') + 1 -- one day in future allows for timezones
if date1 > today then
return message('mt-dd-future')
end
local years
if diff.partial then
years = diff.partial.years
years = type(years) == 'table' and years[2] or years
else
years = diff.years
end
if years > 150 then
return message('mt-invalid-dates-age')
end
local fmt_date, fmt_ymd, problem
if date1.day then -- y, m, d known
problem, fmt_date = dateFormat(args)
fmt_ymd = '%-Y-%m-%d'
elseif date1.month then -- y, m known; d unknown
fmt_date = '%B %-Y'
fmt_ymd = '%-Y-%m-00'
else -- y known; m, d unknown
fmt_date = '%-Y'
fmt_ymd = '%-Y-00-00'
end
local sortKey
local sortable = translateParameters.sortable[args.sortable]
if sortable then
local value = (date1.partial and date1.partial.first or date1).jdz
sortKey = makeSort(value, sortable)
end
local result = (sortKey or '') .. 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',
}))
) .. (problem or '')
local warnings = tonumber(frame.args.warnings)
if warnings and warnings > 0 then
local good = {
df = true,
mf = true,
}
local invalid
local imax = options.textdates and 2 or 6
for k, _ in pairs(args) do
if type(k) == 'number' then
if k > imax then
invalid = tostring(k)
break
end
else
if not good[k] then
invalid = k
break
end
end
end
if invalid then
result = result .. message('mt-warn-param1', invalid)
end
end
return result
end
 
local function ymd_to_gsddateToGsd(frame)
-- ThisImplement implements {{gregorian[[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.
local date = get_datesgetDates(frame, { wantMixture=true, single=true })
if type(date) == 'string' then
return date
Line 388 ⟶ 1,065:
end
 
local function ymd_from_jdjdToDate(frame)
-- Return formatted date from a Julian date.
-- The result isincludes y-m-da ortime y-m-dif H:M:S ifthe input includes a fraction.
-- The word 'Julian' is accepted for the Julian calendar.
local Date = get_exportsgetExports(frame)
local args = frame:getParent().args
local date = Date('juliandate', args[1], args[2])
if date then
return from_en(date:text())
end
return message('Need valid Julian date numbermt-need-jdn')
end
 
local function ymd_to_jddateToJd(frame)
-- Return Julian date (a number) from a date (y-m-d),which ormay datetimeinclude (y-m-da H:M:S)time,
-- or the current date ('currentdate') or current date and time ('currentdatetime').
-- The word 'Julian' is accepted for the Julian calendar.
local Date = get_exportsgetExports(frame)
local args = frame:getParent().args
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
Line 411 ⟶ 1,088:
return tostring(date.jd)
end
return message('Need mt-need-valid year/month/day or "currentdate"-ymd-current')
end
 
local function time_intervaltimeInterval(frame)
-- ThisImplement implements {{time[[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.
local Date = get_exportsgetExports(frame)
local args = frame:getParent().args
local parms = {
want_durationextra = yesmakeExtra(args.duration),
want_scwantDuration = yes([args.sc)duration],
range = yes[args.range] or (args.range == 'dash' and 'dash' or nil),
wantSc = yes[args.sc],
}
local date1fix = Date(strip_to_nil(argsyes[1args.fix]) and 'fix' or 'currentdatetime')
local date1 = Date(fix, 'partial', stripToNil(args[1]) or 'currentdatetime')
if not date1 then
return message('Invalid mt-invalid-start date in first parameter')
end
local date2 = Date(strip_to_nilfix, 'partial', stripToNil(args[2]) or 'currentdatetime')
if not date2 then
return message('Invalid mt-invalid-end date in second parameter')
end
parms.diff = date2 - date1
for argname, translate in pairs(translateParameters) do
local translate_abbr = {
local parm = stripToNil(args[argname])
off = 'abbr_off',
if parm then
on = 'abbr_on',
parm = translate[parm]
letter = 'abbr_letter',
if parm == nil then -- test for nil because false is a valid setting
}
return message('mt-bad-param2', argname, args[argname])
local translate_disp = {
age = 'disp_age',
raw = 'disp_raw',
}
local translate_round = {
on = 'on',
yes = 'on',
months = 'ym',
weeks = 'ymw',
days = 'ymd',
hours = 'ymdh',
}
local translate_sep = {
comma = 'sep_comma',
serialcomma = 'sep_serialcomma',
space = 'sep_space',
}
local show = strip_to_nil(args.show)
if show and not show_components[show] then
return message('Parameter show=' .. show .. ' is invalid')
end
local round = strip_to_nil(args.round)
if round then
local xround = translate_round[round]
if not xround then
return message('Parameter round=' .. round .. ' is invalid')
end
if xround ~= 'on' then
if show and show ~= xround then
return message('Parameter show=' .. show .. ' conflicts with round=' .. round)
end
showparms[argname] = xroundparm
end
round = true
end
if parms.show =round showthen
parms.want_round local round = parms.round
local xabbrshow = strip_to_nil(argsparms.abbr)show
if xabbrround ~= 'on' then
if show then
xabbr = translate_abbr[xabbr]
if notshow.id xabbr~= round then
return message('Parameter abbr=mt-conflicting-show', args..show, args.abbr .. ' is invalid'round)
end
else
parms.show = translateParameters.show[round]
end
end
parms.round = true
end
return from_en(dateDifference(parms))
parms.xabbr = xabbr
end
local xdisp = strip_to_nil(args.disp)
 
if xdisp then
local function templateGeneric(frame)
xdisp = translate_disp[xdisp]
local name = frame.args.template
if not xdisp then
if not name then
return message('Parameter disp=' .. args.disp .. ' is invalid')
return message('mt-template-x')
end
end
parms.xdisp = xdisp
local xsep = strip_to_nil(args.sep)
if xsep then
xsep = translate_sep[xsep]
if not xsep then
return message('Parameter sep=' .. args.sep .. ' is invalid')
end
end
return ageGeneric(frame:newChild{title = mw.title.new(name, 10), args = frame.args})
parms.xsep = xsep
return age_generic(parms)
end
 
return {
age_daysage_generic = age_daysageGeneric, -- can emulate several age -- Template:Age_in_daystemplates
age_full_yearsbirth_date_and_age = age_full_yearsbda, -- Template:AgeBirth_date_and_age
age_infantdeath_date_and_age = age_infantdda, -- Template:Age_for_infantDeath_date_and_age
age_ydgsd = age_yddateToGsd, -- Template:Age_in_years_and_daysGregorian_serial_date
age_ymextract = age_ymdateExtract, -- Template:Age_in_years_and_monthsExtract
age_ymdjd_to_date = age_ymdjdToDate, -- Template:Age_in_years,_months_and_days?
age_ymwdJULIANDAY = age_ymwddateToJd, -- Template:Age_in_years,_months,_weeks_and_daysJULIANDAY
gsdtime_interval = ymd_to_gsdtimeInterval, -- Template:Gregorian_serial_dateTime_interval
JULIANDAY[''] = ymd_to_jdtemplateGeneric, -- Template:JULIANDAYsame as age_generic, but can be invoked directly
time_interval = time_interval, -- Template:Time_interval
ymd_from_jd = ymd_from_jd, -- Template:?
ymd_to_jd = ymd_to_jd, -- Template:JULIANDAY (alias)
}