モジュール:Age

-- Implement various "age of" and other date-related templates.
local _Date, _currentDate
local function getExports(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:Date2' .. sandbox)
		_Date = datemod._Date
		_currentDate = datemod._current
	end
	return _Date, _currentDate
end
local Collection  -- a table to hold items
Collection = {
	add = function (self, item)
		if item ~= nil then
			self.n = self.n + 1
			selfself.n = item
		end
	end,
	join = function (self, sep)
		return table.concat(self, sep)
	end,
	remove = function (self, pos)
		if self.n > 0 and (pos == nil or (0 '
		b = 
	else
		a = 'エラー: '
		b = 
	end
	if mw.title.getCurrentTitle():inNamespaces(0) then
		-- Category only in namespaces: 0=article.
		category = categoriesid or 'error'
	end
	return
		a ..
		mw.text.nowiki(msg) ..
		b ..
		(category or )
end
local function formatNumber(number)
	-- Return the given number formatted with commas as group separators,
	-- given that the number is an integer.
	local numstr = tostring(number)
	local length = #numstr
	local places = Collection.new()
	local pos = 0
	repeat
		places:add(pos)
		pos = pos + 3
	until pos >= length
	places:add(length)
	local groups = Collection.new()
	for i = places.n, 2, -1 do
		local p1 = length - placesi + 1
		local p2 = length - placesi - 1
		groups:add(numstr:sub(p1, p2))
	end
	return groups:join(',')
end
local function makeSort(value, sortable)
	-- Return a sort key if requested.
	-- Assume value is a valid number which has not overflowed.
	if sortable == 'sortable_table' or sortable == 'sortable_on' or sortable == 'sortable_debug' then
		local sortkey
		if value == 0 then
			sortkey = '5000000000000000000'
		else
			local mag = math.floor(math.log10(math.abs(value)) + 1e-14)
			local prefix
			if value > 0 then
				prefix = 7000 + mag
			else
				prefix = 2999 - mag
				value = value + 10^(mag+1)
			end
			sortkey = string.format('%d', prefix) .. string.format('%015.0f', math.floor(value * 10^(14-mag)))
		end
		local lhs, rhs
		if sortable == 'sortable_table' then
			lhs = 'data-sort-value="'
			rhs = '"|'
		else
			lhs = sortable == 'sortable_debug' and
				 or
				
			rhs = '♠'
		end
		return lhs .. sortkey .. rhs
	end
end
local translateParameters = {
	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 = {
		hide = { id = 'hide' },
		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' },
		h = { 'H', id = 'h' },
		hm = { 'H', 'M', id = 'hm' },
		hms = { 'H', 'M', 'S', id = 'hms' },
		d = { 'd', id = 'd' },
		dh = { 'd', 'H', id = 'dh' },
		dhm = { 'd', 'H', 'M', id = 'dhm' },
		dhms = { 'd', 'H', 'M', 'S', id = 'dhms' },
		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' },
	},
	sortable = {
		off = false,
		on = 'sortable_on',
		table = 'sortable_table',
		debug = 'sortable_debug',
	},
}
local function dateExtract(frame)
	-- Return part of a date after performing an optional operation.
	local Date = getExports(frame)
	local args = frame:getParent().args
	local parms = {}
	for i, v in ipairs(args) do
		parmsi = v
	end
	if yes(args.fix) then
		table.insert(parms, 'fix')
	end
	if yes(args.partial) then
		table.insert(parms, 'partial')
	end
	local show = stripToNil(args.show) or 'dmy'
	local date = Date(unpack(parms))
	if not date then
		if show == 'format' then
			return 'error'
		end
		return message('正しい日付が必要です')
	end
	local add = stripToNil(args.add)
	if add then
		for item in add:gmatch('%S+') do
			date = date + item
			if not date then
				return message('"' .. item .. '"を追加できませんでした')
			end
		end
	end
	local prefix, result
	local sortable = translateParameters.sortableargs.sortable
	if sortable then
		local value = (date.partial and date.partial.first or date).jdz
		prefix = makeSort(value, sortable)
	end
	if show ~= 'hide' then
		result = dateshow
		if result == nil then
			result = date:text(show)
		elseif type(result) == 'boolean' then
			result = result and '1' or '0'
		else
			result = tostring(result)
		end
	end
	return (prefix or ) .. (result or )
end
local function rangeJoin(range)
	-- Return text to be used between a range of ages.
	return range == 'dash' and '–' or ' or '
end
local function makeText(values, components, names, options)
	-- Return wikitext representing an age or duration.
	local text = Collection.new()
	local count = #values
	local sep = names.sep or 
	for i, v in ipairs(values) do
		-- v is a number (say 4 for 4 years), or a table ({4,5} for 4 or 5 years).
		local islist = type(v) == 'table'
		if (islist or v > 0) or (text.n == 0 and i == count) or (text.n > 0 and components.keepZero) then
			local fmt, vstr
			if i == 1 and options.format == 'format_commas' then
				-- Numbers after the first should be small and not need formatting.
				fmt = formatNumber
			else
				fmt = tostring
			end
			if islist then
				vstr = fmt(v1) .. rangeJoin(options.range) .. fmt(v2)
			else
				vstr = fmt(v)
			end
			local name = namescomponentsi
			if name then
				local plural = names.plural
				if not plural or (islist and v2 or v) == 1 then
					plural = 
				end
				text:add(vstr .. sep .. name .. 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  1 or (text.n == 1 and text1: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
		(options.prefix or ) ..
		sign ..
		text:join() ..
		(options.suffix or )
end
local function dateDifference(parms)
	-- Return a formatted date difference using the given parameters
	-- which have been validated.
	local names = {
		abbr_off = {
			plural = ,
			sep = ,
			y = '年',
			m = 'ヶ月',
			w = '週',
			d = '日',
			H = '時間',
			M = '分',
			S = '秒',
		},
		abbr_on = {
			y = '年',
			m = 'ヶ月',
			w = '週',
			d = '日',
			H = '時間',
			M = '分',
			S = '秒',
		},
		abbr_infant = {      -- for 
			plural = ,
			sep = ,
			y = '年',
			m = 'ヶ月',
			w = '週',
			d = '日',
			H = '時間',
			M = '分',
			S = '秒',
		},
		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 defaultJoin
	if abbr ~= 'abbr_off' then
		defaultJoin = 'sep_space'
	end
	if not show then
		show = 'ymd'
		if parms.disp == 'disp_age' then
			if diff.years = 1 then
					show = 'ym'
				else
					show = 'md'
				end
			else
				show = 'y'
			end
		end
	end
	if type(show) ~= 'table' then
		show = translateParameters.showshow
	end
	if parms.disp == 'disp_raw' then
		defaultJoin = 'sep_space'
		abbr = 'abbr_raw'
	elseif parms.wantSc then
		defaultJoin = 'sep_serialcomma'
	end
	local diffOptions = {
		round = parms.round,
		duration = parms.wantDuration,
		range = parms.range and true or nil,
	}
	local prefix
	if parms.sortable then
		local value = diff.age_days + (parms.wantDuration and 1 or 0)  -- days and fraction of a day
		if diff.isnegative then
			value = -value
		end
		prefix = makeSort(value, parms.sortable)
	end
	local textOptions = {
		prefix = prefix,
		suffix = parms.suffix,  -- not currently used
		format = parms.format,
		join = parms.sep or defaultJoin,
		isnegative = diff.isnegative,
		range = parms.range,
	}
	if show.id == 'hide' then
		return prefix or 
	end
	local values = { diff:age(show.id, diffOptions) }
	if values1 then
		return makeText(values, show, namesabbr, textOptions)
	end
	if diff.partial then
		-- Handle a more complex range such as
		-- range=yes → 1 year, 12 days or 2 years, 11 days
		local opt = {
			format = textOptions.format,
			join = textOptions.join,
			isnegative = textOptions.isnegative,
		}
		return
			(textOptions.prefix or ) ..
			makeText({ diff.partial.mindiff:age(show.id, diffOptions) }, show, namesabbr, opt) ..
			rangeJoin(textOptions.range) ..
			makeText({ diff.partial.maxdiff:age(show.id, diffOptions) }, show, namesabbr, opt) ..
			(textOptions.suffix or )
	end
	return message('show=' .. show.id .. ' は使えません')
end
local function getDates(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 optionally replaced with the current date.
	-- If wantMixture 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, currentDate = getExports(frame)
	getopt = getopt or {}
	local args = frame:getParent().args
	local fields = {}
	local isNamed = args.year or args.year1 or args.year2 or
		args.month or args.month1 or args.month2 or
		args.day or args.day1 or args.day2
	if isNamed then
		fields1 = args.year1 or args.year
		fields2 = args.month1 or args.month
		fields3 = args.day1 or args.day
		fields4 = args.year2
		fields5 = args.month2
		fields6 = args.day2
	else
		for i = 1, 6 do
			fieldsi = argsi
		end
	end
	local imax = 0
	for i = 1, 6 do
		fieldsi = stripToNil(fieldsi)
		if fieldsi then
			imax = i
		end
	end
	local fix = getopt.fix and 'fix' or 
	local noDefault = imax == 0 and getopt.noMissing
	local partialText = getopt.partial and 'partial' or 
	local dates = {}
	if isNamed or imax >= 3 then
		local nrDates = getopt.single and 1 or 2
		if getopt.wantMixture then
			-- Cannot be partial since empty fields are set from current.
			local components = { 'year', 'month', 'day' }
			for i = 1, nrDates * 3 do
				fieldsi = fieldsi or currentDatecomponentsi > 3 and i - 3 or i
			end
			for i = 1, nrDates do
				local index = i == 1 and 1 or 4
				datesi = Date(fieldsindex, fieldsindex+1, fieldsindex+2)
			end
		else
			-- If partial dates are allowed, accept
			--     year only, or
			--     year and month only
			-- Do not accept year and day without a month because that makes no sense
			-- (and because, for example, Date('partial', 2001, nil, 12) sets day = nil, not 12).
			for i = 1, nrDates do
				local index = i == 1 and 1 or 4
				local y, m, d = fieldsindex, fieldsindex+1, fieldsindex+2
				if (getopt.partial and y and (m or not d)) or (y and m and d) then
					datesi = Date(fix, partialText, y, m, d)
				elseif not y and not m and not d and not noDefault then
					datesi = Date('currentdate')
				end
			end
		end
	elseif not noDefault then
		getopt.textdates = true  -- have parsed each date from a single text field
		dates1 = Date(fix, partialText, fields1 or 'currentdate')
		if not getopt.single then
			dates2 = Date(fix, partialText, fields2 or 'currentdate')
		end
	end
	if not dates1 then
		return message('正しい年月日が必要です')
	end
	if getopt.single then
		return dates1
	end
	if not dates2 then
		return message('2つ目の日付は年月日でなければなりません')
	end
	return dates1, dates2
end
local function ageGeneric(frame)
	-- Return the result required by the specified template.
	-- Can use sortable=x where x = on/table/off/debug in any supported template.
	-- Some templates default to sortable=on but can be overridden.
	local name = frame.args.template
	if not name then
		return message('The template invoking this must have "|template=x" where x is the wanted operation')
	end
	local args = frame:getParent().args
	local specs = {
		age_days = {                -- 
			show = 'd',
			disp = 'disp_raw',
		},
		age_days_nts = {            -- 
			show = 'd',
			disp = 'disp_raw',
			format = 'format_commas',
			sortable = 'on',
		},
		duration_days = {           -- 
			show = 'd',
			disp = 'disp_raw',
			duration = true,
		},
		duration_days_nts = {       -- 
			show = 'd',
			disp = 'disp_raw',
			format = 'format_commas',
			sortable = 'on',
			duration = true,
		},
		age_full_years = {          -- 
			show = 'y',
			abbr = 'abbr_raw',
		},
		age_full_years_nts = {      -- 
			show = 'y',
			abbr = 'abbr_raw',
			format = 'format_commas',
			sortable = 'on',
		},
		age_in_years = {            -- 
			show = 'y',
			abbr = 'abbr_raw',
			negative = 'error',
			range = 'dash',
		},
		age_in_years_nts = {        -- 
			show = 'y',
			abbr = 'abbr_raw',
			negative = 'error',
			range = 'dash',
			format = 'format_commas',
			sortable = 'on',
		},
		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',
			sortable = 'on',
		},
		age_m = {                   -- 
			show = 'm',
			disp = 'disp_raw',
		},
		age_w = {                   -- 
			show = 'w',
			disp = 'disp_raw',
		},
		age_wd = {                  -- 
			show = 'wd',
		},
		age_yd = {                  -- 
			show = 'yd',
			format = 'format_commas',
			sep = args.sep ~= 'and' and 'sep_comma' or nil,
		},
		age_yd_nts = {              -- 
			show = 'yd',
			format = 'format_commas',
			sep = args.sep ~= 'and' and 'sep_comma' or nil,
			sortable = 'on',
		},
		age_ym = {                  -- 
			show = 'ym',
			sep = 'sep_comma',
		},
		age_ymd = {                 -- 
			show = 'ymd',
			range = true,
		},
		age_ymwd = {                -- 
			show = 'ymwd',
			wantMixture = true,
		},
	}
	local spec = specsname
	if not spec then
		return message('指定されたテンプレート名が不正です')
	end
	if name == 'age_days' then
		local su = stripToNil(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 partial, autofill
	local range = stripToNil(args.range) or spec.range
	if range then
		-- Suppose partial dates are used and age could be 11 or 12 years.
		-- "|range=" (empty value) has no effect (spec is used).
		-- "|range=yes" or spec.range == true sets range = true (gives "11 or 12")
		-- "|range=dash" or spec.range == 'dash' sets range = 'dash' (gives "11–12").
		-- "|range=no" or spec.range == 'no' sets range = nil and fills each date in the diff (gives "12").
		--     ("on" is equivalent to "yes", and "off" is equivalent to "no").
		-- "|range=OTHER" sets range = nil and rejects partial dates.
		range = ({ dash = 'dash', off = 'no', no = 'no', true = true })range or yes(range)
		if range then
			partial = true  -- accept partial dates with a possible age range for the result
			if range == 'no' then
				autofill = true  -- missing month/day in first or second date are filled from other date or 1
				range = nil
			end
		end
	end
	local getopt = {
		fix = yes(args.fix),
		partial = partial,
		wantMixture = spec.wantMixture,
	}
	local date1, date2 = getDates(frame, getopt)
	if type(date1) == 'string' then
		return date1
	end
	local format = stripToNil(args.format)
	if format then
		format = 'format_' .. format
	elseif name == 'age_days' and getopt.textdates then
		format = 'format_commas'
	end
	local parms = {
		diff = date2:subtract(date1, { fill = autofill }),
		wantDuration = spec.duration or yes(args.duration),
		range = range,
		wantSc = yes(args.sc),
		show = args.show == 'hide' and 'hide' or spec.show,
		abbr = spec.abbr,
		disp = spec.disp,
		format = format or spec.format,
		round = yes(args.round),
		sep = spec.sep,
		sortable = translateParameters.sortableargs.sortable or spec.sortable,
	}
	if (spec.negative or frame.args.negative) == 'error' and parms.diff.isnegative then
		return message('The second date should not be before the first date')
	end
	return dateDifference(parms)
end
local function bda(frame)
	-- Implement Template:Birth date and age.
	local args = frame:getParent().args
	local options = { noMissing=true, single=true }
	local date = getDates(frame, options)
	if type(date) == 'string' then
		return date  -- error text
	end
	local Date = getExports(frame)
	local diff = Date('currentdate') - date
	if diff.isnegative or diff.years > 150 then
		return message('歳の計算に使われた生年月日が不正です')
	end
	local disp, show = 'disp_raw', 'y'
	if diff.years %-Y-%m-%d) ' .. result
	result = ' ' ..
		date:text(result) ..
		' ' ..
		'(age ' ..
		dateDifference({
			diff = diff,
			show = show,
			abbr = 'abbr_off',
			disp = disp,
			sep = 'sep_space',
		}) ..
		')'
	local warnings = tonumber(frame.args.warnings)
	if warnings and warnings > 0 then
		local good = {
			df = true,
			mf = true,
			template = true,
			day = true,
			day1 = true,
			month = true,
			month1 = true,
			year = true,
			year1 = true,
		}
		local invalid
		local imax = options.textdates and 1 or 3
		for k, _ in pairs(args) do
			if type(k) == 'number' then
				if k > imax then
					invalid = tostring(k)
					break
				end
			else
				if not goodk then
					invalid = k
					break
				end
			end
		end
		if invalid then
			result = result .. message('invalid parameter ' .. invalid, 'warning')
		end
	end
	return result
end
local function dateToGsd(frame)
	-- Implement Template: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 = getDates(frame, { wantMixture=true, single=true })
	if type(date) == 'string' then
		return date
	end
	return tostring(date.gsd)
end
local function jdToDate(frame)
	-- Return formatted date from a Julian date.
	-- The result includes a time if the input includes a fraction.
	-- The word 'Julian' is accepted for the Julian calendar.
	local Date = getExports(frame)
	local args = frame:getParent().args
	local date = Date('juliandate', args1, args2)
	if date then
		return date:text()
	end
	return message('正しいユリウス暦における日付番号が必要です')
end
local function dateToJd(frame)
	-- Return Julian date (a number) from a date which may include a time,
	-- or the current date ('currentdate') or current date and time ('currentdatetime').
	-- The word 'Julian' is accepted for the Julian calendar.
	local Date = getExports(frame)
	local args = frame:getParent().args
	local date = Date(args1, args2, args3, args4, args5, args6, args7)
	if date then
		return tostring(date.jd)
	end
	return message('正しい年月日か、"currentdate"が必要です')
end
local function timeInterval(frame)
	-- Implement Template: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 = getExports(frame)
	local args = frame:getParent().args
	local parms = {
		wantDuration = yes(args.duration),
		range = yes(args.range) or (args.range == 'dash' and 'dash' or nil),
		wantSc = yes(args.sc),
	}
	local fix = yes(args.fix) and 'fix' or 
	local date1 = Date(fix, 'partial', stripToNil(args1) or 'currentdatetime')
	if not date1 then
		return message('第1引数（開始日）が不正です')
	end
	local date2 = Date(fix, 'partial', stripToNil(args2) or 'currentdatetime')
	if not date2 then
		return message('第2引数（終結日）が不正です')
	end
	parms.diff = date2 - date1
	for argname, translate in pairs(translateParameters) do
		local parm = stripToNil(argsargname)
		if parm then
			parm = translateparm
			if parm == nil then  -- test for nil because false is a valid setting
				return message('Parameter ' .. argname .. '=' .. argsargname .. ' is invalid')
			end
			parmsargname = 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('show=' .. args.show .. ' と round=' .. args.round .. ' が衝突を起こしています')
				end
			else
				parms.show = translateParameters.showround
			end
		end
		parms.round = true
	end
	return dateDifference(parms)
end
return {
	age_generic = ageGeneric,           -- can emulate several age templates
	birth_date_and_age = bda,           -- Template:Birth_date_and_age
	gsd = dateToGsd,                    -- Template:Gregorian_serial_date
	extract = dateExtract,              -- Template:Extract
	jd_to_date = jdToDate,              -- Template:?
	JULIANDAY = dateToJd,               -- Template:JULIANDAY
	time_interval = timeInterval,       -- Template:Time_interval
}