Date and Time Conversions
Автор: Dr. Brian Marks, Formcroft Ltd.
Источник: The Rexx Language Association
Built-in functions which convert between different formats of dates and times were on the Rexx programmers' "wish-list" for a long time. The reasons for this are clear. In addition to improved presentation of dates, conversion means that simple programming can answer questions like "How many days are between these dates?" and "What is the date of the first Wednesday in May next year?".
When the committee developing the ANSI standard discussed the matter in 1993 there was already a proposal from Mike Cowlishaw on the syntax to be used: second and third arguments allowed on the TIME and DATE built-ins that would specify a value to be converted and the format that value was in. Only the first letter of the format specifier is significant, so we talk of format options as letters. The committee work was in deciding which formats could be converted to which formats, and deciding the actual conversion algorithms.
For dates, the letters B,D,E,N,M,O,S,U, and W are familiar to programmers for specifying the format of the current date. M and W are not suitable formats for a date value that is to be converted because Month or Weekday alone is not enough to specify a date. Some of the allowed conversions have to "guess" the century. These conversions might have been prohibited, but that would be harsh since a "guessing rule" can be chosen which is almost certain to be right for some applications, for example for your personal diary. However, particular applications may need to use better rules than the one chosen for Rexx, for example in converting birth dates, since those dates are known to be in the past.
For times, the letters for conversion formats are C,H,L,M,N and S; elapsed times are not involved.
The details of the formats that are acceptable for conversion are the same as the details of the output when the current date or time is requested. So, for example, 2:54pm is acceptable as a time to be converted but 02:54pm is not.
Rexx has always considered TIME and DATE as associated within a clause, so if you SAY DATE() TIME() around midnight there is no risk that you will get the date from one day and the time within another day. That, however, is not enough to make DATE&TIME a true timestamp, where the same value never occurs twice from the same source of times. The committee added a rule that the time in microseconds will increase between calls and provided TIME('O') to determine the effect of local daylight saving etc. With these additions a true timestamp can be constructed.
Here is the status of the IBM support for these parts of the standard from Christian Michel, Object Rexx Development, GSDL Boeblingen Germany:
"The two problems of the DATE function in Object Rexx described by Brian Marks have been identified and corrected. Converting to the 'S' format will now preserve leading zeros as required by the ANSI standard. We have also added the conversion from the 'D' format. This functionality apparently was left out of the Object Rexx implementation at the time the ANSI standard was finalized. The updated DATE function will be available with bug fix releases of Object Rexx. These will either be distributed via the OS/2 service channels for Object Rexx included in OS/2 Warp V4, or through our Object Rexx for Windows 95/NT service pages on the Internet."
In September 1997 the REXX standardizing committee proposed extra arguments on DATE coversions to allow the character used as the separator between fields to be specified. You can have this feature, and all other time and date conversions, by adding the following code to your program, even if the product you are using does not provide date conversions.
/*---------------------------------------------------------------------------*/
/* Time Bif Specifications */
/* Acknowledgements: Klaus Hansjakob provided the basic algorithm for */
/* time conversion. Brian Marks provided the coding specific to the */
/* Rexx builtin functions. Ian Collier and Kurt Maerker have provided */
/* corrections to errors. */
/* */
/* Note: This version has the extensions which the Rexx standardizing */
/* committee proposes for conversion of delimiters. That is not part */
/* of the current Rexx standard. */
/*---------------------------------------------------------------------------*/
Time: procedure
/* This routine is essentially the code from the standard, put in
stand-alone form. The only 'tricky bit' is that there is no Rexx way
for it to fail with the same error codes as a "real" implementation
would. It can however give a SYNTAX error, albeit not the desirable
one. This causing of an error is done by returning with no value.
Since the routine will have been called as a function, this produces
an error. */
/* Backslash is avoided as some systems don't handle that negation sign. */
if arg()>3 then
return
numeric digits 18
if arg(1,'E') then
if pos(translate(left(arg(1),1)),"CEHLMNRS")=0 then
return
/* (The standard would also allow 'O' but what this code is running
on would not.) */
if arg(3,'E') then if pos(translate(left(arg(3),1)),"CHLMNS")=0 then
return
/* If the third argument is given then the second is mandatory. */
if arg(3,'E') & arg(2,'E')=0 then
return
/* Default the first argument. */
if arg(1,'E') then
Option = translate(left(arg(1),1))
else
Option = 'N'
/* If there is no second argument, the current time is returned. */
if arg(2,'E') = 0 then
if arg(1,'E') then
return 'TIME'(arg(1))
else
return 'TIME'()
/* One cannot convert to elapsed times. */
if pos(Option, 'ERO') > 0 then
return
InValue = arg(2)
if arg(3,'E') then
InOption = arg(3)
else
InOption = 'N'
HH = 0
MM = 0
SS = 0
HourAdjust = 0
select
when InOption == 'C' then do
parse var InValue HH ':' . +1 MM +2 XX
if HH = 12 then
HH = 0
if XX == 'pm' then
HourAdjust = 12
end
when InOption == 'H' then
HH = InValue
when InOption == 'L' | InOption == 'N' then
parse var InValue HH ':' MM ':' SS
when InOption == 'M' then
MM = InValue
otherwise
SS = InValue
end
if datatype(HH,'W')=0 | datatype(MM,'W')=0 | datatype(SS,'N')=0 then
return
HH = HH + HourAdjust
/* Convert to microseconds */
Micro = trunc((((HH * 60) + MM) * 60 + SS) * 1000000)
/* There is no special message for time-out-of-range; the bad-format
message is used. */
if Micro 24*3600*1000000 then
return
/* Reconvert to further check the original. */
if TimeFormat(Micro,InOption) == InValue then
return TimeFormat(Micro, Option)
return
TimeFormat: procedure
/* Convert from microseconds to given format. */
/* The day will be irrelevant; actually it will be the first day possible. */
x = Time2Date2(arg(1))
parse value x with Year Month Day Hour Minute Second Microsecond Base Days
select
when arg(2) == 'C' then
select
when Hour>12 then
return Hour-12':'right(Minute,2,'0')'pm'
when Hour=12 then
return '12:'right(Minute,2,'0')'pm' when Hour>0 then
return Hour':'right(Minute,2,'0')'am'
when Hour=0 then
return '12:'right(Minute,2,'0')'am'
end
when arg(2) == 'H' then return Hour
when arg(2) == 'L' then
return right(Hour,2,'0')':'right(Minute,2,'0')':'right(Second,2,'0'),
|| '.'right(Microsecond,6,'0')
when arg(2) == 'M' then
return 60*Hour+Minute
when arg(2) == 'N' then
return right(Hour,2,'0')':'right(Minute,2,'0')':'right(Second,2,'0')
otherwise /* arg(2) == 'S' */
return 3600*Hour+60*Minute+Second
end
Time2Date:
/* These are checks on the range of the date. */
if arg(1) = 315537897600000000 then
return 'Bad'
return Time2Date2(arg(1))
Time2Date2: Procedure
/* Convert a timestamp to a date.
Argument is a timestamp (the number of microseconds relative to
0001 01 01 00:00:00.000000)
Returns a date in the form:
year month day hour minute second microsecond base days */
/* Argument is relative to the virtual date 0001 01 01 00:00:00.000000 */
Time = arg(1)
Second = Time % 1000000 ; Microsecond = Time // 1000000
Minute = Second % 60 ; Second = Second // 60
Hour = Minute % 60 ; Minute = Minute // 60
Day = Hour % 24 ; Hour = Hour // 24
/* At this point, the days are the days since the 0001 base date. */
BaseDays = Day
Day = Day + 1
/* Compute either the fitting year, or some year not too far earlier.
Compute the number of days left on the first of January of this year. */
Year = Day % 366
Day = Day - (Year*365 + Year%4 - Year%100 + Year%400)
Year = Year + 1
/* Now if the number of days left is larger than the number of days
in the year we computed, increment the year, and decrement the
number of days accordingly. */
do while Day > (365 + Leap(Year))
Day = Day - (365 + Leap(Year))
Year = Year + 1
end
/* At this point, the days left pertain to this year. */
YearDays = Day
/* Now step through the months, increment the number of the month,
and decrement the number of days accordingly (taking into
consideration that in a leap year February has 29 days), until
further reducing the number of days and incrementing the month
would lead to a negative number of days */
Days = '31 28 31 30 31 30 31 31 30 31 30 31'
do Month = 1 to words(Days)
ThisMonth = Word(Days, Month) + (Month = 2) * Leap(Year)
if Day <= ThisMonth then leave
Day = Day - ThisMonth
end
return Year Month Day Hour Minute Second Microsecond BaseDays YearDays
Leap: procedure
/* Return 1 if the year given as argument is a leap year, or 0
otherwise. */
return (arg(1)//4 = 0) & ((arg(1)//100 <> 0) | (arg(1)//400 = 0))
/*---------------------------------------------------------------------------*/
/* Date Bif Specifications */
/*---------------------------------------------------------------------------*/
date: procedure
/* This routine is essentially the code from the standard, put in
stand-alone form. The only 'tricky bit' is that there is no Rexx way
for it to fail with the same error codes as a "real" implementation
would. It can however give a SYNTAX error, albeit not the desirable
one. This causing of an error is done by returning with no value.
Since the routine will have been called as a function, this produces
an error. */
if arg() > 5 then return
numeric digits 18
if arg(1,'E') then
if pos(translate(left(arg(1),1)),"BDEMNOSUW")=0 then
return
if arg(3,'E') then
if pos(translate(left(arg(3),1)),"BDENOSU")=0 then
return
/* If the third argument is given then the second is mandatory. */
if arg(3,'E') & arg(2,'E')=0 then
return
/* Default the first argument. */
if arg(1,'E') then /* OutOption */
Option = translate(left(arg(1),1))
else
Option = 'N'
/* If there is no second argument, the current time is returned. */
if arg() <= 1 then
if arg(1,'E') then
return 'DATE'(arg(1))
else
return 'DATE'()
if arg(3,'E') then /* InOption */
InOption = arg(3)
else
InOption = 'N'
/*>> In September 97 the standardizing committee decided how DATE should <<
>> be extended to generalize the separators used. <
if Option == 'S' then
OutSeparator = ''
else
OutSeparator = translate(Option,"xx/x //x","BDEMNOUW")
if arg(4,'E') then do /* OutSeparator */
/*-----------------------------------------------------------------------*/
/* The text for the following error 40.46 is: */
/* '<bif> argument <argnumber>, "<value>", is a format incompatible with */
/* separator specified in argument <argnumber>' */
/*-----------------------------------------------------------------------*/
if OutSeparator == 'x' then
return
OutSeparator = arg(4)
/*-----------------------------------------------------------------------*/
/* The text for the following error 40.45 is; */
/* '<bif> argument <argnumber> must be a single non-alphanumeric */
/* character or the null string; found <value>"' */
/*-----------------------------------------------------------------------*/
if length(OutSeparator) > 1 | datatype(OutSeparator,'A') then
return
end
if InOption == 'S' then
InSeparator = ''
else
InSeparator = translate(InOption,"xx/ //","BDENOU")
if arg(5,'E') then do /* InSeparator */
if InSeparator == 'x' then
return
InSeparator = arg(5)
if length(InSeparator) > 1 | datatype(InSeparator,'A') then
return
end
/* English spellings are used, even if messages not in English are used. */
Months = 'January February March April May June July',
'August September October November December'
WeekDays = 'Monday Tuesday Wednesday Thursday Friday Saturday Sunday'
Value = arg(2)
/* First try for Year Month Day */
Logic = 'NS'
select
when InOption == 'N' then do
if InSeparator == '' then do
if length(Value)<9 then return
Year = right(Value,4)
MonthIs = substr(right(Value,7),1,3)
Day = left(Value,length(Value)-7)
end
else
parse var Value Day (InSeparator) MonthIs (InSeparator) Year
do Month = 1 to 12
if left(word(Months, Month), 3) == MonthIs then leave
end Month
end
when InOption == 'S' then
if InSeparator == '' then
parse var Value Year +4 Month +2 Day
else
parse var Value Year (InSeparator) Month (InSeparator) Day
otherwise
Logic = 'EOU' /* or BD */
end
/* Next try for year without century */
if logic = 'EOU' then
Select
when InOption == 'E' then
if InSeparator == '' then
parse var Value Day +2 Month +2 YY
else
parse var Value Day (InSeparator) Month (InSeparator) YY
when InOption == 'O' then
if InSeparator == '' then
parse var Value YY +2 Month +2 Day
else
parse var Value YY (InSeparator) Month (InSeparator) Day
when InOption == 'U' then
if InSeparator == '' then
parse var Value Month +2 Day +2 YY
else
parse var Value Month (InSeparator) Day (InSeparator) YY
otherwise
Logic = 'BD'
end
if Logic = 'EOU' then do
/* The century is assumed, on the basis of the current year. */
if datatype(YY,'W')=0 then
return
YearNow = left('DATE'('S'),4)
Year = YY
do while Year < YearNow-50
Year = Year + 100
end
end /* Century assumption */
if Logic <> 'BD' then do
/* Convert Month & Day to Days of year. */
if datatype(Month,'W')=0 | datatype(Day,'W')=0 | datatype(Year,'W')=0 then
return
Days = word('0 31 59 90 120 151 181 212 243 273 304 334',Month),
+ (Month>2)*Leap(Year) + Day-1
end
else
if datatype(Value,'W')=0 then
return
if InOption == 'D' then do
Year = left('DATE'('S'),4)
Days = Value - 1 /* 'D' includes current day */
end
/* Convert to BaseDays */
if InOption <> 'B' then
BaseDays = (Year-1)*365 + (Year-1)%4 - (Year-1)%100 + (Year-1)%400 + Days
else
Basedays = Value
/* Convert to microseconds from 0001 */
Micro = BaseDays * 86400 * 1000000
/* Reconvert to check the original. (eg for Month = 99) */
if DateFormat(Micro,InOption,InSeparator) == Value then
return DateFormat(Micro,Option,OutSeparator)
return
DateFormat:
/* Convert from microseconds to given format and separator. */
x = Time2Date(arg(1))
if x = 'Bad' then
return 'Bad'
parse value x with Year Month Day Hour Minute Second Microsecond Base Days
select
when arg(2) == 'B' then
return Base
when arg(2) == 'D' then
return Days
when arg(2) == 'E' then
return right(Day,2,'0')(arg(3))right(Month,2,'0')(arg(3))right(Year,2,'0')
when arg(2) == 'M' then
return word(Months,Month)
when arg(2) == 'N' then
return (Day)(arg(3))left(word(Months,Month),3)(arg(3))right(Year,4,'0')
when arg(2) == 'O' then
return right(Year,2,'0')(arg(3))right(Month,2,'0')(arg(3))right(Day,2,'0')
when arg(2) == 'S' then
return right(Year,4,'0')(arg(3))right(Month,2,'0')(arg(3))right(Day,2,'0')
when arg(2) == 'U' then
return right(Month,2,'0')(arg(3))right(Day,2,'0')(arg(3))right(Year,2,'0')
otherwise /* arg(2) == 'W' */
return word(Weekdays,1+Base//7)
end
/* It must be a variant of Murphy's law that if you write some code that
others might use it turns out that the code depends on something that
different interpreters treat differently. In this particular case,
interpreters differ on whether the error of a function failing to
return a result is an error that the level calling the function sees,or an error that the function itself sees. */
GoodDate: procedure
signal on syntax name Better_Be_Unique1
/* Next two clauses are deliberately on the same line. */
GoodDateSigl = RecordSigl(); GoodDateResult = date(arg(2),arg(1),arg(2))
if GoodDateResult='*' then
return 0
return 1
Better_Be_Unique1:
if sigl==GoodDateSigl then
/* This code being run by interpreter that raises error in the caller */
return 0
/* This code being run by interpreter that raises error in the callee */
return '*'
RecordSigl:
return sigl
GoodTime: procedure
signal on syntax name Better_Be_Unique2
/* Next two clauses are deliberately on the same line. */
GoodTimeSigl = RecordSigl(); GoodTimeResult = time(arg(2),arg(1),arg(2))
if GoodTimeResult='*' then
return 0
return 1
Better_Be_Unique2:
if sigl==GoodTimeSigl then
/* This code being run by interpreter that raises error in the caller */
return 0
/* This code being run by interpreter that raises error in the callee */
return '*'