Difference between revisions of "Module:Age/sandbox"

From blackwiki
Jump to navigation Jump to search
blackwiki>Johnuniq
(try a substitute for unpack)
blackwiki>Johnuniq
(major refactor to implement most options of many age templates, and new Template:Time interval)
Line 1: Line 1:
 
-- Implement various "age of" and other date-related templates.
 
-- 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 datemod = require('Module:Date')
local current = datemod._current
+
local current_date = datemod._current
 
local Date = datemod._Date
 
local Date = datemod._Date
 
local MINUS = '−'  -- Unicode U+2212 MINUS SIGN
 
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)
 
local function strip_to_nil(text)
Line 15: Line 42:
 
end
 
end
  
local function number_name(number, singular, plural, sep)
+
local function yes(parameter)
-- Return the given number, converted to a string, with the
+
-- Return true if parameter should be interpreted as "yes".
-- separator (default space) and singular or plural name appended.
+
-- Do not want to accept mixed upper/lowercase unless done by current templates.
plural = plural or (singular .. 's')
+
-- Need to accept "on" because "round=on" is wanted.
sep = sep or ' '
+
return ({ y = true, yes = true, on = true })[parameter]
return tostring(number) .. sep .. ((number == 1) and singular or plural)
 
 
end
 
end
  
Line 36: Line 62:
 
return anchor ..
 
return anchor ..
 
'<strong class="error">Error: ' ..
 
'<strong class="error">Error: ' ..
msg ..
+
mw.text.nowiki(msg) ..
 
'</strong>' ..
 
'</strong>' ..
category .. '\n'
+
category
 
end
 
end
  
local function date_component(named, positional, component)
+
local function make_text(values, components, names, options)
-- Return the first of the two arguments (named like {{example|year=2001}}
+
-- Return wikitext representing an age or duration.
-- or positional like {{example|2001}}) that is not nil and is not empty.
+
local text = collection()
-- If both are nil, return the current date component, if specified.
+
local count = #values
-- This translates empty arguments passed to the template to nil, and
+
local plural = names.plural or ''
-- optionally replaces a nil argument with a value from the current date.
+
local sep = names.sep or ''
named = strip_to_nil(named)
+
for i, v in ipairs(values) do
if named then
+
if v > 0 or (text.n == 0 and i == count) or (text.n > 0 and components.keepzero) then
return named
+
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
 
end
positional = strip_to_nil(positional)
+
for i, v in ipairs(text) do
if positional then
+
if i < text.n then
return positional
+
text[i] = v .. (i + 1 < text.n and first or last)
 +
end
 
end
 
end
if component then
+
local sign = ''
return current[component]
+
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
 
end
return nil
+
return sign .. text:join() .. (options.append or '')
 
end
 
end
  
local function get_dates(frame, want_mixture)
+
local show_components = {
-- Return date1, date2 from template parameters.
+
y = { 'y' },
-- Either may be nil if given arguments are invalid.
+
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 = '&nbsp;',
 +
y = 'year',
 +
m = 'month',
 +
w = 'week',
 +
d = 'day',
 +
H = 'hour',
 +
M = 'minute',
 +
},
 +
abbr_on = {
 +
plural = 's',
 +
sep = '&nbsp;',
 +
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
 
-- If want_mixture is true, a missing date component is replaced
 
-- from the current date, so can get a bizarre mixture of
 
-- from the current date, so can get a bizarre mixture of
 
-- specified/current y/m/d as has been done by some "age" templates.
 
-- specified/current y/m/d as has been done by some "age" templates.
-- TODO Accept a date as a string like "1 April 2016".
 
local date1, date2
 
 
local args = frame:getParent().args
 
local args = frame:getParent().args
if want_mixture then
+
local fields = {}
date1 = Date(
+
local is_named = args.year or args.year1 or args.year2
date_component(args.year1 , args[1], 'year' ),
+
if is_named then
date_component(args.month1, args[2], 'month'),
+
fields[1] = args.year1 or args.year
date_component(args.day1 , args[3], 'day'  )
+
fields[2] = args.month1 or args.month
)
+
fields[3] = args.day1 or args.day
date2 = Date(
+
fields[4] = args.year2
date_component(args.year2 , args[4], 'year' ),
+
fields[5] = args.month2
date_component(args.month2, args[5], 'month'),
+
fields[6] = args.day2
date_component(args.day2 , args[6], 'day'  )
 
)
 
 
else
 
else
local fields = {}
 
 
for i = 1, 6 do
 
for i = 1, 6 do
fields[i] = strip_to_nil(args[i])
+
fields[i] = args[i]
 
end
 
end
date1 = Date(fields[1], fields[2], fields[3])
+
end
if fields[4] and fields[5] and fields[6] then
+
local imax = 0
date2 = Date(fields[4], fields[5], fields[6])
+
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
 
else
date2 = Date('currentdate')
+
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
 
end
 
end
return date1, date2
+
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
  
 
local function age_days(frame)
 
local function age_days(frame)
-- Return age in days between two given dates, or
+
-- This implements {{age in days}}.
-- between given date and current date.
+
local date1, date2 = get_dates(frame, false)
-- This code implements the logic in [[Template:Age in days]].
+
if type(date1) == 'string' then
-- Like {{Age in days}}, a missing argument is replaced from the current
+
return date1
-- date, so can get a bizarre mixture of specified/current y/m/d.
+
end
local date1, date2 = get_dates(frame, true)
+
local args = frame:getParent().args
if not (date1 and date2) then
+
local parms = {
return message('Need valid year, month, day')
+
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
 
end
local sign = ''
+
local args = frame:getParent().args
local result = date2.jd - date1.jd
+
local parms = {
if result < 0 then
+
diff = date2 - date1,
sign = ''
+
show = 'y',
result = -result
+
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
 
end
return sign .. tostring(result)
+
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
 
end
  
local function _age_ym(diff)
+
local function age_yd(frame)
-- Return text specifying date difference in years, months.
+
-- This implements {{age in years and days}}.
local sign = diff.isnegative and MINUS or ''
+
local date1, date2 = get_dates(frame, false)
local years, months = diff:age('ym')
+
if type(date1) == 'string' then
local mtext = number_name(months, 'month')
+
return date1
local result
 
if years > 0 then
 
local ytext = number_name(years, 'year')
 
if months == 0 then
 
result = ytext
 
else
 
result = ytext .. ',&nbsp;' .. mtext
 
end
 
else
 
if months == 0 then
 
sign = ''
 
end
 
result = mtext
 
 
end
 
end
return sign .. result
+
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
 
end
  
 
local function age_ym(frame)
 
local function age_ym(frame)
-- Return age in years and months between two given dates, or
+
-- This implements {{age in years and months}}.
-- between given date and current date.
 
-- The older date is usually first; result is negative if the first is newer.
 
 
local date1, date2 = get_dates(frame, false)
 
local date1, date2 = get_dates(frame, false)
if not date1 then
+
if type(date1) == 'string' then
return message('Need valid year, month, day')
+
return date1
 
end
 
end
if not date2 then
+
local args = frame:getParent().args
return message('Second date should be year, month, day')
+
local parms = {
end
+
diff = date2 - date1,
return _age_ym(date2 - date1)
+
xsep = 'sep_comma',
 +
show = 'ym',
 +
want_duration = yes(args.duration),
 +
want_round = yes(args.round),
 +
}
 +
return age_generic(parms)
 
end
 
end
  
--[[ TODO
 
All these templates usually have the older date first (otherwise result is negative).
 
                    * = date1, date2 positional; date2 defaults to current
 
age_days(frame) * also accepts named y,m,d and text dates
 
age_years(frame) *
 
age_yd(frame) * also accepts text dates
 
age_ym(frame) *
 
age_ymd(frame) * also accepts named y,m,d; can omit d for "or"
 
age_ymwd(frame) . named only; defaults to current
 
]]
 
local function age_years(frame)
 
-- TODO d1, d2 positional; d2 defaults to current; usually d1 < d2
 
end
 
local function age_yd(frame)
 
-- TODO
 
end
 
 
local function age_ymd(frame)
 
local function age_ymd(frame)
-- TODO
+
-- 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
 
end
 +
 
local function age_ymwd(frame)
 
local function age_ymwd(frame)
-- TODO
+
-- 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
 
end
  
local function gsd_ymd(frame)
+
local function ymd_to_gsd(frame)
 +
-- This implements {{gregorian serial date}}.
 
-- Return Gregorian serial date of the given date, or the current date.
 
-- Return Gregorian serial date of the given date, or the current date.
-- Like {{Gregorian serial date}}, a missing argument is replaced from the
+
-- The returned value is negative for dates before 1 January 1 AD
-- current date, so can get a bizarre mixture of specified/current y/m/d.
+
-- despite the fact that GSD is not defined for such dates.
-- This also accepts positional arguments, although the original template does not.
+
local date = get_dates(frame, true, true)
-- The returned value is negative for dates before 1 January 1 AD despite
+
if type(date) == 'string' then
-- the fact that GSD is not defined for earlier dates.
+
return date
local args = frame:getParent().args
 
local date = Date(
 
date_component(args.year , args[1], 'year' ),
 
date_component(args.month, args[2], 'month'),
 
date_component(args.day  , args[3], 'day' )
 
)
 
if date then
 
return tostring(date.gsd)
 
 
end
 
end
return message('Need valid year, month, day')
+
return tostring(date.gsd)
 
end
 
end
  
Line 203: Line 420:
 
end
 
end
 
return message('Need valid Julian date number')
 
return message('Need valid Julian date number')
end
 
 
local function unpacked(args)
 
local fields = {}
 
for i = 1, 100 do
 
local arg = strip_to_nil(args[i])
 
if not arg then
 
break
 
end
 
fields[i] = arg
 
end
 
return unpack(fields)
 
 
end
 
end
  
 
local function ymd_to_jd(frame)
 
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),
 
-- 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 datetime ('currentdatetime').
+
-- or the current date ('currentdate') or current date and time ('currentdatetime').
 
-- The word 'Julian' is accepted for the Julian calendar.
 
-- The word 'Julian' is accepted for the Julian calendar.
 
local args = frame:getParent().args
 
local args = frame:getParent().args
local date = Date(unpacked(args))
+
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7])
 
if date then
 
if date then
 
return tostring(date.jd)
 
return tostring(date.jd)
 
end
 
end
 
return message('Need valid year/month/day or "currentdate"')
 
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
 
end
  
 
return {
 
return {
age_days = age_days,
+
age_days = age_days,               -- Template:Age_in_days
age_years = age_years,
+
age_full_years = age_full_years,   -- Template:Age
age_yd = age_yd,
+
age_infant = age_infant,            -- Template:Age_for_infant
age_ym = age_ym,
+
age_yd = age_yd,                   -- Template:Age_in_years_and_days
age_ymd = age_ymd,
+
age_ym = age_ym,                   -- Template:Age_in_years_and_months
age_ymwd = age_ymwd,
+
age_ymd = age_ymd,                 -- Template:Age_in_years,_months_and_days
gsd = gsd_ymd,
+
age_ymwd = age_ymwd,               -- Template:Age_in_years,_months,_weeks_and_days
JULIANDAY = ymd_to_jd,
+
gsd = ymd_to_gsd,                   -- Template:Gregorian_serial_date
ymd_from_jd = ymd_from_jd,
+
JULIANDAY = ymd_to_jd,             -- Template:JULIANDAY
ymd_to_jd = ymd_to_jd,
+
time_interval = time_interval,      -- Template:Time_interval
 +
ymd_from_jd = ymd_from_jd,         -- Template:?
 +
ymd_to_jd = ymd_to_jd,             -- Template:JULIANDAY (alias)
 
}
 
}

Revision as of 07:59, 6 June 2016

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 = '&nbsp;',
			y = 'year',
			m = 'month',
			w = 'week',
			d = 'day',
			H = 'hour',
			M = 'minute',
		},
		abbr_on = {
			plural = 's',
			sep = '&nbsp;',
			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)
}