[comp.lang.pascal] Day-of-week algorithm: Formula and proof

rjchen@phoenix.Princeton.EDU (Raymond Juimong Chen) (04/15/89)

From my personal archives...

PROCEDURE Zeller(d: DATE): WEEKDAY;
(* zeller : date -> Weekday
  Pre:  InvDate(d) % date after 14-Sep-1752 in UK
  Post: true
   zeller(d) ==
	let m1 == month(d) - 2,
	    y1 == year(d)
	in let m  == if m1 < 1 then m1 + 12 else m1,
	       y2 == if m1 < 1 then y1 -  1 else y1
	   in let c == y2 truncate 100,
		  y == y2 mod 100
	      in weekdays[(((((26  m) - 2) truncate 10) +
		    (day(d) +
		     ((c truncate 4) +
		      ((5  c) +
		       ((y truncate 4) + y))))) mod 7) + 1];
What's going on?
	The first catch in this function is February --- it's an 'odd
	man out' as far as the months go, so let's redefine the start of
	year as the 1st March, then we need only worry about leap-years
	in the 'year' part of the formula.  Next:

	We want a function which gives a mapping:
	1->2, 2->5, 3->7 etc ie the number of days ''extra'' over a 28
	day month.  This is the '((26 m) -2) truncate 10' term
	to this we have to add the offset into the month so that
	when we look at it mod 7 we have a mapping to weekday.
	Oh by the way this term also gives a constant offset of 2.  This
	chap Zeller was very clever at hiding it!

	Unfortunately it isn't that simple: there is an extra day in
	each year (mod 7) so add y (the year in the centuary).  Right so
	far, but leap years?  Well *within* a centuary these are regular
	-- every 4 years, hence y truncate 4.
	Ok so why emphasise within?  well centuaries are only leap-years
	if they are divisible by 400.   We've already divided by 100 to
	get c so our term becomes c truncate 4.
	Now about other centuaries: 100 mod 7 = 2, that is to say the -2c
	in the original formula.  Catch is some programming languages
	do strange things on Modulus a negative number (Modula2 in
	particular) so let's use a positive term instead: +5c gives the
	correct result.

*)
VAR
        k, m, C, Y, temp: INTEGER;
BEGIN
  k := d.day;
  m := INTEGER(ORD(d.month)) -1;  (* Really -2, but enums start with 0 *)
(*	Which either proves that Modula-2 is innappropriate in this context, or
 *	that an enumeration is.  Unfortunately an enumeration is a requirement,
 *	for this exercise, and unlike C++ I can't chose the values of types in
 *	an enumeration in Modula-2.  There is of course also the limitation of
 *	this implementation which prevents DATE being a real obscured DATA Type
 *	(only pointers)...  Modula2 Gah!
 *)
	temp := d.year;
	IF m < 1 THEN
		m := m + 12;
		temp := temp - 1;
	END;
	C := temp DIV 100;
	Y := temp MOD 100;
	RETURN VAL(WEEKDAY,CARDINAL(((26*m - 2) DIV 10 + k + Y
		+ Y DIV 4 + C DIV 4 + 5C) MOD 7));
END Zeller;

-- 
Raymond Chen	UUCP: ...allegra!princeton!{phoenix|pucc}!rjchen
		BITNET: rjchen@phoenix.UUCP, rjchen@pucc
		ARPA: rjchen@phoenix.PRINCETON.EDU, rjchen@pucc.PRINCETON.EDU
"Say something, please!  ('Yes' would be best.)" - The Doctor