Doomsday Algorithm | Calculating the day of the week for any date

While there is already a much simpler method for calculating the day of the week for any date using Lua’s built in os.date function, that has its limitations such as not working with dates before January 1st 1970, plus it’s not as fun; this led me to replicating the doomsday algorithm (devised in 1973) in order to work out the day of the week for any date regardless of whether it is 1000 years into the future, or 1000 years into the past. All feedback is appreciated and if you want to check out the doomsday algorithm for yourself, James Grime explains it in layman’s terms in this video: Doomsday Algorithm

local date = "9/12/2021" --Day/Month/Year

local function formatDay(day)
	if day == 0 then
		return "Sunday"
	elseif day == 1 then
		return "Monday"
	elseif day == 2 then
		return "Tuesday"
	elseif day == 3 then
		return "Wednesday"
	elseif day == 4 then
		return "Thursday"
	elseif day == 5 then
		return "Friday"
	elseif day == 6 then
		return "Saturday"
	end
end

local function formatDate(date)
	local year = date[3]
	local month = date[2]
	local day = date[1]
	if day == "1" then
		day = "1st"
	elseif day == "2" then
		day = "2nd"
	elseif day == "3" then
		day = "3rd"
	elseif day == "21" then
		day = "21st"
	elseif day == "22" then
		day = "22nd"
	elseif day == "23" then
		day = "23rd"
	elseif day == "31" then
		day = "31st"
	else
		day = day.."th"
	end
	local months = {
		[1] = "January",
		[2] = "February",
		[3] = "March",
		[4] = "April",
		[5] = "May",
		[6] = "June",
		[7] = "July",
		[8] = "August",
		[9] = "September",
		[10] = "October",
		[11] = "November",
		[12] = "December"
	}
	month = months[tonumber(month)]
	return day.." "..month..", "..year
end

local function checkIsLeapYear(year)
	if year/4 == math.round(year/4) then
		if year/100 == math.round(year/100) then
			if year/400 == math.round(year/400) then
				return true
			else
				return false
			end
		else
			return true
		end
	else
		return false
	end
end

local function checkDate(date)
	local monthsWith30Days = {4,6,9,11}
	local day = tonumber(date[1])
	local month = tonumber(date[2])
	local year = tonumber(date[3])
	if day > 0 and day < 32 and month > 0 and month < 13 and year >= 1000 then
		if table.find(monthsWith30Days, month) then
			if day > 30 then
				return false
			else
				return true
			end
		elseif month == 2 then
			if checkIsLeapYear(year) then
				if day > 29 then
					return false
				else
					return true
				end
			else
				if day > 28 then
					return false
				else
					return true
				end
			end
		end
	else
		return false
	end
	return true
end

local function calculateCenturyDoomsday(year)
	local centuryStartingPoints = {
		[1000] = 5,
		[1100] = 3,
		[1200] = 2,
		[1300] = 0,
		[1400] = 5,
		[1500] = 3,
		[1600] = 2,
		[1700] = 0,
		[1800] = 5,
		[1900] = 3,
		[2000] = 2,
		[2100] = 0,
		[2200] = 5,
		[2300] = 3,
		[2400] = 2,
		[2500] = 0,
		[2600] = 5,
		[2700] = 3,
		[2800] = 2,
		[2900] = 0,
		[3000] = 5
	}
	year = year:sub(1,2).."00"
	year = tonumber(year)
	if centuryStartingPoints[year] then
		return centuryStartingPoints[year]
	else
		local yearsDifference = year - 3000
		if yearsDifference%400 == 0 then
			return 5
		else
			local remainder = yearsDifference%400
			if remainder == 100 then
				return 3
			elseif remainder == 200 then
				return 2
			elseif remainder == 300 then
				return 0
			end
		end
	end
end

local function calculateYearDoomsday(year, centuryDoomsday)
	local yearStartingPoints = {28,56,84}
	year = year:sub(3)
	year = tonumber(year)
	if table.find(yearStartingPoints, year) then
		return centuryDoomsday
	else
		local closestMultiple = math.floor(year/12)
		local yearsToAddOn = year - (closestMultiple*12)
		return (centuryDoomsday + yearsToAddOn + closestMultiple + math.floor(yearsToAddOn/4))%7
	end
end

local function calculateDate(doomsdayForYear, date)
	local doomsdays = {
		{
			Month = 3,
			Day = 14
		},
		{
			Month = 4,
			Day = 4
		},
		{
			Month = 5,
			Day = 9
		},
		{
			Month = 6,
			Day = 6
		},
		{
			Month = 7,
			Day = 11
		},
		{
			Month = 8,
			Day = 8
		},
		{
			Month = 9,
			Day = 5
		},
		{
			Month = 10,
			Day =10
		},
		{
			Month = 11,
			Day = 7
		},
		{
			Month = 12,
			Day = 12
		},
	}
	local year = tonumber(date[3])
	local month = tonumber(date[2]) 
	local day = tonumber(date[1])
	local daysDifference
	if checkIsLeapYear(year) then
		if month == 1 then
			if day > 4 then
				daysDifference = day - 4
				return (doomsdayForYear + daysDifference)%7
			else
				daysDifference = 4 - day
				local weekdayForDate = 7 - (7 - (doomsdayForYear - daysDifference))
				if weekdayForDate < 0 then
					local formattedDate
					repeat
						formattedDate = 7 - math.abs(weekdayForDate)
					until formattedDate > 0
					return formattedDate
				end
				return weekdayForDate
			end
		elseif month == 2 then
			daysDifference = 29 - day
			return (doomsdayForYear + daysDifference)%7
		end
	else
		if month == 1 then
			if day > 3 then
				daysDifference = day - 3
				return (doomsdayForYear + daysDifference)%7
			else
				daysDifference = 3 - day
				local weekdayForDate = 7 - (7 - (doomsdayForYear - daysDifference))
				if weekdayForDate < 0 then
					local formattedDate
					repeat
						formattedDate = 7 - math.abs(weekdayForDate)
					until formattedDate > 0
					return formattedDate
				end
				return weekdayForDate
			end
		elseif month == 2 then
			daysDifference = 28 - day
			return (doomsdayForYear + daysDifference)%7
		else
			local doomsdayForMonth
			for _, specialDate in pairs(doomsdays) do
				if specialDate.Month == month then
					doomsdayForMonth = specialDate.Day
					break
				end
			end
			if day > doomsdayForMonth then
				daysDifference = day - doomsdayForMonth
				return (doomsdayForYear + daysDifference)%7
			else
				daysDifference = doomsdayForMonth - day
				local weekdayForDate = 7 - (7 - (doomsdayForYear - daysDifference))
				if weekdayForDate < 0 then
					local formattedDate
					repeat
						formattedDate = 7 - math.abs(weekdayForDate)
					until formattedDate > 0
					return formattedDate
				end
				return weekdayForDate
			end
		end
	end
end

local function workOutDate()
	local splitDate = date:split("/")
	if not checkDate(splitDate) then
		if tonumber(splitDate[3]) < 1000 then
			warn("Cannot compute years under 1000!")
		else
			warn(date.." is not an arbitrary date!")
		end
		return
	end
	local doomsdayForCentury = calculateCenturyDoomsday(splitDate[3])
	local doomsdayForYear = calculateYearDoomsday(splitDate[3], doomsdayForCentury)
	local date = calculateDate(doomsdayForYear, splitDate)
	print(formatDate(splitDate).." is a "..formatDay(date))
end

workOutDate()
2 Likes

That’s pretty cool! We need more stuff like this :+1:.

1 Like