2026年2月20日金曜日

DateMathで日時の指定を簡単に

DateMathとは?

日時を指定するときの書き方で"now"とか"now-3h"と表記します。ユーティリティのマクロで日時を指定するときに細かく年月日を書くのは意外と面倒です。なのでDateMathの記法が使えると使い易いマクロができます。DateMathの記法の例を示します。
  • now
  • now-30m
  • now-3h
  • now-3d
  • now-1w
日時を切りよくまとめる記法はスラッシュを使います。now/hであれば現在の時刻を0分に、now/d ならば午前0時0分に合わせます。
  • now/m
  • now/h
  • now/d
  • now/h-3h
  • now/d-1d

日時の書式を統一するマクロ

私は最近日時をオフセット(2026-02-20T13:45+09:00)付きで表すことが多いです。理由は海外の作業者にログを抽出するときに迷いが無いから。というかよく間違えて異なる時間帯で抽出されることが多い。そのDateMathの記法も含めてパラメータで渡した日時を E8601DX25.の書式に合わせるマクロを作りました。コードを読んで使えそうであれば試してみてください。

/* 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;
使い方の例はこちらです。オフセットの時間が無いE8601DT19の場合はパラメータTZでタイムゾーンのオフセットを指定することができます。書式が間違っているときにはmacvarのマクロ変数の値が欠損になります。長さ0の値であればエラー特別できます。
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