Difference between revisions of "Module:Age/sandbox"

From blackwiki
Jump to navigation Jump to search
blackwiki>Johnuniq
(require Module:Date/sandbox when invoked from a sandbox template)
blackwiki>Johnuniq
(for {{time interval}}: replace abbr=letter with abbr=on per talk; for {{age in days}}: show_unit=abbr/full; format=commas; templates can invoke age_generic so remove redundant age_xxx functions)
Line 72: Line 72:
 
'</strong>' ..
 
'</strong>' ..
 
category
 
category
 +
end
 +
 +
local function formatnumber(numstr)
 +
-- Return the given number formatted with commas as group separators,
 +
-- given that numstr consists of en digits only (no decimal mark).
 +
local length = #numstr
 +
local places = collection()
 +
local pos = 0
 +
repeat
 +
places:add(pos)
 +
pos = pos + 3
 +
until pos >= length
 +
places:add(length)
 +
local groups = collection()
 +
for i = places.n, 2, -1 do
 +
local p1 = length - places[i] + 1
 +
local p2 = length - places[i - 1]
 +
groups:add(numstr:sub(p1, p2))
 +
end
 +
return groups:join(',')
 
end
 
end
  
Line 82: Line 102:
 
for i, v in ipairs(values) do
 
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
 
if v > 0 or (text.n == 0 and i == count) or (text.n > 0 and components.keepzero) then
 +
local vstr = tostring(v)
 +
if i == 1 and options.format == 'format_commas' then
 +
-- Numbers after the first should be small and not need formatting.
 +
vstr = formatnumber(vstr)
 +
end
 
local name = names[components[i]]
 
local name = names[components[i]]
 
if name then
 
if name then
text:add(v .. sep .. name .. (v == 1 and '' or plural))
+
text:add(vstr .. sep .. name .. (v == 1 and '' or plural))
 
else
 
else
text:add(tostring(v))
+
text:add(vstr)
 
end
 
end
 
end
 
end
Line 111: Line 136:
 
local sign = ''
 
local sign = ''
 
if options.isnegative then
 
if options.isnegative then
-- Do not show negative zero.
+
-- Do not display negative zero.
 
if text.n > 1 or (text.n == 1 and text[1]:sub(1, 1) ~= '0' ) then
 
if text.n > 1 or (text.n == 1 and text[1]:sub(1, 1) ~= '0' ) then
sign = '−'  -- Unicode U+2212 MINUS SIGN
+
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
 
end
 
end
return sign .. text:join() .. (options.append or '')
+
return sign .. text:join() .. (options.suffix or '')
 
end
 
end
  
local show_components = {
+
local translate_parameters = {
y = { 'y' },
+
abbr = {
ym = { 'y', 'm' },
+
off = 'abbr_off',
ymd = { 'y', 'm', 'd' },
+
on = 'abbr_on',
ymw = { 'y', 'm', 'w' },
+
},
ymwd = { 'y', 'm', 'w', 'd' },
+
disp = {
yd = { 'y', 'd', keepzero = true },
+
age = 'disp_age',
m = { 'm' },
+
raw = 'disp_raw',
md = { 'm', 'd' },
+
},
w = { 'w' },
+
format = {
wd = { 'w', 'd' },
+
raw = 'format_raw',
d = { 'd' },
+
commas = 'format_commas',
dh = { 'd', 'H' },
+
},
dhm = { 'd', 'H', 'M' },
+
round = {
ymdh = { 'y', 'm', 'd', 'H' },
+
on = 'on',
ymdhm = { 'y', 'm', 'd', 'H', 'M' },
+
yes = 'on',
ymwdh = { 'y', 'm', 'w', 'd', 'H' },
+
months = 'ym',
ymwdhm = { 'y', 'm', 'w', 'd', 'H', 'M' },
+
weeks = 'ymw',
 +
days = 'ymd',
 +
hours = 'ymdh',
 +
},
 +
sep = {
 +
comma = 'sep_comma',
 +
[','] = 'sep_comma',
 +
serialcomma = 'sep_serialcomma',
 +
space = 'sep_space',
 +
},
 +
show = {
 +
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' },
 +
},
 
}
 
}
  
local function age_generic(parms)
+
local function date_difference(parms)
 
-- Return a formatted date difference using the given parameters
 
-- Return a formatted date difference using the given parameters
 
-- which have been validated.
 
-- which have been validated.
Line 155: Line 212:
 
},
 
},
 
abbr_on = {
 
abbr_on = {
 +
y = 'y',
 +
m = 'm',
 +
w = 'w',
 +
d = 'd',
 +
H = 'h',
 +
M = 'm',
 +
},
 +
abbr_infant = {      -- for {{age for infant}}
 
plural = 's',
 
plural = 's',
 
sep = '&nbsp;',
 
sep = '&nbsp;',
Line 163: Line 228:
 
H = 'hr',
 
H = 'hr',
 
M = 'min',
 
M = 'min',
},
 
abbr_letter = {
 
y = 'y',
 
m = 'm',
 
w = 'w',
 
d = 'd',
 
H = 'h',
 
M = 'm',
 
 
},
 
},
 
abbr_raw = {},
 
abbr_raw = {},
Line 176: Line 233:
 
local diff = parms.diff  -- must be a valid date difference
 
local diff = parms.diff  -- must be a valid date difference
 
local show = parms.show  -- may be nil; default is set below
 
local show = parms.show  -- may be nil; default is set below
local xabbr = parms.xabbr or 'abbr_off'
+
local abbr = parms.abbr or 'abbr_off'
local append, default_join
+
local suffix, default_join
if xabbr ~= 'abbr_off' then
+
if abbr ~= 'abbr_off' then
 
default_join = 'sep_space'
 
default_join = 'sep_space'
 
end
 
end
 
if not show then
 
if not show then
 
show = 'ymd'
 
show = 'ymd'
if parms.xdisp == 'disp_age' then
+
if parms.disp == 'disp_age' then
 
if diff.years < 3 then
 
if diff.years < 3 then
 
default_join = 'sep_space'
 
default_join = 'sep_space'
Line 192: Line 249:
 
end
 
end
 
else
 
else
-- TODO Have commented out following as templates do not seem to do this.
 
-- append = ' old'
 
 
show = 'y'
 
show = 'y'
 
end
 
end
 
end
 
end
 
end
 
end
if parms.xdisp == 'disp_raw' then
+
if type(show) ~= 'table' then
 +
show = translate_parameters.show[show]
 +
end
 +
if parms.disp == 'disp_raw' then
 
default_join = 'sep_space'
 
default_join = 'sep_space'
xabbr = 'abbr_raw'
+
abbr = 'abbr_raw'
 
elseif parms.want_sc then
 
elseif parms.want_sc then
 
default_join = 'sep_serialcomma'
 
default_join = 'sep_serialcomma'
 
end
 
end
 
local options = {
 
local options = {
append = append,
+
suffix = suffix,  -- not currently used
join = parms.xsep or default_join,
+
format = parms.format,
 +
join = parms.sep or default_join,
 
isnegative = diff.isnegative,
 
isnegative = diff.isnegative,
 
}
 
}
local values = { diff:age(show, parms.want_round, parms.want_duration) }
+
local values = { diff:age(show.id, parms.round, parms.want_duration) }
return make_text(values, show_components[show], names[xabbr], options)
+
return make_text(values, show, names[abbr], options)
 
end
 
end
  
local function get_dates(frame, want_mixture, single)
+
local function get_dates(frame, getopt)
 
-- Parse template parameters and return one of:
 
-- Parse template parameters and return one of:
 
-- * date        (a date table, if single)
 
-- * date        (a date table, if single)
Line 222: Line 281:
 
-- 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.
 +
-- Some results may be placed in table getopt.
 
local Date, current_date = get_exports(frame)
 
local Date, current_date = get_exports(frame)
 
local args = frame:getParent().args
 
local args = frame:getParent().args
Line 245: Line 305:
 
end
 
end
 
end
 
end
 +
getopt = getopt or {}
 +
local single = getopt.single
 
local dates = {}
 
local dates = {}
 
if is_named or imax > 2 then
 
if is_named or imax > 2 then
 
local nr_dates = single and 1 or 2
 
local nr_dates = single and 1 or 2
if want_mixture then
+
if getopt.want_mixture then
 
local components = { 'year', 'month', 'day' }
 
local components = { 'year', 'month', 'day' }
 
for i = 1, nr_dates * 3 do
 
for i = 1, nr_dates * 3 do
Line 269: Line 331:
 
end
 
end
 
else
 
else
 +
getopt.textdates = true
 
dates[1] = Date(fields[1] or 'currentdate')
 
dates[1] = Date(fields[1] or 'currentdate')
 
if not single then
 
if not single then
Line 286: Line 349:
 
end
 
end
  
-- LATER: If decide to use call_generic, probably do not need all the functions
+
local function age_generic(frame, name)
-- which call it as a template such as {{age}} could invoke call_generic with
+
-- Return the result required by the specified template.
-- a parameter like "template=age_full_years".
+
name = name or frame.args['template']
local function call_generic(frame, name)
+
if not name then
-- Return the result required by the template identified by name.
+
return message('The template invoking this must have "|tamplate=x" where x is the wanted operation')
 +
end
 
local args = frame:getParent().args
 
local args = frame:getParent().args
 
local specs = {
 
local specs = {
 
age_days = {
 
age_days = {
 
show = 'd',
 
show = 'd',
xdisp = 'disp_raw',
+
disp = 'disp_raw',
 
},
 
},
 
age_full_years = {
 
age_full_years = {
 
show = 'y',
 
show = 'y',
xabbr = 'abbr_raw',
+
abbr = 'abbr_raw',
 
},
 
},
 
age_infant = {
 
age_infant = {
-- Do not set show as special processing occurs later.
+
-- Do not set show because special processing is done later.
xabbr = yes(args.abbr) and 'abbr_on' or 'abbr_off',
+
abbr = yes(args.abbr) and 'abbr_infant' or 'abbr_off',
xdisp = 'disp_age',
+
disp = 'disp_age',
xsep = 'sep_space',
+
sep = 'sep_space',
 +
},
 +
age_m = {
 +
show = 'm',
 +
disp = 'disp_raw',
 +
},
 +
age_w = {
 +
show = 'w',
 +
disp = 'disp_raw',
 +
},
 +
age_wd = {
 +
show = 'wd',
 
},
 
},
 
age_yd = {
 
age_yd = {
 
show = 'yd',
 
show = 'yd',
xsep = args.sep ~= 'and' and 'sep_comma' or nil,
+
sep = args.sep ~= 'and' and 'sep_comma' or nil,
 
},
 
},
 
age_ym = {
 
age_ym = {
 
show = 'ym',
 
show = 'ym',
xsep = 'sep_comma',
+
sep = 'sep_comma',
 
},
 
},
 
age_ymd = {
 
age_ymd = {
Line 324: Line 399:
 
}
 
}
 
local spec = specs[name]
 
local spec = specs[name]
local date1, date2 = get_dates(frame, spec.want_mixture)
+
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 getopt = { want_mixture = spec.want_mixture }
 +
local date1, date2 = get_dates(frame, getopt)
 
if type(date1) == 'string' then
 
if type(date1) == 'string' then
 
return date1
 
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
 
end
 
local parms = {
 
local parms = {
 
diff = date2 - date1,
 
diff = date2 - date1,
 
want_duration = yes(args.duration),
 
want_duration = yes(args.duration),
want_round = yes(args.round),
 
 
want_sc = yes(args.sc),
 
want_sc = yes(args.sc),
 
show = spec.show,
 
show = spec.show,
xabbr = spec.xabbr,
+
abbr = spec.abbr,
xdisp = spec.xdisp,
+
disp = spec.disp,
xsep = spec.xsep,
+
format = format,
 +
round = yes(args.round),
 +
sep = spec.sep,
 
}
 
}
return age_generic(parms)
+
return date_difference(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
 
end
  
Line 381: Line 441:
 
-- The returned value is negative for dates before 1 January 1 AD
 
-- The returned value is negative for dates before 1 January 1 AD
 
-- despite the fact that GSD is not defined for such dates.
 
-- despite the fact that GSD is not defined for such dates.
local date = get_dates(frame, true, true)
+
local date = get_dates(frame, { want_mixture=true, single=true })
 
if type(date) == 'string' then
 
if type(date) == 'string' then
 
return date
 
return date
Line 434: Line 494:
 
end
 
end
 
parms.diff = date2 - date1
 
parms.diff = date2 - date1
local translate_abbr = {
+
for argname, translate in pairs(translate_parameters) do
off = 'abbr_off',
+
local parm = strip_to_nil(args[argname])
on = 'abbr_on',
+
if parm then
letter = 'abbr_letter',
+
parm = translate[parm]
}
+
if not parm then
local translate_disp = {
+
return message('Parameter ' .. argname .. '=' .. args[argname] .. ' is invalid')
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
 
end
show = xround
+
parms[argname] = parm
 
end
 
end
round = true
 
 
end
 
end
parms.show = show
+
if parms.round then
parms.want_round = round
+
local round = parms.round
local xabbr = strip_to_nil(args.abbr)
+
local show = parms.show
if xabbr then
+
if round ~= 'on' then
xabbr = translate_abbr[xabbr]
+
if show then
if not xabbr then
+
if show.id ~= round then
return message('Parameter abbr=' .. args.abbr .. ' is invalid')
+
return message('Parameter show=' .. args.show .. ' conflicts with round=' .. args.round)
end
+
end
end
+
else
parms.xabbr = xabbr
+
parms.show = translate_parameters.show[round]
local xdisp = strip_to_nil(args.disp)
+
end
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.round = true
 
end
 
end
parms.xsep = xsep
+
return date_difference(parms)
return age_generic(parms)
 
 
end
 
end
  
 
return {
 
return {
age_days = age_days,               -- Template:Age_in_days
+
age_generic = age_generic,         -- can emulate several age templates
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
 
gsd = ymd_to_gsd,                  -- Template:Gregorian_serial_date
 
JULIANDAY = ymd_to_jd,              -- Template:JULIANDAY
 
JULIANDAY = ymd_to_jd,              -- Template:JULIANDAY

Revision as of 07:48, 10 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 _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 formatnumber(numstr)
	-- Return the given number formatted with commas as group separators,
	-- given that numstr consists of en digits only (no decimal mark).
	local length = #numstr
	local places = collection()
	local pos = 0
	repeat
		places:add(pos)
		pos = pos + 3
	until pos >= length
	places:add(length)
	local groups = collection()
	for i = places.n, 2, -1 do
		local p1 = length - places[i] + 1
		local p2 = length - places[i - 1]
		groups:add(numstr:sub(p1, p2))
	end
	return groups:join(',')
end

local function 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 vstr = tostring(v)
			if i == 1 and options.format == 'format_commas' then
				-- Numbers after the first should be small and not need formatting.
				vstr = formatnumber(vstr)
			end
			local name = names[components[i]]
			if name then
				text:add(vstr .. sep .. name .. (v == 1 and '' or 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 sign .. text:join() .. (options.suffix or '')
end

local translate_parameters = {
	abbr = {
		off = 'abbr_off',
		on = 'abbr_on',
	},
	disp = {
		age = 'disp_age',
		raw = 'disp_raw',
	},
	format = {
		raw = 'format_raw',
		commas = 'format_commas',
	},
	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 = {
		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' },
	},
}

local function date_difference(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 = {
			y = 'y',
			m = 'm',
			w = 'w',
			d = 'd',
			H = 'h',
			M = 'm',
		},
		abbr_infant = {      -- for {{age for infant}}
			plural = 's',
			sep = '&nbsp;',
			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 suffix, 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 options = {
		suffix = suffix,  -- not currently used
		format = parms.format,
		join = parms.sep or default_join,
		isnegative = diff.isnegative,
	}
	local values = { diff:age(show.id, parms.round, parms.want_duration) }
	return make_text(values, show, names[abbr], options)
end

local function get_dates(frame, getopt)
	-- 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.
	-- Some results may be placed in table getopt.
	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
	getopt = getopt or {}
	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
			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
		getopt.textdates = true
		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_generic(frame, name)
	-- Return the result required by the specified template.
	name = name or frame.args['template']
	if not name then
		return message('The template invoking this must have "|tamplate=x" where x is the wanted operation')
	end
	local args = frame:getParent().args
	local specs = {
		age_days = {
			show = 'd',
			disp = 'disp_raw',
		},
		age_full_years = {
			show = 'y',
			abbr = 'abbr_raw',
		},
		age_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',
		},
		age_m = {
			show = 'm',
			disp = 'disp_raw',
		},
		age_w = {
			show = 'w',
			disp = 'disp_raw',
		},
		age_wd = {
			show = 'wd',
		},
		age_yd = {
			show = 'yd',
			sep = args.sep ~= 'and' and 'sep_comma' or nil,
		},
		age_ym = {
			show = 'ym',
			sep = 'sep_comma',
		},
		age_ymd = {
			show = 'ymd',
			want_mixture = true,
		},
		age_ymwd = {
			show = 'ymwd',
		},
	}
	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 getopt = { 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 = yes(args.duration),
		want_sc = yes(args.sc),
		show = spec.show,
		abbr = spec.abbr,
		disp = spec.disp,
		format = format,
		round = yes(args.round),
		sep = spec.sep,
	}
	return date_difference(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, { want_mixture=true, single=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
	for argname, translate in pairs(translate_parameters) do
		local parm = strip_to_nil(args[argname])
		if parm then
			parm = translate[parm]
			if not parm then
				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

return {
	age_generic = age_generic,          -- can emulate several age templates
	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)
}