Difference between revisions of "Module:Age/sandbox"

From blackwiki
Jump to navigation Jump to search
blackwiki>Johnuniq
(major refactor to implement most options of many age templates, and new Template:Time interval)
blackwiki>Johnuniq
(oops, need in use Module:Date/sandbox)
Line 14: Line 14:
 
]]
 
]]
  
local datemod = require('Module:Date')
+
local datemod = require('Module:Date/sandbox')
 
local current_date = datemod._current
 
local current_date = datemod._current
 
local Date = datemod._Date
 
local Date = datemod._Date

Revision as of 10:08, 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/sandbox')
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)
}