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.
--[[ Code for some date functions, including implementations of:
        {{Age in days}}                age_days
        {{Age in years and months}}    age_ym
        {{Gregorian serial date}}      gsd_ymd
Calendar functions will be needed in many areas, so this may be superseded
by some other system, perhaps using PHP functions accessed via mw.
]]


local mtext = {
local MINUS = ''  -- Unicode U+2212 MINUS SIGN
-- Message and other text that should be localized.
-- 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
local function number_name(number, singular, plural, sep)
if translate then
    -- Return the given number, converted to a string, with the
-- Functions to translate from en to local language and reverse go here.
    -- separator (default space) and singular or plural name appended.
-- See example at [[:bn:Module:বয়স]].
    plural = plural or (singular .. 's')
else
    sep = sep or ' '
from_en = function (text)
    return tostring(number) .. sep .. ((number == 1) and singular or plural)
return text
    -- this uses an interesting trick of Lua:
end
    --  * and reurns false if the first argument is false, and the second otherwise, so (number==1) and singular returns singular if its 1, returns false if it is only 1
isZero = function (text)
    --  * or returns the first argument if it is not false, and the second argument if the first is false
return tonumber(text) == 0
    --  * so, if number is 1, and evaluates (true and singular) returning (singular); or evaluates (singular or plural), finds singular non-false, and returns singular
end
    --  * but, if number is not 1, and evaluates (false and singular) returning (false); or evaluates (false or plural), and is forced to return plural
end
end


local _Date, _currentDate
local function strip_to_nil(text)
local function getExports(frame)
    -- If text is a non-blank string, return its content with no leading
-- Return objects exported from the date module or its sandbox.
    -- or trailing whitespace.
if not _Date then
    -- Otherwise return nil (a nil or empty string argument gives a nil
local sandbox = frame:getTitle():find(mtext['txt-sandbox'], 1, true) and ('/' .. mtext['txt-sandbox']) or ''
    -- result, as does a string argument of only whitespace).
local datemod = require(mtext['txt-module-date'] .. sandbox)
    if type(text) == 'string' then
local realDate = datemod._Date
        local result = text:match("^%s*(.-)%s*$")
_currentDate = datemod._current
        if result ~= '' then
if to_en then
            return result
_Date = function (...)
        end
local args = {}
    end
for i, v in ipairs({...}) do
    return nil
args[i] = to_en(v)
end
return realDate(unpack(args))
end
else
_Date = realDate
end
end
return _Date, _currentDate
end
end


local Collection  -- a table to hold items
local function is_leap_year(year)
Collection = {
    -- Return true if year is a leap year, assuming Gregorian calendar.
add = function (self, item)
    return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
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
end


local function dateFormat(args)
local function days_in_month(year, month)
-- Return string for wanted date format.
    -- Return number of days (1..31) in given month (1..12).
local default = mtext['txt-format-default']
    local month_days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
local other = default == 'df' and 'mf' or 'df'
    if month == 2 and is_leap_year(year) then
local wanted = stripToNil(args[other]) and other or default
        return 29
return wanted == 'df' and mtext['txt-dmy'] or mtext['txt-mdy']
    end
    return month_days[month]
end
end


local function substituteParameters(text, ...)
-- A table to get current year/month/day (UTC), but only if needed.
-- Return text after substituting any given parameters for $1, $2, etc.
local current = setmetatable({}, {
return mw.message.newRawMessage(text, ...):plain()
        __index = function (self, key)
end
            local d = os.date('!*t')
            self.year = d.year
            self.month = d.month
            self.day = d.day
            return rawget(self, key)
        end
    })


local function yes(parameter)
local function date_component(named, positional, component)
-- Return true if parameter should be interpreted as "yes".
    -- Return the first of the two arguments that is not nil and is not empty.
-- Do not want to accept mixed upper/lowercase unless done by current templates.
    -- If both are nil, return the current date component, if specified.
-- Need to accept "on" because "round=on" is wanted.
    -- The returned value is nil or is a number.
return ({ y = true, yes = true, on = true })[parameter]
    -- This translates empty arguments passed to the template to nil, and
    -- optionally replaces a nil argument with a value from the current date.
    named = strip_to_nil(named)
    if named then
        return tonumber(named)
    end
    positional = strip_to_nil(positional)
    if positional then
        return tonumber(positional)
    end
    if component then
        return current[component]
    end
    return nil
end
end


local function message(msg, ...)
local function gsd(year, month, day)
-- Return formatted message text for an error or warning.
    -- Return the Gregorian serial day (an integer >= 1) for the given date,
local function getText(msg)
    -- or return nil if the date is invalid (only check that year >= 1).
return mtext[msg] or error('Bug: message "' .. tostring(msg) .. '" not defined')
    -- This is the number of days from the start of 1 AD (there is no year 0).
end
    -- This code implements the logic in [[Template:Gregorian serial date]].
local categories = {
    if year < 1 then
error = mtext['txt-category'],
        return nil
warning = mtext['txt-category'],
    end
}
    local floor = math.floor
local a, b, k, category
    local days_this_year = (month - 1) * 30.5 + day
local text = substituteParameters(getText(msg), ...)
    if month > 2 then
if isWarning[msg] then
        if is_leap_year(year) then
a = '<sup>[<i>'
            days_this_year = days_this_year - 1
b = '</i>]</sup>'
        else
k = 'warning'
            days_this_year = days_this_year - 2
else
        end
a = '<strong class="error">' .. getText('txt-error')
        if month > 8 then
b = '</strong>'
            days_this_year = days_this_year + 0.9
k = 'error'
        end
end
    end
if mw.title.getCurrentTitle():inNamespaces(0) then
    days_this_year = floor(days_this_year + 0.5)
-- Category only in namespaces: 0=article.
    year = year - 1
category = '[[' .. categories[k] .. ']]'
    local days_from_past_years = year * 365
end
        + floor(year / 4)
return
        - floor(year / 100)
a ..
        + floor(year / 400)
mw.text.nowiki(text) ..
    return days_from_past_years + days_this_year
b ..
(category or '')
end
end


local function formatNumber(number)
local Date = {
-- Return the given number formatted with commas as group separators,
    -- A naive date that assumes the Gregorian calendar always applied.
-- given that the number is an integer.
    year = 0,  -- 1 to 9999 (0 if never set)
local numstr = tostring(number)
    month = 1, -- 1 to 12
local length = #numstr
    day = 1,    -- 1 to 31
local places = Collection.new()
    isvalid = false,
local pos = 0
    new = function (self, o)
repeat
        o = o or {}
places:add(pos)
        setmetatable(o, self)
pos = pos + 3
        self.__index = self
until pos >= length
        return o
places:add(length)
    end
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)
function Date:__lt(rhs)
-- Return result of spelling number, or
    -- Return true if self < rhs.
-- return number (as a string) if cannot spell it.
    if self.year < rhs.year then
-- i == 1 for the first number which can optionally start with an uppercase letter.
        return true
number = tostring(number)
    elseif self.year == rhs.year then
return require(mtext['txt-module-convertnumeric']).spell_number(
        if self.month < rhs.month then
number,
            return true
nil,                      -- fraction numerator
        elseif self.month == rhs.month then
nil,                      -- fraction denominator
            return self.day < rhs.day
i == 1 and options.upper,  -- true: 'One' instead of 'one'
        end
not options.us,            -- true: use 'and' between tens/ones etc
    end
options.adj,              -- true: hyphenated
    return false
options.ordinal            -- true: 'first' instead of 'one'
    -- probably simplify to return (self.year < rhs.year) or ((self.year == rhs.year) and ((self.month < rhs.month) or ((self.month == rhs.month) and (self.day < rhs.day))))
) or number
    -- would be just as efficient, as lua does not evaluate second argument of (true or second_argument)
    -- or similarly return self.year < rhs.year ? true : self.year > rhs.year ? false : self.month < rhs.month ? true : self.month > rhs.month ? false : self.day < rhs.day
end
end


local function makeExtra(args, flagCurrent)
function Date:set_current()
-- Return extra text that will be inserted before the visible result
    -- Set date from current time (UTC) and return self.
-- but after any sort key.
    self.year = current.year
local extra = args.prefix or ''
    self.month = current.month
if mw.ustring.len(extra) > 1 then
    self.day = current.day
-- Parameter "~" gives "~3" whereas "over" gives "over 3".
    self.isvalid = true
if extra:sub(-6, -1) ~= '&nbsp;' then
    return self
extra = extra .. ' '
end
end
if flagCurrent then
extra = '<span class="currentage"></span>' .. extra
end
return extra
end
end


local function makeSort(value, sortable)
function Date:set_ymd(y, m, d)
-- Return a sort key if requested.
    -- Set date from year, month, day (strings or numbers) and return self.
-- Assume value is a valid number which has not overflowed.
    -- LATER: If m is a name like "March" or "mar", translate it to a month.
if sortable == 'sortable_table' or sortable == 'sortable_on' or sortable == 'sortable_debug' then
    y = tonumber(y)
local sortKey
    m = tonumber(m)
if value == 0 then
    d = tonumber(d)
sortKey = '5000000000000000000'
    if type(y) == 'number' and type(m) == 'number' and type(d) == 'number' then
else
        self.year = y
local mag = math.floor(math.log10(math.abs(value)) + 1e-14)
        self.month = m
if value > 0 then
        self.day = d
sortKey = 7000 + mag
        self.isvalid = (1 <= y and y <= 9999 and 1 <= m and m <= 12 and
else
                        1 <= d and d <= days_in_month(y, m))
sortKey = 2999 - mag
    end
value = value + 10^(mag+1)
    return self
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
end


local translateParameters = {
local DateDiff = {
abbr = {
    -- Simple difference between two dates, assuming Gregorian calendar.
off = 'abbr_off',
    isnegative = false, -- true if second date is before first
on = 'abbr_on',
    years = 0,
},
    months = 0,
disp = {
    days = 0,
age = 'disp_age',
    new = function (self, o)
raw = 'disp_raw',
        o = o or {}
},
        setmetatable(o, self)
format = {
        self.__index = self
raw = 'format_raw',
        return o
commas = 'format_commas',
    end
},
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)
function DateDiff:set(date1, date2)
-- Return part of a date after performing an optional operation.
    -- Set difference between the two dates, and return self.
local Date = getExports(frame)
    -- Difference is negative if the second date is older than the first.
local args = frame:getParent().args
    local isnegative
local parms = {}
    if date2 < date1 then
for i, v in ipairs(args) do
        isnegative = true
parms[i] = v
        date1, date2 = date2, date1
end
    else
if yes(args.fix) then
        isnegative = false
table.insert(parms, 'fix')
    end
end
    -- It is known that date1 <= date2.
if yes(args.partial) then
    local y1, m1, d1 = date1.year, date1.month, date1.day
table.insert(parms, 'partial')
    local y2, m2, d2 = date2.year, date2.month, date2.day
end
    local years, months, days = y2 - y1, m2 - m1, d2 - d1
local show = stripToNil(args.show) or 'dmy'
    if days < 0 then
local date = Date(unpack(parms))
        days = days + days_in_month(y1, m1)
if not date then
        months = months - 1
if show == 'format' then
    end
return 'error'
    if months < 0 then
end
        months = months + 12
return message('mt-need-valid-date')
        years = years - 1
end
    end
local add = stripToNil(args.add)
    self.years, self.months, self.days, self.isnegative = years, months, days, isnegative
if add then
    return self
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
end


local function rangeJoin(range)
function DateDiff:age_ym()
-- Return text to be used between a range of ages.
    -- Return text specifying difference in years, months.
return range == 'dash' and '' or mtext['txt-or']
    local sign = self.isnegative and MINUS or ''
    local mtext = number_name(self.months, 'month')
    local result
    if self.years > 0 then
        local ytext = number_name(self.years, 'year')
        if self.months == 0 then
            result = ytext
        else
            result = ytext .. ',&nbsp;' .. mtext
        end
    else
        if self.months == 0 then
            sign = ''
        end
        result = mtext
    end
    return sign .. result
end
end


local function makeText(values, components, names, options, noUpper)
local function error_wikitext(text)
-- Return wikitext representing an age or duration.
    -- Return message for display when template parameters are invalid.
local text = Collection.new()
    local prefix = '[[Module talk:Age|Module error]]:'
local count = #values
    local cat = '[[Category:Age error]]'
local sep = names.sep or ''
    return '<span style="color:black; background-color:pink;">' ..
for i, v in ipairs(values) do
            prefix .. ' ' .. text .. cat .. '</span>'
-- 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
if type(name) == 'table' then
name = mw.getContentLanguage():plural(islist and v[2] or v, name)
end
text:add(vstr .. sep .. name)
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 = mtext['txt-comma-and']
else
first = ', '
last = mtext['txt-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
end


local function dateDifference(parms)
local function age_days(frame)
-- Return a formatted date difference using the given parameters
    -- Return age in days between two given dates, or
-- which have been validated.
    -- between given date and current date.
local names = {
    -- This code implements the logic in [[Template:Age in days]].
-- Each name is:
    -- Like {{Age in days}}, a missing argument is replaced from the current
-- * a string if no plural form of the name is used; or
    -- date, so can get a bizarre mixture of specified/current y/m/d.
-- * a table of strings, one of which is selected using the rules at
    local args = frame:getParent().args
--  https://translatewiki.net/wiki/Plural/Mediawiki_plural_rules
    local year1  = date_component(args.year1 , args[1], 'year' )
abbr_off = {
    local month1 = date_component(args.month1, args[2], 'month')
sep = '&nbsp;',
    local day1  = date_component(args.day1  , args[3], 'day'  )
y = {'year', 'years'},
    local year2 = date_component(args.year2 , args[4], 'year' )
m = {'month', 'months'},
    local month2 = date_component(args.month2, args[5], 'month')
w = {'week', 'weeks'},
    local day2  = date_component(args.day2 , args[6], 'day' )
d = {'day', 'days'},
    local gsd1 = gsd(year1, month1, day1)
H = {'hour', 'hours'},
    local gsd2 = gsd(year2, month2, day2)
M = {'minute', 'minutes'},
    if gsd1 and gsd2 then
S = {'second', 'seconds'},
        local sign = ''
},
        local result = gsd2 - gsd1
abbr_on = {
        if result < 0 then
y = 'y',
            sign = MINUS
m = 'm',
            result = -result
w = 'w',
        end
d = 'd',
        return sign .. tostring(result)
H = 'h',
    end
M = 'm',
    return error_wikitext('Cannot handle dates before the year 1 AD')
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 = {},
}
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('mt-bad-show', show.id)
end
end


local function getDates(frame, getopt)
local function age_ym(frame)
-- Parse template parameters and return one of:
    -- Return age in years and months between two given dates, or
-- * date        (a date table, if single)
    -- between given date and current date.
-- * date1, date2 (two date tables, if not single)
    local args = frame:getParent().args
-- * text        (a string error message)
    local fields = {}
-- A missing date is optionally replaced with the current date.
    for i = 1, 6 do
-- If wantMixture is true, a missing date component is replaced
        fields[i] = strip_to_nil(args[i])
-- from the current date, so can get a bizarre mixture of
    end
-- specified/current y/m/d as has been done by some "age" templates.
    local date1, date2
-- Some results may be placed in table getopt.
    if fields[1] and fields[2] and fields[3] then
local Date, currentDate = getExports(frame)
        date1 = Date:new():set_ymd(fields[1], fields[2], fields[3])
getopt = getopt or {}
    end
local function flagCurrent(text)
    if not (date1 and date1.isvalid) then
-- This allows the calling template to detect if the current date has been used,
        return error_wikitext('Need date: year, month, day')
-- that is, whether both dates have been entered in a template expecting two.
    end
-- For example, an infobox may want the age when an event occurred, not the current age.
    if fields[4] and fields[5] and fields[6] then
-- Don't bother detecting if wantMixture is used because not needed and it is a poor option.
        date2 = Date:new():set_ymd(fields[4], fields[5], fields[6])
if not text then
        if not date2.isvalid then
if getopt.noMissing then
            return error_wikitext('Second date should be year, month, day')
return nil  -- this gives a nil date which gives an error
        end
end
    else
text = 'currentdate'
        date2 = Date:new():set_current()
if getopt.flag == 'usesCurrent' then
    end
getopt.usesCurrent = true
    return DateDiff:new():set(date1, date2):age_ym()
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 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 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
local y, m, d = 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
--    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 then
dates[i] = Date(flagCurrent())
end
end
end
else
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(getopt.missing1 or 'mt-need-valid-ymd')
end
if getopt.single then
return dates[1]
end
if not dates[2] then
return message(getopt.missing2 or 'mt-need-valid-ymd2')
end
return dates[1], dates[2]
end
end


local function ageGeneric(frame)
local function gsd_ymd(frame)
-- Return the result required by the specified template.
    -- Return Gregorian serial day of the given date, or the current date.
-- Can use sortable=x where x = on/table/off/debug in any supported template.
    -- Like {{Gregorian serial date}}, a missing argument is replaced from the
-- Some templates default to sortable=on but can be overridden.
    -- current date, so can get a bizarre mixture of specified/current y/m/d.
local name = frame.args.template
    -- This accepts positional arguments, although the original template does not.
if not name then
    local args = frame:getParent().args
return message('mt-template-x')
    local year  = date_component(args.year , args[1], 'year' )
end
    local month = date_component(args.month, args[2], 'month')
local args = frame:getParent().args
     local day   = date_component(args.day  , args[3], 'day' )
local specs = {
    local result = gsd(year, month, day)
age_days = {                -- {{age in days}}
    if result then
show = 'd',
        return tostring(result)
disp = 'disp_raw',
    end
},
    return error_wikitext('Cannot handle dates before the year 1 AD')
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('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, { 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('mt-date-wrong-order')
end
return from_en(dateDifference(parms))
end
end


local function bda(frame)
return { age_days = age_days, age_ym = age_ym, gsd = gsd_ymd }
-- 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
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 from_en(date:text())
end
return message('mt-need-jdn')
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('mt-need-valid-ymd-current')
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('mt-invalid-start')
end
local date2 = Date(fix, 'partial', stripToNil(args[2]) or 'currentdatetime')
if not date2 then
return message('mt-invalid-end')
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('mt-bad-param2', argname, args[argname])
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('mt-conflicting-show', args.show, args.round)
end
else
parms.show = translateParameters.show[round]
end
end
parms.round = true
end
return from_en(dateDifference(parms))
end
 
return {
age_generic = ageGeneric,          -- can emulate several age templates
birth_date_and_age = bda,          -- Template:Birth_date_and_age
death_date_and_age = dda,          -- Template:Death_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
}
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: