Module:Age/sandbox
< Module:Age
Jump to navigation
Jump to search
Revision as of 07:59, 6 June 2016 by blackwiki>Johnuniq (major refactor to implement most options of many age templates, and new Template:Time interval)
Documentation for this module may be created at Module:Age/sandbox/doc
-- Implement various "age of" and other date-related templates.
--[[ TODO
Check the "This implements" functions to see what parameters are accepted by the
existing template. The code here might not be emulating what the template does.
Some functions have not been tested yet.
These features have not yet been implemented:
* Some templates accept an ambiguous date with a missing component.
* Some templates provide a hidden sort key.
* Some templates have extra parameters not yet implemented.
* {{age in years}} Age as a range (month/day unknown).
* {{age in months}}
* {{age in weeks and days}}
* {{age in weeks}}
]]
local datemod = require('Module:Date')
local current_date = datemod._current
local Date = datemod._Date
local MINUS = '−' -- Unicode U+2212 MINUS SIGN
local function collection()
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
join = function (self, sep)
return table.concat(self, sep)
end,
}
end
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).
if type(text) == 'string' then
text = text:match('(%S.-)%s*$')
end
return text
end
local function yes(parameter)
-- Return true if parameter should be interpreted as "yes".
-- Do not want to accept mixed upper/lowercase unless done by current templates.
-- Need to accept "on" because "round=on" is wanted.
return ({ y = true, yes = true, on = true })[parameter]
end
local function message(msg, nocat)
-- Return formatted message text for an error.
-- Can append "#FormattingError" to URL of a page with a problem to find it.
local anchor = '<span id="FormattingError" />'
local category
if not nocat and mw.title.getCurrentTitle():inNamespaces(0, 10) then
-- Category only in namespaces: 0=article, 10=template.
category = '[[Category:Age error]]'
else
category = ''
end
return anchor ..
'<strong class="error">Error: ' ..
mw.text.nowiki(msg) ..
'</strong>' ..
category
end
local function make_text(values, components, names, options)
-- Return wikitext representing an age or duration.
local text = collection()
local count = #values
local plural = names.plural or ''
local sep = names.sep or ''
for i, v in ipairs(values) do
if v > 0 or (text.n == 0 and i == count) or (text.n > 0 and components.keepzero) then
local name = names[components[i]]
if name then
text:add(v .. sep .. name .. (v == 1 and '' or plural))
else
text:add(tostring(v))
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 show negative zero.
if text.n > 1 or (text.n == 1 and text[1]:sub(1, 1) ~= '0' ) then
sign = MINUS
end
end
return sign .. text:join() .. (options.append or '')
end
local show_components = {
y = { 'y' },
ym = { 'y', 'm' },
ymd = { 'y', 'm', 'd' },
ymw = { 'y', 'm', 'w' },
ymwd = { 'y', 'm', 'w', 'd' },
yd = { 'y', 'd', keepzero = true },
m = { 'm' },
md = { 'm', 'd' },
w = { 'w' },
wd = { 'w', 'd' },
d = { 'd' },
dh = { 'd', 'H' },
dhm = { 'd', 'H', 'M' },
ymdh = { 'y', 'm', 'd', 'H' },
ymdhm = { 'y', 'm', 'd', 'H', 'M' },
ymwdh = { 'y', 'm', 'w', 'd', 'H' },
ymwdhm = { 'y', 'm', 'w', 'd', 'H', 'M' },
}
local function age_generic(parms)
-- Return a formatted date difference using the given parameters
-- which have been validated.
-- TODO Handle 'partial' dates, for example, a year only.
local names = {
abbr_off = {
plural = 's',
sep = ' ',
y = 'year',
m = 'month',
w = 'week',
d = 'day',
H = 'hour',
M = 'minute',
},
abbr_on = {
plural = 's',
sep = ' ',
y = 'yr',
m = 'mo',
w = 'wk',
d = 'day',
H = 'hr',
M = 'min',
},
abbr_letter = {
y = 'y',
m = 'm',
w = 'w',
d = 'd',
H = 'h',
M = 'm',
},
abbr_raw = {},
}
local diff = parms.diff -- must be a valid date difference
local show = parms.show -- may be nil; default is set below
local xabbr = parms.xabbr or 'abbr_off'
local append, default_join
if xabbr ~= 'abbr_off' then
default_join = 'sep_space'
end
if not show then
show = 'ymd'
if parms.xdisp == 'disp_age' then
if diff.years < 3 then
default_join = 'sep_space'
if diff.years >= 1 then
show = 'ym'
else
show = 'md'
end
else
-- TODO Have commented out following as templates do not seem to do this.
-- append = ' old'
show = 'y'
end
end
end
if parms.xdisp == 'disp_raw' then
default_join = 'sep_space'
xabbr = 'abbr_raw'
elseif parms.want_sc then
default_join = 'sep_serialcomma'
end
local options = {
append = append,
join = parms.xsep or default_join,
isnegative = diff.isnegative,
}
local values = { diff:age(show, parms.want_round, parms.want_duration) }
return make_text(values, show_components[show], names[xabbr], options)
end
local function get_dates(frame, want_mixture, single)
-- Parse template parameters and return one of:
-- * date (a date table, if single)
-- * date1, date2 (two date tables, if not single)
-- * text (a string error message)
-- A missing date is replaced with the current date.
-- If want_mixture is true, a missing date component is replaced
-- from the current date, so can get a bizarre mixture of
-- specified/current y/m/d as has been done by some "age" templates.
local args = frame:getParent().args
local fields = {}
local is_named = args.year or args.year1 or args.year2
if is_named 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] = strip_to_nil(fields[i])
if fields[i] then
imax = i
end
end
local dates = {}
if is_named or imax > 2 then
local nr_dates = single and 1 or 2
if want_mixture then
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 y and m and d then
dates[i] = Date(y, m, d)
elseif not (y or m or d) then
dates[i] = Date('currentdate')
end
end
end
else
dates[1] = Date(fields[1] or 'currentdate')
if not single then
dates[2] = Date(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
local function age_days(frame)
-- This implements {{age in days}}.
local date1, date2 = get_dates(frame, false)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
xdisp = 'disp_raw',
show = 'd',
want_duration = yes(args.duration),
want_round = yes(args.round),
}
return age_generic(parms)
end
local function age_full_years(frame)
-- This implements {{age}}.
local date1, date2 = get_dates(frame, false)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
show = 'y',
want_duration = yes(args.duration),
want_round = yes(args.round),
xabbr = 'abbr_raw',
}
return age_generic(parms)
end
local function age_infant(frame)
-- This implements {{age for infant}}.
local date1, date2 = get_dates(frame, false)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
want_duration = yes(args.duration),
want_round = yes(args.round),
xabbr = yes(args.abbr) and 'abbr_on' or 'abbr_off',
xdisp = 'disp_age',
xsep = 'sep_space',
}
return age_generic(parms)
end
local function age_yd(frame)
-- This implements {{age in years and days}}.
local date1, date2 = get_dates(frame, false)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
show = 'yd',
want_duration = yes(args.duration),
want_round = yes(args.round),
xsep = args.sep ~= 'and' and 'sep_comma' or nil,
}
return age_generic(parms)
end
local function age_ym(frame)
-- This implements {{age in years and months}}.
local date1, date2 = get_dates(frame, false)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
xsep = 'sep_comma',
show = 'ym',
want_duration = yes(args.duration),
want_round = yes(args.round),
}
return age_generic(parms)
end
local function age_ymd(frame)
-- This implements {{age in years, months and days}}.
local date1, date2 = get_dates(frame, true)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
show = 'ymd',
want_duration = yes(args.duration),
want_round = yes(args.round),
want_sc = yes(args.sc),
}
return age_generic(parms)
end
local function age_ymwd(frame)
-- This implements {{age in years, months, weeks and days}}.
local date1, date2 = get_dates(frame, false)
if type(date1) == 'string' then
return date1
end
local args = frame:getParent().args
local parms = {
diff = date2 - date1,
show = 'ymwd',
want_duration = yes(args.duration),
want_round = yes(args.round),
want_sc = yes(args.sc),
}
return age_generic(parms)
end
local function ymd_to_gsd(frame)
-- This implements {{gregorian serial date}}.
-- Return Gregorian serial date of the given date, or the current date.
-- The returned value is negative for dates before 1 January 1 AD
-- despite the fact that GSD is not defined for such dates.
local date = get_dates(frame, true, true)
if type(date) == 'string' then
return date
end
return tostring(date.gsd)
end
local function ymd_from_jd(frame)
-- Return formatted date from a Julian date.
-- The result is y-m-d or y-m-d H:M:S if input includes a fraction.
-- The word 'Julian' is accepted for the Julian calendar.
local args = frame:getParent().args
local date = Date('juliandate', args[1], args[2])
if date then
return date:text()
end
return message('Need valid Julian date number')
end
local function ymd_to_jd(frame)
-- Return Julian date (a number) from a date (y-m-d), or datetime (y-m-d H:M:S),
-- 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 = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
if date then
return tostring(date.jd)
end
return message('Need valid year/month/day or "currentdate"')
end
local function time_interval(frame)
-- This implements {{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 args = frame:getParent().args
local parms = {
want_duration = yes(args.duration),
want_sc = yes(args.sc),
}
local date1 = Date(strip_to_nil(args[1]) or 'currentdatetime')
if not date1 then
return message('Invalid start date in first parameter')
end
local date2 = Date(strip_to_nil(args[2]) or 'currentdatetime')
if not date2 then
return message('Invalid end date in second parameter')
end
parms.diff = date2 - date1
local translate_abbr = {
off = 'abbr_off',
on = 'abbr_on',
letter = 'abbr_letter',
}
local translate_disp = {
age = 'disp_age',
raw = 'disp_raw',
}
local translate_round = {
on = 'on',
yes = 'on',
months = 'ym',
weeks = 'ymw',
days = 'ymd',
hours = 'ymdh',
}
local translate_sep = {
comma = 'sep_comma',
serialcomma = 'sep_serialcomma',
space = 'sep_space',
}
local show = strip_to_nil(args.show)
if show and not show_components[show] then
return message('Parameter show=' .. show .. ' is invalid')
end
local round = strip_to_nil(args.round)
if round then
local xround = translate_round[round]
if not xround then
return message('Parameter round=' .. round .. ' is invalid')
end
if xround ~= 'on' then
if show and show ~= xround then
return message('Parameter show=' .. show .. ' conflicts with round=' .. round)
end
show = xround
end
round = true
end
parms.show = show
parms.want_round = round
local xabbr = strip_to_nil(args.abbr)
if xabbr then
xabbr = translate_abbr[xabbr]
if not xabbr then
return message('Parameter abbr=' .. args.abbr .. ' is invalid')
end
end
parms.xabbr = xabbr
local xdisp = strip_to_nil(args.disp)
if xdisp then
xdisp = translate_disp[xdisp]
if not xdisp then
return message('Parameter disp=' .. args.disp .. ' is invalid')
end
end
parms.xdisp = xdisp
local xsep = strip_to_nil(args.sep)
if xsep then
xsep = translate_sep[xsep]
if not xsep then
return message('Parameter sep=' .. args.sep .. ' is invalid')
end
end
parms.xsep = xsep
return age_generic(parms)
end
return {
age_days = age_days, -- Template:Age_in_days
age_full_years = age_full_years, -- Template:Age
age_infant = age_infant, -- Template:Age_for_infant
age_yd = age_yd, -- Template:Age_in_years_and_days
age_ym = age_ym, -- Template:Age_in_years_and_months
age_ymd = age_ymd, -- Template:Age_in_years,_months_and_days
age_ymwd = age_ymwd, -- Template:Age_in_years,_months,_weeks_and_days
gsd = ymd_to_gsd, -- Template:Gregorian_serial_date
JULIANDAY = ymd_to_jd, -- Template:JULIANDAY
time_interval = time_interval, -- Template:Time_interval
ymd_from_jd = ymd_from_jd, -- Template:?
ymd_to_jd = ymd_to_jd, -- Template:JULIANDAY (alias)
}