Sample Source Code
Scheduler: recurrences.js

About the Program

Sharecare is an online application for behavioral healthcare programs and providers. It includes access, clinical, fiscal, and reporting subsystems.

The Scheduler component keeps track of todo items, availability, scheduled events, and clinical appointments. Its functionality is similar to the Outlook calendar, but with extended availability and overlap definitions, more complex recurrence patterns, and both clinical and fiscal validations.

About the Code

This file contains much of the logic for rendering recurrences.

Some of the methods called aren't part of the default JavaScript classes (such as "Date.after( Date )"). These were added to the base classes with function signatures which exactly match those in Java. The code in this file, after an automated transformation, will be reused in a Java program which will extend the rendering of recurrences further into the future.

The Code

// Common JavaScript routines that help with recurrences.

// Methods:
//      RecurrencePattern( recur_unit, recur_freq, offset_unit, offset_count, start_date, end_date )
//      RenderRecurrenceIntoDateList( recurrence_obj )
//      BuildRecurrenceDescription( recurrence_obj )

// Recurrences come in four flavors (the 'recur_unit'):
// 'day':
//     Recurs every 'recur_freq' days.
// 'weekday':
//     Recurs every 'recur_freq' weekdays (skipping over weekends).
// 'week':
//     Recurs every 'recur_freq' weeks on a particular day of the week
//     (given by 'offset_unit').
// 'month':
//     Recurs every 'recur_freq' months on a particular day.
//     'offset_freq' tells whether to count from the beginning or the end of the month.
//     'offset_unit' gives the number of units in that direction; it can be 'day' or 'weekday',
//     or any named day of the week (as with weekly recurrences).

// C-style "#ifndef/#define" code to ensure this isn't loaded twice
if ( ! document.LOADED_RECURRENCES ) {
document.LOADED_RECURRENCES = true;

// Constructor for the RecurrencePattern object.
function RecurrencePattern( recur_unit, recur_freq, offset_unit, offset_count, start_date, end_date )
{
    this.recur_unit = recur_unit;
    this.recur_freq = recur_freq;
    this.offset_unit = offset_unit;
    this.offset_count = offset_count;
    this.start_date = start_date;
    this.end_date = end_date;
}

// Return the list of dates for the given recurrence pattern.
// Note: some of the syntax (if/else instead of switch, double quotes)
// looks quaint, but was chosen for maximal ease of portability to Java.
function RenderRecurrenceIntoDateList( recurrence_obj, today_date )
{
    var next_date, next_month;
    var move_by, moved_by;
    var direction
    var date_list = "";
    var r = recurrence_obj;

    // based on the recurrence unit...
    if ( r.recur_unit.equals( "day" ) )
    {
        // start with the specified day
        next_date = r.start_date;

        // for all possible dates...
        while ( ! next_date.after( r.end_date ) )
        {
            // add this date (if in the future)
            if ( ! today_date.after( next_date ) )
                date_list = ListAppend( date_list, next_date.toCommonString() );

            // advance N days
            next_date.add( "day", r.recur_freq );
        }

    }
    else if ( r.recur_unit.equals( "weekday" ) )
    {
        // start with the first weekday on or after the start
        next_date = r.start_date;
        while ( ! next_date.isWeekday() )
            next_date.add( "day", 1 );

        // for all possible dates...
        while ( ! next_date.after( r.end_date ) )
        {
            // add this date (if in the future)
            if ( ! today_date.after( next_date ) )
                date_list = ListAppend( date_list, next_date.toCommonString() );

            // advance N weekdays
            moved_by = 0;
            while ( moved_by < r.recur_freq )
            {
                next_date.add( "day", 1 );
                // skip over any weekend days
                while ( ! next_date.isWeekday() )
                    next_date.add( "day", 1 );
                moved_by++;
            }
        }

    }
    else if ( r.recur_unit.equals( "week" ) )
    {
        // start with the first XXXday on or after the start
        next_date = r.start_date;
        while ( ! r.offset_unit.equals( next_date.dayOfWeekAsString( ) ) )
            next_date.add( "day", 1 );

        // for all possible dates...
        while ( ! next_date.after( r.end_date ) )
        {
            // add this date (if in the future)
            if ( ! today_date.after( next_date ) )
                date_list = ListAppend( date_list, next_date.toCommonString() );

            // advance N weeks
            next_date.add( "week", r.recur_freq );
        }

    }
    else if ( r.recur_unit.equals( "month" ) )
    {
        // separate the direction from the units
        direction = 1
        if ( r.offset_count > 0 )
        {
            direction = 1
            move_by = r.offset_count - 1;
        }
        else
        {
            direction = -1
            move_by = -r.offset_count - 1;
        }

        // start with the first day of the starting month
        next_month = new Date( r.start_date );
        next_month.firstOfTheMonth();

        // for all possible dates...
        while ( ! next_month.after( r.end_date ) )
        {
            // start at the appropriate end of the month
            next_date = new Date( next_month );
            if ( direction > 0 )
                ; // beginning of the month is just fine
            else
            {
                // start at the end of the month instead
                next_date.lastOfTheMonth();
            }

            // apply the appropriate offset
            if ( r.offset_unit.equals( "day" ) )
            {
                next_date.add( "day", direction * move_by );
            }
            else if ( r.offset_unit.equals( "weekday" ) )
            {
                // start at the first weekday
                while ( ! next_date.isWeekday() )
                    next_date.add( "day", direction );

                // move the appropriate number of weekdays
                moved_by = 0;
                while ( moved_by < move_by )
                {
                    next_date.add( "day", direction );
                    // skip over any weekend days
                    while ( ! next_date.isWeekday() )
                        next_date.add( "day", direction );
                    moved_by++;
                }
            }
            else // it's a day of the week
            {
                // move to the appropriate day
                while ( ! r.offset_unit.equals( next_date.dayOfWeekAsString() ) )
                    next_date.add( "day", direction );

                // then shift by N weeks
                next_date.add( "week", direction * move_by );
            }

            // if this date is still in this month, and in range, and not in the past, add this date
            if ( next_date.getMonth() == next_month.getMonth() )
                if ( ! ( next_date.before( r.start_date ) || next_date.after( r.end_date ) ) )
                    if ( ! today_date.after( next_date ) )
                        date_list = ListAppend( date_list, next_date.toCommonString() );

            // advance N months
            next_month.add( "month", r.recur_freq );
        }
    }

    return date_list;
}

// Return a description of the given recurrence pattern.
function BuildRecurrenceDescription( recurrence_obj )
{
    var desc;
    var r = recurrence_obj;

    switch ( r.recur_unit )
    {
        case 'day':
            desc = NumberToFrequency( r.recur_freq ) + ' day';
            break;

        case 'weekday':
            desc = NumberToFrequency( r.recur_freq ) + ' weekday';
            break;

        case 'week':
            desc = NumberToFrequency( r.recur_freq ) + ' ' + r.offset_unit;
            break;

        case 'month':
            // number of units into the month
            desc = 'the ' + NumberToOrdinal( Math.abs( r.offset_count ) ) + ' ' + r.offset_unit;

            // offset sign
            desc += ' from the ' + ( ( r.offset_count < 0 ) ? 'end' : 'beginning' ) + ' of';

            // month frequency
            desc += ' ' + NumberToFrequency( r.recur_freq ) + ' month';
            break;

        default:
            alert( 'Coding error: unknown recurrence unit ' + r.recur_unit + '!' );
            return;
    }

    // date range (possibly open-ended)
    if ( r.end_date != '' )
        desc += ', between ' + r.start_date.toCommonString() + ' and ' + r.end_date.toCommonString();
    else
        desc += ', from ' + r.start_date.toCommonString() + ' on';

    return desc;
}

// Return a string which is the list with the new value appended.
// Compatibility method: signature is equivalent to Cold Fusion.
function ListAppend( list, value, delimiters )
{
    // default delimiter is a comma
    if ( ! delimiters )
        delimiters = ',';
    // the first delimiter specified will be used
    if ( delimiters.length > 1 )
        delimiters = delimiters.charAt( 0 );

    // append the new value to the list
    if ( ( ! list ) || ( ! list.length ) || ( list.length == 0 ) )
        list = value;
    else
        list = list + delimiters + value;

    return list;
}

// Return true if two strings are identical.
// Compatibility method: signature is equivalent to Java.
new String();
String.prototype.equals = function( other_string )
{
    return ( this == other_string );
}

// Return the ordinal form of a number.
function NumberToOrdinal( num )
{
    var small_ordinals = [ 'zeroth', 'first', 'second', 'third' ];
    var ordinal, suffix;

    // small numbers get the word form
    if ( num < small_ordinals.length )
        ordinal = small_ordinals[ num ];
    else
    {
        // most numbers end with 'th'.
        // numbers ending with 1, 2, or 3 get 'st', 'nd', or 'rd'...
        // except if they're in the teens.  then they're 'th'.
        suffix = 'th';
        if ( ( ( num % 100 ) > 10 ) && ( ( num % 100 ) < 14 ) )
            suffix = 'th';
        else if ( num % 10 == 1 )
            suffix = 'st';
        else if ( num % 10 == 2 )
            suffix = 'nd';
        else if ( num % 10 == 3 )
            suffix = 'rd';

        ordinal = num + suffix;
    }

    return ordinal;
}

// Return the frequency form of a number ('every', 'every other', 'every third', etc).
function NumberToFrequency( num )
{
    var small_freqs = [ 'no', 'every', 'every other' ];
    var freq;

    if ( num < small_freqs.length )
        freq = small_freqs[ num ];
    else
        freq = 'every ' + NumberToOrdinal( num );

    return freq;
}

}// End of C-style "#ifndef/#define"