Module:Age
This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
This Lua module is used on many pages, so changes to it will be widely noticed. Please test any changes in the module's /sandbox or /testcases subpages. Please consider discussing changes on the talk page before implementing them. |
Templates supported
Module:Age implements the following templates:
Template | Required wikitext |
---|---|
{{extract}} | {{#invoke:age|extract}}
|
{{gregorian serial date}} | {{#invoke:age|gsd}}
|
{{time interval}} | {{#invoke:age|time_interval}}
|
{{age in days}} | {{#invoke:age|age_generic|template=age_days}}
|
{{age in days nts}} | {{#invoke:age|age_generic|template=age_days_nts}}
|
{{duration in days}} | {{#invoke:age|age_generic|template=duration_days}}
|
{{duration in days nts}} | {{#invoke:age|age_generic|template=duration_days_nts}}
|
{{age}} | {{#invoke:age|age_generic|template=age_full_years}}
|
{{age nts}} | {{#invoke:age|age_generic|template=age_full_years_nts}}
|
{{age in years}} | {{#invoke:age|age_generic|template=age_in_years}}
|
{{age in years nts}} | {{#invoke:age|age_generic|template=age_in_years_nts}}
|
{{age for infant}} | {{#invoke:age|age_generic|template=age_infant}}
|
{{age in months}} | {{#invoke:age|age_generic|template=age_m}}
|
{{age in weeks}} | {{#invoke:age|age_generic|template=age_w}}
|
{{age in weeks and days}} | {{#invoke:age|age_generic|template=age_wd}}
|
{{age in years and days}} | {{#invoke:age|age_generic|template=age_yd}}
|
{{age in years and days nts}} | {{#invoke:age|age_generic|template=age_yd_nts}}
|
{{age in years and months}} | {{#invoke:age|age_generic|template=age_ym}}
|
{{age in years, months and days}} | {{#invoke:age|age_generic|template=age_ymd}}
|
{{age in years, months, weeks and days}} | {{#invoke:age|age_generic|template=age_ymwd}}
|
{{birth date and age}} | {{#invoke:age|birth_date_and_age}}
|
Redirects
Template | Redirects to |
---|---|
{{hla}} | {{age in years, months and days}} |
{{age in months, weeks and days}} | {{age in years, months, weeks and days}} |
{{bda}} | {{birth date and age}} |
{{gsd}} | {{gregorian serial date}} |
The age templates expect the older date to be first. The implementations of age_in_years
and age_in_years_nts
display an error message if that is not the case. If similar checking is wanted for other templates, negative=error
can be added to the invoke. For example, {{age}} might use:
{{#invoke:age|age_generic|template=age_full_years|negative=error}}
If negative=error
does not apply, a negative difference is indicated with a minus sign (−).
Date formats
Dates can use numbered or named parameters to specify year/month/day. Alternatively, a full date can be entered in a variety of formats. For example:
{{age in years and months|year1=2001|month1=1|day1=10|year2=2012|month2=2|day2=20}}
→ 11 years, 1 month{{age in years and months|year=2001|month=1|day=10|year2=2012|month2=2|day2=20}}
→ 11 years, 1 month{{age in years and months|2001|1|10|2012|2|20}}
→ 11 years, 1 month{{age in years and months|2001-1-10|2012-2-20}}
→ 11 years, 1 month{{age in years and months|10 Jan 2001|20 Feb 2012}}
→ 11 years, 1 month{{age in years and months|January 10, 2001|Feb 20, 2012}}
→ 11 years, 1 month
If the first or second date is omitted, the current date is used. For example:
{{age in years and months|year2=2012|month2=2|day2=20}}
→ −12 years, 8 months{{age in years and months||||2012|2|20}}
→ −12 years, 8 months{{age in years and months||2012-2-20}}
→ −12 years, 8 months{{age in years and months||20 Feb 2012}}
→ −12 years, 8 months{{age in years and months||Feb 20, 2012}}
→ −12 years, 8 months{{age in years and months|year1=2001|month1=1|day1=10}}
→ 23 years, 10 months{{age in years and months|year=2001|month=1|day=10}}
→ 23 years, 10 months{{age in years and months|2001|1|10}}
→ 23 years, 10 months{{age in years and months|2001-1-10}}
→ 23 years, 10 months{{age in years and months|10 Jan 2001}}
→ 23 years, 10 months{{age in years and months|January 10, 2001}}
→ 23 years, 10 months
Parameters
The following options are available:
Parameter | Description |
---|---|
duration=on |
The finishing date is included in the result; that adds one day to the age. |
fix=on |
Adjust invalid time units. See Template:Extract#Fix. |
format=commas |
A value of 1,000 or more is displayed with commas. |
format=raw |
Numbers are displayed without commas and negative numbers are displayed with a hyphen for {{#expr}} . In addition, {{age}} outputs a plain number and will not include a span to indicate if the result relies on the current date.
|
format=cardinal |
Display the resulting number using words such as "five" instead of 5. See below. |
format=ordinal |
Display the resulting number using words such as "fifth" instead of 5. See below. |
range=dash |
Accept a year only, or a year and month only, and show a range of ages with an en dash (–). |
range=yes |
Accept a year or year/month, and show the range with "or". |
range=no |
Accept a year only, or year/month, but show only a single age as if full dates had been entered. |
round=on |
The age is rounded to the nearest least-significant time unit. |
sc=on |
A serial comma is used (only useful when three or more values are displayed). |
sc=yes |
Same as sc=on .
|
show=hide |
The age is not displayed; may be useful with sortable=on .
|
sortable=on |
Insert a hidden sort key before the result (for use in sortable tables). |
sortable=table |
Insert a sort key using table syntax data-sort-value="value"| .
|
sortable=debug |
Same as sortable=on but the sort key is displayed for testing.
|
sortable=off |
No sort key (can override the default for a template like {{age nts}}). |
Examples using the range
parameter follow.
{{age in years and months|year=2001|month=1|year2=2012|month2=2|range=yes}}
→ 11 years, 0 or 1 month{{age in years and months|2001|1||2012|2|range=yes}}
→ 11 years, 0 or 1 month{{age in years and months|Jan 2001|Feb 2012|range=yes}}
→ 11 years, 0 or 1 month{{age in years and months|Jan 2001|Feb 2012|range=dash}}
→ 11 years, 0–1 month{{age in years and months|Jan 2001|Feb 2012|range=no}}
→ 11 years, 1 month (assume 1 Jan 2001 to 1 Feb 2012){{age in years and months|12 Jan 2001|Feb 2012|range=no}}
→ 11 years, 1 month (assume 12 Jan 2001 to 12 Feb 2012){{age in years and months|2001|2012|range=no}}
→ 11 years (assume 1 Jan 2001 to 1 Jan 2012){{age in years and months|2001|23 Feb 2012|range=no}}
→ 11 years (assume 23 Feb 2001 to 23 Feb 2012)
The sort key is based on the age in days, and fractions of a day if a time is specified.
{{age in years and months|10 Jan 2001|20 Feb 2012|sortable=debug}}
→ 7003405800000000000♠11 years, 1 month{{age in years and months|10 Jan 2001|6:00 am 20 Feb 2012|sortable=debug}}
→ 7003405825000000000♠11 years, 1 month{{age in years and months|10 Jan 2001|6:00 am 20 Feb 2012|sortable=debug|show=hide}}
→ 7003405825000000000♠
An extra day is added for a duration.
{{age in years and months|20 Jan 2001|19 Feb 2012}}
→ 11 years (one day short of 11 years, 1 month){{age in years and months|20 Jan 2001|19 Feb 2012|duration=on}}
→ 11 years, 1 month
The least-significant time unit can be rounded.
{{age in years and months|20 Jan 2001|10 Feb 2012}}
→ 11 years{{age in years and months|20 Jan 2001|10 Feb 2012|round=on}}
→ 11 years, 1 month (round to nearest month)
Large numbers can be formatted with commas.
{{age in years and months|120|2012|format=commas|range=yes}}
→ 1,891 or 1,892 years{{age in years and months|120|2012|format=commas|range=dash}}
→ 1,891–1,892 years
Spelling numbers
The templates that use age_generic
can display numbers in words rather than using numerals. The result can be a cardinal number (such as "five") or an ordinal number (such as "fifth"). The first letter can be in uppercase, and US spelling of numbers can be used. Examples:
{{age|1898|01|01|2018|02|01|format=cardinal}}
→ one hundred and twenty{{age|1898|01|01|2018|02|01|format=cardinal_us}}
→ one hundred twenty{{age|1898|01|01|2018|02|01|format=Cardinal}}
→ One hundred and twenty{{age|1898|01|01|2018|02|01|format=Cardinal_us}}
→ One hundred twenty{{age|1898|01|01|2018|02|01|format=Ordinal}}
→ One hundred and twentieth{{age|1898|01|01|2018|02|01|format=Ordinal_us}}
→ One hundred twentieth{{age|1898|01|01|2018|02|01|format=ordinal}}
→ one hundred and twentieth{{age|1898|01|01|2018|02|01|format=ordinal_us}}
→ one hundred twentieth{{age|1980|1990|range=yes|format=Cardinal}}
→ Nine or ten{{age in years, months and days|April 1980|1995|format=Cardinal|range=yes}}
→ Fourteen or fifteen years
See also
- {{time interval}} • This template supports all age/duration calculations and provides more options such as abbreviating or omitting units.
-- Implement various "age of" and other date-related templates.
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('sandbox', 1, true) and '/sandbox' or ''
local datemod = require('Module:Date' .. sandbox)
_Date = datemod._Date
_currentDate = datemod._current
end
return _Date, _currentDate
end
local Collection -- a table to hold items
Collection = {
add = function (self, item)
if item ~= nil then
self.n = self.n + 1
self[self.n] = item
end
end,
join = function (self, sep)
return table.concat(self, sep)
end,
remove = function (self, pos)
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 stripToNil(text)
-- If text is a string, return its trimmed content, or nil if empty.
-- Otherwise return text (which may, for example, be nil).
if type(text) == 'string' then
text = text:match('(%S.-)%s*$')
end
return text
end
local function yes(parameter)
-- Return true if parameter should be interpreted as "yes".
-- 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, id)
-- Return formatted message text for an error or warning.
local categories = {
error = '[[Category:Age error]]',
warning = '[[Category:Age error]]', -- same as error until determine whether 'Age warning' would be worthwhile
}
local a, b, category
if id == 'warning' then
a = '<sup>[<i>'
b = '</i>]</sup>'
else
a = '<strong class="error">Error: '
b = '</strong>'
end
if mw.title.getCurrentTitle():inNamespaces(0) then
-- Category only in namespaces: 0=article.
category = categories[id or 'error']
end
return
a ..
mw.text.nowiki(msg) ..
b ..
(category or '')
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('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".
extra = extra .. ' '
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 lhs, rhs
if sortable == 'sortable_table' then
lhs = 'data-sort-value="'
rhs = '"|'
else
lhs = sortable == 'sortable_debug' and
'<span style="border:1px solid;display:inline;" class="sortkey">' or
'<span style="display:none" class="sortkey">'
rhs = '♠</span>'
end
return lhs .. sortKey .. rhs
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' },
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('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('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 = date:text(show)
elseif type(result) == 'boolean' then
result = result and '1' or '0'
else
result = tostring(result)
end
end
return (sortKey or '') .. (result or '')
end
local function rangeJoin(range)
-- Return text to be used between a range of ages.
return range == 'dash' and '–' or ' or '
end
local function makeText(values, components, names, options, noUpper)
-- Return wikitext representing an age or duration.
local text = Collection.new()
local count = #values
local sep = names.sep or ''
for i, v in ipairs(values) do
-- v is a number (say 4 for 4 years), or a table ({4,5} for 4 or 5 years).
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
local plural = names.plural
if not plural or (islist and v[2] or v) == 1 then
plural = ''
end
text:add(vstr .. sep .. name .. plural)
else
text:add(vstr)
end
end
end
local first, last
if options.join == 'sep_space' then
first = ' '
last = ' '
elseif options.join == 'sep_comma' then
first = ', '
last = ', '
elseif options.join == 'sep_serialcomma' and text.n > 2 then
first = ', '
last = ', and '
else
first = ', '
last = ' and '
end
for i, v in ipairs(text) do
if i < text.n then
text[i] = v .. (i + 1 < text.n and first or last)
end
end
local sign = ''
if options.isnegative then
-- Do not display 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 = '-' -- plain hyphen so result can be used in a calculation
else
sign = '−' -- Unicode U+2212 MINUS SIGN
end
end
end
return
(options.sortKey or '') ..
(options.extra or '') ..
sign ..
text:join() ..
(options.suffix or '')
end
local function dateDifference(parms)
-- Return a formatted date difference using the given parameters
-- which have been validated.
local names = {
abbr_off = {
plural = 's',
sep = ' ',
y = 'year',
m = 'month',
w = 'week',
d = 'day',
H = 'hour',
M = 'minute',
S = 'second',
},
abbr_on = {
y = 'y',
m = 'm',
w = 'w',
d = 'd',
H = 'h',
M = 'm',
S = 's',
},
abbr_infant = { -- for {{age for infant}}
plural = 's',
sep = ' ',
y = 'yr',
m = 'mo',
w = 'wk',
d = 'day',
H = 'hr',
M = 'min',
S = 'sec',
},
abbr_raw = {},
}
local diff = parms.diff -- must be a valid date difference
local show = parms.show -- may be nil; default is set below
local abbr = parms.abbr or 'abbr_off'
local defaultJoin
if abbr ~= 'abbr_off' then
defaultJoin = 'sep_space'
end
if not show then
show = 'ymd'
if parms.disp == 'disp_age' then
if diff.years < 3 then
defaultJoin = 'sep_space'
if diff.years >= 1 then
show = 'ym'
else
show = 'md'
end
else
show = 'y'
end
end
end
if type(show) ~= 'table' then
show = translateParameters.show[show]
end
if parms.disp == 'disp_raw' then
defaultJoin = 'sep_space'
abbr = 'abbr_raw'
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
return sortKey or ''
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('Parameter show=' .. show.id .. ' is not supported here')
end
local function getDates(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.
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
text = 'currentdate'
if getopt.flag == 'usesCurrent' then
getopt.usesCurrent = true
end
end
return text
end
local args = frame:getParent().args
local fields = {}
local isNamed = args.year or args.year1 or args.year2 or
args.month or args.month1 or args.month2 or
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
fields[3] = args.day1 or args.day
fields[4] = args.year2
fields[5] = args.month2
fields[6] = args.day2
else
for i = 1, 6 do
fields[i] = args[i]
end
end
local imax = 0
for i = 1, 6 do
fields[i] = stripToNil(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 tonumber(fields[i]) == 0 then
fields[i] = nil
getopt.partial = true
end
end
end
local fix = getopt.fix and 'fix' or ''
local noDefault = imax == 0 and getopt.noMissing
local partialText = getopt.partial and 'partial' or ''
local dates = {}
if isNamed or imax >= 3 then
local nrDates = getopt.single and 1 or 2
if getopt.wantMixture then
-- Cannot be partial since empty fields are set from current.
local components = { 'year', 'month', 'day' }
for i = 1, nrDates * 3 do
fields[i] = fields[i] or currentDate[components[i > 3 and i - 3 or i]]
end
for i = 1, nrDates do
local index = i == 1 and 1 or 4
dates[i] = Date(fields[index], fields[index+1], fields[index+2])
end
else
-- 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).
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 and not m and not d and not noDefault then
dates[i] = Date(flagCurrent())
end
end
end
elseif not noDefault then
getopt.textdates = true -- have parsed each date from a single text field
dates[1] = Date(fix, partialText, flagCurrent(fields[1]))
if not getopt.single then
dates[2] = Date(fix, partialText, flagCurrent(fields[2]))
end
end
if not dates[1] then
return message('Need valid year, month, day')
end
if getopt.single then
return dates[1]
end
if not dates[2] then
return message('Second date should be year, month, day')
end
return dates[1], dates[2]
end
local function ageGeneric(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.
local name = frame.args.template
if not name then
return message('The template invoking this must have "|template=x" where x is the wanted operation')
end
local args = frame:getParent().args
local specs = {
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 = 'no',
},
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' or nil,
},
age_yd_nts = { -- {{age in years and days nts}}
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',
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,
},
}
local spec = specs[name]
if not spec then
return message('The specified template name is not valid')
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, { fill = autofill }),
wantDuration = spec.duration or yes(args.duration),
range = range,
wantSc = yes(args.sc),
show = args.show == 'hide' and 'hide' or spec.show,
abbr = spec.abbr,
disp = spec.disp,
extra = makeExtra(args, getopt.usesCurrent and format ~= 'format_raw'),
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 message('The second date should not be before the first date')
end
return dateDifference(parms)
end
local function bda(frame)
-- Implement [[Template:Birth date and age]].
local args = frame:getParent().args
local options = { 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('Invalid birth date for calculating age')
end
local disp, show = 'disp_raw', '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 df = stripToNil(args.df) -- day first (dmy); default is month first (mdy)
local result = df and
'%-d %B %-Y' or
'%B %-d, %-Y'
result = '(<span class="bday">%-Y-%m-%d</span>) </span>' .. result
result = '<span style="display:none"> ' ..
date:text(result) ..
'<span class="noprint ForceAgeToShow"> ' ..
'(age ' ..
dateDifference({
diff = diff,
show = show,
abbr = 'abbr_off',
disp = disp,
sep = 'sep_space',
}) ..
')</span>'
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('invalid parameter ' .. invalid, 'warning')
end
end
return result
end
local function dateToGsd(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.
local date = getDates(frame, { wantMixture=true, single=true })
if type(date) == 'string' then
return date
end
return tostring(date.gsd)
end
local function jdToDate(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.
local Date = getExports(frame)
local args = frame:getParent().args
local date = Date('juliandate', args[1], args[2])
if date then
return date:text()
end
return message('Need valid Julian date number')
end
local function dateToJd(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.
local Date = getExports(frame)
local args = frame:getParent().args
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
if date then
return tostring(date.jd)
end
return message('Need valid year/month/day or "currentdate"')
end
local function timeInterval(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.
local Date = getExports(frame)
local args = frame:getParent().args
local parms = {
extra = makeExtra(args),
wantDuration = yes(args.duration),
range = yes(args.range) or (args.range == 'dash' and 'dash' or nil),
wantSc = yes(args.sc),
}
local fix = yes(args.fix) and 'fix' or ''
local date1 = Date(fix, 'partial', stripToNil(args[1]) or 'currentdatetime')
if not date1 then
return message('Invalid start date in first parameter')
end
local date2 = Date(fix, 'partial', stripToNil(args[2]) or 'currentdatetime')
if not date2 then
return message('Invalid end date in second parameter')
end
parms.diff = date2 - date1
for argname, translate in pairs(translateParameters) do
local parm = stripToNil(args[argname])
if parm then
parm = translate[parm]
if parm == nil then -- test for nil because false is a valid setting
return message('Parameter ' .. argname .. '=' .. args[argname] .. ' is invalid')
end
parms[argname] = parm
end
end
if parms.round then
local round = parms.round
local show = parms.show
if round ~= 'on' then
if show then
if show.id ~= round then
return message('Parameter show=' .. args.show .. ' conflicts with round=' .. args.round)
end
else
parms.show = translateParameters.show[round]
end
end
parms.round = true
end
return dateDifference(parms)
end
return {
age_generic = ageGeneric, -- can emulate several age templates
birth_date_and_age = bda, -- Template:Birth_date_and_age
gsd = dateToGsd, -- Template:Gregorian_serial_date
extract = dateExtract, -- Template:Extract
jd_to_date = jdToDate, -- Template:?
JULIANDAY = dateToJd, -- Template:JULIANDAY
time_interval = timeInterval, -- Template:Time_interval
}