0154

Second Tuesday Payment Run

Algorithm
Easy
dates
calendar
scheduling

Second Tuesday Payment Run

Treasury pays suppliers on schedule rules, not on calendar dates: "the second Tuesday of every month", "the last Friday", "the first workday on or after the 15th". Someone has been reading these off a wall calendar and marking the runs by hand — and the February entry has been wrong twice. You are writing the scheduler that turns a rule into real dates.

Requirements

Create a codeunit named "Payment Run Scheduler" with four public procedures:

procedure NthWeekdayOfMonth(Year: Integer; Month: Integer; WeekdayNumber: Integer; Occurrence: Integer): Date
procedure LastWeekdayOfMonth(Year: Integer; Month: Integer; WeekdayNumber: Integer): Date
procedure FirstWorkdayOnOrAfter(StartingDate: Date): Date
procedure PaymentRunDates(StartYear: Integer; StartMonth: Integer; MonthCount: Integer; WeekdayNumber: Integer; Occurrence: Integer): List of [Date]

Weekday numbers run 1 (Monday) through 7 (Sunday) — the numbering Date2DWY uses.

What you may rely on — the tests never violate this: Year is between 2020 and 2030, Month is 1 to 12, WeekdayNumber is 1 to 7, and Occurrence is 1 to 5 — except for PaymentRunDates, which is only ever called with occurrences 1 to 4.

The rules:

  1. NthWeekdayOfMonth returns the date of the Occurrence-th day with that weekday number in the given month: occurrence 1 is the earliest such day in the month — which may be the 1st itself — occurrence 2 the next one, and so on.
  2. Every month contains at least four of every weekday, but only some stretch to a fifth. If Occurrence is 5 and the month has no fifth such weekday, raise an error with a message that contains the text no fifth.
  3. LastWeekdayOfMonth returns the date of the last day with that weekday number in the given month — the fifth occurrence when the month has five, otherwise the fourth. Watch out for February: how long it is depends on the year.
  4. FirstWorkdayOnOrAfter returns StartingDate itself when it is a workday — Monday through Friday — and otherwise the first workday after it. The result may fall in a later month, or even a later year, than StartingDate.
  5. PaymentRunDates returns one date per month, in chronological order: the Occurrence-th WeekdayNumber of the start month, then of the following month, and so on for MonthCount months in a row. The sequence of months keeps going past December into January of the next year.
  6. If MonthCount is below 1, raise an error with a message that contains the text at least one month.

Pick your codeunit's object ID in the 50100–50199 range and reference other objects by name, never by ID.

What the tests check

The tests call all four procedures on fixed dates: a month whose 1st already is the wanted weekday, the second Tuesday of an ordinary month, a fifth Friday, a month where the fifth Monday is missing (the error rule), last weekdays in a five-occurrence and a four-occurrence month, the last weekday of February in a leap year and in a common year, a mid-month workday that stays put, a Friday that stays put — Friday is a workday — a Saturday 15th that rolls to Monday, a Sunday month-end that rolls into the next month, a Sunday 31 December that rolls into the next year, a four-month payment run that crosses a year boundary, and a single-month run. Both error rules are checked against the exact message fragments quoted above — the month-count rule with zero and with a random negative value. Two tests use fully randomized inputs and compare your result against an independent day-by-day walk over the calendar, so hardcoding the fixed examples will not pass.

Learn More

Hint 1
Every rule here reduces to two questions about a single date: what is its weekday number, and which month does it belong to? Resist reaching for one big formula — a Date plus an Integer is a Date, and seven days after a Tuesday is the next Tuesday. Small loops over real dates beat clever arithmetic.
Hint 2
For the nth weekday: start at the 1st of the month, step one day forward until Date2DWY(TheDate, 1) matches, then jump whole weeks — 7 * (Occurrence - 1) days. If the jump lands outside the month (Date2DMY(TheDate, 2) no longer matches), the fifth occurrence does not exist and that is your error case. For the last weekday, start from the month's final day and walk backwards — CalcDate('<CM>', AnyDayOfThatMonth) hands you that final day, leap February included.
Hint 3
FirstWorkdayOnOrAfter is a two-line loop: while the weekday number is 6 or 7, add one day — plain date arithmetic carries you into the next month or year on its own. PaymentRunDates is a for-loop from 1 to MonthCount calling NthWeekdayOfMonth once per month: keep the year and month as plain integers, and when the month passes 12, reset it to 1 and add one to the year.
ALBusiness Central 28.4
Press Compile to check your code compiles — Submit runs the tests.
The code editor is desktop-only
Open this problem on a computer to write and run code. Reading the description, tests and discussion works fine here.