Module:Age: Difference between revisions
Jump to navigation
Jump to search
Content added Content deleted
mNo edit summary |
(update from Module:Age/sandbox: date handling moved to Module:Date, many more features) |
||
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 mediawiki. |
|||
]] |
|||
local _Date, _current_date |
|||
local MINUS = '−' -- Unicode U+2212 MINUS SIGN |
|||
local function get_exports(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 |
|||
_current_date = datemod._current |
|||
end |
|||
return _Date, _current_date |
|||
end |
|||
local function |
local function collection() |
||
-- Return a table to hold items. |
|||
return { |
|||
-- separator (default space) and singular or plural name appended. |
|||
n = 0, |
|||
plural = plural or (singular .. 's') |
|||
add = function (self, item) |
|||
sep = sep or ' ' |
|||
self.n = self.n + 1 |
|||
return tostring(number) .. sep .. ((number == 1) and singular or plural) |
|||
self[self.n] = item |
|||
-- this uses an interesting trick of Lua: |
|||
end, |
|||
-- * and returns 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 |
|||
join = function (self, sep) |
|||
-- * or returns the first argument if it is not false, and the second argument if the first is false |
|||
return table.concat(self, sep) |
|||
-- * 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 function strip_to_nil(text) |
local function strip_to_nil(text) |
||
-- If text is a string, return its trimmed content, or nil if empty. |
|||
-- Otherwise return text (which may, for example, be nil). |
|||
-- or trailing whitespace. |
|||
if type(text) == 'string' then |
|||
-- Otherwise return nil (a nil or empty string argument gives a nil |
|||
text = text:match('(%S.-)%s*$') |
|||
-- result, as does a string argument of only whitespace). |
|||
end |
|||
if type(text) == 'string' then |
|||
return text |
|||
local result = text:match("^%s*(.-)%s*$") |
|||
if result ~= '' then |
|||
return result |
|||
end |
|||
end |
|||
return nil |
|||
end |
end |
||
local function |
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. |
|||
return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0) |
|||
-- Need to accept "on" because "round=on" is wanted. |
|||
return ({ y = true, yes = true, on = true })[parameter] |
|||
end |
end |
||
local function |
local function message(msg, nocat) |
||
-- Return formatted message text for an error. |
|||
-- Can append "#FormattingError" to URL of a page with a problem to find it. |
|||
local month_days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } |
|||
local anchor = '<span id="FormattingError" />' |
|||
if month == 2 and is_leap_year(year) then |
|||
local category |
|||
return 29 |
|||
if not nocat and mw.title.getCurrentTitle():inNamespaces(0, 10) then |
|||
end |
|||
-- Category only in namespaces: 0=article, 10=template. |
|||
return month_days[month] |
|||
category = '[[Category:Age error]]' |
|||
else |
|||
category = '' |
|||
end |
|||
return anchor .. |
|||
'<strong class="error">Error: ' .. |
|||
mw.text.nowiki(msg) .. |
|||
'</strong>' .. |
|||
category |
|||
end |
end |
||
local function formatnumber(number) |
|||
-- A table to get current year/month/day (UTC), but only if needed. |
|||
-- Return the given number formatted with commas as group separators, |
|||
local current = setmetatable({}, { |
|||
-- given that the number is an integer. |
|||
__index = function (self, key) |
|||
local numstr = tostring(number) |
|||
local length = #numstr |
|||
self.year = d.year |
|||
local places = collection() |
|||
self.month = d.month |
|||
local pos = 0 |
|||
self.day = d.day |
|||
repeat |
|||
return rawget(self, key) |
|||
places:add(pos) |
|||
end |
|||
pos = pos + 3 |
|||
until pos >= length |
|||
places:add(length) |
|||
local function date_component(named, positional, component) |
|||
local groups = collection() |
|||
-- Return the first of the two arguments that is not nil and is not empty. |
|||
for i = places.n, 2, -1 do |
|||
-- If both are nil, return the current date component, if specified. |
|||
local p1 = length - places[i] + 1 |
|||
-- The returned value is nil or is a number. |
|||
local p2 = length - places[i - 1] |
|||
-- This translates empty arguments passed to the template to nil, and |
|||
groups:add(numstr:sub(p1, p2)) |
|||
-- optionally replaces a nil argument with a value from the current date. |
|||
end |
|||
named = strip_to_nil(named) |
|||
return groups:join(',') |
|||
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 |
local function make_sort(value, sortable) |
||
-- Return a sort key in a span if specified. |
|||
-- Assume value is a valid number which has not overflowed. |
|||
-- or return nil if the date is invalid (only check that year >= 1). |
|||
if sortable == 'sortable_on' or sortable == 'sortable_debug' then |
|||
-- This is the number of days from the start of 1 AD (there is no year 0). |
|||
local sortkey |
|||
-- This code implements the logic in [[Template:Gregorian serial date]]. |
|||
if value == 0 then |
|||
sortkey = '5000000000000000000' |
|||
return nil |
|||
else |
|||
end |
|||
local mag = math.floor(math.log10(math.abs(value)) + 1e-14) |
|||
local prefix |
|||
local days_this_year = (month - 1) * 30.5 + day |
|||
if value > 0 then |
|||
prefix = 7000 + mag |
|||
if is_leap_year(year) then |
|||
else |
|||
days_this_year = days_this_year - 1 |
|||
prefix = 2999 - mag |
|||
else |
|||
value = value + 10^(mag+1) |
|||
days_this_year = days_this_year - 2 |
|||
end |
|||
sortkey = string.format('%d', prefix) .. string.format('%015.0f', math.floor(value * 10^(14-mag))) |
|||
if month > 8 then |
|||
end |
|||
days_this_year = days_this_year + 0.9 |
|||
local lhs = sortable == 'sortable_debug' and |
|||
end |
|||
'<span style="border:1px solid;display:inline;" class="sortkey">' or |
|||
end |
|||
'<span style="display:none" class="sortkey">' |
|||
days_this_year = floor(days_this_year + 0.5) |
|||
return lhs .. sortkey .. '♠</span>' |
|||
year = year - 1 |
|||
end |
|||
local days_from_past_years = year * 365 |
|||
+ floor(year / 4) |
|||
- floor(year / 100) |
|||
+ floor(year / 400) |
|||
return days_from_past_years + days_this_year |
|||
end |
end |
||
local |
local translate_parameters = { |
||
abbr = { |
|||
-- A naive date that assumes the Gregorian calendar always applied. |
|||
off = 'abbr_off', |
|||
year = 0, -- 1 to 9999 (0 if never set) |
|||
on = 'abbr_on', |
|||
month = 1, -- 1 to 12 |
|||
}, |
|||
day = 1, -- 1 to 31 |
|||
disp = { |
|||
isvalid = false, |
|||
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' }, |
|||
d = { 'd', id = 'd' }, |
|||
dh = { 'd', 'H', id = 'dh' }, |
|||
dhm = { 'd', 'H', 'M', id = 'dhm' }, |
|||
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', |
|||
debug = 'sortable_debug', |
|||
}, |
|||
} |
} |
||
function |
local function date_extract(frame) |
||
-- Return part of a date after performing an optional operation. |
|||
-- Return true if self < rhs. |
|||
local Date = get_exports(frame) |
|||
if self.year < rhs.year then |
|||
local args = frame:getParent().args |
|||
return true |
|||
local parms = {} |
|||
elseif self.year == rhs.year then |
|||
for i, v in ipairs(args) do |
|||
if self.month < rhs.month then |
|||
parms[i] = v |
|||
return true |
|||
end |
|||
elseif self.month == rhs.month then |
|||
if yes(args.fix) then |
|||
return self.day < rhs.day |
|||
table.insert(parms, 'fix') |
|||
end |
|||
end |
|||
if yes(args.partial) then |
|||
return false |
|||
table.insert(parms, 'partial') |
|||
-- 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)))) |
|||
end |
|||
-- would be just as efficient, as lua does not evaluate second argument of (true or second_argument) |
|||
local date = Date(unpack(parms)) |
|||
-- 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 |
|||
if not date then |
|||
return message('Need valid date') |
|||
end |
|||
local add = strip_to_nil(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 prefix, result |
|||
local sortable = translate_parameters.sortable[args.sortable] |
|||
if sortable then |
|||
local value = (date.partial and date.partial.first or date).jdz |
|||
prefix = make_sort(value, sortable) |
|||
end |
|||
local show = strip_to_nil(args.show) or 'dmy' |
|||
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 (prefix or '') .. (result or '') |
|||
end |
end |
||
local function make_text(values, components, names, options) |
|||
function Date:set_current() |
|||
-- Return wikitext representing an age or duration. |
|||
-- Set date from current time (UTC) and return self. |
|||
local text = collection() |
|||
self.year = current.year |
|||
local count = #values |
|||
self.month = current.month |
|||
local sep = names.sep or '' |
|||
self.day = current.day |
|||
for i, v in ipairs(values) do |
|||
self.isvalid = true |
|||
-- v is a number (say 4 for 4 years), or a table ({4,5} for 4 or 5 years). |
|||
return self |
|||
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 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 |
|||
local join = options.range == 'dash' and '–' or ' or ' |
|||
vstr = fmt(v[1]) .. join .. 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.prefix or '') .. |
|||
sign .. |
|||
text:join() .. |
|||
(options.suffix or '') |
|||
end |
end |
||
function |
local function date_difference(parms) |
||
-- Return a formatted date difference using the given parameters |
|||
-- Set date from year, month, day (strings or numbers) and return self. |
|||
-- which have been validated. |
|||
-- LATER: If m is a name like "March" or "mar", translate it to a month. |
|||
local names = { |
|||
y = tonumber(y) |
|||
abbr_off = { |
|||
m = tonumber(m) |
|||
plural = 's', |
|||
d = tonumber(d) |
|||
sep = ' ', |
|||
if type(y) == 'number' and type(m) == 'number' and type(d) == 'number' then |
|||
y = 'year', |
|||
m = 'month', |
|||
w = 'week', |
|||
self.day = d |
|||
d = 'day', |
|||
self.isvalid = (1 <= y and y <= 9999 and 1 <= m and m <= 12 and |
|||
H = 'hour', |
|||
1 <= d and d <= days_in_month(y, m)) |
|||
M = 'minute', |
|||
end |
|||
}, |
|||
return self |
|||
abbr_on = { |
|||
y = 'y', |
|||
m = 'm', |
|||
w = 'w', |
|||
d = 'd', |
|||
H = 'h', |
|||
M = 'm', |
|||
}, |
|||
abbr_infant = { -- for {{age for infant}} |
|||
plural = 's', |
|||
sep = ' ', |
|||
y = 'yr', |
|||
m = 'mo', |
|||
w = 'wk', |
|||
d = 'day', |
|||
H = 'hr', |
|||
M = 'min', |
|||
}, |
|||
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 default_join |
|||
if abbr ~= 'abbr_off' then |
|||
default_join = 'sep_space' |
|||
end |
|||
if not show then |
|||
show = 'ymd' |
|||
if parms.disp == 'disp_age' then |
|||
if diff.years < 3 then |
|||
default_join = '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 = translate_parameters.show[show] |
|||
end |
|||
if parms.disp == 'disp_raw' then |
|||
default_join = 'sep_space' |
|||
abbr = 'abbr_raw' |
|||
elseif parms.want_sc then |
|||
default_join = 'sep_serialcomma' |
|||
end |
|||
local diff_options = { |
|||
round = parms.round, |
|||
duration = parms.want_duration, |
|||
range = parms.range and true or nil, |
|||
} |
|||
local prefix |
|||
if parms.sortable then |
|||
local value = diff.age_days + (parms.want_duration and 1 or 0) -- days and fraction of a day |
|||
if diff.isnegative then |
|||
value = -value |
|||
end |
|||
prefix = make_sort(value, parms.sortable) |
|||
end |
|||
local text_options = { |
|||
prefix = prefix, |
|||
suffix = parms.suffix, -- not currently used |
|||
format = parms.format, |
|||
join = parms.sep or default_join, |
|||
isnegative = diff.isnegative, |
|||
range = parms.range, |
|||
} |
|||
if show.id == 'hide' then |
|||
return prefix or '' |
|||
end |
|||
local values = { diff:age(show.id, diff_options) } |
|||
return make_text(values, show, names[abbr], text_options) |
|||
end |
end |
||
local |
local function get_dates(frame, getopt) |
||
-- Parse template parameters and return one of: |
|||
-- Simple difference between two dates, assuming Gregorian calendar. |
|||
-- * date (a date table, if single) |
|||
-- * date1, date2 (two date tables, if not single) |
|||
years = 0, |
|||
-- * text (a string error message) |
|||
months = 0, |
|||
-- A missing date is replaced with the current date. |
|||
days = 0, |
|||
-- If want_mixture is true, a missing date component is replaced |
|||
new = function (self, o) |
|||
-- from the current date, so can get a bizarre mixture of |
|||
o = o or {} |
|||
-- specified/current y/m/d as has been done by some "age" templates. |
|||
setmetatable(o, self) |
|||
-- Some results may be placed in table getopt. |
|||
self.__index = self |
|||
local Date, current_date = get_exports(frame) |
|||
return o |
|||
getopt = getopt or {} |
|||
end |
|||
local fix = getopt.fix and 'fix' or '' |
|||
} |
|||
local partial = getopt.range and 'partial' or '' |
|||
local args = frame:getParent().args |
|||
function DateDiff:set(date1, date2) |
|||
local fields = {} |
|||
-- Set difference between the two dates, and return self. |
|||
local is_named = args.year or args.year1 or args.year2 or |
|||
-- Difference is negative if the second date is older than the first. |
|||
args.month or args.month1 or args.month2 or |
|||
local isnegative |
|||
args.day or args.day1 or args.day2 |
|||
if date2 < date1 then |
|||
if is_named then |
|||
isnegative = true |
|||
fields[1] = args.year1 or args.year |
|||
date1, date2 = date2, date1 |
|||
fields[2] = args.month1 or args.month |
|||
else |
|||
fields[3] = args.day1 or args.day |
|||
isnegative = false |
|||
fields[4] = args.year2 |
|||
end |
|||
fields[5] = args.month2 |
|||
-- It is known that date1 <= date2. |
|||
fields[6] = args.day2 |
|||
local y1, m1, d1 = date1.year, date1.month, date1.day |
|||
else |
|||
local y2, m2, d2 = date2.year, date2.month, date2.day |
|||
for i = 1, 6 do |
|||
local years, months, days = y2 - y1, m2 - m1, d2 - d1 |
|||
fields[i] = args[i] |
|||
if days < 0 then |
|||
end |
|||
days = days + days_in_month(y1, m1) |
|||
end |
|||
months = months - 1 |
|||
local imax = 0 |
|||
end |
|||
for i = 1, 6 do |
|||
if months < 0 then |
|||
fields[i] = strip_to_nil(fields[i]) |
|||
months = months + 12 |
|||
if fields[i] then |
|||
years = years - 1 |
|||
imax = i |
|||
end |
|||
end |
|||
self.years, self.months, self.days, self.isnegative = years, months, days, isnegative |
|||
end |
|||
return self |
|||
local single = getopt.single |
|||
local dates = {} |
|||
if is_named or imax > 2 then |
|||
local nr_dates = single and 1 or 2 |
|||
if getopt.want_mixture then |
|||
-- Cannot be partial since empty fields are set from current. |
|||
local components = { 'year', 'month', 'day' } |
|||
for i = 1, nr_dates * 3 do |
|||
fields[i] = fields[i] or current_date[components[i > 3 and i - 3 or i]] |
|||
end |
|||
for i = 1, nr_dates do |
|||
local index = i == 1 and 1 or 4 |
|||
dates[i] = Date(fields[index], fields[index+1], fields[index+2]) |
|||
end |
|||
else |
|||
for i = 1, nr_dates do |
|||
local index = i == 1 and 1 or 4 |
|||
local y, m, d = fields[index], fields[index+1], fields[index+2] |
|||
if (partial and y) or (y and m and d) then |
|||
dates[i] = Date(fix, partial, y, m, d) |
|||
elseif not (y or m or d) then |
|||
dates[i] = Date('currentdate') |
|||
end |
|||
end |
|||
end |
|||
else |
|||
getopt.textdates = true |
|||
dates[1] = Date(fix, partial, fields[1] or 'currentdate') |
|||
if not single then |
|||
dates[2] = Date(fix, partial, fields[2] or 'currentdate') |
|||
end |
|||
end |
|||
if not dates[1] then |
|||
return message('Need valid year, month, day') |
|||
end |
|||
if 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 |
end |
||
function |
local function age_generic(frame) |
||
-- Return the result required by the specified template. |
|||
-- Can use sortable=x where x = on/off/debug in any supported template. |
|||
local sign = self.isnegative and MINUS or '' |
|||
-- Some templates default to sortable=on but can be overridden with sortable=off. |
|||
local mtext = number_name(self.months, 'month') |
|||
local name = frame.args.template |
|||
local result |
|||
if not name then |
|||
return message('The template invoking this must have "|template=x" where x is the wanted operation') |
|||
local ytext = number_name(self.years, 'year') |
|||
end |
|||
if self.months == 0 then |
|||
local args = frame:getParent().args |
|||
result = ytext |
|||
local specs = { |
|||
else |
|||
age_days = { -- {{age in days}} |
|||
show = 'd', |
|||
end |
|||
disp = 'disp_raw', |
|||
else |
|||
}, |
|||
if self.months == 0 then |
|||
age_days_nts = { -- {{age in days nts}} |
|||
show = 'd', |
|||
end |
|||
disp = 'disp_raw', |
|||
result = mtext |
|||
format = 'format_commas', |
|||
end |
|||
sortable = 'on', |
|||
return sign .. result |
|||
}, |
|||
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', |
|||
}, |
|||
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', |
|||
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', |
|||
want_mixture = 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 = strip_to_nil(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 range = spec.range or yes(args.range) or (args.range == 'dash' and 'dash' or nil) |
|||
local getopt = { |
|||
fix = yes(args.fix), |
|||
range = range, |
|||
want_mixture = spec.want_mixture, |
|||
} |
|||
local date1, date2 = get_dates(frame, getopt) |
|||
if type(date1) == 'string' then |
|||
return date1 |
|||
end |
|||
local format = strip_to_nil(args.format) |
|||
if format then |
|||
format = 'format_' .. format |
|||
elseif name == 'age_days' and getopt.textdates then |
|||
format = 'format_commas' |
|||
end |
|||
local parms = { |
|||
diff = date2 - date1, |
|||
want_duration = spec.duration or yes(args.duration), |
|||
range = range, |
|||
want_sc = yes(args.sc), |
|||
show = args.show == 'hide' and 'hide' or spec.show, |
|||
abbr = spec.abbr, |
|||
disp = spec.disp, |
|||
format = format or spec.format, |
|||
round = yes(args.round), |
|||
sep = spec.sep, |
|||
sortable = translate_parameters.sortable[args.sortable or spec.sortable], |
|||
} |
|||
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 date_difference(parms) |
|||
end |
end |
||
local function |
local function date_to_gsd(frame) |
||
-- This implements {{gregorian serial date}}. |
|||
-- Return message for display when template parameters are invalid. |
|||
-- Return Gregorian serial date of the given date, or the current date. |
|||
local prefix = '[[Module talk:Age|Module error]]:' |
|||
-- The returned value is negative for dates before 1 January 1 AD |
|||
local cat = '[[Category:Age error]]' |
|||
-- despite the fact that GSD is not defined for such dates. |
|||
return '<span style="color:black; background-color:pink;">' .. |
|||
local date = get_dates(frame, { want_mixture=true, single=true }) |
|||
prefix .. ' ' .. text .. cat .. '</span>' |
|||
if type(date) == 'string' then |
|||
return date |
|||
end |
|||
return tostring(date.gsd) |
|||
end |
end |
||
local function |
local function jd_to_date(frame) |
||
-- Return formatted date from a Julian date. |
|||
-- The result includes a time if the input includes a fraction. |
|||
-- between given date and current date. |
|||
-- The word 'Julian' is accepted for the Julian calendar. |
|||
-- This code implements the logic in [[Template:Age in days]]. |
|||
local Date = get_exports(frame) |
|||
-- Like {{Age in days}}, a missing argument is replaced from the current |
|||
local args = frame:getParent().args |
|||
-- date, so can get a bizarre mixture of specified/current y/m/d. |
|||
local date = Date('juliandate', args[1], args[2]) |
|||
local args = frame:getParent().args |
|||
if date then |
|||
local year1 = date_component(args.year1 , args[1], 'year' ) |
|||
return date:text() |
|||
local month1 = date_component(args.month1, args[2], 'month') |
|||
end |
|||
local day1 = date_component(args.day1 , args[3], 'day' ) |
|||
return message('Need valid Julian date number') |
|||
local year2 = date_component(args.year2 , args[4], 'year' ) |
|||
local month2 = date_component(args.month2, args[5], 'month') |
|||
local day2 = date_component(args.day2 , args[6], 'day' ) |
|||
local gsd1 = gsd(year1, month1, day1) |
|||
local gsd2 = gsd(year2, month2, day2) |
|||
if gsd1 and gsd2 then |
|||
local sign = '' |
|||
local result = gsd2 - gsd1 |
|||
if result < 0 then |
|||
sign = MINUS |
|||
result = -result |
|||
end |
|||
return sign .. tostring(result) |
|||
end |
|||
return error_wikitext('Cannot handle dates before the year 1 AD') |
|||
end |
end |
||
local function |
local function date_to_jd(frame) |
||
-- Return Julian date (a number) from a date which may include a time, |
|||
-- Return age in years and months between two given dates, or |
|||
-- or the current date ('currentdate') or current date and time ('currentdatetime'). |
|||
-- The word 'Julian' is accepted for the Julian calendar. |
|||
local args = frame:getParent().args |
|||
local Date = get_exports(frame) |
|||
local args = frame:getParent().args |
|||
for i = 1, 6 do |
|||
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7]) |
|||
fields[i] = strip_to_nil(args[i]) |
|||
if date then |
|||
end |
|||
return tostring(date.jd) |
|||
local date1, date2 |
|||
end |
|||
if fields[1] and fields[2] and fields[3] then |
|||
return message('Need valid year/month/day or "currentdate"') |
|||
date1 = Date:new():set_ymd(fields[1], fields[2], fields[3]) |
|||
end |
|||
if not (date1 and date1.isvalid) then |
|||
return error_wikitext('Need date: year, month, day') |
|||
end |
|||
if fields[4] and fields[5] and fields[6] then |
|||
date2 = Date:new():set_ymd(fields[4], fields[5], fields[6]) |
|||
if not date2.isvalid then |
|||
return error_wikitext('Second date should be year, month, day') |
|||
end |
|||
else |
|||
date2 = Date:new():set_current() |
|||
end |
|||
return DateDiff:new():set(date1, date2):age_ym() |
|||
end |
end |
||
local function |
local function time_interval(frame) |
||
-- This implements {{time interval}}. |
|||
-- Return Gregorian serial day of the given date, or the current date. |
|||
-- There are two positional arguments: date1, date2. |
|||
-- Like {{Gregorian serial date}}, a missing argument is replaced from the |
|||
-- The default for each is the current date and time. |
|||
-- current date, so can get a bizarre mixture of specified/current y/m/d. |
|||
-- Result is date2 - date1 formatted. |
|||
-- This accepts positional arguments, although the original template does not. |
|||
local Date = get_exports(frame) |
|||
local args = frame:getParent().args |
|||
local parms = { |
|||
local month = date_component(args.month, args[2], 'month') |
|||
want_duration = yes(args.duration), |
|||
local day = date_component(args.day , args[3], 'day' ) |
|||
range = yes(args.range) or (args.range == 'dash' and 'dash' or nil), |
|||
local result = gsd(year, month, day) |
|||
want_sc = yes(args.sc), |
|||
if result then |
|||
} |
|||
return tostring(result) |
|||
local fix = yes(args.fix) and 'fix' or '' |
|||
end |
|||
local date1 = Date(fix, 'partial', strip_to_nil(args[1]) or 'currentdatetime') |
|||
return error_wikitext('Cannot handle dates before the year 1 AD') |
|||
if not date1 then |
|||
return message('Invalid start date in first parameter') |
|||
end |
|||
local date2 = Date(fix, 'partial', strip_to_nil(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(translate_parameters) do |
|||
local parm = strip_to_nil(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 = translate_parameters.show[round] |
|||
end |
|||
end |
|||
parms.round = true |
|||
end |
|||
return date_difference(parms) |
|||
end |
end |
||
return { |
|||
return { age_days = age_days, age_ym = age_ym, gsd = gsd_ymd } |
|||
age_generic = age_generic, -- can emulate several age templates |
|||
gsd = date_to_gsd, -- Template:Gregorian_serial_date |
|||
extract = date_extract, -- Template:Extract |
|||
jd_to_date = jd_to_date, -- Template:? |
|||
JULIANDAY = date_to_jd, -- Template:JULIANDAY |
|||
time_interval = time_interval, -- Template:Time_interval |
|||
} |