Module:calendar

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Provides a convert function that can invoked to convert years from the Gregorian calendar to the Islamic calendar (Lunar Hijri) or Solar Hijri calendar, as appropriate in citations. This function should be used from {{xdate}}.


-- The Lunar Hijri conversion functions are translated from
-- https://github.com/GeniusTS/hijri-dates/blob/master/src/Converter.php
-- which is under the MIT license, and the Solar Hijri functions are translated
-- from https://github.com/xsoh/solarHijri-js/blob/master/index.js,
-- which is under the MIT license.
-- convert and _convert are wrappers unique to
-- this module.

local export = {}

do
	local floor = math.floor
	
	function export.Gregorian_to_Julian(year, month, day)
	    if month < 3 then
	        year = year - 1
	        month = month + 12
	    end
	
	    local a = floor(year / 100.0)
	    local b
	    if year == 1582 and (month > 10 or (month == 10 and day > 4)) then
	    	b = -10
	    elseif year == 1582 and month == 10 then
	    	b = 0
	    elseif year < 1583 then
	    	b = 0
	    else
	    	b = 2 - a + floor(a / 4.0)
	    end
	
	    return floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + day + b - 1524
	end
	
	function export.Lunar_Hijri_to_Julian(year, month, day)
	    return floor((11 * year + 3) / 30) + floor(354 * year) + floor(30 * month)
	        - floor((month - 1) / 2) + day + 1948440 - 386
	end
	
	function export.Julian_to_Gregorian(Julian_day)
	    local b = 0
	    if Julian_day > 2299160 then
	        local a = floor((Julian_day - 1867216.25) / 36524.25)
	        b = 1 + a - floor(a / 4.0)
	    end
	
	    local bb = Julian_day + b + 1524
	    local cc = floor((bb - 122.1) / 365.25)
	    local dd = floor(365.25 * cc)
	    local ee = floor((bb - dd) / 30.6001)
	
	    local day = (bb - dd) - floor(30.6001 * ee)
	    local month = ee - 1
	
	    if ee > 13 then
	        cc = cc + 1
	        month = ee - 13
	    end
	
	    local year = cc - 4716
	
	    return { year = year, month = month, day = day }
	end
	
	function export.Julian_to_Lunar_Hijri(Julian_day)
	    local y = 10631.0 / 30.0
	    local epoch_astro = 1948084
	    local shift1 = 8.01 / 60.0
	
	    local z = Julian_day - epoch_astro
	    local cyc = floor(z / 10631.0)
	    z = z - 10631 * cyc
	    local j = floor((z - shift1) / y)
	    z = z - floor(j * y + shift1)
	
	    local year = 30 * cyc + j
	    local month = floor((z + 28.5001) / 29.5)
	    if month == 13 then
	        month = 12
	    end
	
	    local day = z - floor(29.5001 * month - 29)
	
	    return { year = year, month = month, day = day }
	end
end

do
	-- translated from here:
	-- https://github.com/xsoh/solarHijri-js/blob/master/index.js
	
	local floor, ceil = math.floor, math.ceil
	
	-- Utility helper functions.
	
	local function idiv(a, b)
		local c = a / b
		return (c > 0 and floor or ceil)(c)
	end
	
	local function mod(a, b)
		return a - idiv(a, b) * b
	end
	
	--[[
	local function is_valid_Solar_Hijri_date(year, month, day)
		return	year >= -61 and year <= 3177 and
						month >= 1 and month <= 12 and
						day >= 1 and day <= Solar_Hijri_month_length(year, month)
	end
	--]]
	
	local function is_leap_Solar_Hijri_year(year)
		return Solar_Hijri_Cal(year).leap == true
	end
	
	local function Solar_Hijri_month_length(year, month)
		if month <= 5 then
			return 30
		elseif month >= 7 then
			return 31
		end
		-- here where month = 6.
		if is_leap_Solar_Hijri_year(year) then
			return 30
		else
			return 29
		end
	end
	
	local function Gregorian_to_Julian(year, month, day)
	  local d = idiv((year + idiv(month - 8, 6) + 100100) * 1461, 4)
	      + idiv(153 * mod(month + 9, 12) + 2, 5)
	      + day - 34840408
	  d = d - idiv(idiv(year + 100100 + idiv(month - 8, 6), 100) * 3, 4) + 752
	  return d
	end
	
	local function Julian_to_Gregorian(Julian_day)
		local j = 4 * Julian_day + 139361631
		j = j + idiv(idiv(4 * Julian_day + 183187720, 146097) * 3, 4) * 4 - 3908
		local i = idiv(mod(j, 1461), 4) * 5 + 308
		
		local month = mod(idiv(i, 153), 12) + 1
		return {
			year = idiv(j, 1461) - 100100 + idiv(8 - month, 6),
			month = month,
			day = idiv(mod(i, 153), 5) + 1,
		}
	end
	
	local function Solar_Hijri_Cal(year)
		local Gregorian_year = year + 621
		local pGregorian_year = Gregorian_year + 1
	
		return	{
			leap = (pGregorian_year % 4 == 0 and pGregorian_year % 100 ~= 0) or pGregorian_year % 400 == 0,
			Gregorian_year = Gregorian_year,
			start_date = 23, -- September 23
		}
	end
	
	local function Julian_to_Solar_Hijri(Julian_day)
		local Gregorian_year = Julian_to_Gregorian(Julian_day).year
		local year = Gregorian_year - 621
		local r = Solar_Hijri_Cal(year)
		
		local Julian_day_1st_Libra = Gregorian_to_Julian(Gregorian_year, 9, r.start_date)
	
		local k = Julian_day - Julian_day_1st_Libra
		if k >= 0 and k <= 99 then
			local leap = r.leap and 1 or 0
			return	{
				year = year,
				month = 1 + idiv(k, 30),
				day = mod(k, 30) + 1,
			}
		else
			k = k + 365
			year = year - 1
			r = Solar_Hijri_Cal(year)
			leap = r.leap and 1 or 0
	
			if k <= 178 then
				k = k + leap
				return	{
					year = year,
					month = 1 + idiv(k, 30),
					day = mod(k, 30) + 1,
				}
			else
				k = k - 179
				return	{
					year = year,
					month = 7 + idiv(k, 31),
					day = mod(k, 31) + 1,
				}
			end
	
		end
	end
	
	local function Solar_Hijri_to_Julian(year, month, day)
		local r = Solar_Hijri_Cal(year);
		local leap = r.leap and 0 or -1
		return Gregorian_to_Julian(r.Gregorian_year, 9, r.start_date)
			+ (month - 1) * 30 + idiv(month, 7) * (month - 7) + day - 1 + (idiv(month, 7) * leap)
	end
	
	function export.Gregorian_to_Solar_Hijri(year, month, day)
		return Julian_to_Solar_Hijri(Gregorian_to_Julian(year, month, day))
	end
	
	function export.Solar_Hijri_to_Gregorian(year, month, day)
		return Julian_to_Gregorian(Solar_Hijri_to_Julian(year, month, day))
	end
end

function export._convert(from, to, year)
	local f1, f2
	if from == "gregorian" then
		if to == "lunar_hijri" then
			f1 = "Gregorian_to_Julian"
			f2 = "Julian_to_Lunar_Hijri"
		elseif to == "solar_hijri" then
			f1 = "Gregorian_to_Solar_Hijri"
		else
			error("not implemented")
		end
	elseif from == "lunar_hijri" then
		if to == "gregorian" then
			f1 = "Lunar_Hijri_to_Julian"
			f2 = "Julian_to_Gregorian"
		else
			error("not implemented")
		end
	elseif from == "solar_hijri" then
		if to == "gregorian" then
			f1 = "Solar_Hijri_to_Gregorian"
		else
			error("not implemented")
		end
	else
		error("not implemented")
	end
	
	local converter
	if f2 then
		function converter(year, month, day)
			return export[f2](export[f1](year, month, day))
		end
	else
		function converter(year, month, day)
			return export[f1](year, month, day)
		end
	end
	
	-- assuming month is 1-based
	return converter(year, 1, 1), converter(year, 12, 30)
end

local aliases = {
	ah = "lunar_hijri",
	sh = "solar_hijri",
	gc = "gregorian",
}

local function normalize_calendar_code(code)
	code = code:lower():gsub(" ", "_")
	return aliases[code] or code
end

function export.convert(frame)
	assert(frame and frame.args and type(frame.args) == "table")
	local args = pairs(frame.args)(frame.args) and frame.args
		or frame:getParent().args
	local from = args[1] or error("Expected calendar name in parameter 1")
	local to = args[2] or error("Expected calendar name in parameter 2")
	local year = tonumber(args[3]) or error("Expected number in parameter 3")
	from, to = normalize_calendar_code(from), normalize_calendar_code(to)
	
	local date1, date2 = export._convert(from, to, year)
	local res = ""
	local date = date1
	while date.year <= date2.year do
		local year_string = os.date("%Y", os.time(date))
		if res ~= "" then
			res = res .. "/"
		end
		res = res .. year_string
		date.year = date.year + 1
	end
	return res
end

function export.debug(from, to, year)
	return export.convert { args = { from, to, year } }
end

return export