よく使う割に忘れてしまう日時の書式のメモです。
最近はタイムゾーンのオフセットを表示することが多いのですが、出力書式の選択を間違えると正しく日時、オフセットが表示されません。良く使うISOのE8601書式の例を示します。
私が良く使う書式は、E8601LX. です。それを使う理由は、海外の作業者に日時を指定するときにタイムゾーンのオフセットが無いと間違った日時でログ抽出されるからです。
SASプログラムとログを解析するユーティリティを開発するための備忘録です。This is a memo to develop utility that analyzes the SAS log.
よく使う割に忘れてしまう日時の書式のメモです。
最近はタイムゾーンのオフセットを表示することが多いのですが、出力書式の選択を間違えると正しく日時、オフセットが表示されません。良く使うISOのE8601書式の例を示します。
/* Convert the DT value to E8601LX25 format and return it to the macro variable. */
%macro formatDateTime(dt=, tz=, macvar=, debug=0);
%let &macvar=;
%local macname tmp;
%let macname=formatDateTime;
/* Check parameters */
%if %length(&dt)=0 %then %do;
%put ERROR: Missing parameter dt in &macname;
%goto exit;
%end;
%if %length(&macvar)=0 %then %do;
%put ERROR: Missing parameter macvar in &macname;
%goto exit;
%end;
/* Check time zone. */
%if %length(&tz)>0 %then %do;
%let tz=%sysfunc(dequote(&tz));
%if %sysfunc(tzoneoff(&tz))=. %then %do;
%put ERROR: The time zone offset(&tz) value cannot be obtained.;
%goto exit;
%end;
%end;
/* Format and inspect to handle even if parameter is quoted */
data _null_;
attrib dt length=$36;;
attrib now length=8 format=E8601LX25.;
attrib tz length=$32;
attrib len length=8;
attrib offset1 length=8;
attrib offset2 length=8;
attrib d length=8 format=DATE.;
attrib hh length=8;
attrib mm length=8;
attrib offset length=$8;
attrib token length=$64;
attrib token1 length=$32;
attrib token2 length=$32;
/* Remove double or single quotation marks */
dt = dequote(symget('dt'));
tz = compress(dequote(symget('tz')));
len = length(dt);
/* 1. Check E8601DX25 Format. */
if prxmatch('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}/', dt) and (len eq 25) then do;
/* Check whether it can be read as a valid date and time */
now = inputn(dt, 'E8601DX25.');
if not missing(now) then do;
call symputx('tmp', compress(dt));
end;
stop;
end;
/* 2. Check EE8601DT19 Format. */
if prxmatch('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', dt) and (len eq 19) then do;
/* Check whether it can be read as a valid date and time */
now = inputn(dt, 'E8601DT19.');
if not missing(now) then do;
length off 8;
length offsetstr $16;
if not missing(tz) then off = tzoneoff(tz);
else off = tzoneoff();
sign = ifc(off < 0, '-', '+');
offsetstr=cats(sign, put(hour(abs(off)), z2.), ':', put(minute(abs(off)), z2.));
call symputx('tmp', compress(cats(dt, offsetstr)));
end;
%if 1=0 %then %do;
/* Calculate the offset time according to the time zone */
offset1 = tzoneoff();
if missing(tz) then offset2 = offset1;
else offset2 = tzoneoff(tz);
/* Once the time zone is correctly obtained, set the date and time. */
if missing(offset2) then do;
call symputx('tmp', compress(put(now, E8601DX25.)));
end;
else if not missing(offset2) then do;
now = now + (offset2 - offset1);
/* Set the offset sign */
sign = ifc(offset2 < 0, '-', '+');
/* Construct the result string and set it to the macro variable. */
d = datepart(now);
hh=hour(abs(offset2));
mm=minute(abs(offset2));
ymd=cats(put(year(d), z4.), '-', put(month(d), z2.), '-', put(day(d), z2.));
hms=cats(put(hour(now), z2.), ':', put(minute(now), z2.), ':', put(second(now), z2.));
offset=cats(sign, put(hh, z2.), ':', put(mm, z2.));
call symputx('tmp', compress(cats(ymd, 'T', hms, offset)));
end;
%end;
stop;
end;
/* Check the DateMath format. */
re = prxparse('/now(\/(y|M|w|d|h|m|s))*(-\d+(y|M|w|d|h|m|s))*/');
start = prxmatch(re, dt);
if start > 0 then do;
token = prxposn(re, 0, dt);
if length(token) eq length(dt) then do;
if index(dt, '-')>0 then do;
token1 = substr(dt, 1, index(dt,'-')-1);
token2 = substr(dt, index(dt,'-'));
end;
else do;
token1 = dt;
token2 = '';
end;
/* If the left-hand value contains a slash symbol, round the decimal places. */
now = datetime();
if index(token1,'/') > 0 then do;
p=substr(token1, length(token1));
if p eq 's' then interval='DTSECOND';
else if p eq 'm' then interval='DTMINUTE';
else if p eq 'h' then interval='DTHOUR';
else if p eq 'd' then interval='DTDAY';
else if p eq 'w' then interval='DTWEEK';
else if p eq 'M' then interval='DTMONTH';
else if p eq 'y' then interval='DTYEAR';
else interval='DTSECOND';
now=intnx(compress(interval), now, 0, 'BEGIN');
end;
/* If there is a minus sign, adjust the date and time. */
if not missing(token2) then do;
n=input(substr(token2, 1, length(token2)-1), best.);
p=substr(token2, length(token2));
if p eq 's' then interval='DTSECOND';
else if p eq 'm' then interval='DTMINUTE';
else if p eq 'h' then interval='DTHOUR';
else if p eq 'd' then interval='DTDAY';
else if p eq 'w' then interval='DTWEEK';
else if p eq 'M' then interval='DTMONTH';
else if p eq 'y' then interval='DTYEAR';
else interval='DTSECOND';
now=intnx(compress(interval), now, n, 'SAME');
end;
call symputx('tmp', compress(put(now, E8601LX25.)));
end;
end;
run;
%exit:
%let &macvar=&tmp;
%if &debug=1 %then %put DEBUG: &macname(&=dt, &=tz, &=macvar, &=debug) &macvar=&tmp;
%mend;
80 %let result=; 81 %formatDateTime(dt="now/h-3h", macvar=result, debug=1); DEBUG: formatDateTime(DT="now/h-3h", TZ=, MACVAR=result, DEBUG=1) result=2026-02-20T10:00:00+09:00 82 %let result=; 83 %formatDateTime(dt="2026-02-20T00:00:00", tz="America/New_York", macvar=result, debug=1); DEBUG: formatDateTime(DT="2026-02-20T00:00:00", TZ=America/New_York, MACVAR=result, DEBUG=1) result=2026-02-20T00:00:00-05:00 84
%macro formatSerialDateTime(dt=, tz=, macvar=, debug=0);
%let &macvar=;
%local notes tmp;
/* Suppress NOTE logs */
%let notes=%sysfunc(getoption(notes));
options nonotes;
/* Set default value */
%if %length(&dt)=0 %then %let dt=%sysfunc(datetime());
%let tmp=;
data _null_;
attrib result length=$64;
attrib dt length=8 label='datetime';
attrib tz length=$32 label='timezone';
attrib offset1 length=8 label='Default offset';
attrib offset2 length=8 label='Time zone offset specified by the parameter';
/* Remove quotation marks from parameters */
dt = dequote(symget('dt'));
tz = dequote(symget('tz'));
/* Calculate the offset time according to the time zone */
offset1 = tzoneoff();
if missing(tz) then offset2 = offset2;
else offset2 = tzoneoff(tz);
/* Once the time zone is correctly obtained, set the date and time. */
if not missing(offset2) then do;
dt = dt + (offset2 - offset1);
/* Set the offset sign */
if offset2<0 then sign= "-";
else sign="+";
/* Construct the result string and set it to the macro variable. */
length year month day hour minute second hh mm 8;
length offset $8;
year = year(datepart(dt));
month = month(datepart(dt));
day = day(datepart(dt));
hour = hour(dt);
minute = minute(dt);
second = second(dt);
hh = hour(abs(offset2));
mm = minute(abs(offset2));
ymd = cats(put(year,z4.), '-', put(month, z2.), '-', put(day,z2.));
hms = cats(put(hour,z2.), ':', put(minute,z2.), ':', put(second,z2.));
offset = cats(sign, put(hh,z2.), ':', put(mm,z2.));
result = cats(ymd, 'T', hms, offset);
end;
call symputx('tmp', compress(result));
run;
%if &syserr=0 %then %let &macvar=&tmp;
options ¬es;
%if &debug=1 %then %put DEBUG: formatSerialDateTime(&=dt, &=tz, &=macvar, &=debug) &macvar=&&&macvar;
%mend;
%macro test_foo;
%local now;
%let now=%sysfunc(datetime());
%let result=;
%formatSerialDateTime(dt=&now, tz="Asia/Tokyo", macvar=result, debug=1);
%put &=result;
%let result=;
%formatSerialDateTime(dt=&now, tz='America/New_York', macvar=result, debug=1);
%put &=result;
%let result=;
%formatSerialDateTime(dt=&now, tz='Europe/Istanbul', macvar=result, debug=1);
%put &=result;
%let result=;
%formatSerialDateTime(dt=&now, tz='Asia/Kolkata', macvar=result, debug=1);
%put &=result;
%let result=;
%formatSerialDateTime(dt=&now, tz='UTC', macvar=result, debug=1);
%put &=result;
%mend;
options nomprint nosource nomlogic nosymbolgen;
%test_foo;
/* Remove quotation marks from parameter values */
%let macvar=%sysfunc(dequote(%superq(macvar)));
/* Check missing parameter */
%local /readonly list=from to step query out macvar debug;
%local i n item;
%let n = %sysfunc(countw(&list));
%do i = 1 %to &n;
%let item = %scan(&list, &i);
%if %length(&&&item)=0 %then %do;
%put ERROR: Missing parameter &item in %sysfunc(lowcase(&sysmacroname));
%goto exit;
%end;
%end;
/* Assign a temprary file reference name */
%let macvar=_RF%sysfunc(putn(%sysfunc(monotonic()), z5.));
/* Clear the file reference name */
%if %sysfunc(fileref(&macvar))<=0 %then %do;
filename &macvar clear;
%end;
とある案件で操作説明の動画を作るように依頼されて仕方なくやってみた。そのときにコツと思える箇所があったので、他の人に役立つようにメモする。
良く使うけど、良く忘れるファイル名の操作のマクロをまとめておきます。DATAステップは使わずにマクロの文だけで作っているのでDATAステップの中でも値をはめこみ易いのです。高度なエディタだとマクロの終端を認識してくれないことがあるので、これらのマクロは最後の方に書きます。
このマクロの弱点は、括弧とか特殊文字がファイル名やディレクトリに混じると文字列の終端が判定できなくてERRORになることです。括弧を含む又は括弧の開始があって終了がないようなときはERRORになるので要注意です。
/* Strip directory and suffix from filenames. */
%macro basename(path);
%local dequotePath orgLength dequoteLength lastSlash x;
%let dequotePath=%sysfunc(dequote(&path));
%let orgLength=%length(&path);
%let dequoteLength=%length(&dequotePath);
%let lastSlash=%sysfunc(findc("&dequotePath", "/", -%length(&dequotePath)));
%let x=%substr(&dequotePath, %eval(&lastSlash));
%quote(&x)
%mend;
/* Strip non-directory suffix from file name */
%macro dirname(path);
%local dequotePath orgLength dequoteLength lastSlash x;
%let dequotePath=%sysfunc(dequote(&path));
%let orgLength=%length(&path);
%let dequoteLength=%length(&dequotePath);
%let lastSlash=%sysfunc(findc("&dequotePath", "/", -%length(&dequotePath)));
%let x=%substr(&dequotePath, 1, %eval(&lastSlash-2));
%quote(&x)
%mend;
/* Return number of obs. */
%macro nobs(mydata);
%local NOBS;
%let NOBS=0;
%if %sysfunc(exist(&mydata)) %then %do;
%let mydataID=%sysfunc(OPEN(&mydata., IN));
%let NOBS=%sysfunc(ATTRN(&mydataID, NOBS));
%let RC=%sysfunc(CLOSE(&mydataID));
%end;
&NOBS
%mend;