Here is an example to calculate EOM of the current or prior month:

* EOM: End of Month
DEFINE DATA
LOCAL
1 #DATE (D) INIT <*DATX>
1 #DATEA (A8)
1 REDEFINE #DATEA
2 YYYY (N4)
2 MM (N2)
2 DD (N2)
END-DEFINE
INPUT (AD=M) #DATE
ADD 31 TO #DATE /* for current EOM only, otherwise prior EOM
MOVE EDITED #DATE (EM=YYYYMMDD) TO #DATEA
#DATEA.DD := 1 /* first of current month
MOVE EDITED #DATEA TO #DATE (EM=YYYYMMDD)
SUBTRACT 1 FROM #DATE /* end of prior month
PRINT 'EOM:' #DATE
END

if you omit the ADD 31 TO #DATE you will get the EOM of the prior month, else you will get the EOM of the current month. Thus you are enabled to go to any EOM in the past or the future.

I suggest that you add 28 instead of 31, otherwise a date of 1/29/06 results in an EOM value of 2/28/06.

Actually, here’s the coe that I’ve used for years.

DEFINE DATA LOCAL
1 #DATE (D) INIT <D'01/28/2006'>
1 #T (A8) 1 REDEFINE #T /* Temp date
2 #DD (N2)
1 #D (D) /* For calculations
END-DEFINE
*
INPUT (AD=M) #DATE
MOVE EDITED #DATE (EM=DDMMYYYY) TO #T
IF #DD < 28
ASSIGN #DD = 28 /* Every month has 28
END-IF
REPEAT UNTIL #DD = 1 /* 'Search' for next month
MOVE EDITED #T TO #D (EM=DDMMYYYY) /* Convert to D format
ADD 1 TO #D
MOVE EDITED #D (EM=DDMMYYYY) TO #T /* Convert to alpha
END-REPEAT
SUBTRACT 1 FROM #D /* Last day of given month
PRINT 'EOM:' #DATE #D
END

This works regardless of the number of days in the following month.

I have to leave the office in a few minutes, but thought I would contribute some code for a similar problem that might help with this problem. The task was to accept a date format variable and create a date format variable with the same year and month, but day = 1. The most efficient code I could develop (important since this code is executed 50 million, yes, million, times a day) was the following:

DEFINE DATA LOCAL
1 #DATE-IN (D)
1 #DATE-OUT (D)
1 #DAYS-ALPHA (A2)
1 REDEFINE #DAYS-ALPHA
2 #DAYS-NUMERIC (N2)
END-DEFINE
*
INCLUDE AATITLER
INCLUDE AASETC
*
MOVE *DATX TO #DATE-IN
WRITE 5T ‘=’ #DATE-IN /
*
MOVE EDITED #DATE-IN (EM=DD) TO #DAYS-ALPHA
COMPUTE #DATE-OUT = #DATE-IN - #DAYS-NUMERIC + 1
*
WRITE 5X ‘=’ #DATE-OUT
END

Just got back in the office and see that no one has played with this. Since I needed to unwind from a frustrating day, I decided to use the code above to derive code to answer the specific request:

DEFINE DATA LOCAL
1 #DATE-IN (D)
1 #DATE-OUT (D)
1 #DAYS-ALPHA (A2)
1 REDEFINE #DAYS-ALPHA
2 #DAYS-NUMERIC (N2)
END-DEFINE
*
INCLUDE AATITLER
INCLUDE AASETC
*
MOVE *DATX TO #DATE-IN
WRITE 5T ‘=’ #DATE-IN /
*
MOVE EDITED #DATE-IN (EM=DD) TO #DAYS-ALPHA
COMPUTE #DATE-OUT = #DATE-IN - #DAYS-NUMERIC
*
WRITE 5X ‘=’ #DATE-OUT 5X ‘END OF PREVIOUS MONTH’
*
COMPUTE #DATE-IN = #DATE-OUT + 32
MOVE EDITED #DATE-IN (EM=DD) TO #DAYS-ALPHA
COMPUTE #DATE-OUT = #DATE-IN - #DAYS-NUMERIC
WRITE 5X ‘=’ #DATE-OUT 5X ‘END OF CURRENT MONTH’
*
END

PAGE # 1 DATE: Sep 14, 2006
PROGRAM: DATE06 LIBRARY: INSIDE
#DATE-IN: 06-09-14
#DATE-OUT: 06-08-31 END OF PREVIOUS MONTH
#DATE-OUT: 06-09-30 END OF CURRENT MONTH

This solution has but one EXAMINE and one COMPUTE for the end of the previous month, and two COMPUTEs and one EXAMINE for the end of the current month.

True, it does not work for a value of zero, but so what? What is the end of the previous month if the date is zero??

It was just a hint, because I would make my modules as save as possible. From my point of view a date-field containting zero means “undefined”. So I would return “undefinded” in my EOM-calculation-module.

I took a slightly different approach…
Given a year and a month, this routine returns the # of days in the month.

DEFINE DATA
PARAMETER
1 YEAR (N4)
1 MONTH (N2)
1 DAYS (N2)
LOCAL
1 #DAYS (N2/12) INIT <31,28,31,30,31,30,31,31,30,31,30,31>
1 #REM (N2)
1 #Y (N4)
END-DEFINE
*
DIVIDE 4 INTO YEAR GIVING #Y REMAINDER #REM
IF MONTH = 2
AND #REM = 0
DAYS := 29
ELSE
DAYS := #DAYS(MONTH)
END-IF
END

You are not accounting for the full definition of leap year. Years divisible by 100 are not leap years unless they are divisible by 400.

Better version:

DEFINE DATA
PARAMETER
1 YEAR (N4)
1 MONTH (N2)
1 DAYS (N2)
LOCAL
1 #DAYS (N2/12) INIT <31,28,31,30,31,30,31,31,30,31,30,31>
1 #REM-4 (N2)
1 #REN-100 (N2)
1 #REM-400 (N2)
1 #Y (N4)
END-DEFINE
*
DAYS := #DAYS(MONTH)
IF MONTH = 2
DIVIDE 400 INTO YEAR GIVING #Y REMAINDER #REM-400
IF #REM-400 = 0
ADD 1 TO DAYS
ELSE
DIVIDE 100 INTO YEAR GIVING #Y REMAINDER #REM-100
IF #REM-100 NE 0
DIVIDE 4 INTO YEAR GIVING #Y REMAINDER #REM-4
IF #REM-4 = 0
ADD 1 TO DAYS
END-IF
END-IF
END-IF
END

When I posted my Calendar routine, there was quite a number of enhancements proposed (different languages, different week starting days, etc). One very useful enhancement was Matthias’s routine for testing if a given year was a leap year. The idea is to let Natural do the work using an edit mask instead of the program doing multiple divides and looking at remainders.

Herewith a timing comparison between using multiple divides and an edit mask for this year. I have commented out (IGNORE) code that would be in common. To derive a true comparison, the FOR loop times should be subtracted from the other two loop times.

DEFINE DATA
LOCAL
1 YEAR (N4) INIT <2007>
1 MONTH (N2) INIT <2>
1 DAYS (N2)
1 #DAYS (N2/12) INIT <31,28,31,30,31,30,31,31,30,31,30,31>
1 #REM-4 (N2)
1 #REM-100 (N2)
1 #REM-400 (N2)
1 #Y (N4)
1 #CPU-ELAPSED (I4)
1 #CPU-TIME (I4)
1 #LOOP (I4)
1 #TESTYEAR (N8) INIT <00000229>
1 REDEFINE #TESTYEAR
2 #TESTER (N4)
1 REDEFINE #TESTYEAR
2 #TESTYEARA (A8)
END-DEFINE
*
INCLUDE AASETC
INCLUDE AATITLER
*
MOVE CPU-TIME TO #CPU-TIME
SETA. SETTIME
FOR #LOOP = 1 TO 100000
DIVIDE 400 INTO YEAR GIVING #Y REMAINDER #REM-400
IF #REM-400 = 0
IGNORE / ADD 1 TO DAYS
ELSE
DIVIDE 100 INTO YEAR GIVING #Y REMAINDER #REM-100
IF #REM-100 NE 0
DIVIDE 4 INTO YEAR GIVING #Y REMAINDER #REM-4
IF #REM-4 = 0
IGNORE /* ADD 1 TO DAYS
END-IF
END-IF
END-IF
END-FOR
COMPUTE #CPU-ELAPSED = *CPU-TIME - #CPU-TIME
WRITE 5T ‘DIVIDE TIMES:’ *TIMD (SETA.) #CPU-ELAPSED
*
MOVE CPU-TIME TO #CPU-TIME
SETB. SETTIME
FOR #LOOP = 1 TO 100000
MOVE YEAR TO #TESTER
IF #TESTYEARA = MASK (YYYYMMDD)
IGNORE / ADD 1 TO DAYS
END-IF
END-FOR
COMPUTE #CPU-ELAPSED = *CPU-TIME - #CPU-TIME
WRITE 5T ‘MASK TIMES:’ *TIMD (SETB.) #CPU-ELAPSED
*
MOVE *CPU-TIME TO #CPU-TIME
SETC. SETTIME
FOR #LOOP = 1 TO 100000
IGNORE
END-FOR
COMPUTE #CPU-ELAPSED = *CPU-TIME - #CPU-TIME
WRITE 5T ‘FOR LOOP TIMES:’ *TIMD (SETC.) #CPU-ELAPSED
END

PAGE # 1 DATE: Mar 30, 2007
PROGRAM: CALTIM01 LIBRARY: SYSTEM
DIVIDE TIMES: 41 411
MASK TIMES: 8 75
FOR LOOP TIMES: 2 19

Note, we are not talking small numbers. After the subtraction of the FOR loop, the elapsed times show a ratio of over six to one, while the CPU ration is seven to one. In addition, although such observations are indeed subjective, I think the IF…MASK test is easier to read.