跳转到内容

模組:Julianday

本页使用了标题或全文手工转换
维基百科,自由的百科全书

local p = {}
local mError = setmetatable({}, {
	__index = function (t, k)
		local _mError = require('Module:Error')
		mError = _mError
		return _mError[k]
	end
})

-------- //// Lua Api Start //// --------
local function yearIsLeap(year, useJulian)
	if useJulian then
		return year % 4 == 0
	end
	return (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)
end

local function getDaysInYear(year, useJulian)
	return yearIsLeap(year, useJulian) and 366 or 365
end

local function getDaysInMonth(year, month, useJulian)
	if month == 2 then
		return yearIsLeap(year, useJulian) and 29 or 28
	end
	return (month == 4 or month == 6 or month == 9 or month == 11) and 30 or 31
end

local function getDaysSinceYearStart(year, month, day, useJulian)
	local result = 0
	for m = 1, month - 1, 1 do
		result = result + getDaysInMonth(year, m, useJulian)
	end
	result = result + day - 1
	return result
end

function getPassedDaysRaw(startYear, startMonth, startDay, endYear, endMonth, endDay, useJulian)
	local result = 0
	local flag = 1 -- 結果應該是向前計算還是向後計算
	if
		startYear > endYear
		or (startYear == endYear and startMonth > endMonth)
		or (startYear == endYear and startMonth == endMonth and startDay > endDay)
	then
		-- 交換,確保 end 比 start 還晚,以免還得處理加減法問題
		startYear, startMonth, startDay, endYear, endMonth, endDay = endYear, endMonth, endDay, startYear, startMonth, startDay
		flag = -1
	end
	
	if startYear < endYear then
		-- 如果結束日期和開始日期不在同一個年份,快進到兩個同一年
		for y = startYear, endYear - 1, 1 do
			result = result + getDaysInYear(y, useJulian)
		end
	end

	result = result - getDaysSinceYearStart(startYear, startMonth, startDay, useJulian) + getDaysSinceYearStart(endYear, endMonth, endDay, useJulian)
	return result * flag
end

local function getTargetDateRaw(startYear, startMonth, startDay, during, useJulian)
	local endYear, endMonth, endDay = startYear, startMonth, startDay
	local daysLeft = during

	if daysLeft > 0 then
		-- 往未來推算
		while daysLeft > 0 do
			local daysInYear = getDaysInYear(endYear, useJulian) -- 該年總天數
			local daysPassed = getDaysSinceYearStart(endYear, endMonth, endDay, useJulian) -- 該年已過去的天數
			local daysRemaining = daysInYear - daysPassed -- 當年剩餘天數

			if daysLeft >= daysRemaining then
				-- 如果還要加的天數超過當年剩餘天數,就直接跨年
				daysLeft = daysLeft - daysRemaining
				endYear = endYear + 1
				endMonth = 1
				endDay = 1
			else
				-- 否則逐月增加
				while daysLeft > 0 do
					local daysInMonth = getDaysInMonth(endYear, endMonth, useJulian) -- 當月總天數
					local remainingInMonth = daysInMonth - endDay + 1 -- 當月剩餘天數

					if daysLeft >= remainingInMonth then
						-- 如果還要加的天數超過當月剩餘天數,就跨月
						daysLeft = daysLeft - remainingInMonth
						endMonth = endMonth + 1
						endDay = 1

						if endMonth > 12 then
							endMonth = 1
							endYear = endYear + 1
						end
					else
						-- 直接加天數
						endDay = endDay + daysLeft
						daysLeft = 0
					end
				end
			end
		end
	elseif daysLeft < 0 then
		-- 往過去推算
		while daysLeft < 0 do
			local daysPassed = getDaysSinceYearStart(endYear, endMonth, endDay, useJulian) -- 當年已過去的天數

			if -daysLeft >= daysPassed then
				-- 如果還要減的天數超過當年已過去的天數,就直接跨年
				daysLeft = daysLeft + daysPassed + 1 -- 把這天也一起減去(daysLeft 是負的)
				endYear = endYear - 1
				endMonth = 12
				endDay = 31
			else
				-- 否則逐月減少
				while daysLeft < 0 do
					if -daysLeft >= endDay then
						-- 跨月處理
						daysLeft = daysLeft + endDay
						endMonth = endMonth - 1
						if endMonth == 0 then
							endMonth = 12
							endYear = endYear - 1
						end
						endDay = getDaysInMonth(endYear, endMonth, useJulian) -- 上個月總天數
					else
						-- 如果當月可以直接減去天數
						endDay = endDay + daysLeft
						daysLeft = 0
					end
				end
			end
		end
	end

	return endYear, endMonth, endDay
end

local function getTimeHavePassed(hour, minute, second)
	return hour * 3600 + minute * 60 + second
end

local function buildModule(startAt)
	local startAtTimeHavePassed = getTimeHavePassed(startAt.hour, startAt.minute, startAt.second)
	return {
		getDaysSimple = function (year, month, day)
			-- 不考慮時間的簡易版本
			return getPassedDaysRaw(startAt.year, startAt.month, startAt.day, year, month, day, startAt.useJulian)
		end,
		getDays = function (year, month, day, hour, minute, second)
			local rawDay = getPassedDaysRaw(startAt.year, startAt.month, startAt.day, year, month, day, startAt.useJulian)
			local inputTimeHavePassed = getTimeHavePassed(hour, minute, second)
			return rawDay + (inputTimeHavePassed - startAtTimeHavePassed) / 86400
		end,
		fromDay = function (rawDay)
			local duringPart = math.floor(rawDay)
			local timePart = rawDay - duringPart
			local timePartSeconds = math.floor(timePart * 86400 + 0.5) + startAtTimeHavePassed -- 四捨五入總秒數
			if timePartSeconds >= 86400 then
				-- 換算過來已經換日了,改用無條件進位以避免後續計算換日
				duringPart = duringPart + 1
				timePartSeconds = timePartSeconds - 86400
			end
			
			local year, month, day = getTargetDateRaw(startAt.year, startAt.month, startAt.day, duringPart, startAt.useJulian)
			local hour, minute, second = 0, 0, 0
			
			if timePartSeconds ~= 0 then
				if math.abs(timePartSeconds) > 86400 then
					error('Unknown error.')
				end
				hour = math.floor(timePartSeconds / 3600)
				timePartSeconds = timePartSeconds % 3600
				minute = math.floor(timePartSeconds / 60)
				second = timePartSeconds % 60
			end
			
			return {
				year = year,
				month = month,
				day = day,
				hour = hour,
				minute = minute,
				second = second,
			}
		end,
	}
end

local function mockModule(dayModule, offset)
	return {
		getDays = function (year, month, day, hour, minute, second)
			return dayModule.getDays(year, month, day, hour, minute, second) - offset
		end,
		fromDay = function (rawDay)
			return dayModule.fromDay(rawDay + offset)
		end,
	}
end

p._mockJuliandayModule = mockModule

p._JuliandayJulian    = buildModule({ year = -4712, month = 1,  day = 1,  hour = 12, minute = 0,  second = 0,  useJulian = true })
p._JuliandayGregorian = buildModule({ year = -4713, month = 11, day = 24, hour = 12, minute = 0,  second = 0,  useJulian = false })

--[=[
以下大概率沒啥用,註釋起來,有需要請自己
<syntaxhighlight lang="lua">
local mJulianday = require('Module:Julianday')
local JulianReduced = mJulianday._mockJuliandayModule(mJulianday._JuliandayJulian, -2400000)
</syntaxhighlight>
以此類推
]=]

-- p._JuliandayJulianReduced                  = mockModule(p._JuliandayJulian, -2400000)
-- p._JuliandayJulianModified                 = mockModule(p._JuliandayJulian, -2400000.5)
-- p._JuliandayJulianTruncatedNASA            = mockModule(p._JuliandayJulian, -2440000.5)
-- p._JuliandayJulianTruncatedNISTLast        = mockModule(p._JuliandayJulian, -2450000.5)
-- p._JuliandayJulianTruncatedNISTLastCurrent = mockModule(p._JuliandayJulian, -2460000.5)

-- p._JuliandayReduced                        = mockModule(p._JuliandayGregorian, -2400000)
-- p._JuliandayModified                       = mockModule(p._JuliandayGregorian, -2400000.5)
-- p._JuliandayTruncatedNASA                  = mockModule(p._JuliandayGregorian, -2440000.5)
-- p._JuliandayTruncatedNISTLast              = mockModule(p._JuliandayGregorian, -2450000.5)
-- p._JuliandayTruncatedNISTLastCurrent       = mockModule(p._JuliandayGregorian, -2460000.5)

p._Julianday = {
	getDays = function (year, month, day, hour, minute, second)
		if
			(year > 1582) or
			(year == 1582 and (
				month > 10 or
				(month == 10 and (
					day > 15 or
					(day == 15 and hour >= 12)
				))
			))
		then
			return p._JuliandayGregorian.getDays(year, month, day, hour, minute, second)
		-- elseif
			-- (year < 1582) or
			-- (year == 1582 and (
				-- month < 10 or
				-- (month == 10 and (
					-- day <= 4 or
					-- (day == 5 and hour <= 12)
				-- ))
			-- ))
		-- then
			-- return p._JuliandayJulian.getDays(year, month, day, hour, minute, second)
		else
			-- error(string.format('Fail to execute _Julianday.getDays on %04d-%02d-%02dT%02d:%02d:%02d: time is undefined.', year, month, day, hour, minute, second))
			return p._JuliandayJulian.getDays(year, month, day, hour, minute, second)
		end
	end,
	fromDay = function (rawDay)
		return (rawDay >= 22991601 and p._JuliandayGregorian or p._JuliandayJulian).fromDay(rawDay)
	end,
}

-- getPassedDaysRaw 取得兩個日期間經過的時間
-- @param {number} startYear 開始年分
-- @param {number} startMonth 開始月分
-- @param {number} startDay 開始日期
-- @param {number} endYear 開始年分
-- @param {number} endMonth 開始月分
-- @param {number} endDay 開始日期
-- @param {boolean} [useJulian] 是否應該用格里曆來計算持續日期
-- @return {number} 經過的時間
p.getPassedDaysRaw = getPassedDaysRaw

-- getPassedDays 取得兩個日期間經過的時間,自動切換儒略曆和格里曆
-- 請注意,如果輸入的日期在 1582-10-05 ~ 1582-10-14 之間,可能會出現未定義行為
-- 此函數不檢查是否經過切換曆法的中午 12:00,請自己實作
-- @param {number} startYear 開始年分
-- @param {number} startMonth 開始月分
-- @param {number} startDay 開始日期
-- @param {number} endYear 開始年分
-- @param {number} endMonth 開始月分
-- @param {number} endDay 開始日期
-- @return {number} 經過的時間
function p._getPassedDays(startYear, startMonth, startDay, endYear, endMonth, endDay)
	-- 檢查日期是否跨越曆法轉換
	local isStartBeforeCutoff = (startYear < julianCutoff.year) or
		(startYear == julianCutoff.year and startMonth < julianCutoff.month) or
		(startYear == julianCutoff.year and startMonth == julianCutoff.month and startDay <= julianCutoff.day)

	local isEndAfterCutoff = (endYear > gregorianCutoff.year) or
		(endYear == gregorianCutoff.year and endMonth > gregorianCutoff.month) or
		(endYear == gregorianCutoff.year and endMonth == gregorianCutoff.month and endDay >= gregorianCutoff.day)

	if isStartBeforeCutoff then
		if isEndAfterCutoff then
			-- 跨越曆法轉換,分段計算
			local daysBefore = getPassedDays(startYear, startMonth, startDay, gregorianCutoff.year, julianCutoff.month, julianCutoff.day, true) -- 使用儒略曆
			local daysAfter = getPassedDays(gregorianCutoff.year, gregorianCutoff.month, gregorianCutoff.day, endYear, endMonth, endDay, false) -- 使用格里曆
			return daysBefore + daysAfter
		end
		--都在儒略曆
		return getPassedDays(startYear, startMonth, startDay, endYear, endMonth, endDay, true)
	else
		--都在格里曆
		return getPassedDays(startYear, startMonth, startDay, endYear, endMonth, endDay, false)
	end
end

-- getTargetDateRaw 取得經過持續日期後的目標時間
-- @param {number} startYear 開始年分
-- @param {number} startMonth 開始月分
-- @param {number} startDay 開始日期
-- @param {number} during 持續時間
-- @param {boolean} [useJulian] 是否應該用格里曆來計算持續日期
-- @return {number, number, number} 目標時間
p._getTargetDateRaw = getTargetDateRaw

-- getTargetDate 取得經過持續日期後的目標時間,自動切換儒略曆和格里曆
-- 請注意,如果輸入的日期在 1582-10-05 ~ 1582-10-14 之間,可能會出現未定義行為
-- @param {number} startYear 開始年分
-- @param {number} startMonth 開始月分
-- @param {number} startDay 開始日期
-- @param {number} during 持續時間
-- @param {boolean} [useJulian] 是否應該用格里曆來計算持續日期
-- @return {number, number, number} 目標時間
function p._getTargetDate(startYear, startMonth, startDay, during)
	-- 檢查起始日期是否在1582年10月4日之前
	local is_before_gregorian = (startYear < 1582) or (startYear == 1582 and startMonth < 10) or(startYear == 1582 and startMonth == 10 and startDay <= 4)

	if isBeforeGregorian then
		-- 如果是格里曆實施前,計算起始日期的儒略日
		local startJd = JuliandayJulian.getDaysSimple(startYear, startMonth, startDay)
		
		-- 判斷目標日期是否跨越格里曆實施
		local targetJd = startJd + during
		local gregorianStartJd = 2299161 -- JuliandayGregorian.getDaysSimple(1582, 10, 15)
		
		if targetJd >= gregorianStartJd then
			-- 跨越格里曆實施,需要特殊處理
			local daysBeforeSwitch = gregorianStartJd - startJd -1 -- 計算起始日期到曆法切換前的天數

			-- 計算在儒略曆下的日期
			local daysBeforeSwitch = getTargetDateRaw(startYear, startMonth, startDay, daysBeforeSwitch, true)
			-- 計算剩餘天數
			local remainingDays = during - daysBeforeSwitch -1

			-- 從格里曆 1582-10-15 開始計算剩餘天數
			return getTargetDateRaw(1582, 10, 15, remainingDays, false)
		else
			-- 沒有跨越,直接使用儒略曆計算
			return getTargetDateRaw(startYear, startMonth, startDay, during, true)
		end
		else
	  -- 起始日期已經是格里曆,直接使用格里曆計算
	  return getTargetDateRaw(startYear, startMonth, startDay, during, false)
  end
end

-------- //// Lua Api End //// --------
-------- //// Wikitext Api Start //// --------

local _getArgs
local function getArgs(...)
	if not _getArgs then
		_getArgs = require('Module:Arguments').getArgs
	end
	return _getArgs(...)
end

local _yesno
local function yesno(...)
	if not _yesno then
		_yesno = require('Module:Yesno')
	end
	return _yesno(...)
end

local _current
local function getCurrentTime()
	if not _current then
		_current = os.date("*t")
	end
	return _current
end

local function tonumberWrap(input)
	local result = tonumber(input)
	if result == nil and input then
		error('Fail to parse "' .. input .. '" to number.')
	end
	return result
end

local function getDaysWrapper(dayModule, startAtNoon, wrapper)
	wrappers = wrappers or {}
	return function (frame)
		local args = getArgs(frame, {
			frameOnly = false,
			parentFirst = true,
			wrappers = wrappers
		})

		if not args[1] then
			local current = getCurrentTime()
			return dayModule.getDays(current.year, current.month, current.day, current.hour, current.min, current.sec)
		end
		local year = tonumberWrap(args[1])
		local month = tonumberWrap(args[2]) or 1
		local day = tonumberWrap(args[3]) or 1
		local hour = tonumberWrap(args[4]) or (startAtNoon and 12 or 0)
		local minute = tonumberWrap(args[5]) or 0
		local second = tonumberWrap(args[6]) or 0
		return dayModule.getDays(year, month, day, hour, minute, second)
	end
end

-- implementation [[Template:JD]]
p.Julianday = getDaysWrapper(p._Julianday, true, {
	'Template:JD'
})

-- implementation [[Template:JULIANDAY]]
p.JuliandayGregorian = getDaysWrapper(p._JuliandayGregorian, true, {
	'Template:JULIANDAY'
})

-- implementation
-- * [[Template:JULIANDAY.YEAR]]
-- * [[Template:JULIANDAY.MONTH]]
-- * [[Template:JULIANDAY.DAY]]
-- * [[Template:JULIANDAY.HOUR]]
-- * [[Template:JULIANDAY.MINUTE]]
-- * [[Template:JULIANDAY.SECOND]]
-- * [[Template:JULIANDAY.TIMESTAMP]]
function p.JuliandayGregorianFromDay(frame)
	local args = getArgs(frame, {
		frameOnly = false,
		parentFirst = true,
		wrappers = {
			'Template:JULIANDAY.YEAR',
			'Template:JULIANDAY.MONTH',
			'Template:JULIANDAY.DAY',
			'Template:JULIANDAY.HOUR',
			'Template:JULIANDAY.MINUTE',
			'Template:JULIANDAY.SECOND',
			'Template:JULIANDAY.TIMESTAMP',
		}
	})

	local itemToGet = frame.args.type
	if
		itemToGet ~= 'year' and
		itemToGet ~= 'month' and
		itemToGet ~= 'day' and
		itemToGet ~= 'hour' and
		itemToGet ~= 'minute' and
		itemToGet ~= 'second' and
		itemToGet ~= 'timestamp'
	then
		error('Type "' .. itemToGet .. '" is invalid.')
	end
	
	if not args[1] then
		return mError.error({ '[[Module:Julianday]].JuliandayGregorianFromDay: Missing input.' })
	end

	local ts = tonumberWrap(args[1])
	local raw = p._JuliandayGregorian.fromDay(ts)
	
	if itemToGet == 'timestamp' then
		return string.format(
			yesno(args.useISO) and '%04d-%02d-%02dT%02d:%02d:%02d' or '%04d%02d%02d%02d%02d%02d',
			raw.year,
			raw.month,
			raw.day,
			raw.hour,
			raw.minute,
			raw.second
		)
	end

	return raw[itemToGet]
end

-- implementation [[Template:JULIANDAY.JULIAN]]
p.JuliandayJulian = getDaysWrapper(p._JuliandayJulian, true, {
	'Template:JULIANDAY.JULIAN'
})

-------- //// Wikitext Api End //// --------
return p