reptile7's JavaScript blog
Wednesday, April 29, 2015
 
Occam's Scriptic Chainsaw
Blog Entry #350

We return now to our deconstruction of the Java Goodies Today Calendar script. At the end of the previous post we were going through a todaycal.html <script> that writes data to the text inputs of the calform form.

#table1 { margin-left: auto; margin-right: auto; border: 4px solid black; }
#monthnameInput { text-align: center; }
...
</table></form>
<script type="text/javascript"><!--
var myDate = new Date( );
var Month = myDate.getMonth( );
document.calform.elements[1].value = myDate.getFullYear( );
document.calform.elements[4].value = Months[Month];
document.calform.elements[43].value =
"Today: " + Months[Month].substring(0, 3) + " " + myDate.getDate( ) + ", " + myDate.getFullYear( );
FillCalendar( );
// --></script>


The script concludes with a call to a FillCalendar( ) function that in conjunction with 7 other functions loads date numbers for the myDate month into the calendar: we'll give FillCalendar( ) and its friends the third degree in today's and the next posts.

What month is it, again?

The FillCalendar( ) function first declares a set of 8 variables.

function FillCalendar( ) { var Year, Month, Midx, NewYearDay, MonthStartDay; var NumDaysInMonth, i, t;

Next, the FillCalendar( ) function gets the calendar year and month from the elements[1] and elements[4] fields and assigns them to the Year and Month variables, respectively.

Year = parseFloat(document.calform.elements[1].value);
Month = document.calform.elements[4].value;


I have no idea why David uses the parseFloat( ) function to numberify the year string, which should not hold a floating-point number; I'd use parseInt( ) for this operation.

The FillCalendar( ) function subsequently maps the Month month onto its getMonth( ) index via a DetermineMonthIdx( ) function, which gets a bit of help from a Trim( ) function, and assigns the index to the Midx variable.

Midx = DetermineMonthIdx( );
if (Midx == -1) { window.alert("Can't recognize that month"); return; }


function DetermineMonthIdx( ) { var i, month, month_s, len; month = Trim(document.calform.elements[4].value); len = month.length; for (i = 0; i < 12; i++) { month_s = Months[i].substring(0, len); if (month_s.toUpperCase( ) == month.toUpperCase( )) return i; } return -1; }

function Trim(TheString) { var len; len = TheString.length; while (TheString.substring(0, 1) == " ") { // Trim left TheString = TheString.substring(1, len); len = TheString.length; } while (TheString.substring(len - 1, len) == " ") { // Trim right TheString = TheString.substring(0, len - 1); len = TheString.length; } return TheString; }

Very briefly, here's what's happening:
(1) The DetermineMonthIdx( ) function calls on the Trim( ) function to remove any leading/trailing white space in the Month string.
(2) The DetermineMonthIdx( ) function then runs through the Months[i] array in search of the Month name; when a match is found, the i index is returned to the DetermineMonthIdx( ) call.

The above code is not worth discussing in detail as it is excess baggage: we globally obtained the myDate getMonth( ) index earlier (via the var Month = myDate.getMonth( ); statement, vide supra) and there's no need to go through this rigmarole to get it again.

Even if we did need (some of) this code, let me note that:
• The PadSpaces( ) function, itself superfluous once we text-align:center; the elements[4].value, doesn't append any spaces to its TheString argument, and therefore the Trim( ) function's second while loop, which removes trailing white space from Trim( )'s own TheString argument, is unnecessary. (OK, we would have needed the second while loop if we had left the initial " January " value in place, but why would we have done that?)
• The DetermineMonthIdx( ) function's for loop can be simplified to: for (i = 0; i < Months.length; i++) if (month == Months[i]) return i;. The month_s and month strings have the same origin - that being the Months[i] array - and therefore there's no point whatsoever in harmonizing the cases of their characters via the toUpperCase( ) method.

What a mess, huh? But wait, it gets worse...

All is chaos on New Year's Day

The DetermineMonthIdx( ) action is followed by a call to a FindNewYearStartingDay( ) function

NewYearDay = FindNewYearStartingDay(Year);

that itself leverages a NumLeapYears( ) function and an IsLeapYear( ) function to determine the getDay( ) index for the first day of the Year year; the index is returned to the FindNewYearStartingDay( ) call and assigned to the NewYearDay variable.

function FindNewYearStartingDay(Year) { var LeapYears, Years, Day; LeapYears = NumLeapYears(1995, Year); if (Year >= 1995) Years = (Year - 1995) + LeapYears; else Years = (Year - 1995) - LeapYears; if (Year >= 1995) Day = Math.round(((Years / 7 - Math.floor(Years / 7)) * 7) + 0.1); else Day = Math.round(((Years / 7 - Math.ceil(Years / 7)) * 7) - 0.1); if (Year >= 1995) { if(IsLeapYear(Year)) Day--; } else Day += 7; if (Day < 0) Day = 6; if (Day > 6) Day = 0; return Day; }

function NumLeapYears(StartYear, EndYear) { var LeapYears, i; if (EndYear >= StartYear) { for (LeapYears = 0; StartYear <= EndYear; StartYear++) if (IsLeapYear(StartYear)) LeapYears++; } else { for (LeapYears = 0; EndYear <= StartYear; EndYear++) if (IsLeapYear(EndYear)) LeapYears++; } return LeapYears; }

function IsLeapYear(Year) { if (Math.round(Year / 4) == Year / 4) { if (Math.round(Year / 100) == Year / 100) { if (Math.round(Year / 400) == Year / 400) return true; else return false; } else return true; } return false; }

In going from year x to year x+1, the 1 January day of the week moves forward by one day if x is not a leap year or by two days if x is a leap year, and vice versa in going from year x to year x-1, e.g., 1 January 2015 was a Thursday, 1 January 2016 will be a Friday, and 1 January 2017 will be a Sunday. Using 1 January 1995 - a Sunday, with a 0 getDay( ) return - as a point of reference, the above code calculates the number of getDay( ) days that 1 January moves forward or backward in going from 1 January 1995 to 1 January Year and stores that number in a Years variable; Years is then normalized with respect to the 0-6 getDay( ) scale so as to give the 1 January Year getDay( ) index, which is stored in a Day variable.

The NumLeapYears( ) function counts the number of leap years in the range 1995 to Year, inclusive. However, if we are moving forward and Year is a leap year, then we'll hit 1 January Year before we hit 29 February Year; as a result, the NumLeapYears( ) LeapYears return will actually be one higher than it should be, and the FindNewYearStartingDay( ) function corrects therefor via an if (Year >=1995) { if (IsLeapYear(Year)) Day--; } clause. If we're moving backward and Year is a leap year, then we'll hit 29 February Year before we hit 1 January Year, and the LeapYears count will be correct.

For its part, the IsLeapYear( ) function returns true if Year is a leap year and false otherwise; it uses division operations and the Math.round( ) method (vis-à-vis modulo operations) to flag Years that are multiples of 4, 100, and 400.

The FindNewYearStartingDay( ) function's conversion of the Years number to the Day index

if (Year >= 1995) Day = Math.round(((Years / 7 - Math.floor(Years / 7)) * 7) + 0.1);
else Day = Math.round(((Years / 7 - Math.ceil(Years / 7)) * 7) - 0.1);


is admittedly not so straightforward.
(a) Years itself holds the getDay( ) movement as a total number of days.
(b) Years/7 gives the getDay( ) movement as a number of weeks.
(c) Subtracting the integer part of Years/7 from Years/7 gives the getDay( ) movement as a percentage of one week.
(d) Multiplying the (c) percentage by 7 gives the getDay( ) movement as a number of days for one week, which is what we want.
As to what purpose the +0.1 and -0.1 operations serve, your guess is as good as mine.

If Years is a multiple of 7 - as is true for, e.g., 2000, 1995, and 1989 - then Day is initially 0. In this case:
(i) If Year is 1995 or a post-1995 non-leap year, then the Day = 0 value is good to go.
(ii) If Year is a post-1995 leap year, then Day is decremented to -1 by the if (IsLeapYear(Year)) Day--; conditional and is brought back on scale by an if (Day < 0) Day = 6; conditional.
(iii) If Year precedes 1995, then Day is mystifyingly pushed to 7 by an else Day += 7; clause and is brought back on scale by an if (Day > 6) Day = 0; conditional.

Having said all this, I hope I don't break anyone's heart if I point out that the FindNewYearStartingDay( )/NumLeapYears( )/IsLeapYear( ) functionality can be replaced by a single line of code:

NewYearDay = new Date(Year, 0, 1).getDay( );

And there's more...

On the origin of calendars

The FindNewYearStartingDay( ) action is followed by a call to a FindMonthStartDay( ) function

MonthStartDay = FindMonthStartDay(NewYearDay, Year, Midx);

that adjusts the NewYearDay value so as to give the getDay( ) index for the first day of the myDate month; the index is returned to the FindMonthStartDay( ) call and assigned to the MonthStartDay variable.

function FindMonthStartDay(NewYearDay, Year, Month) { var MonthStartDay; AddArray = new Array(12); AddArray[0] = 0; AddArray[1] = 3; AddArray[2] = 3; AddArray[3] = 6; AddArray[4] = 1; AddArray[5] = 4; AddArray[6] = 6; AddArray[7] = 2; AddArray[8] = 5; AddArray[9] = 0; AddArray[10] = 3; AddArray[11] = 5; MonthStartDay = NewYearDay + AddArray[Month]; if (IsLeapYear(Year) && Month > 1) MonthStartDay ++; if (MonthStartDay > 6) MonthStartDay -= 7; return MonthStartDay; }

If you look at a calendar for a non-leap year, you'll see that, getDay( )-wise, 1 February and 1 March occur 3 days after 1 January, 1 April occurs 6 days after 1 January, 1 May occurs 1 day after 1 January, etc.: these getDay( ) differences are organized as an AddArray[ ] array so that we can obtain the MonthStartDay index by adding AddArray[Month] to the NewYearDay index.
• For a leap year, post-February MonthStartDays are incremented accordingly.
• If MonthStartDay goes above 6, it is brought back on scale by an if (MonthStartDay > 6) MonthStartDay -= 7; conditional.

The FindMonthStartDay( ) functionality can also be cashed in for a single line of code:

MonthStartDay = new Date(Year, Month, 1).getDay( );

We'll continue our romp through the FillCalendar( ) function in the following entry.

Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)