Module:Age/sandbox
< Module:Age
Jump to navigation
Jump to search
Revision as of 04:46, 7 June 2016 by blackwiki>Johnuniq (require Module:Date/sandbox when invoked from a sandbox template)
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 _Date, _current_date
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 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 = '−' -- Unicode U+2212 MINUS SIGN
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 Date, current_date = get_exports(frame)
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
-- LATER: If decide to use call_generic, probably do not need all the functions
-- which call it as a template such as {{age}} could invoke call_generic with
-- a parameter like "template=age_full_years".
local function call_generic(frame, name)
-- Return the result required by the template identified by name.
local args = frame:getParent().args
local specs = {
age_days = {
show = 'd',
xdisp = 'disp_raw',
},
age_full_years = {
show = 'y',
xabbr = 'abbr_raw',
},
age_infant = {
-- Do not set show as special processing occurs later.
xabbr = yes(args.abbr) and 'abbr_on' or 'abbr_off',
xdisp = 'disp_age',
xsep = 'sep_space',
},
age_yd = {
show = 'yd',
xsep = args.sep ~= 'and' and 'sep_comma' or nil,
},
age_ym = {
show = 'ym',
xsep = 'sep_comma',
},
age_ymd = {
show = 'ymd',
want_mixture = true,
},
age_ymwd = {
show = 'ymwd',
},
}
local spec = specs[name]
local date1, date2 = get_dates(frame, spec.want_mixture)
if type(date1) == 'string' then
return date1
end
local parms = {
diff = date2 - date1,
want_duration = yes(args.duration),
want_round = yes(args.round),
want_sc = yes(args.sc),
show = spec.show,
xabbr = spec.xabbr,
xdisp = spec.xdisp,
xsep = spec.xsep,
}
return age_generic(parms)
end
local function age_days(frame)
-- This implements {{age in days}}.
return call_generic(frame, 'age_days')
end
local function age_full_years(frame)
-- This implements {{age}}.
return call_generic(frame, 'age_full_years')
end
local function age_infant(frame)
-- This implements {{age for infant}}.
return call_generic(frame, 'age_infant')
end
local function age_yd(frame)
-- This implements {{age in years and days}}.
return call_generic(frame, 'age_yd')
end
local function age_ym(frame)
-- This implements {{age in years and months}}.
return call_generic(frame, 'age_ym')
end
local function age_ymd(frame)
-- This implements {{age in years, months and days}}.
return call_generic(frame, 'age_ymd')
end
local function age_ymwd(frame)
-- This implements {{age in years, months, weeks and days}}.
return call_generic(frame, 'age_ymwd')
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 Date = get_exports(frame)
local args = frame:getParent().args
local date = Date('juliandate', args[1], args[2])
if date then
return date:text()
end
return message('Need valid Julian date number')
end
local function 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 Date = get_exports(frame)
local args = frame:getParent().args
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
if date then
return tostring(date.jd)
end
return message('Need valid year/month/day or "currentdate"')
end
local function 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 Date = get_exports(frame)
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)
}