Editing Module:Age

Warning: You are not logged in. Your IP address will be publicly visible if you make any edits. Read the Privacy Policy to learn what information we collect about you and how we use it.

If you log in or create an account, your edits will be attributed to your username, along with other benefits.

The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.

Latest revision Your text
Line 1: Line 1:
-- Implement various "age of" and other date-related templates.
-- Implement various "age of" and other date-related templates.


local mtext = {
local _Date, _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-param1'] =            'Invalid parameter $1',
['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',
['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-bad-param1'] = true,
}
 
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.
-- Return objects exported from the date module or its sandbox.
if not _Date then
if not _Date then
local sandbox = frame:getTitle():find(mtext['txt-sandbox'], 1, true) and ('/' .. mtext['txt-sandbox']) or ''
local sandbox = frame:getTitle():find('sandbox', 1, true) and '/sandbox' or ''
local datemod = require(mtext['txt-module-date'] .. sandbox)
local datemod = require('Module:Date' .. sandbox)
local realDate = datemod._Date
_Date = datemod._Date
_currentDate = datemod._current
_current_date = 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
end
return _Date, _currentDate
return _Date, _current_date
end
end


local Collection  -- a table to hold items
local function collection()
Collection = {
-- Return a table to hold items.
add = function (self, item)
return {
if item ~= nil then
n = 0,
add = function (self, item)
self.n = self.n + 1
self.n = self.n + 1
self[self.n] = item
self[self.n] = item
end
end,
end,
join = function (self, sep)
join = function (self, sep)
return 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 stripToNil(text)
local function strip_to_nil(text)
-- If text is a string, return its trimmed content, or nil if empty.
-- If text is a string, return its trimmed content, or nil if empty.
-- Otherwise return text (which may, for example, be nil).
-- Otherwise return text (which may, for example, be nil).
Line 116: Line 34:
end
end
return text
return text
end
local function dateFormat(args)
-- Return string for wanted date format.
local default = mtext['txt-format-default']
local other = default == 'df' and 'mf' or 'df'
local wanted = stripToNil(args[other]) and other or default
return wanted == 'df' and mtext['txt-dmy'] or mtext['txt-mdy']
end
local function substituteParameters(text, ...)
-- Return text after substituting any given parameters for $1, $2, etc.
return mw.message.newRawMessage(text, ...):plain()
end
end


Line 138: Line 43:
end
end


local function message(msg, ...)
local function message(msg, nocat)
-- Return formatted message text for an error or warning.
-- Return formatted message text for an error.
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"></span>'
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
else
a = '<strong class="error">' .. getText('txt-error')
category = ''
b = '</strong>'
k = 'error'
end
end
if mw.title.getCurrentTitle():inNamespaces(0) then
return anchor ..
-- Category only in namespaces: 0=article.
'<strong class="error">Error: ' ..
category = '[[' .. categories[k] .. ']]'
mw.text.nowiki(msg) ..
end
'</strong>' ..
return
category
a ..
mw.text.nowiki(text) ..
b ..
(category or '')
end
end


local function formatNumber(number)
local function formatnumber(number)
-- Return the given number formatted with commas as group separators,
-- Return the given number formatted with commas as group separators,
-- given that the number is an integer.
-- given that the number is an integer.
local numstr = tostring(number)
local numstr = tostring(number)
local length = #numstr
local length = #numstr
local places = Collection.new()
local places = collection()
local pos = 0
local pos = 0
repeat
repeat
Line 181: Line 73:
until pos >= length
until pos >= length
places:add(length)
places:add(length)
local groups = Collection.new()
local groups = collection()
for i = places.n, 2, -1 do
for i = places.n, 2, -1 do
local p1 = length - places[i] + 1
local p1 = length - places[i] + 1
Line 190: Line 82:
end
end


local function spellNumber(number, options, i)
local function make_sort(value, sortable)
-- Return result of spelling number, or
-- Return a sort key in a span if specified.
-- 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.
-- Assume value is a valid number which has not overflowed.
if sortable == 'sortable_table' or sortable == 'sortable_on' or sortable == 'sortable_debug' then
if sortable == 'sortable_on' or sortable == 'sortable_debug' then
local sortKey
local sortkey
if value == 0 then
if value == 0 then
sortKey = '5000000000000000000'
sortkey = '5000000000000000000'
else
else
local mag = math.floor(math.log10(math.abs(value)) + 1e-14)
local mag = math.floor(math.log10(math.abs(value)) + 1e-14)
local prefix
if value > 0 then
if value > 0 then
sortKey = 7000 + mag
prefix = 7000 + mag
else
else
sortKey = 2999 - mag
prefix = 2999 - mag
value = value + 10^(mag+1)
value = value + 10^(mag+1)
end
end
sortKey = string.format('%d', sortKey) .. string.format('%015.0f', math.floor(value * 10^(14-mag)))
sortkey = string.format('%d', prefix) .. string.format('%015.0f', math.floor(value * 10^(14-mag)))
end
end
local result
local lhs = sortable == 'sortable_debug' and
if sortable == 'sortable_table' then
'<span style="border:1px solid;display:inline;" class="sortkey">' or
result = 'data-sort-value="_SORTKEY_"|'
'<span style="display:none" class="sortkey">'
elseif sortable == 'sortable_debug' then
return lhs .. sortkey .. '♠</span>'
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
end
end


local translateParameters = {
local translate_parameters = {
abbr = {
abbr = {
off = 'abbr_off',
off = 'abbr_off',
Line 285: Line 141:
ymw = { 'y', 'm', 'w', id = 'ymw' },
ymw = { 'y', 'm', 'w', id = 'ymw' },
ymwd = { 'y', 'm', 'w', 'd', id = 'ymwd' },
ymwd = { 'y', 'm', 'w', 'd', id = 'ymwd' },
yd = { 'y', 'd', id = 'yd', keepZero = true },
yd = { 'y', 'd', id = 'yd', keepzero = true },
m = { 'm', id = 'm' },
m = { 'm', id = 'm' },
md = { 'm', 'd', id = 'md' },
md = { 'm', 'd', id = 'md' },
Line 293: Line 149:
hm = { 'H', 'M', id = 'hm' },
hm = { 'H', 'M', id = 'hm' },
hms = { 'H', 'M', 'S', id = 'hms' },
hms = { 'H', 'M', 'S', id = 'hms' },
M = { 'M', id = 'M' },
s = { 'S', id = 's' },
d = { 'd', id = 'd' },
d = { 'd', id = 'd' },
dh = { 'd', 'H', id = 'dh' },
dh = { 'd', 'H', id = 'dh' },
Line 307: Line 161:
off = false,
off = false,
on = 'sortable_on',
on = 'sortable_on',
table = 'sortable_table',
debug = 'sortable_debug',
debug = 'sortable_debug',
},
},
}
}


local spellOptions = {
local function date_extract(frame)
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.
-- Return part of a date after performing an optional operation.
local Date = getExports(frame)
local Date = get_exports(frame)
local args = frame:getParent().args
local args = frame:getParent().args
local parms = {}
local parms = {}
Line 337: Line 179:
table.insert(parms, 'partial')
table.insert(parms, 'partial')
end
end
local show = stripToNil(args.show) or 'dmy'
local date = Date(unpack(parms))
local date = Date(unpack(parms))
if not date then
if not date then
if show == 'format' then
return message('Need valid date')
return 'error'
end
return message('mt-need-valid-date')
end
end
local add = stripToNil(args.add)
local add = strip_to_nil(args.add)
if add then
if add then
for item in add:gmatch('%S+') do
for item in add:gmatch('%S+') do
date = date + item
date = date + item
if not date then
if not date then
return message('mt-cannot-add', item)
return message('Cannot add "' .. item .. '"')
end
end
end
end
end
end
local sortKey, result
local prefix, result
local sortable = translateParameters.sortable[args.sortable]
local sortable = translate_parameters.sortable[args.sortable]
if sortable then
if sortable then
local value = (date.partial and date.partial.first or date).jdz
local value = (date.partial and date.partial.first or date).jdz
sortKey = makeSort(value, sortable)
prefix = make_sort(value, sortable)
end
end
local show = strip_to_nil(args.show) or 'dmy'
if show ~= 'hide' then
if show ~= 'hide' then
result = date[show]
result = date[show]
if result == nil then
if result == nil then
result = from_en(date:text(show))
result = date:text(show)
elseif type(result) == 'boolean' then
elseif type(result) == 'boolean' then
result = result and '1' or '0'
result = result and '1' or '0'
else
else
result = from_en(tostring(result))
result = tostring(result)
end
end
end
end
return (sortKey or '') .. makeExtra(args) .. (result or '')
return (prefix or '') .. (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
end


local function makeText(values, components, names, options, noUpper)
local function make_text(values, components, names, options)
-- Return wikitext representing an age or duration.
-- Return wikitext representing an age or duration.
local text = Collection.new()
local text = collection()
local count = #values
local count = #values
local sep = names.sep or ''
local sep = names.sep or ''
Line 386: Line 220:
-- v is a number (say 4 for 4 years), or a table ({4,5} for 4 or 5 years).
-- v is a number (say 4 for 4 years), or a table ({4,5} for 4 or 5 years).
local islist = type(v) == 'table'
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
if (islist or v > 0) or (text.n == 0 and i == count) or (text.n > 0 and components.keepzero) then
local fmt, vstr
local fmt, vstr
if options.spell then
if i == 1 and options.format == 'format_commas' 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.
-- Numbers after the first should be small and not need formatting.
fmt = formatNumber
fmt = formatnumber
else
else
fmt = tostring
fmt = tostring
end
end
if islist then
if islist then
vstr = fmt(v[1]) .. rangeJoin(options.range)
local join = options.range == 'dash' and '–' or '&nbsp;or '
noUpper = true
vstr = fmt(v[1]) .. join .. fmt(v[2])
vstr = vstr .. fmt(v[2])
else
else
vstr = fmt(v)
vstr = fmt(v)
Line 407: Line 236:
local name = names[components[i]]
local name = names[components[i]]
if name then
if name then
if type(name) == 'table' then
local plural = names.plural
name = mw.getContentLanguage():plural(islist and v[2] or v, name)
if not plural or (islist and v[2] or v) == 1 then
plural = ''
end
end
text:add(vstr .. sep .. name)
text:add(vstr .. sep .. name .. plural)
else
else
text:add(vstr)
text:add(vstr)
Line 425: Line 255:
elseif options.join == 'sep_serialcomma' and text.n > 2 then
elseif options.join == 'sep_serialcomma' and text.n > 2 then
first = ', '
first = ', '
last = mtext['txt-comma-and']
last = ', and '
else
else
first = ', '
first = ', '
last = mtext['txt-and']
last = ' and '
end
end
for i, v in ipairs(text) do
for i, v in ipairs(text) do
Line 447: Line 277:
end
end
return
return
(options.sortKey or '') ..
(options.prefix or '') ..
(options.extra or '') ..
sign ..
sign ..
text:join() ..
text:join() ..
Line 454: Line 283:
end
end


local function dateDifference(parms)
local function date_difference(parms)
-- Return a formatted date difference using the given parameters
-- Return a formatted date difference using the given parameters
-- which have been validated.
-- which have been validated.
local names = {
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 = {
abbr_off = {
plural = 's',
sep = '&nbsp;',
sep = '&nbsp;',
y = {'year', 'years'},
y = 'year',
m = {'month', 'months'},
m = 'month',
w = {'week', 'weeks'},
w = 'week',
d = {'day', 'days'},
d = 'day',
H = {'hour', 'hours'},
H = 'hour',
M = {'minute', 'minutes'},
M = 'minute',
S = {'second', 'seconds'},
S = 'second',
},
},
abbr_on = {
abbr_on = {
Line 482: Line 308:
},
},
abbr_infant = {      -- for {{age for infant}}
abbr_infant = {      -- for {{age for infant}}
plural = 's',
sep = '&nbsp;',
sep = '&nbsp;',
y = {'yr', 'yrs'},
y = 'yr',
m = {'mo', 'mos'},
m = 'mo',
w = {'wk', 'wks'},
w = 'wk',
d = {'day', 'days'},
d = 'day',
H = {'hr', 'hrs'},
H = 'hr',
M = {'min', 'mins'},
M = 'min',
S = {'sec', 'secs'},
S = 'sec',
},
},
abbr_raw = {},
abbr_raw = {},
Line 496: Line 323:
local show = parms.show  -- may be nil; default is set below
local show = parms.show  -- may be nil; default is set below
local abbr = parms.abbr or 'abbr_off'
local abbr = parms.abbr or 'abbr_off'
local defaultJoin
local default_join
if abbr ~= 'abbr_off' then
if abbr ~= 'abbr_off' then
defaultJoin = 'sep_space'
default_join = 'sep_space'
end
end
if not show then
if not show then
Line 504: Line 331:
if parms.disp == 'disp_age' then
if parms.disp == 'disp_age' then
if diff.years < 3 then
if diff.years < 3 then
defaultJoin = 'sep_space'
default_join = 'sep_space'
if diff.years >= 1 then
if diff.years >= 1 then
show = 'ym'
show = 'ym'
Line 516: Line 343:
end
end
if type(show) ~= 'table' then
if type(show) ~= 'table' then
show = translateParameters.show[show]
show = translate_parameters.show[show]
end
end
if parms.disp == 'disp_raw' then
if parms.disp == 'disp_raw' then
defaultJoin = 'sep_space'
default_join = 'sep_space'
abbr = 'abbr_raw'
abbr = 'abbr_raw'
elseif parms.wantSc then
elseif parms.want_sc then
defaultJoin = 'sep_serialcomma'
default_join = 'sep_serialcomma'
end
end
local diffOptions = {
local diff_options = {
round = parms.round,
round = parms.round,
duration = parms.wantDuration,
duration = parms.want_duration,
range = parms.range and true or nil,
range = parms.range and true or nil,
}
}
local sortKey
local prefix
if parms.sortable then
if parms.sortable then
local value = diff.age_days + (parms.wantDuration and 1 or 0)  -- days and fraction of a day
local value = diff.age_days + (parms.want_duration and 1 or 0)  -- days and fraction of a day
if diff.isnegative then
if diff.isnegative then
value = -value
value = -value
end
end
sortKey = makeSort(value, parms.sortable)
prefix = make_sort(value, parms.sortable)
end
end
local textOptions = {
local text_options = {
extra = parms.extra,
prefix = prefix,
suffix = parms.suffix, -- not currently used
format = parms.format,
format = parms.format,
join = parms.sep or defaultJoin,
join = parms.sep or default_join,
isnegative = diff.isnegative,
isnegative = diff.isnegative,
range = parms.range,
range = parms.range,
sortKey = sortKey,
spell = parms.spell,
suffix = parms.suffix,  -- not currently used
}
}
if show.id == 'hide' then
if show.id == 'hide' then
return sortKey or ''
return prefix or ''
end
end
local values = { diff:age(show.id, diffOptions) }
local values = { diff:age(show.id, diff_options) }
if values[1] then
if values[1] then
return makeText(values, show, names[abbr], textOptions)
return make_text(values, show, names[abbr], text_options)
end
end
if diff.partial then
return message('Parameter show=' .. show.id .. ' is not supported here')
-- 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
end


local function getDates(frame, getopt)
local function get_dates(frame, getopt)
-- Parse template parameters and return one of:
-- Parse template parameters and return one of:
-- * date        (a date table, if single)
-- * date        (a date table, if single)
-- * date1, date2 (two date tables, if not single)
-- * date1, date2 (two date tables, if not single)
-- * text        (a string error message)
-- * text        (a string error message)
-- A missing date is optionally replaced with the current date.
-- A missing date is replaced with the current date.
-- If wantMixture is true, a missing date component is replaced
-- If want_mixture is true, a missing date component is replaced
-- from the current date, so can get a bizarre mixture of
-- from the current date, so can get a bizarre mixture of
-- specified/current y/m/d as has been done by some "age" templates.
-- specified/current y/m/d as has been done by some "age" templates.
-- Some results may be placed in table getopt.
-- Some results may be placed in table getopt.
local Date, currentDate = getExports(frame)
local Date, current_date = get_exports(frame)
getopt = getopt or {}
getopt = getopt or {}
local function flagCurrent(text)
local fix = getopt.fix and 'fix' or ''
-- This allows the calling template to detect if the current date has been used,
local partial = getopt.range and 'partial' or ''
-- 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 args = frame:getParent().args
local fields = {}
local fields = {}
local isNamed = args.year or args.year1 or args.year2 or
local is_named = args.year or args.year1 or args.year2 or
args.month or args.month1 or args.month2 or
args.month or args.month1 or args.month2 or
args.day or args.day1 or args.day2
args.day or args.day1 or args.day2
if isNamed then
if is_named then
fields[1] = args.year1 or args.year
fields[1] = args.year1 or args.year
fields[2] = args.month1 or args.month
fields[2] = args.month1 or args.month
Line 620: Line 415:
local imax = 0
local imax = 0
for i = 1, 6 do
for i = 1, 6 do
fields[i] = stripToNil(fields[i])
fields[i] = strip_to_nil(fields[i])
if fields[i] then
if fields[i] then
imax = i
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
end
end
local fix = getopt.fix and 'fix' or ''
local single = getopt.single
local partialText = getopt.partial and 'partial' or ''
local dates = {}
local dates = {}
if isNamed or imax >= 3 then
if is_named or imax > 2 then
local nrDates = getopt.single and 1 or 2
local nr_dates = single and 1 or 2
if getopt.wantMixture then
if getopt.want_mixture then
-- Cannot be partial since empty fields are set from current.
-- Cannot be partial since empty fields are set from current.
local components = { 'year', 'month', 'day' }
local components = { 'year', 'month', 'day' }
for i = 1, nrDates * 3 do
for i = 1, nr_dates * 3 do
fields[i] = fields[i] or currentDate[components[i > 3 and i - 3 or i]]
fields[i] = fields[i] or current_date[components[i > 3 and i - 3 or i]]
end
end
for i = 1, nrDates do
for i = 1, nr_dates do
local index = i == 1 and 1 or 4
local index = i == 1 and 1 or 4
local y, m, d = fields[index], fields[index+1], fields[index+2]
dates[i] = 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
end
else
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 index = i == 1 and 1 or 4
local y, m, d = fields[index], fields[index+1], fields[index+2]
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
if (partial and y) or (y and m and d) then
dates[i] = Date(fix, partialText, y, m, d)
dates[i] = Date(fix, partial, y, m, d)
elseif not y and not m and not d then
elseif not (y or m or d) then
dates[i] = Date(flagCurrent())
dates[i] = Date('currentdate')
end
end
end
end
end
end
else
else
getopt.textdates = true -- have parsed each date from a single text field
getopt.textdates = true
dates[1] = Date(fix, partialText, flagCurrent(fields[1]))
dates[1] = Date(fix, partial, fields[1] or 'currentdate')
if not getopt.single then
if not single then
dates[2] = Date(fix, partialText, flagCurrent(fields[2]))
dates[2] = Date(fix, partial, fields[2] or 'currentdate')
end
end
end
end
if not dates[1] then
if not dates[1] then
return message(getopt.missing1 or 'mt-need-valid-ymd')
return message('Need valid year, month, day')
end
end
if getopt.single then
if single then
return dates[1]
return dates[1]
end
end
if not dates[2] then
if not dates[2] then
return message(getopt.missing2 or 'mt-need-valid-ymd2')
return message('Second date should be year, month, day')
end
end
return dates[1], dates[2]
return dates[1], dates[2]
end
end


local function ageGeneric(frame)
local function age_generic(frame)
-- Return the result required by the specified template.
-- Return the result required by the specified template.
-- Can use sortable=x where x = on/table/off/debug in any supported template.
-- Can use sortable=x where x = on/off/debug in any supported template.
-- Some templates default to sortable=on but can be overridden.
-- Some templates default to sortable=on but can be overridden with sortable=off.
local name = frame.args.template
local name = frame.args.template
if not name then
if not name then
return message('mt-template-x')
return message('The template invoking this must have "|template=x" where x is the wanted operation')
end
end
local args = frame:getParent().args
local args = frame:getParent().args
Line 727: Line 499:
show = 'y',
show = 'y',
abbr = 'abbr_raw',
abbr = 'abbr_raw',
flag = 'usesCurrent',
omitZero = true,
range = 'no',
},
},
age_full_years_nts = {      -- {{age nts}}
age_full_years_nts = {      -- {{age nts}}
Line 771: Line 540:
age_yd = {                  -- {{age in years and days}}
age_yd = {                  -- {{age in years and days}}
show = 'yd',
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,
sep = args.sep ~= 'and' and 'sep_comma' or nil,
sortable = 'on',
sortable = 'on',
Line 790: Line 553:
age_ymwd = {                -- {{age in years, months, weeks and days}}
age_ymwd = {                -- {{age in years, months, weeks and days}}
show = 'ymwd',
show = 'ymwd',
wantMixture = true,
want_mixture = true,
},
},
}
}
local spec = specs[name]
local spec = specs[name]
if not spec then
if not spec then
return message('mt-template-bad-name')
return message('The specified template name is not valid')
end
end
if name == 'age_days' then
if name == 'age_days' then
local su = stripToNil(args['show unit'])
local su = strip_to_nil(args['show unit'])
if su then
if su then
if su == 'abbr' or su == 'full' then
if su == 'abbr' or su == 'full' then
Line 806: Line 569:
end
end
end
end
local partial, autofill
local range = spec.range or yes(args.range) or (args.range == 'dash' and 'dash' or nil)
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 = {
local getopt = {
fix = yes(args.fix),
fix = yes(args.fix),
flag = stripToNil(args.flag) or spec.flag,
range = range,
omitZero = spec.omitZero,
want_mixture = spec.want_mixture,
partial = partial,
wantMixture = spec.wantMixture,
}
}
local date1, date2 = getDates(frame, getopt)
local date1, date2 = get_dates(frame, getopt)
if type(date1) == 'string' then
if type(date1) == 'string' then
return date1
return date1
end
end
local format = stripToNil(args.format)
local format = strip_to_nil(args.format)
local spell = spellOptions[format]
if format then
if format then
format = 'format_' .. format
format = 'format_' .. format
Line 844: Line 586:
end
end
local parms = {
local parms = {
diff = date2:subtract(date1, { fill = autofill }),
diff = date2 - date1,
wantDuration = spec.duration or yes(args.duration),
want_duration = spec.duration or yes(args.duration),
range = range,
range = range,
wantSc = yes(args.sc),
want_sc = yes(args.sc),
show = args.show == 'hide' and 'hide' or spec.show,
show = args.show == 'hide' and 'hide' or spec.show,
abbr = spec.abbr,
abbr = spec.abbr,
disp = spec.disp,
disp = spec.disp,
extra = makeExtra(args, getopt.usesCurrent and format ~= 'format_raw'),
format = format or spec.format,
format = format or spec.format,
round = yes(args.round),
round = yes(args.round),
sep = spec.sep,
sep = spec.sep,
sortable = translateParameters.sortable[args.sortable or spec.sortable],
sortable = translate_parameters.sortable[args.sortable or spec.sortable],
spell = spell,
}
}
if (spec.negative or frame.args.negative) == 'error' and parms.diff.isnegative then
if (spec.negative or frame.args.negative) == 'error' and parms.diff.isnegative then
return message('mt-date-wrong-order')
return message('The second date should not be before the first date')
end
return from_en(dateDifference(parms))
end
 
local function bda(frame)
-- Implement [[Template:Birth date and age]].
local args = frame:getParent().args
local options = {
missing1 = 'mt-need-valid-bd',
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 result = substituteParameters(
mtext['txt-bda'],
date:text('%-Y-%m-%d'),
from_en(date:text(dateFormat(args))),
from_en(dateDifference({
diff = diff,
show = show,
abbr = 'abbr_off',
disp = disp,
sep = 'sep_space',
}))
)
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-bad-param1', invalid)
end
end
return result
end
 
local function dda(frame)
-- Implement [[Template:Death date and age]].
local args = frame:getParent().args
local options = {
missing1 = 'mt-need-valid-dd',
missing2 = 'mt-need-valid-bd2',
noMissing = true,
partial = true,
}
local date1, date2 = getDates(frame, options)
if type(date1) == 'string' then
return date1
end
local diff = date1 - date2
if diff.isnegative then
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
if date1.day then  -- y, m, d known
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 result = 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',
}))
)
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-bad-param1', invalid)
end
end
end
return result
return date_difference(parms)
end
end


local function dateToGsd(frame)
local function date_to_gsd(frame)
-- Implement [[Template:Gregorian serial date]].
-- This implements {{gregorian serial date}}.
-- Return Gregorian serial date of the given date, or the current 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
-- The returned value is negative for dates before 1 January 1 AD
-- despite the fact that GSD is not defined for such dates.
-- despite the fact that GSD is not defined for such dates.
local date = getDates(frame, { wantMixture=true, single=true })
local date = get_dates(frame, { want_mixture=true, single=true })
if type(date) == 'string' then
if type(date) == 'string' then
return date
return date
Line 1,033: Line 616:
end
end


local function jdToDate(frame)
local function jd_to_date(frame)
-- Return formatted date from a Julian date.
-- Return formatted date from a Julian date.
-- The result includes a time if the input includes a fraction.
-- The result includes a time if the input includes a fraction.
-- The word 'Julian' is accepted for the Julian calendar.
-- The word 'Julian' is accepted for the Julian calendar.
local Date = getExports(frame)
local Date = get_exports(frame)
local args = frame:getParent().args
local args = frame:getParent().args
local date = Date('juliandate', args[1], args[2])
local date = Date('juliandate', args[1], args[2])
if date then
if date then
return from_en(date:text())
return date:text()
end
end
return message('mt-need-jdn')
return message('Need valid Julian date number')
end
end


local function dateToJd(frame)
local function date_to_jd(frame)
-- Return Julian date (a number) from a date which may include a time,
-- Return Julian date (a number) from a date which may include a time,
-- or the current date ('currentdate') or current date and time ('currentdatetime').
-- or the current date ('currentdate') or current date and time ('currentdatetime').
-- The word 'Julian' is accepted for the Julian calendar.
-- The word 'Julian' is accepted for the Julian calendar.
local Date = getExports(frame)
local Date = get_exports(frame)
local args = frame:getParent().args
local args = frame:getParent().args
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
Line 1,056: Line 639:
return tostring(date.jd)
return tostring(date.jd)
end
end
return message('mt-need-valid-ymd-current')
return message('Need valid year/month/day or "currentdate"')
end
end


local function timeInterval(frame)
local function time_interval(frame)
-- Implement [[Template:Time interval]].
-- This implements {{time interval}}.
-- There are two positional arguments: date1, date2.
-- There are two positional arguments: date1, date2.
-- The default for each is the current date and time.
-- The default for each is the current date and time.
-- Result is date2 - date1 formatted.
-- Result is date2 - date1 formatted.
local Date = getExports(frame)
local Date = get_exports(frame)
local args = frame:getParent().args
local args = frame:getParent().args
local parms = {
local parms = {
extra = makeExtra(args),
want_duration = yes(args.duration),
wantDuration = yes(args.duration),
range = yes(args.range) or (args.range == 'dash' and 'dash' or nil),
range = yes(args.range) or (args.range == 'dash' and 'dash' or nil),
wantSc = yes(args.sc),
want_sc = yes(args.sc),
}
}
local fix = yes(args.fix) and 'fix' or ''
local fix = yes(args.fix) and 'fix' or ''
local date1 = Date(fix, 'partial', stripToNil(args[1]) or 'currentdatetime')
local date1 = Date(fix, 'partial', strip_to_nil(args[1]) or 'currentdatetime')
if not date1 then
if not date1 then
return message('mt-invalid-start')
return message('Invalid start date in first parameter')
end
end
local date2 = Date(fix, 'partial', stripToNil(args[2]) or 'currentdatetime')
local date2 = Date(fix, 'partial', strip_to_nil(args[2]) or 'currentdatetime')
if not date2 then
if not date2 then
return message('mt-invalid-end')
return message('Invalid end date in second parameter')
end
end
parms.diff = date2 - date1
parms.diff = date2 - date1
for argname, translate in pairs(translateParameters) do
for argname, translate in pairs(translate_parameters) do
local parm = stripToNil(args[argname])
local parm = strip_to_nil(args[argname])
if parm then
if parm then
parm = translate[parm]
parm = translate[parm]
if parm == nil then  -- test for nil because false is a valid setting
if parm == nil then  -- test for nil because false is a valid setting
return message('mt-bad-param2', argname, args[argname])
return message('Parameter ' .. argname .. '=' .. args[argname] .. ' is invalid')
end
end
parms[argname] = parm
parms[argname] = parm
Line 1,098: Line 680:
if show then
if show then
if show.id ~= round then
if show.id ~= round then
return message('mt-conflicting-show', args.show, args.round)
return message('Parameter show=' .. args.show .. ' conflicts with round=' .. args.round)
end
end
else
else
parms.show = translateParameters.show[round]
parms.show = translate_parameters.show[round]
end
end
end
end
parms.round = true
parms.round = true
end
end
return from_en(dateDifference(parms))
return date_difference(parms)
end
end


return {
return {
age_generic = ageGeneric,           -- can emulate several age templates
age_generic = age_generic,         -- can emulate several age templates
birth_date_and_age = bda,          -- Template:Birth_date_and_age
gsd = date_to_gsd,                 -- Template:Gregorian_serial_date
death_date_and_age = dda,          -- Template:Death_date_and_age
extract = date_extract,           -- Template:Extract
gsd = dateToGsd,                   -- Template:Gregorian_serial_date
jd_to_date = jd_to_date,           -- Template:?
extract = dateExtract,             -- Template:Extract
JULIANDAY = date_to_jd,           -- Template:JULIANDAY
jd_to_date = jdToDate,             -- Template:?
time_interval = time_interval,     -- Template:Time_interval
JULIANDAY = dateToJd,               -- Template:JULIANDAY
time_interval = timeInterval,       -- Template:Time_interval
}
}
Please note that all contributions to Nonbinary Wiki are considered to be released under the Creative Commons Attribution-ShareAlike (see Nonbinary Wiki:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)

Template used on this page: