Jump to content

Module:Calendar

Permanently protected module
From Wikisource

--[[
  __  __           _       _         ____      _                _            
 |  \/  | ___   __| |_   _| | ___ _ / ___|__ _| | ___ _ __   __| | __ _ _ __ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) |   / _` | |/ _ \ '_ \ / _` |/ _` | '__|
 | |  | | (_) | (_| | |_| | |  __/_| |__| (_| | |  __/ | | | (_| | (_| | |   
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\__,_|_|\___|_| |_|\__,_|\__,_|_|   
                                                                             
This module is intended for date conversion between Gregorian and Julian calendars

Please do not modify this code without applying the changes first at Module:Calendar/sandbox 
and testing at Module:Calendar/sandbox/testcases 

Maintainers:
* Jarekt
]]
require('strict')

-- ==================================================
-- === External functions ===========================
-- ==================================================
local p = {}

-- ===========================================================================
-- === Version of the function to be called from other LUA codes
-- ===========================================================================

-----------------------------------------------------------------------------
--[[
Convert calendar date to  "Julian day number" (jdn) 
code based on  https://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_day_number
explanation based on http://www.cs.utsa.edu/~cs1063/projects/Spring2011/Project1/project1.html

Example usage:
  jdn = _date2jdn("2017-09-01", 1)

Inputs:
  ISOdate:    date in YYYY-MM-DD format
  gregorian:  int - "1" for gregorian calendar and "0" for Julian (optional with "1" as default)
  
Outputs:
  1: "Julian day number" as integer or number of days since Monday, January 1, 4713 BC

]]
function p._date2jdn(ISOdate, gregorian)
    ISOdate = mw.text.trim(ISOdate)
    if (#ISOdate<10 or #ISOdate>11) then
    	return nil
    end
    -- date string has to start with YYYY-MM-DD date, possibly with a "-" in front
	local year, month, day = ISOdate:match( "^(-?%d%d%d%d)-(%d%d)-(%d%d)" )
	if not year then
		return nil
	elseif tonumber(year) < 0 then
		-- If year is less than 0, add one to convert from  the common era system in which
	    -- the year -1 (1 B.C.E) is followed by year 1 (1 C.E.) to a zero based date system
		year = year + 1
	end
	local a, b, c, d, y, m
	a = math.floor((14-month) / 12) -- will be 1 for January and February, and 0 for other months.
	y = year + 4800 - a             -- years since year –4800
	m = month + 12*a - 3            -- month number where 10 for January, 11 for February, 0 for March, 1 for April
	c = math.floor((153*m + 2)/5)   -- number of days since March 1
	if (gregorian or 1) >0 then	
		b = math.floor(y/4) - math.floor(y/100) + math.floor(y/400) -- number of leap years since y==0 (year –4800)
		d = 32045                   -- offset so the result will be 0 for January 1, 4713 BCE
	else
		b = math.floor(y/4)         -- number of leap years since y==0 (year –4800)
		d = 32083                   -- offset so the result will be 0 for January 1, 4713 BCE
	end
	return day + c + 365*y + b - d
end

-----------------------------------------------------------------------------
--[[
Convert "Julian day number" (jdn) to a calendar date 
code based on  https://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_day_number
explanation based on http://www.cs.utsa.edu/~cs1063/projects/Spring2011/Project1/project1.html

Example usage:
  jdn = _date2jdn("2017-09-01", 1)

Inputs:
  jdn:       integer - Julian day number or number of days since Monday, January 1, 4713 BC
  gregorian: int - "1" for gregorian calendar and "0" for Julian (optional with "1" as default)
  
Outputs:
  1: date in YYYY-MM-DD forma

]]
-- Convert "Julian day number" (jdn) to a calendar date
-- "gregorian" is a 1 for gregorian calendar and 0 for Julian
-- based on https://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_day_number
function p._jdn2date(jdn, gregorian)
  local f, e, g, h, year, month, day

	f = jdn + 1401
	if (gregorian or 1) >0 then
		 f = f + math.floor((math.floor((4*jdn + 274277) / 146097) * 3) / 4) - 38
	end
	e = 4*f + 3
	g = math.floor(math.fmod(e, 1461) / 4)
	h = 5*g + 2
	day   = math.floor(math.fmod (h,153) / 5) + 1
	month = math.fmod (math.floor(h/153) + 2, 12) + 1
	year  = math.floor(e/1461) - 4716 + math.floor((14 - month) / 12)
	
	-- If year is less than 1, subtract one to convert from a zero based date system to the
	-- common era system in which the year -1 (1 B.C.E) is followed by year 1 (1 C.E.).
	if year < 1 then
			year = year - 1
	end
	
	return string.format('%04i-%02i-%02i', year, month, day)
end

--[[
Tests if a string is a valid Gregorian date
Example usage:
  jdn = _valid_date("2017-02-30")

Inputs:
  ISOdate: date in YYYY-MM-DD string format  
  
Outputs:
  true or false

]]
function p._valid_date(ISOdate)
	local jdn = p._date2jdn(ISOdate, 1)
	if jdn then
		return (p._jdn2date(jdn, 1)==ISOdate)
	else
		return true -- date is not in YYYY-MM-DD format do not flag as invalid
	end
end


-- ===========================================================================
-- === Versions of the function to be called from template namespace
-- ===========================================================================
--[[
Gregorian2Julian
 
Convert a date from Gregorian to Julian calendar
 
Usage:
{{#invoke:Calendar|Gregorian2Julian|YYYY-MM-DD}}
 
Parameters:
    1: Gregorian date in YYYY-MM-DD format
Output:
	1: Julian date in YYYY-MM-DD format or error message "Error parsing input date: ....."
]]
function p.Gregorian2Julian(frame)
	local jdn = p._date2jdn(frame.args[1], 1)
	if jdn then
		return p._jdn2date(jdn, 0)
	else
		return "Error parsing input date: " .. frame.args[1]
	end
end

-----------------------------------------------------------------------------
--[[
Julian2Gregorian
 
Convert a date from Julian to Gregorian calendar
 
Usage:
{{#invoke:Calendar|Julian2Gregorian|YYYY-MM-DD}}
 
Parameters:
    1: Julian date in YYYY-MM-DD format
Output:
	1: Gregorian date in YYYY-MM-DD format or error message "Error parsing input date: ....."
]]
function p.Julian2Gregorian(frame)
	local jdn = p._date2jdn(frame.args[1], 0)
	if jdn then
		return p._jdn2date(jdn, 1)
	else
		return "Error parsing input date: " .. frame.args[1]
	end
end

-----------------------------------------------------------------------------
--[[
DayOfWeek
 
Return day of week based on gregorian date. Returned value is in English. However 
one can easily combine it with #Switch parser function [w:Help:Switch parser function]
to translate to other languages
 
Usage:
{{#invoke:Calendar|DayOfWeek|date|lang=}}
 
Parameters:
    1: date in YYYY-MM-DD format using Gregorian calendar
Output:
	1: day of the week in English or error message "Error parsing input date: ....."
]]

function p.DayOfWeek(frame)
	local jdn = p._date2jdn(frame.args[1], 1)
	local day = math.fmod(jdn, 7) + 1
	if day then
		local LUT = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }
		return LUT[day]
	else
		return "Error parsing input date: " .. frame.args[1]
	end
end

-----------------------------------------------------------------------------
--[[
date2jdn
 
Convert calendar date to  "Julian day number" (jdn) 
 
Usage:
{{#invoke:Calendar|date2jdn|date}}
 
Parameters:
    1: date in YYYY-MM-DD format (optional, if missing than today's date will be used)
	2: calendar: "1" for Gregorian calendar and "0" for Julian (optional with "1" as default)
Output:
    1: "Julian day number" as integer or number of days since Monday, January 1, 4713 BC
]]

function p.date2jdn(frame)
	return p._date2jdn(frame.args[1] or os.date('%F'), frame.args[2] or 1)
end

-----------------------------------------------------------------------------
--[[
jdn2date
 
Convert "Julian day number" (jdn) to calendar date
 
Usage:
{{#invoke:Calendar|jdn2date|jdn}}
 
Parameters:
    1: "Julian day number" as integer or number of days since Monday, January 1, 4713 BC
	2: calendar: "1" for gregorian calendar and "0" for Julian (optional with "1" as default)
Output:
    1: date in YYYY-MM-DD format 
]]
function p.jdn2date(frame)
	return p._jdn2date(frame.args[1], frame.args[2] or 1)
end

--[[
Tests if a string is a valid Gregorian date
Example usage:
	{{#invoke:Calendar|valid_date|"2017-02-30"}}

Inputs:
  ISOdate: date in YYYY-MM-DD string format  
  
Outputs:
  true or false

]]
function p.valid_date(frame)
	if p._valid_date(frame.args[1]) then
		return 'date is valid'
	else
		return 'date is not valid'
	end
end

return p