LDC #97: Creating a Filing Calendar

Everyone in the EDGAR industry has experienced the customer who wants to know what the filing deadline is. For most companies the deadlines are easily found using a search engine. But what happens when your customer has an unusual fiscal year end? You can scramble to figure it out or you can just plug numbers into this week’s blog script does all the date calculations for you.

In order to create such a script we need a few things:
1. Logic to figure out on which day of the week a specific date falls.

2. Logic for adding and rounding dates.

3. Rules for federal holidays.

4. Rules for the SEC’s filing deadlines.

Day planner and calendar
Let’s start with the first item, which concerns calculating the day of the week. Luckily for us, Legato has a function that does this for us: the GetDayOfWeek function. Given a date it returns an integer representing the day of the week.

That’s one item down. Now we can move to logic for adding and rounding dates. As discussed on a few previous blogs, Legato stores dates in the Windows FILETIME format, which is to say a date is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). While this is a mouthful to say, it is great for us since it makes date math super simple. Want to add a day to our date? We can do it simply by adding the number of 100-nanosecond intervals in a day (for those wondering, that is 864,000,000,000 intervals) to the date. That takes care of adding dates, so how about rounding? We need several types of rounding. We will need logic to deal with holidays that occur on weekends. If a federal holiday falls on a Saturday, it is observed the previous Friday instead. If it falls on Sunday, it is observed on the following Monday. For SEC deadlines, when the deadline falls on a weekend or holiday, the deadline is moved to the next business day. Since we need to run these operations many times, let’s create a few defines and functions. We will likely need more rounding functions but this is a good starting point.

// FILETIME defines
#define FT_MILSECOND    10000
#define FT_SECOND       10000000
#define FT_MINUTE       (60 * FT_SECOND)
#define FT_HOUR         (60 * FT_MINUTE)
#define FT_DAY          (24 * FT_HOUR)

//
// Date Adjustment Functions
// -------------------------------------
qword add_months(qword date, int months) {

    int comps[];

    comps = GetDateTimeComponents(date);
    months = (comps["Month"] + months + 1) % 12;

    while (comps["Month"] != months) {
      date += FT_DAY;
      comps = GetDateTimeComponents(date);
      }
    return date;
    }

We now have defines that we can use to add/subtract units of time from our dates. If we wanted, we could create more defines for weeks, months, and years, but remember that not all months are the same length. Likewise, leap years are longer than normal years, so we would probably want to stay away from inaccurate defines. Instead we need to create a different way to add months to our date that actually checks if the month changed. Our first function, add_months, uses the GetDateTimeComponents function to tell which month our date is in. Then it adds a day until the result from GetDateTimeComponents matches our target month. This slightly more convoluted way of advancing the date deals with uneven months and leap years.

We have all we need to add and subtract days and months from dates. So we can move onto the rounding functions.

qword adjust_to_workday(qword date) {

    int day;

    day = GetDayOfWeek(date);
    // Sunday to Monday
    if (day == DOW_SUNDAY) {
      return date + FT_DAY;
      }
    // Saturday to Friday
    if (day == DOW_SATURDAY) {
      return date - FT_DAY;
      }
    return date;
    }

qword adjust_to_businessday(qword date) {

    int day;

    day = GetDayOfWeek(date);
    // Sunday to Monday
    if (day == DOW_SUNDAY) {
      return date + FT_DAY;
      }
    // Saturday to Monday
    if (day == DOW_SATURDAY) {
      return date + FT_DAY + FT_DAY;
      }
    return date;
    }

All of our rounding functions are going to take a qword representing a date and return a qword that is the adjusted date. There may be other parameters, but they will all function in this manner. Let’s take a look at the first rounding function, adjust_to_workday. The function uses the GetDayOfWeek SDK function to see if the day of the week is Sunday or Saturday and, if so, adjusts accordingly. Remember: holidays are adjusted to the closest workday so if the date is on Sunday, we add a day (using our defines) to move to Monday. If the date is on Saturday we subtract a day to go back to Friday. We then return the date (which may have not been edited at all, depending on the circumstances). We’ll use this function when calculating holidays. The next function, adjust_to_businessday, works in a similar fashion except it moves dates on Saturday or Sunday to Monday. This function will be used when calculating a final deadline.

Now that we have some basic rounding functions, let’s talk about federal holidays. I’m sure there are at least a few readers that rely on the Internet or a coworker to tell them when a specific holiday occurs. There are some easy ones. Independence Day is always the 4th of July, for example, and New Years Day is always January 1st. So calculating the observed day off for holidays like these is easy. Get the date of the holiday for the year, like “2018-01-01”, and then pass that date to our adjust_to_workday function and we are done. What about other holidays like memorial day or Labor Day? These holidays have specific rules for when they occur. For example, Memorial Day is always the last Monday in May. Likewise, Labor Day is the first Monday in September. So you can see we will need a way to calculate how many of a particular day of the week is in a month (for example, the number of Mondays in May).

I decided to take a simpler approach. Since each of these holidays has a specific range of dates on which they can occur, we can instead check every day in that range to see if it is correct. Let’s take Memorial Day, for example. It is the last Monday in May. May has 31 days. Using that information, if May 31st is a Monday then it is Memorial Day. This is the latest Memorial Day can occur. If it is a Sunday then Memorial Day would be 25th. This is the earliest Memorial Day can occur. Armed with this information we can simply check to see on which day of the week May 25th occurs and move forward days until we hit a Monday. That Monday is a Memorial Day. With this method, we need a function that moves a date to the next occurrence of a day of the week.

qword adjust_to_day_of_week(qword date, int dow) {

    int day;

    day = GetDayOfWeek(date);
    while (day != dow) {
      date += FT_DAY;
      day = GetDayOfWeek(date);
      }
    return date;
    }

As you can see, given a date and a day of the week, adjust_to_day_of_week gets the current day of the week, and, if the day isn’t the one we want, we add a day and try again. We now have all the information needed to calculate the federal holidays in a year. Let’s create some global variables to store the holidays and then create a function that calculates the holidays dates.

    string              holiday_names[];
    qword               holidays[];
    int                 holiday_count;

...

//
// Data Loading Functions
// -------------------------------------
void add_holidays(string year) {

    // New Year's Day
    holiday_names[holiday_count] = "New Years Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-01-01"));

    // Birthday of Martin Luther King, Jr. (Third Monday of January [15th-21st])
    holiday_names[holiday_count] = "Martin Luther King, Jr.";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-01-15"), DOW_MONDAY);

    // Washington's Birthday (Third Monday of February [15th-21st])
    holiday_names[holiday_count] = "Washington's Birthday";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-02-15"), DOW_MONDAY);

    // Memorial Day (Last Monday of May [25th-31st])
    holiday_names[holiday_count] = "Memorial Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-05-25"), DOW_MONDAY);

    // Independence Day (July 4)
    holiday_names[holiday_count] = "Independence Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-07-04"));

    // Labor Day (First Monday of September [1st-7th])
    holiday_names[holiday_count] = "Labor Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-09-01"), DOW_MONDAY);

    // Columbus Day (Second Monday of October [8th-14th])
    holiday_names[holiday_count] = "Columbus Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-10-08"), DOW_MONDAY);

    // Veterans Day (November 11)
    holiday_names[holiday_count] = "Veterans Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-11-11"));

    // Thanksgiving Day (Fourth Thursday of November [22th-28th])
    holiday_names[holiday_count] = "Thanksgiving Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-11-22"), DOW_THURSDAY);

    // Christmas Day (December 25)
    holiday_names[holiday_count] = "Christmas Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-12-25"));
    }

We have three global variables, one that stores the names of the holidays, another that stores the observed dates, and the last is the number of items in each array. We could use the ArrayGetAxisDepth SDK function to retrieve this information, but the global variable is a little easier since we are appending items to the list.

The function takes the year we are examining and then adds the relevant holidays to the global array. The logic for the holidays follows the two cases laid out above. Either we take a specific date and adjust to a workday, or we calculate a day of the week for a given range. Note that these ranges are not adjusted to workdays because the observed holidays must fall on a workday (most are Monday). We use the StringToDate function to create the dates given our years. This function takes many date formats, but I used the ISO-8601 date format since it is not ambiguous like European or American date formats.

Okay, the easy stuff is out of the way. Now we can talk about SEC deadlines. The deadlines for annual forms are usually based on the company’s fiscal year end. Many quarterly forms use the company’s fiscal quarters as their basis. These dates can vary widely by industry and company. For this example, we can use the fiscal year end to calculate the quarter ends. To simplify things further, we can also round the fiscal year end to the end of the month. This covers most public companies, but for a more robust calendar we could have the user enter all of their quarter ends.

Since we want to calculate the quarters, we need a few more rounding functions. We need one to round the fiscal year end to the end of the month. We also need another function to round the quarter end to the date of the closest month end.

qword round_to_end_of_month(qword date) {

    int comps[];
    int month;

    comps = GetDateTimeComponents(date);
    month = comps["Month"];

    while (month == comps["Month"]) {
      date += FT_DAY;
      comps = GetDateTimeComponents(date);
      }
    date -= FT_DAY;
    return date;
    }

qword round_to_month(qword date) {

    qword fdate, bdate;
    int compsf[], compsb[];
    int month;

    compsf = GetDateTimeComponents(date);
    compsb = compsf;
    month = compsf["Month"];
    fdate = date;
    bdate = date;

    // Go Both directions
    while ((month == compsf["Month"]) && (month == compsb["Month"])) {
      fdate += FT_DAY;
      bdate -= FT_DAY;
      compsf = GetDateTimeComponents(fdate);
      compsb = GetDateTimeComponents(bdate);
      }
    // Backwards left month
    if (month != compsb["Month"]) {
      date = bdate;
      }
    // Forwards left month
    if (month != compsf["Month"]) {
      date = fdate - FT_DAY;
      }
    return date;
    }

Let’s start with the round_to_end_of_month function. This function is similar to the add_months function in that it uses the GetDateTimeComponents SDK function to get the current month. Only we add days until the month value changes. This means that the month ended. If we go back a day we can get the last day of the month. The next function, round_to_month, is another variation on this method. The difference is in this function we go both forward and backwards in time. The first time the month changes, the loop exits and we set the date to either the backwards date (which is the end of the previous month) of the forward date (which is the first day of the next month) minus a day. If the day we gave the function is in the exact middle of the month, the function favors the forward direction since it checks the forward direction last.

We are almost to the point where we can make a calendar. With these functions we have the logic to manipulate our dates, but we don’t yet have any logic to create the deadline dates. We are also missing logic to compare our dates to the holiday list.

Adjusting a date for a holiday is an easy procedure, so let’s get it out of the way.

...
    int                 last_adjusted;
...

qword adjust_for_holiday(qword date) {

    int ix;

    last_adjusted = -1;

    for (ix = 0; ix < holiday_count; ix++) {
      if (holidays[ix] == date) {
        date += FT_DAY;
        last_adjusted = ix;
        return adjust_to_businessday(date);
        }
      }
    return date;
    }

First we have another global. This global is used to track if a date was adjusted by a holiday and which holiday it was. We don’t really need this information, but it’s something we can report to the user in the final calendar. In the function, we reset our new global variable and then check the date against our global array of holidays. If it is in the array we move the date to the next day, set the last_adjusted global to the index of the holiday, and then return the adjusted the date using the adjust_to_businessday function. This last part is important since a holiday could fall on a Friday which means the next day is a weekend. In that case we need to adjust the date even more to get to the next Monday.

Our last two steps are creating a list of forms with deadlines and then lastly creating the filing deadline calendar, which is the point of our script. Sometimes something that seems very simple can take so many steps, but that’s programming.

At any rate, we need a list of forms. Let’s create some global variables to store the list and a function to populate the list. For this script, that function is going to be hard coded with forms, but you could easily adapt the script to read the forms list from an XML, CSV or even text file.

...
    string              forms[][];
    int                 form_count;
...
void load_forms() {

    // 10-K
    forms[form_count]["name"]   = "10-K";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["days"]   = "90";
    form_count++;
...
    // 10-Q
    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "Q1";
    forms[form_count]["days"]   = "40";
    form_count++;
...
    // 20-F
    forms[form_count]["name"]   = "20-F";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["months"] = "4";
    form_count++;
...
    // NT 20-F
    forms[form_count]["name"]   = "NT 20-F";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "20-F|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;
    }

The forms variable is a table of strings that contains our form information. This includes the name, filer type, deadline basis, and the number of days or months the deadline is from the basis. It seems like a lot to take in, but let’s dive right into the load_forms function. The first form is a “10-K” for “Non-Accelerated” filers. The deadline is 90 days past the fiscal year end or “FYE”. So we set the information into the array. The function here is missing some of the forms as they are very similar. The completed script below will have more of the forms filled in. The form “10-Q” is very similar except in this case we have the deadline for an “Accelerated” filer and the deadline is based on the first quarter end date or “Q1”. The code for “20-F” is very similar as well except instead of days the deadline is measured in months. Lastly, we have an non-timely or NT form. The deadline for these forms is the day after the deadline for the timely form. This means the base is the other form. Some forms like “10-Q” have multiple deadlines so we add a vertical bar to indicate what the base of the source form is as well. So the base for an “NT 10-Q” for the second quarter would be “10-Q|Q2”.

Now we have a list of forms that contains information on how to calculate their deadlines, a list of holidays, and lastly some date manipulation functions to help us move the deadlines around. So we can finally start the build_calendar function.

//
// Calendar Creation Functions
// -------------------------------------
void build_calendar(string fye, string year, string filer) {
    qword               date, qs[], deadline;
    string              formbasis[2], base;
    int                 days,
                        ix,
                        bx, mx;

    if (year == "") {
      date = GetLocalTime();
      year = FormatDate(date, "Y");
      }
    
    // Adjust FYE Formatting
    fye = ReplaceInString(fye, "/", "-");
    if (fye[2] != '-') {
      fye = "0" + fye;
      }
    if (GetStringLength(fye) != 5) {
      fye = GetStringSegment(fye, 0, 3) + "0" + GetStringSegment(fye, 3, 1);
      }
    date = StringToDate(year + "-" + fye);
    date = round_to_end_of_month(date);
    fye = FormatDate(date, "m-d");
    if (fye == "02-29") {
      fye = "02-28";
      }
    AddMessage("Fiscal year: %s to %d ending on %s", TextToInteger(year) - 1, year, ReplaceInString(fye, "-", "/"));    
    
    // Get Quarter Ends
    date = StringToDate(FormatString("%d", TextToInteger(year) - 1) + "-" + fye);
    for (ix = 0; ix < 4; ix++) {
      date += FT_DAY * 90;
      qs[ix] = round_to_month(date);
      AddMessage("Quarter %d: " + FormatDate(qs[ix], "F j, Y"), ix + 1);
      }

    // Get Days of Federal Holidays (For 3 years)
    add_holidays(FormatString("%d", TextToInteger(year) - 1));
    add_holidays(year);
    add_holidays(FormatString("%d", TextToInteger(year) + 1));

    AddMessage("Filing Deadlines...");
    load_forms();    

...
    }

The first part of this function takes the year variable and sets it to the current year if it was not specified. The function then adjusts the passed fye variable to the end of the current month. We also change the formatting of the fye string to use dashes instead. This will help us create ISO dates later in the function. Now we can determine the fiscal quarters from the fye and year variables. We do this by adding 90 days to the date and then rounding the resulting date to the closest month end. This may not be the best way to do the quarter calculation but it works for all fiscal year end dates that are also the end of a month. During the loop we also employ the AddMessage function to report to the user the quarter dates we have chosen. Using the year variable we add the holidays to the calendar. We add the year before and after as well. This is because New Year’s Day may be observed in the previous year if it happens to fall on a Saturday. Then we call our load_forms function to load all the form data.

void build_calendar(string fye, string year, string filer) {
...
    for (ix = 0; ix < form_count; ix++) {
      if ((filer != "") && (forms[ix]["filer"] != "") && (filer != forms[ix]["filer"])) {
        continue;
        }
      base = forms[ix]["base"];
      switch(base) {
        case "FYE":
          date = StringToDate(FormatString("%d", TextToInteger(year) - 1) + "-" + fye);
          break;
        case "Q1":
          date = qs[0];
          break;
        case "Q2":
          date = qs[1];
          break;
        case "Q3":
          date = qs[2];
          break;
        case "Q4":
          date = qs[3];
          break;
        default:
          formbasis = ExplodeString(forms[ix]["base"], "|");
          for (bx = 0; bx < form_count; bx++) {
            if ((forms[bx]["name"] == formbasis[0]) && (forms[bx]["base"] == formbasis[1]) && (forms[bx]["filer"] == forms[ix]["filer"])) {
              base = formbasis[1];
              date = HexToInteger(forms[bx]["deadline"]);
              break;
              }
            }
          break;
        }
      if (forms[ix]["days"] != "") {
        days = TextToInteger(forms[ix]["days"]);
        deadline = adjust_to_businessday(date + (FT_DAY * days));
        }
      if (forms[ix]["months"] != "") {
        days = TextToInteger(forms[ix]["months"]);
        deadline = adjust_to_businessday(add_months(date, days));
        }
      deadline = adjust_for_holiday(deadline);
      forms[ix]["deadline"] = FormatString("%X", deadline);
      if (last_adjusted >= 0) {
        AddMessage("%10s %4s  %-18s %s (delayed by %s)", forms[ix]["name"], base, forms[ix]["filer"], FormatDate(deadline, "D, M d Y"), holiday_names[last_adjusted]);
        }
      else {
        AddMessage("%10s %4s  %-18s %s", forms[ix]["name"], base, forms[ix]["filer"], FormatDate(deadline, "D, M d Y"));
        }
      }
    }

With these steps complete, we can finally get into the logic of building the calendar. We start with a loop over all the forms. We can check to see if the user only wants deadlines for specific filer types and if this form isn’t that type, we skip it. If it is blank we assume the user wants all filer types. Next we have a switch on the base of the form’s deadline. We create a copy of the form base since it may be a compound base like the NT forms use. Each case in the switch sets the date to either the fiscal year end, a quarter end, or, in the default case, we try to find the deadline of another form. No matter what the base value was, date is now set to the basis of the form’s deadline.
Then, using the form’s entry, we add days or months to the date via our defines or the add_months function. We also adjust the resulting date to a business day using our adjust_to_businessday function. Finally, the last step is to take the calculated deadline and check it against the holiday list using our adjust_for_holiday function. We then store the resulting deadline back into our forms list. This is in case another form’s deadline is based on this form’s deadline. We can then output the deadline to the user. If the date was adjusted due to a holiday we also add that information in the output.

We can whip up a quick main function to test our build_calendar and try it out.

void main() {
    build_calendar("12/31", "2018", "");
    }

If we run the script, the output looks likes this:

Fiscal year: 2017 to 2018 ending on 12/31
Quarter 1: March 31, 2018
Quarter 2: June 30, 2018
Quarter 3: September 30, 2018
Quarter 4: December 31, 2018
Filing Deadlines...
      10-K  FYE  Non-Accelerated    Mon, Apr 02 2018
      10-K  FYE  Accelerated        Fri, Mar 16 2018
      10-K  FYE  Large Accelerated  Thu, Mar 01 2018
   NT 10-K  FYE  Non-Accelerated    Tue, Apr 03 2018
   NT 10-K  FYE  Accelerated        Mon, Mar 19 2018
   NT 10-K  FYE  Large Accelerated  Fri, Mar 02 2018
      10-Q   Q1  Non-Accelerated    Tue, May 15 2018
      10-Q   Q1  Accelerated        Thu, May 10 2018
      10-Q   Q1  Large Accelerated  Thu, May 10 2018
      10-Q   Q2  Non-Accelerated    Tue, Aug 14 2018
      10-Q   Q2  Accelerated        Thu, Aug 09 2018
      10-Q   Q2  Large Accelerated  Thu, Aug 09 2018
      10-Q   Q3  Non-Accelerated    Wed, Nov 14 2018
      10-Q   Q3  Accelerated        Fri, Nov 09 2018
      10-Q   Q3  Large Accelerated  Fri, Nov 09 2018
   NT 10-Q   Q1  Non-Accelerated    Wed, May 16 2018
   NT 10-Q   Q1  Accelerated        Fri, May 11 2018
   NT 10-Q   Q1  Large Accelerated  Fri, May 11 2018
   NT 10-Q   Q2  Non-Accelerated    Wed, Aug 15 2018
   NT 10-Q   Q2  Accelerated        Fri, Aug 10 2018
   NT 10-Q   Q2  Large Accelerated  Fri, Aug 10 2018
   NT 10-Q   Q3  Non-Accelerated    Thu, Nov 15 2018
   NT 10-Q   Q3  Accelerated        Tue, Nov 13 2018 (delayed by Veterans Day)
   NT 10-Q   Q3  Large Accelerated  Tue, Nov 13 2018 (delayed by Veterans Day)
      20-F  FYE  Non-Accelerated    Tue, May 01 2018
      20-F  FYE  Accelerated        Tue, May 01 2018
      20-F  FYE  Large Accelerated  Tue, May 01 2018
   NT 20-F  FYE  Non-Accelerated    Wed, May 02 2018
   NT 20-F  FYE  Accelerated        Wed, May 02 2018
   NT 20-F  FYE  Large Accelerated  Wed, May 02 2018

While this script is a little complicated to get started, once it is set up, adding forms and changing dates is a breeze. This script could easily be adapted to create an XML file or an ICS file as the results instead of printing to the Legato log. It could also be adapted to use a dialog and ask the user for their fiscal year end or even their quarter ends. This would add support for companies that use other fiscal year ends (such as a variable FYE based on the last Sunday of the year). Legato’s date functions simplified a lot of the logic for this script. What can Legato simplify for you?
Entire script:

    string              holiday_names[];
    qword               holidays[];
    int                 holiday_count;

    string              forms[][];
    int                 form_count;

    int                 last_adjusted;

    qword               add_months                      (qword date, int months);
    qword               adjust_to_workday               (qword date);
    qword               adjust_to_businessday           (qword date);
    qword               adjust_to_day_of_week           (qword date, int dow);
    qword               round_to_end_of_month           (qword date);
    qword               round_to_month                  (qword date);
    qword               adjust_for_holiday              (qword date);

    void                add_holidays                    (string year);
    void                load_forms                      ();

    void                build_calendar                  (string fye, string year, string filer);

// FILETIME defines
#define FT_MILSECOND    10000
#define FT_SECOND       10000000
#define FT_MINUTE       (60 * FT_SECOND)
#define FT_HOUR         (60 * FT_MINUTE)
#define FT_DAY          (24 * FT_HOUR)

//
// Date Adjustment Functions
// -------------------------------------
qword add_months(qword date, int months) {

    int comps[];

    comps = GetDateTimeComponents(date);
    months = (comps["Month"] + months + 1) % 12;

    while (comps["Month"] != months) {
      date += FT_DAY;
      comps = GetDateTimeComponents(date);
      }
    return date;
    }

qword adjust_to_workday(qword date) {

    int day;

    day = GetDayOfWeek(date);
    // Sunday to Monday
    if (day == DOW_SUNDAY) {
      return date + FT_DAY;
      }
    // Saturday to Friday
    if (day == DOW_SATURDAY) {
      return date - FT_DAY;
      }
    return date;
    }

qword adjust_to_businessday(qword date) {

    int day;

    day = GetDayOfWeek(date);
    // Sunday to Monday
    if (day == DOW_SUNDAY) {
      return date + FT_DAY;
      }
    // Saturday to Monday
    if (day == DOW_SATURDAY) {
      return date + FT_DAY + FT_DAY;
      }
    return date;
    }

qword adjust_to_day_of_week(qword date, int dow) {

    int day;

    day = GetDayOfWeek(date);
    while (day != dow) {
      date += FT_DAY;
      day = GetDayOfWeek(date);
      }
    return date;
    }

qword round_to_end_of_month(qword date) {

    int comps[];
    int month;

    comps = GetDateTimeComponents(date);
    month = comps["Month"];

    while (month == comps["Month"]) {
      date += FT_DAY;
      comps = GetDateTimeComponents(date);
      }
    date -= FT_DAY;
    return date;
    }

qword round_to_month(qword date) {

    qword fdate, bdate;
    int compsf[], compsb[];
    int month;

    compsf = GetDateTimeComponents(date);
    compsb = compsf;
    month = compsf["Month"];
    fdate = date;
    bdate = date;

    // Go Both directions
    while ((month == compsf["Month"]) && (month == compsb["Month"])) {
      fdate += FT_DAY;
      bdate -= FT_DAY;
      compsf = GetDateTimeComponents(fdate);
      compsb = GetDateTimeComponents(bdate);
      }
    // Backwards left month
    if (month != compsb["Month"]) {
      date = bdate;
      }
    // Forwards left month
    if (month != compsf["Month"]) {
      date = fdate - FT_DAY;
      }
    return date;
    }

qword adjust_for_holiday(qword date) {

    int ix;

    last_adjusted = -1;

    for (ix = 0; ix < holiday_count; ix++) {
      if (holidays[ix] == date) {
        date += FT_DAY;
        last_adjusted = ix;
        return adjust_to_businessday(date);
        }
      }
    return date;
    }

//
// Data Loading Functions
// -------------------------------------
void add_holidays(string year) {

    // New Year's Day
    holiday_names[holiday_count] = "New Years Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-01-01"));

    // Birthday of Martin Luther King, Jr. (Third Monday of January [15th-21st])
    holiday_names[holiday_count] = "Martin Luther King, Jr.";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-01-15"), DOW_MONDAY);

    // Washington's Birthday (Third Monday of February [15th-21st])
    holiday_names[holiday_count] = "Washington's Birthday";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-02-15"), DOW_MONDAY);

    // Memorial Day (Last Monday of May [25th-31st])
    holiday_names[holiday_count] = "Memorial Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-05-25"), DOW_MONDAY);

    // Independence Day (July 4)
    holiday_names[holiday_count] = "Independence Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-07-04"));

    // Labor Day (First Monday of September [1st-7th])
    holiday_names[holiday_count] = "Labor Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-09-01"), DOW_MONDAY);

    // Columbus Day (Second Monday of October [8th-14th])
    holiday_names[holiday_count] = "Columbus Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-10-08"), DOW_MONDAY);

    // Veterans Day (November 11)
    holiday_names[holiday_count] = "Veterans Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-11-11"));

    // Thanksgiving Day (Fourth Thursday of November [22th-28th])
    holiday_names[holiday_count] = "Thanksgiving Day";
    holidays[holiday_count++] = adjust_to_day_of_week(StringToDate(year + "-11-22"), DOW_THURSDAY);

    // Christmas Day (December 25)
    holiday_names[holiday_count] = "Christmas Day";
    holidays[holiday_count++] = adjust_to_workday(StringToDate(year + "-12-25"));
    }

void load_forms() {

    // 10-K
    forms[form_count]["name"]   = "10-K";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["days"]   = "90";
    form_count++;

    forms[form_count]["name"]   = "10-K";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["days"]   = "75";
    form_count++;

    forms[form_count]["name"]   = "10-K";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["days"]   = "60";
    form_count++;

    // NT 10-K
    forms[form_count]["name"]   = "NT 10-K";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "10-K|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-K";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "10-K|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-K";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "10-K|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;

    // 10-Q Q1
    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "Q1";
    forms[form_count]["days"]   = "45";
    form_count++;

    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "Q1";
    forms[form_count]["days"]   = "40";
    form_count++;

    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "Q1";
    forms[form_count]["days"]   = "40";
    form_count++;

    // 10-Q Q2
    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "Q2";
    forms[form_count]["days"]   = "45";
    form_count++;

    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "Q2";
    forms[form_count]["days"]   = "40";
    form_count++;

    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "Q2";
    forms[form_count]["days"]   = "40";
    form_count++;

    // 10-Q Q3
    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "Q3";
    forms[form_count]["days"]   = "45";
    form_count++;

    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "Q3";
    forms[form_count]["days"]   = "40";
    form_count++;

    forms[form_count]["name"]   = "10-Q";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "Q3";
    forms[form_count]["days"]   = "40";
    form_count++;

    // NT 10-Q Q1
    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "10-Q|Q1";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "10-Q|Q1";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "10-Q|Q1";
    forms[form_count]["days"]   = "1";
    form_count++;

    // NT 10-Q Q2
    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "10-Q|Q2";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "10-Q|Q2";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "10-Q|Q2";
    forms[form_count]["days"]   = "1";
    form_count++;

    // NT 10-Q Q3
    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "10-Q|Q3";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "10-Q|Q3";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 10-Q";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "10-Q|Q3";
    forms[form_count]["days"]   = "1";
    form_count++;

    // 20-F
    forms[form_count]["name"]   = "20-F";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["months"] = "4";
    form_count++;

    forms[form_count]["name"]   = "20-F";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["months"] = "4";
    form_count++;

    forms[form_count]["name"]   = "20-F";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "FYE";
    forms[form_count]["months"] = "4";
    form_count++;

    // NT 20-F
    forms[form_count]["name"]   = "NT 20-F";
    forms[form_count]["filer"]  = "Non-Accelerated";
    forms[form_count]["base"]   = "20-F|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 20-F";
    forms[form_count]["filer"]  = "Accelerated";
    forms[form_count]["base"]   = "20-F|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;

    forms[form_count]["name"]   = "NT 20-F";
    forms[form_count]["filer"]  = "Large Accelerated";
    forms[form_count]["base"]   = "20-F|FYE";
    forms[form_count]["days"]   = "1";
    form_count++;
    }

//
// Calendar Creation Functions
// -------------------------------------
void build_calendar(string fye, string year, string filer) {
    qword               date, qs[], deadline;
    string              formbasis[2], base;
    int                 days,
                        ix,
                        bx, mx;

    if (year == "") {
      date = GetLocalTime();
      year = FormatDate(date, "Y");
      }

    // Adjust FYE Formatting
    fye = ReplaceInString(fye, "/", "-");
    if (fye[2] != '-') {
      fye = "0" + fye;
      }
    if (GetStringLength(fye) != 5) {
      fye = GetStringSegment(fye, 0, 3) + "0" + GetStringSegment(fye, 3, 1);
      }
    date = StringToDate(year + "-" + fye);
    date = round_to_end_of_month(date);
    fye = FormatDate(date, "m-d");
    if (fye == "02-29") {
      fye = "02-28";
      }
    AddMessage("Fiscal year: %s to %d ending on %s", TextToInteger(year) - 1, year, ReplaceInString(fye, "-", "/"));

    // Get Quarter Ends
    date = StringToDate(FormatString("%d", TextToInteger(year) - 1) + "-" + fye);
    for (ix = 0; ix < 4; ix++) {
      date += FT_DAY * 90;
      qs[ix] = round_to_month(date);
      AddMessage("Quarter %d: " + FormatDate(qs[ix], "F j, Y"), ix + 1);
      }

    // Get Days of Federal Holidays (For 3 years)
    add_holidays(FormatString("%d", TextToInteger(year) - 1));
    add_holidays(year);
    add_holidays(FormatString("%d", TextToInteger(year) + 1));


    AddMessage("Filing Deadlines...");
    load_forms();

    for (ix = 0; ix < form_count; ix++) {
      if ((filer != "") && (forms[ix]["filer"] != "") && (filer != forms[ix]["filer"])) {
        continue;
        }
      base = forms[ix]["base"];
      switch(base) {
        case "FYE":
          date = StringToDate(FormatString("%d", TextToInteger(year) - 1) + "-" + fye);
          break;
        case "Q1":
          date = qs[0];
          break;
        case "Q2":
          date = qs[1];
          break;
        case "Q3":
          date = qs[2];
          break;
        case "Q4":
          date = qs[3];
          break;
        default:
          formbasis = ExplodeString(forms[ix]["base"], "|");
          for (bx = 0; bx < form_count; bx++) {
            if ((forms[bx]["name"] == formbasis[0]) && (forms[bx]["base"] == formbasis[1]) && (forms[bx]["filer"] == forms[ix]["filer"])) {
              base = formbasis[1];
              date = HexToInteger(forms[bx]["deadline"]);
              break;
              }
            }
          break;
        }
      if (forms[ix]["days"] != "") {
        days = TextToInteger(forms[ix]["days"]);
        deadline = adjust_to_businessday(date + (FT_DAY * days));
        }
      if (forms[ix]["months"] != "") {
        days = TextToInteger(forms[ix]["months"]);
        deadline = adjust_to_businessday(add_months(date, days));
        }
      deadline = adjust_for_holiday(deadline);
      forms[ix]["deadline"] = FormatString("%X", deadline);
      if (last_adjusted >= 0) {
        AddMessage("%10s %4s  %-18s %s (delayed by %s)", forms[ix]["name"], base, forms[ix]["filer"], FormatDate(deadline, "D, M d Y"), holiday_names[last_adjusted]);
        }
      else {
        AddMessage("%10s %4s  %-18s %s", forms[ix]["name"], base, forms[ix]["filer"], FormatDate(deadline, "D, M d Y"));
        }
      }
    }

void main() {
    build_calendar("12/31", "2018", "");
    }

 

David Theis has been developing software for Windows operating systems for over fifteen years. He has a Bachelor of Sciences in Computer Science from the Rochester Institute of Technology and co-founded Novaworks in 2006. He is the Vice President of Development and is one of the primary developers of GoFiler, a financial reporting software package designed to create and file EDGAR XML, HTML, and XBRL documents to the U.S. Securities and Exchange Commission.