
//
// CogersMeetingDate.js
//
// Adrian Redgers created this. All rights reserved, no responsibility accepted. (c) 11 October 2003
// 
// Routine to display the date of Cogers meetings in the form:  "Tuesday, 11th October 2003"
//
//
//
// To use:
//    * Save this file as: CogersMeetingDate.js near the HTML that will display the date of the meeting.
//
//    * In that HTML file insert the following line between the <HEAD> </HEAD> tags:
//               <SCRIPT LANGUAGE="javascript" SRC="CogersMeetingDate.js"></SCRIPT>
//      NOTE: make sure the path is correct if CogersMeetingDate.js is in a different directory
//        e.g. if in the directory above then   "../CogersMeetingDate.js"
//      NOTE: use <SCRIPT ...></SCRIPT>  because the self-closing form:  <SCRIPT ... />  doesn't work with Netscape.
//
//    * At the point where you want to print the next meeting date for a club insert the following:
//              <script language="javascript">NextMeetingDate(DINING);</script>
//
//    * Allowed clubs are:  DINING, CITY, WESTMINSTER, WARE, SYLVANS
//
//
//
// Advanced:
//    * If a club meeting is cancelled then edit IsClubHoliday()
//
//    * If a club meeting is moved within a month then edit MoveClubMeeting() with the details
//
//    * If another special public holiday is announced then add it to the IsSpecialBank() function at the
//         bottom of this file.
//      NOTE: you may have to amend another function if that holiday is moved - e.g Spring Bank holiday 2002
//
//    * To add another club, add it to the enumerated list below, and to the CogersMeetingDate(clubName, theDate) function. 
//      NOTE Watch out for *last* xDay of the month (e.g. the last Friday) versus nth xDay (e.g the 2nd Monday)
//      NOTE watch out for the Sylvans - IsClubHoliday()
//
//    * To get the next meeting on or after a given date, (e.g. 11 October, 2003) insert the following:
//              <script language="javascript">DisplayDate(CogersMeetingDate(DINING, new Date('11 October, 2003')));</script>
//      NOTE the comma between 'October' and '2003'
//
//    * To get the *following* meeting *after* a given date insert the following:
//              <script language="javascript">DisplayDate(FollowingMeetingDate(DINING, CogersMeetingDate(DINING, '11 October, 2003') ));</script>
//
//
//


//
// Enumerated list of clubs
//
SYLVANS = 1;
WESTMINSTER = 2;
CITY = 3;
DINING = 4;
WARE = 5;
UNIVERSITY = 6;
CAMBRIDGE = 7

// 
//
// Useful lists: days [0=Sunday, 6=Saturday],  months [0=January, 11=December]
//   Would like to put the month-days here, but February cocks it up if we use it for other years.
//
_days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
_months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
//
// Get today's date
//
today = new Date();                                 // Set to today by default


//
// Get the date of the next meeting for the given club - today if before 8pm
//
function NextCogersMeetingDate(theClub)
{
    return( DisplayDate(CogersMeetingDate(theClub, new Date(today))) );
}


//
// Get the following month's meeting date *after* the given date.
// If the given date is already a meeting date then skip to the next month.
// NOTE: this doesn't display the date - if you want to diplay it use the DisplayDate() as shown above.
//
function FollowingMeetingDate(theClub, theDate)
{
    theDate.setHours(21);                           // Force a skip to the next meeting
    return(CogersMeetingDate(theClub, theDate));
}


//
// Get the date of the meeting for the given club  *on-or-after* the given date
// NOTE: this doesn't display the date - if you want to diplay it use the DisplayDate() as shown above.
//
function CogersMeetingDate(theClub, theDate)
{
    var month = theDate.getMonth()+1;               // Set months from 1-12  to make it easier for me to understand
    var year = theDate.getFullYear();               
    var monthYearVec = new Array(2);                // vec[0] = month   ,  vec[1] = year
    monthYearVec[0] = month;
    monthYearVec[1] = year;

    var returnDateVec = new Array(3);               // Return a vector of the date, plus moved-flag, plus moved from date
                                                    //   vec[0] = actual date of meeting, 
                                                    //   vec[1] = meeting-was-moved flag : true or false
                                                    //   vec[3] = moved-from date
 
    //
    // If there's a meeting today we want to have it as the *next* meeting
    //   but not if we have just missed it (time > 8pm)      
    //
    // Reset the time of the given date to midnight
    //   to ensure a *calculated* meeting-date of 'today' is at least as big as a
    //   *given* date of 'today'
    //
    if(theDate.getHours() < 20)                
    {                                               
        theDate.setMinutes(0);                      
        theDate.setHours(0);                        
        theDate.setSeconds(0);                      
        theDate.setMilliseconds(0);                 
    }
    var saveDate = new Date(theDate);               // Save the given date to check 'on-or-after' it       

    var nth = 0;                                    // nth xDay of the month, e.g. "2nd Tuesday of the month" 
    var xDay = 0;                                   // 0=Sunday  <= xDay <=   6=Saturday
    var last = false;                               // Not  *nth*  but  *last*  xDay of the month  e.g. "last Friday of the month"

    if(DINING == theClub)
    {
        xDay = 5;                                   // Cogers Dining club meets on the *last* Friday of the month
        last = true;
    } else if(WARE == theClub)
    {
        nth = 3;                                    // Ware Cogers meets on 3rd Tuesday of the month
        xDay = 2;
    } else if(CITY == theClub)
    {
        nth = 2;                                    // City of London Cogers meets on 2nd Monday of the month
        xDay = 1                        
    } else if(WESTMINSTER == theClub)
    {
        nth = 4;                                    // Westiminster Cogers meets on 4th Wednesday of the Month
        xDay = 3;
    } else if(SYLVANS == theClub)
    {
        nth = 1;                                    // Sylvans meet on 1st Monday of the Month
        xDay = 1;
    } else if(UNIVERSITY == theClub)
    {
        nth = 3;                                    // UoL Cogers meets on 3rd Wednesday of the Month
        xDay = 3;
    }

    //
    // Calculate the date of the next meeting from the given date.
    // If the calculated date is earlier than the given date, or if the calculated day is a holiday
    //   then skip on to the following month.
    //
    do                                              // Step past holidays and dates before the given date
    {
        if(last)
            theDate = LastXday(xDay, month, year);  
        else
            theDate = NthXday(nth, xDay, month, year);        

        returnDateVec[1] = false;                   // Initially guess meeting was not moved
        returnDateVec[2] = new Date(theDate);       // Initial date of meeting
        theDate = MoveClubMeeting(theClub, theDate); 
                                                    // Move the meeting date if necessary
        returnDateVec[0] = new Date(theDate);       // Actual meeting date - was it moved?
        if(returnDateVec[0].getDay() != returnDateVec[2].getDay())
        {
            returnDateVec[1] = true;                // yes, the meeting was moved
        }        
        monthYearVec[0] = month;
        monthYearVec[1] = year;
        monthYearVec = NextMonth(monthYearVec);     // Increment to the next month (and year if necessary)
        month = monthYearVec[0];
        year = monthYearVec[1];
 
    } while( (theDate < saveDate) || IsHoliday(theDate) || IsClubHoliday(theClub, theDate));
                                                 
    return(returnDateVec);
}


//
// Return true if the club is having a break at this time 
//
function IsClubHoliday(theClub, theDate)
{

// The Sylvans meet every month now
//    if(SYLVANS == theClub)
//    {
//        var month = theDate.getMonth()+1;           // Set month to [1, 12] for clarity.
//        if((3 < month) && (10 > month))             // The Sylvans only meet from October to March
//            return(true);
//    }    

    return(false);
}


//
// Move the date of a meeting within a month if necessary
//
function MoveClubMeeting(theClub, theDate)
{
    //
    // Sylvans meeting on 17th November 2003 was moved to 18th November
    //
    if(SYLVANS == theClub)
    {
        if( (2003 == theDate.getFullYear()) && (11 == theDate.getMonth()+1)         // I prefer my months numbered 1-12, not 0-11
            && (17 == theDate.getDate()) )           
        {
            theDate.setDate(18);
        }
    }    

    //
    // City meeting on 12th April 2004 was moved to 13th April 
    //
    if(CITY == theClub)
    {
        if( (2004 == theDate.getFullYear()) && (4 == theDate.getMonth()+1)         // I prefer my months numbered 1-12, not 0-11
            && (12 == theDate.getDate()) )           
        {
            theDate.setDate(13);
        }
    }    

    return(theDate);
}


//
// Display the given date, e.g.  Thursday, 9th October 2003
//
function DisplayDate(dateVec)
{
    var theDate = dateVec[0];
    
    document.write( _days[theDate.getDay()] + ', ' + theDate.getDate() +OrdinalSuffix(theDate.getDate())+ ' '
        + _months[theDate.getMonth()] + ' ' + theDate.getFullYear());
    if(dateVec[1])
    {   
        theDate = dateVec[2];
        document.write( '&nbsp;&nbsp;(moved from ' + _days[theDate.getDay()] + ', ' + theDate.getDate() +OrdinalSuffix(theDate.getDate())+ ' '
            + _months[theDate.getMonth()] + ')');
    }
    return(dateVec[0]);
}


//
// Returns 'nd', 'rd', or 'th' for the ordinal of the given number as in:
// 1st, 2nd, 3rd, 4th.... 20th, 21st etc
//
function OrdinalSuffix(num)
{
    var numMod = num%100;                           // Get it down to 0 - 99
    
    if((3 < numMod) && (21 > numMod))               // ...but we say '11th', '12th' - not '11st', '12nd' !!
        return('th');

    numMod = numMod%10;                             // Get it down to 0 - 9
    
    if(1 == numMod)
        return('st');

    if(2 == numMod)
        return('nd');

    if(3 == numMod)
        return('rd');

    return('th');                                   // Default
}


//
// Add 1 to the month [1 - 12] , if necessary add 1 to the year
//
function NextMonth(monthYearVec)
{
    monthYearVec[0]++;
    if(12 < monthYearVec[0])
    {
        monthYearVec[0] = 1;
        monthYearVec[1]++;
    }
    return(monthYearVec);
}


//
// Return the last Xday (e.g. the last Friday) of the month
//
function LastXday(xDay, month, year)
{
    daysInMonth = [31,28,31,30,31,30,31,31,30,31,30,31];
    //
    //    Add one to February for leap years,
    //    Use the 400-year rule in case Cogers and Javascript still exist in 2100 AD...I think breaks in 5000 AD
    //
    // Shame to have make this fix here and not make it at the top with daysInMonth[] as a global array,
    //   but in general we don't know what year it is (although today.getFullYear() is a very good guess).
    //
    if( !(year%4) && (year%100 || !(year%400)) )
        daysInMonth[1] = 29;

    month--;                                        // Reset month to range  [0, 11] for Javascript functions.

    //
    // Get the *last* day of this month 
    //
    var meetingDate = new Date(daysInMonth[month]+" "+ _months[month] +", "+year);
    var dayOfWeek = meetingDate.getDay();           // 0 = Sunday , 1 = Monday , ...

    //
    // calculate the last xDay
    //
    if(dayOfWeek < xDay)
        meetingDate.setDate(daysInMonth[month]-dayOfWeek-7+xDay);
    else
        meetingDate.setDate(daysInMonth[month]-dayOfWeek+xDay);
    return(meetingDate);
}


//
// Return the date for the Nth Xday (e.g. the 4th Tuesday) of the given month and year
//             1 <= nth <= 4  
//   0 (=Sunday) <= xDay <= 6 (=Saturday)
//             1 <= month  <= 12
//
function NthXday(nth, xday, month, year)
{
    var dom = 1;                                    // Day of month
    var meetingDate = new Date(dom+" "+_months[--month]+", "+year); 
                                                    // Reset month to range  [0, 11]  for Javascript functions
    //
    // Calculate the nth xDay and reset the meetingDate
    //
    var daysTillXday = 1 + xday - meetingDate.getDay();
    if(daysTillXday > 0)
    {
        dom = daysTillXday+(nth-1)*7;
    } else
    {
        dom = daysTillXday+nth*7;
    }
    meetingDate.setDate(dom);

    return(meetingDate);
}


//
// Return true if we would not have a Cogers meeting on this day due to public holidays or the holiday season
//   Ref: Department of Trade and Industry Bank holidays FAQ: http://www.dti.gov.uk/er/bankhis.htm
//
function IsHoliday(theDate)
{
    var dow = theDate.getDay();                     // Day of week 0=Sunday, 1=Monday...
    var dom = theDate.getDate();                    // Day of the month
    var month = theDate.getMonth()+1;               // Set months from 1-12  to make it easier for me to understand
    var year = theDate.getFullYear();               
    
    if(IsChristmas(dow, dom, month, year,theDate))  // Anything after 23rd December, sometimes even 23rd itself
        return(true);

    if(IsNewYear(dow, dom, month, year,theDate))    // Anything from 1st Jan until New Year's Day bank holiday
        return(true);

    if(IsEaster(dow, dom, month, year,theDate))     // The whole Easter weekend
        return(true);

    if(IsMayDay(dow, dom, month, year,theDate))     // First Monday in May
        return(true);
        
    if(IsSpringBank(dow, dom, month, year,theDate)) // Last Monday in May
        return(true);

    if(IsAugustBank(dow, dom, month, year,theDate)) // Last Friday in August 
        return(true);

    if(IsSpecialBank(dow, dom, month, year,theDate))
        return(true);                               // Special Bank holidays - e.g. Millenium Day, 60th Jubilee etc

    return false;                                   // Default - not a holiday
}


//
// Christmas 
// No meetings between Christmas eve and end of December
//
function IsChristmas(dow, dom, month, year, theDate)        
{
    if((12 == month) && (dom > 23))                 // There won't be any Cogers meetings on Christmas eve.
        return(true);
    //
    // Policy regarding the 23rd December?
    //   No meeting unless its a Tuesday, Wednesday or Thirsday
    //   (not Fri, Sat or Sun, nor Mon)
    //
    if((23 == dom) && ((2 > dow) || (4 < dow)))
        return(true);
        
    return false;
}


//
// New Year holiday season
//
function IsNewYear(dow, dom, month, year, theDate)        
{
    if((month != 1) || (3 < dom))                   // Ok if not close to the the beginning of January
        return(false);
            
    var nyd = new Date('1 January, '+ year);
    var dow_nyd = nyd.getDay();       
    if((6== dow_nyd) || (0 == dow_nyd))             // If NYD falls on Saturday or Sunday then no meeting for dom=1,2,3 
        return(true);
    return false;
}


//
// Return 'true' if theDate is between Good-Friday and Easter Monday inclusive.
//    1 <= month <= 12
//    0 = Sunday  <= dow <= 6 = Saturday
//
function IsEaster(dow, dom, month, year, theDate)        
{
    //
    // Assuming Easter Monday can never be later than 31st April, and Good Friday is never before 1st March
    //   So can't be Easter if theDate not in March or April
    //   Nor a holiday if day-of-week is Tuesday, Wednesday or Thursday.
    //
    if((month != 3) && (month != 4) || ((1 < dow) && (dow < 5)))                
        return(false);
        
    //
    // Algorithm found at: http://forums.wolfram.com/mathgroup/archive/2003/Apr/msg00336.html
    // Uses Javascript Math library.
    // 
    var a = year%19;
    var b = Math.floor(year/100);
    var c = year%100;
    var d = Math.floor(b/4);
    var e = b%4;
    var g = Math.floor((8*b+13)/25);
    var h = (19*a+b-d-g+15)%30;
    var mu = Math.floor((a+11*h)/319);
    var i = Math.floor(c/4);
    var k = c%4;
    var lambda = (2*e+2*i-k-h+mu+32)%7;
    var easterMonth = Math.floor((h-mu+lambda+90)/25);  
                                                    // March (3) or April (4)
    var domSunday = (h-mu+lambda+easterMonth+19)%32;
                                                    // day of month of Easter Sunday
    //
    // Now check theDate is not between beginning of Good Friday and end of Easter Monday:
    //   calc number of days since beginning of March for theDate and EasterSunday, and then check difference
    //
    var eDays = domSunday + (4 == easterMonth ? 31 : 0);
    var tDays = dom + (4 == month ? 31 : 0);
    
    if((tDays < eDays -2) || (eDays + 1 < tDays))
        return(false);
        
    return(true);
}


//
// Is it the May Day bank holiday (first Monday in May)?
//
function IsMayDay(dow, dom, month, year, theDate)
{
    if(month != 5)                                  // May day bank holiday is in May...
        return(false);

    var mayDay = NthXday(1, 1, 5, year);
    
    if(mayDay.getDate() == dom)
        return(true);
    return(false);
}


//
// Is it the August bank holiday (last Monday in August)?
//
function IsAugustBank(dow, dom, month, year, theDate)
{
    if(month != 8)                                  // August bank holiday is in August...
        return(false);

    var augustBank = LastXday(1, 8, year);
    
    if(augustBank.getDate() == dom)
        return(true);
    return(false);
}


//
// Is it the Spring bank holiday (last Monday in May)?
//
function IsSpringBank(dow, dom, month, year, theDate)
{
    //    
    // In 2002 the Spring Bank holiday was moved from Monday 27th May to Tuesday 4th June
    //
    if(2002 == year)
    {
        if((4 == dom) && (6 == month))
            return(true);
        else
            return(false);        
    }

    if(month != 5)                                  // Otherwise, Spring bank holiday is in May...
        return(false);

    var springBank = LastXday(1, 5, year);
    
    if(springBank.getDate() == dom)
        return(true);
    return(false);
}


//
// Is it a special bank holiday?
// Add in others as they occur - watch out if holidays have moved e.g. Spring Bank holiday 2002 was moved
//
function IsSpecialBank(dow, dom, month, year, theDate)
{
    if((1999 == year) && (12 == month) && (31 == dom)) 
        return(true);                               // Millenium holiday, Friday 31st December, 1999 

    if((2002 == year) && (6 == month) && (3 == dom))
        return(true);                               // Queen's Golden Jubilee 2002 Monday 3rd June

    if((2012 == year) && (6 == month) && (5 == dom))
        return(true);                               // Queen's Diamond Jubilee 2012 Monday 5th June (Gawd bless 'er Maj)

    return(false);
}


