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; }
<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.

Sunday, April 19, 2015
Further Adventures in Calendarland
Blog Entry #349

Next up in the Java Goodies Calendars, Clocks, and Calculators sector is a "Today Calendar" script that creates a calendar for the current month or another month of your choosing. The Today Calendar script was authored by David DeLong in 1998.

Unlike the Calendar and Datebook script we've been discussing over the last several entries, the Today Calendar script
(a) is an interactive script that provides controls for changing the calendar month and year and
(b) cleanly separates its HTML and JavaScript.
The script's JavaScript includes no fewer than 11 functions - that should keep us off the streets for a little while, yes?

The script gets the calendar year via the getYear( ) method and duly corrects therefor by adding 1900 to the getYear( ) return. However, for IE 4-8 the getYear( ) and getFullYear( ) returns are equal for years outside the 1900-1999 range and consequently at press time the script creates an April 3915 calendar for those browsers.

Joe provides a functioning script demo at the aforelinked todaycal.html page. The script itself may be accessed here.

Structural overview

The script calendar is laid out via a

<table border="0" cellpadding="0" cellspacing="0"> ... </table>

table that I'll call the table1 table. The table1 table itself sits in a

<table border="1" bgcolor="#000000" cellpadding="1" cellspacing="1"><tr><td align="center"> ... </td></tr></table>

table, which I'll call the table0 table; the table0 table effectively adds a 4px border to the table1 table but otherwise serves no purpose and can be thrown out. The table0 and table1 tables are horizontally centered on the page via a <center> element.

The table1 table contains 9 rows: the first, second, and ninth rows each contain 3 cells whereas the intervening rows each contain 7 cells. The table is commingled with a <form name="calform"> ... </form> form that contains 44 <input> controls, specifically 40 text inputs and 4 push buttons.
• The calform <form> is the only element in the code with an identifier; however, the Strict and Transitional DTDs both say that the name attribute is required for all <input> types except submit buttons and reset buttons.
• The calform form is placed just inside the table1 table in violation of the table element's (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+) content model.

The chrome

The table's first and second rows contain small text inputs

<th cellpadding="0" cellspacing="0" bgcolor="#aaaaaa" colspan="5" align="center"><input size="4" type="text" value="1993"></th>
<th bgcolor="#aaaaaa" colspan="5" align="center"><input size="9" type="text" value=" January "></th>
<!-- AFAIK, the #aaaaaa color doesn't have a name. -->

to which we will write the calendar year and month, respectively. The year and month inputs are placed in <th> cells although I would put them in <td> cells as they display heading data vis-à-vis actual headers. FYI: The cellpadding and cellspacing attributes are only valid for the table element.

The year input is flanked by a button that when clicked loads the month-input month for the previous year into the calendar and a button that when clicked loads the month-input month for the following year into the calendar; for example, if the current month were January 1993, then the button would take the user to a January 1992 calendar and the button would take the user to a January 1994 calendar. Similarly, the month input is flanked by and buttons that when clicked respectively load the previous and following months for the year-input year into the calendar.

The viewport

The third row holds a S, M, T, W, T, F, S set of headers.

<th bgcolor="#ffcccc">S</td>
<th bgcolor="#ccccff">M</td>
<th bgcolor="#ccccff">T</td>
<th bgcolor="#ccccff">S</td>
<!-- The #ffcccc and #ccccff colors don't have names either. -->

In the source the above cells have a <th>/</td> tag mismatch but on the page the headers are bolded as you would expect for <th> cells.


All of the cells in the fourth, fifth, sixth, seventh, and eighth rows and the first two cells in the ninth row hold very small text inputs

<td bgcolor="#ffcccc"><input type="text" size="2"></td>
<td bgcolor="#ccccff"><input type="text" size="2"></td>
<td bgcolor="#ccccff"><input type="text" size="2"></td>

to which we'll write the calendar's date numbers as appropriate, e.g.:

We obviously can't put memos in these things, but with 37 of 'em on hand we at least won't have any 'overflowing month' problems.

Depending on what browser you're using, you may or may not see any bgcolor below the header row because the table cellpadding is set to 0.

Finally, the third cell in the ninth row holds a standard-sized text input

<td bgcolor="#aaaaaa" colspan="5"><input type="text"></td>

to which we'll write a Today: Apr 17, 2015-type string.

Style note:
We can much more conveniently apply the #ffcccc and #ccccff background colors to the calendar columns via the HTML col element.

#sunday { background-color: #ffcccc; }
#notsunday { background-color: #ccccff; }

<!-- With Safari, it is necessary to put the col elements right after the <table> start-tag; other browsers are less picky. -->
<table id="table1" cellpadding="" cellspacing="">
<col id="sunday"><col id="notsunday" span="6">
<!-- Third row: -->

JavaScript intro

The todaycal.html JavaScript first defines a Months array of month names.

<!-- In the document head: -->
<script language="JavaScript"><!--
Months = new Array(12);
Months[0] = "January";
Months[1] = "February";
Months[11] = "December";

/* The rest of this script comprises the 11 functions mentioned in the post intro. */
// --></script>

It then creates a myDate Date object for today's date and gets myDate's month index.

<!-- In the document body, after the centering <center> element: -->
<script language="JavaScript"><!--
myDate = new Date( );
var Month = parseInt(myDate.getMonth( ));
/* Do we need to parseInt( ) the month index? Nope, the getMonth( ) return has a number type. */

Subsequently, the current year is written to the size="4" input in the first row.

document.calform.elements[1].value = 1900 + myDate.getYear( );
/* Of course, this statement should be:
document.calform.elements[1].value = myDate.getFullYear( ); */

Next, the name of the current month (Months[Month]) is loaded into the size="9" input in the second row; 0-3 space characters are prepended to the name by a PadSpaces( ) function in order to (approximately) horizontally center the name in the input - January and February and October and November and December are indented by a space, March and April and August are indented by two spaces, May and June and July get three spaces, September doesn't get any spaces at all.

function PadSpaces(TheString) { var Spaces = " "; // That's 13 spaces, folks. len = Math.round((9 - TheString.length) / 2); return Spaces.substring(0, len) + TheString; }

document.calform.elements[4].value = PadSpaces(Months[Month]);

The PadSpaces( ) function is today not necessary as modern browsers will apply a text-align:center; styling to an input value (with IE 4.5 and Communicator 4.61 on my computer, text-align:center; has no effect on the elements[4].value, i.e., the value is left-justified).

After that, a "Today: " string, the first three letters of Months[Month], a space, the myDate date number, a ", " string, and the current year are concatenated and the resulting string is written to the third input in the ninth row.

document.calform.elements[43].value = "Today: " + Months[Month].substring(0, 3) + " " + myDate.getDate( ) + ", " + (1900 + myDate.getYear( ));

Our next task is to load date numbers into the remaining inputs via the script's FillCalendar( ) function, which we'll take on in the following entry.

Tuesday, April 07, 2015
Calendar and Datebook 2.0
Blog Entry #348

We are at present working through the if...else if...else construct that fills in the date cells of the display month for the calendar coded by the Java Goodies Calendar and Datebook script. We discussed the construct's if clause in the Leading up to today section of the previous post; here's the calendar we would have so far if today were 11 April 2015:

April 2015
Dentist Appointment
Take puppies to the vet
Blah blah blah
Blah blah blah
Blah blah blah

Now, and look into the future

The construct's else if clause

else if (this.m_myDate.getDate( ) == this.m_now.getDate( ) && this.m_myDate.getMonth( ) == this.m_now.getMonth( )) document.writeln("<td valign=\"top\" width=\"14%\" bgcolor=\"#" + this.m_strNow + "\"><b>" + "<font face=\"" + this.font + "\" size=\"" + this.fontSize + "\">" + this.m_myDate.getDate( ) + "</b><br>" + this.getText(this.m_myDate.getDate( )) + "</font></td>");

handles the cell for the this.m_now date of the this.m_now month regardless of year; if today were 11 April 2015, it would apply to 11 April in 2014 or 2015 or 2016 or any other year. The else if cell is given a this.m_strNow (lightcoral) background color but is otherwise processed à la the this.m_strPast cells.

The construct's else clause

else document.writeln("<td valign=\"top\" width=\"14%\" bgcolor=\"#" + this.m_strFutr + "\"><b>" + "<font face=\"" + this.font + "\" size=\"" + this.fontSize + "\">" + this.m_myDate.getDate( ) + "</b><br>" + this.getText(this.m_myDate.getDate( )) + "</font></td>");

covers all other date cell possibilities, more specifically, it is operative for
(a) the this.m_now date and dates that follow the this.m_now date for months prior to the this.m_now month and
(b) dates that follow the this.m_now date for the this.m_now month and
(c) all dates for months that follow the this.m_now month
regardless of year; for example, it would apply to 11 March through 31 March, 12 April through 30 April, and 1 May through 31 May if today were 11 April. The else cells are given a this.m_strFutr (wheat) background color but are otherwise processed à la the this.m_strPast and this.m_strNow cells.

Curtain call

When the for (j = 0; j < 5; j++) { ... } loop has run its course and the calTable table is complete, the ampCalendar_Display( ) function writes out a closing credit:

document.writeln("<font size=\"1\" face=\"Verdana, Arial, Helvetica\">" + "Calendar by " + "<a href=\"\">" + "ampCal</a>, Copyright ©, 1998, Andrew M. Pierce</font>"); }

Calendar by ampCal, Copyright ©, 1998, Andrew M. Pierce

• A size='1' font element attribute maps onto a font-size:x-small; style declaration.
• As was noted in Blog Entry #345, the resource is no longer available.
• The copyright notice contains a literal © character, which should be encoded as \u00A9.

Overflowing cases

The for (j = 0; j < 5; j++) { ... } loop accommodates most months but would cut off
(1) the last Sunday of a 31-day month that begins on a Friday (e.g., May 2015),
(2-3) the last Sunday and last Monday of a 31-day month that begins on a Saturday (e.g., August 2015), and
(4) the last Sunday of a 30-day month that begins on a Saturday (e.g., November 2014).
The easy way to code a 6th data row for these days is to bump up the loop's j endpoint to 6, but we can do better than that.

I thought back to what Joe Burns said about the differing applications of for and while loops in HTML Goodies' JavaScript Primers #24:
In general, you use for loops when you know how many times you want to perform a loop. Use while loops when you are not sure how many times you want to perform a loop.
So perhaps we should be using a while loop to determine the number of rows in the table. As regards the while condition, it occurred to me that the x = 0, 7, 14, ... cell counter could be repurposed as a date counter that
(i) tracks the this.m_myDate.getDate( ) return but
(ii) keeps incrementing (is not reset to 1) when this.m_myDate transitions from the display month to the following month and
(iii) green-lights the creation of a 6th data row if it is still equal to this.m_myDate.getDate( ) at the end of the 5th data row.

var x = this.m_myDate.getDate( ); while (x == this.m_myDate.getDate( )) { document.write("<tr>"); for (i = 0; i < 7; i++) { ... this.m_myDate.setDate(this.m_myDate.getDate( ) + 1); x++; ... } document.write("<\/tr>"); }

As for how this approach works in practice, you may test it yourself via the demo at the end of the post.

Cutting out the memo middlemen

If we give ordinalized cal1, cal2, cal3, ... ids to the display month's date cells, then we can directly append memos to the innerHTML of those cells without having to respectively store and retrieve them via the ampCalendar_setItem( ) and ampCalendar_getText( ) functions, e.g.:

if (this.m_myDate.getDate( ) < this.m_now.getDate( ) && this.m_myDate.getMonth( ) <= this.m_now.getMonth( )) document.write("<td class='preDates' id='cal" + this.m_myDate.getDate( ) + "'><span>" + this.m_myDate.getDate( ) + "<\/span><br><\/td>");

/* In the script1 script in the example.html document: */
ampCal.display( );
document.getElementById("cal3").innerHTML += "Dentist Appointment";
document.getElementById("cal7").innerHTML += "Take puppies to the vet";


The original Calendar and Datebook script isn't interactive in any way, so I thought it would be a good idea to craft a demo therefor that allows the user to create calendars for month <option>s of a <select> menu.

• Rather than 'outputting as we go' via a bunch of document.write( ) commands, the demo assembles the calTable table in one big calString string and then writes calString to the innerHTML of a calDiv div.

• The ampCal object has been 'put out to pasture' because a majority of its properties are either outsourceable to a style sheet (the cell background-colors, the font-size and font-family data) or superfluous (the days per month stuff, the memo storage stuff, the display method if we're going to put the whole shebang in one document) and also because doing so simplifies the code a bit; re the remaining properties, the month and <caption> data are built into the <select>/<option> menu, the year year is prechosen for the user, and the m_now and m_myDate dates are stored in freestanding variables.

Powered by Blogger

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