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    

2026年1月30日金曜日

日時のシリアル値をタイムゾーンのオフセットありの書式へ変換するマクロ

タイムゾーンを指定して日時の書式を設定

タイムゾーンを指定して E8601DXの書式でフォーマットするマクロです。入力は日時のシリアル値、結果は macvar で指定した変数に返ります。

  • 入力のパラメータ DT=2085402932.92693, TZ='America/New_York'
  • 結果 2026-01-30T00:35:33-05:00
SASシステムオプションでTZ変えて書式整えれば良いのですが、システム的に変更を禁止されている場合があります。あと、なんかテストしていて正しく書式が定義されないので自分でロジックを組んでしまったのです。
最近この日時の書式を使うようにしています。理由は海外のエンジニアにログとかメトリクスの抽出を依頼するときに、タイムゾーンがはっきりと分かる方が便利だからです。

作成したマクロ  


%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;




2026年1月17日土曜日

よく使うコードの書き方をスニペットに登録して時間節約

何故 Snippet を使うのか?

よく使う割に書き方を忘れてしまうことがあります。そんなときにSAS Studioのスニペットが役立つということを遅ればせながら実感しました。スニペットは昔からある機能ですが、何故か敬遠というか存在を無視して使っていませんでした。

しかし、頻発する「あれはどうやって書くのか?」という度に検索したり、自分のBlogを読み返したりするのは時間が惜しくなってきました。で、典型例を探して自分のスニペットに登録し始めました。

いまボランティアワークでPrometheusからデータを取得する仕組みを作っています。SAS Viya Monitoring for Kubernetes からメトリクスを取得してシステム管理に役立つ何かを得ようとしています。そんな中で開発をちょっと効率化したいと思ったわけです。

私が登録しているSnippet

参考までに私が登録しているマクロを示します。
  • マクロ変数の値から引用符を取り除く
  • マクロのパラメータで値が無い項目をチェックしてエラーメッセージ出力
  • テンポラリのファイル参照名を定義
  • ファイル参照名が定義されているか判定してクリア
  • その他、PROC HTTPの例とか追加する予定

感じるメリット

書いてみると分かるのですが、諳んじて書くのと注意してコメントまで入れてロジックの穴を点検するのでは気づきがあります。例えばエラー処理で %goto でマクロの終端にジャンプするというのは統一していると方がより良いです。

また分かりやすいコメントも含めておくとより良いです。最近はCopilotに相談しながら英語圏の人でも分かりやすいコメントを入れるようにしています。急いで書くと不自然なコメントになってしまいます。

コードの例  

/* 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;

2025年6月8日日曜日

ClipChampの使い方メモ、最短で学ぶ

 とある案件で操作説明の動画を作るように依頼されて仕方なくやってみた。そのときにコツと思える箇所があったので、他の人に役立つようにメモする。

  1.  目的に合わせて多少操作が迷ってスマートじゃなくても動画 MP4のファイルを作るのが優先
  2. MP4のファイルはClipChampで分割、複製できるので割らなくても良い
  3. タイトル、トラジション、説明画像はPPTで作成して .PNG でエクスポート
  4. ClipChampで、MP4の動画と.PNGをインポート
  5. 動画の余分な部分はスプリットして削除
  6. スプリットした間にタイトル、セクションや解説の.PNGを挿入
  7. 動画の一部を拡大したいときは複製してクロップで切り抜いてからサイズ調整
  8. 協調したいところにテキストや矢印を配置
  9. 動画やスライドの継ぎ目にトラジションの効果を入れる
  10. 音声合成で読み上げの文書を入れる
  11. その後で自動でキャプション作成し、生成されたトランスクリプションを手修正
  12. 必要に応じて音楽を足す
  13. MP4エクスポートして保存

1.まずは操作をTeams会議で録画して動画をつくる

多少、操作が覚束ないところがあっても後でスプリットして削除できる。間に「この部分は割愛」と表示して切り取る。またはスプリットで分けた期間を3倍速にして巻きで再生することができる。

2. 動画はあらかじめ分割しなくてもOK

ClipChampの中で動画を分割、複数コピーすることができる。同じ動画を複製して拡大部分を表示することも可能です。

3. タイトル、セクション、説明用のスライドはPPTで作成

細かい図や説明はPPTで作る方が楽だと思う。

4. ClipChamp で、MP4の動画と.PNGをインポート

これは+ボタンをクリックしてメディアを足すだけで簡単操作です。

5. 動画の余分な部分はスプリットして削除

動画を再生しながら切り取りたいところで停止してハサミのアイコンで分割します。

6. スプリットした間にタイトル、セクションや解説の.PNGを挿入

スプリットした動画の間隔を広げて、その空いた隙間の枠に.PNGを落とせばセクション区切りができます。セクション区切りを表示する期間はマウス、キー入力で調整できる。スプリットしてその間を削除しても、インポートしたメディアからその期間が消えてしまうわけではないので、バシバシスプリットして良いです。

7. 動画の一部を拡大したいときは複製してクロップ&サイズ調整

同じ動画を別なトラックにコピーして、クロップで切り取り拡大すると注目してほしい箇所をオーバーレイで表示できます。

8. 協調したいところにテキストや矢印を配置

これも操作は直観的にできるので説明省略

9. 動画やスライドの継ぎ目にトラジションの効果を入れる

タイトルと動画、セクションと動画の変わるところにトラジションの効果を入れると、場面が変わるタイミングがわかって視聴者の注意力を助ける効果が期待できる。

10. 音声合成で読み上げの文書を入れる

説明のキャプションをテキストで入れる方法もあるが、「録画と作成」から「音声変換」でテキストを打ち込み説明を入れるのが先。その理由は後からその音声でキャプションを作れるから。自分の声を当てたい人は別ね。

11. 自動でキャプション作成し、生成されたトランスクリプションを手修正

これも操作で迷わないと思う。右上のキャプションから自動キャプションを生成する。生成されたトランスクリプションの表示位置が分かりずらいが、右側の下に表示される。領域が縦長でかつカーソルの位置が見づらいので編集しにくい。

12. 必要に応じて音楽を足す

眠気防止の効果は有りそうだ。

13. MP4エクスポートして保存

社内だとクラウドで共有だが、外部に提供するときはMP4にエクスポートします。

2025年5月29日木曜日

ライブラリ参照名を定義しないでデータセットを格納する例

 ライブラリ参照名をテンポラリで作りたくないときがあります。そんなときにはディレクトリを指定してライブラリ参照名を定義しない使い方があります。古いTipsだけど忘れていた。



2025年5月13日火曜日

basename, dirname, nobs

良く使うけど、良く忘れるファイル名の操作のマクロをまとめておきます。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;

2024年12月6日金曜日

get-k8s-info.sh Viyaの情報をまとめて集めるツール

 Viyaの運用を行うのであれば常備しておきたいツールです。テクニカルサポート問合せのときに使います。実行方法はREADMEのCommand-Line Mode Exampleを見てください。デプロイしたときの site.yaml のあるディレクトリを引数で指定します。

リンクが間違っていたので修正

get-k8s-info.sh Script