◇Windowsのバッチファイルでラップタイム（実行時間）を計測する

# ◇ラップタイム（実行時間）を計測する
ＰＣやプログラムなどの実行時間を計測したいときに使用できるバッチファイルです。
Ｐｙｔｈｏｎ実行用のバッチファイルに仕込んでいたものを独立させて使えるようにしました。
案外使い道は広くできたと思います。


### 使用方法
使用方法はサンプルのように使いたいバッチに```Call```を使って本バッチファイルを呼び出すだけです。
2度目の呼び出し時にＥＲＲＯＲＬＥＶＥＬに結果を返します。
計測には開始日時を記録するためのログファイルを作成しますが、引数にログファイルのパスを指定した場合は計測終了後もログファイルは残ります。
ログファイルを指定することで開始と終了はまったく別のバッチファイルなどから利用できます。
同じファイルにログの記録も追記することもできます。
ログファイルが残っていれば計測できるため、ひとつのバッチファイルである必要はありませんし複数の計測もでき様々な計測に利用可能です。


### 基本動作
- 計測開始時にログファイルを作成、開始日時を書き込みます。
 （事前にログファイルを削除しないと望んだ処理にならない場合があります）
- ログファイルを指定していない場合、自動で作成され計測終了時にファイルは削除されます。
- ログファイルを指定していた場合、ファイルに実行状況を追記できます。
- ログファイルを指定していた場合、計測終了時にログファイルに終了日時、実行時間を追記します。
- 計測終了時にエラーレベルに実行時間を返します。

エラーレベルに返される実行時間は数値9桁のHHHHHMMSSの形式になります。
HHHHH時間（５桁）MM分（２桁）SS秒（２桁）です。
99999時間以上の計測が可能なので単純計算で11年以上の計測ができます。


### バッチファイル

<details><summary>ラップタイムを計測するバッチファイル</summary>

```bat:ElapseDayTime.bat
@echo off


:: 遅延展開 バグの原因になる場合があるので注意;
setlocal enabledelayedexpansion


:: 引数を編集しやすいように加工;
set ARGC=0&&set ARGV=
for %%a in (%*) do (set /a ARGC+=1&&set ARGV!ARGC!=%%a)


:: 現在の日時を取得;
for /f "tokens=1-7 delims=/:. " %%1 in ("%date% %time: =0%") do (set CurrentYear=%%1&&set CurrentMM=%%2&&set CurrentDD=%%3&&set CurrentH=%%4&&set CurrentM=%%5&&set CurrentS=%%6&&set CurrentMS=%%7)
set CurrentDateTime=%CurrentYear%/%CurrentMM%/%CurrentDD% %CurrentH%:%CurrentM%:%CurrentS%


:: 引数別の処理;
if %ARGC% equ 0 (
  set LogFolder=%~dp0
  set LogFile=%~dpn0.log
)
if %ARGC% equ 1 (
  set LogFolder=%~dp1
  set LogFile=%~f1
)


:: ログ記録用フォルダが存在しない場合はエラー;
if not exist "%LogFolder%" (exit /b -1)


:: ファイルが無い場合は初回と判断してファイル作成
if not exist "%LogFile%" (
  echo %CurrentYear%/%CurrentMM%/%CurrentDD% %CurrentH%:%CurrentM%:%CurrentS%>"%LogFile%"
  exit /b 0
)


:: ファイルが存在すればファイルから日時を読み込み;
call :ReadFirstEndLine "%%LogFile%%"
set StartDateTime=!FirstLineData!
for /f "tokens=1-6 delims=/: " %%1 in ("!StartDateTime!") do (set StartYear=%%1&&set StartMM=%%2&&set StartDD=%%3&&set StartH=%%4&&set StartM=%%5&&set StartS=%%6)


:: 開始日時と終了日時から日数と秒数を計算;
call :CalcDayTime "%%StartDateTime%%"
set StartDays=%Days%
set StartTimes=%Times%
call :CalcDayTime "%%CurrentDateTime%%"
set EndDays=%Days%
set EndTimes=%Times%


:: ラップタイムの計算と表示フォーマットの成型;
set /a LAPSeconds = ((%EndDays% - %StartDays%) * 24 * 60 * 60) + %EndTimes% - %StartTimes%
call :TimeDisplayFormat "%%LAPSeconds%%"
set LAPTimes=%H%%M%%S%
set DisplayFormat=%H%:%M%:%S%


:: 引数別の処理;
if %ARGC% equ 0 (
  del %LogFile%
)
if %ARGC% equ 1 (
  echo %CurrentYear%/%CurrentMM%/%CurrentDD% %CurrentH%:%CurrentM%:%CurrentS%>>"%LogFile%"
  echo %DisplayFormat%>>"%LogFile%"
)


:: 終了;
if "%DEBUG%" equ "1" (call :DisplayData)
exit /b %LAPTimes%


:: 指定ファイルの最初と最後の一行を読み込む
:ReadFirstEndLine
set ReadFile=%1
for /f "usebackq tokens=*" %%a in (%ReadFile%) do (set FirstLineData=%%a&&goto :Break_Read)
:Break_Read
:: goto抜けのおまじない;
for /f "usebackq tokens=*" %%a in (%ReadFile%) do (set EndLineData=%%a)
exit /b


:: グレゴリオ暦から日数を計算;
:CalcDayTime
set DateTime=%1
set DateTime=%DateTime:~1,-1%
for /f "tokens=1-6 delims=/: " %%1 in ("%DateTime%") do (set YYYY=%%1&&set MM=%%2&&set DD=%%3&&set H=%%4&&set M=%%5&&set S=%%6)
:: 8進数対策（0から始まる数値を計算できるように変換）;
set /a Year   = 1%YYYY% - 10000
set /a Month  = 1%MM%   - 100
set /a Day    = 1%DD%   - 100
set /a Hour   = 1%H%    - 100
set /a Minute = 1%M%    - 100
set /a Second = 1%S%    - 100
:: グレゴリオ暦の計算式で計算した場合のユリウス暦の日数の誤差;
set /a DAYS_JinG = 0 - 3
:: グレゴリオ暦の元年からの日数を計算;
if %Month% leq 2 (set /a Year -= 1 && set /a Month += 12)
set /a GY = (%Year% - 1) * 365
set /a GL = (%Year% / 4) - (%Year% / 100) + (%Year% / 400)
set /a GM = ((%Month% * 979) - 1033) / 32
set /a GD = %GY% + %GL% + %GM% + %Day% - 1
set /a Days  = %GD% + %DAYS_JinG%
set /a Times = (%Hour% * 60 * 60) + (%Minute% * 60) + %Second%
exit /b


:: 秒をHHHHMMSS形式に加工;
:TimeDisplayFormat
set Times=000000000%1
set Times=%Times:~-10%
set /a Times = 1%Times% - 100000000
set /a H = (%Times% / 60) / 60
set /a M = (%Times% - (%H% * 60 * 60)) / 60
set /a S = %Times% - (%H% * 60 * 60) - (%M% * 60)
set H=00000%H%
set H=%H:~-5%
set M=0%M%
set M=%M:~-2%
set S=0%S%
set S=%S:~-2%
exit /b
```
</details>


<details><summary>ラップタイムを計測するサンプル</summary>

```bat:LapTime.bat
@echo off


:: %ESC%でエスケープシーケンスを利用できるようにする;
for /f "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (set ESC=%%b)


echo.
echo %ESC%[102mラップタイムの計測デモンストレーション%ESC%[0m
echo.


:: ファイルが存在すればラップタイムを計算;
set LAPFILE=%~dpn0.log
if exist "%LAPFILE%" (goto :LapTime)


:: 計測開始;
echo --------------------------------------
echo 計測開始
echo.
call ElapseDayTime.bat "%LAPFILE%"
if %ERRORLEVEL% equ -1 (
  echo ERROR
  goto :EndProcess
)


echo おためし中>>"%LAPFILE%"


:: 待機処理;
timeout.exe /t 3 /nobreak
echo %ESC%[2A%ESC%[0J
echo %ESC%[1A%ESC%[0K%ESC%[2A
echo.


:LapTime
:: 計測終了;
call ElapseDayTime.bat "%LAPFILE%"
set LapTimes=00000000%ERRORLEVEL%
set LapTimes=%LapTimes:~-9%
set H=%LapTimes:~0,5%
set M=%LapTimes:~5,2%
set S=%LapTimes:~-2%
echo 計測終了
echo LapTimes = %LapTimes%
echo Lap Time = %H%:%M%:%S%
echo --------------------------------------
echo Type "%LAPFILE%"
echo --------------------------------------
Type "%LAPFILE%"
echo --------------------------------------
echo.


:: 終了処理（エクスプローラーから起動されていた場合は処理分岐）;
:EndProcess
echo %cmdcmdline%|find /i "%~f0">NUL
if %errorlevel% equ 0 (timeout.exe /t 120 /nobreak)
goto :eof
```
</details>


### 日数計算

日数計算はC#の書籍なども執筆されていらっしゃる岩永信之さまのＨＰ[++C++; // 未確認飛行 C について 経過日数の計算][]の計算式を利用させて頂きました。
特に月ごとの日数計算は面倒な処理を考える必要がなく秀逸です。
シフト演算はバッチでも使えますがここでは使っていません。

### バッチファイルの制限
バッチの変数やエラーレベル、計算で扱える数値範囲はsigned int型±2147483647で桁数は10桁です。
バッチファイルを```CALL```で呼び出しエラーレベルによって結果をやりとりしたいのでここで扱えるのは数値のみです。
また、バッチファイル中の計算でも同様です。
日や年月を跨いで計算する必要を考えると年月日、最低でも日は必要になります。
最初は開始、終了日時の記録を呼び出し元のバッチファイルへエラーレベルで返してなんとかしようと考えていました。
エラーレベルで扱える10桁、0～9まで扱うと9桁では無理と判断し文字列をファイルに記録することにしました。
外部ファイルにすることで制限が緩和され自由になり応用もできシンプルな作りになりました。

### 経過時間をエラーレベルで返す
エラーレベルはsigned int型±2147483647です。
呼び出し元のプログラムには面倒な処理させないで結果を渡すことが理想です。
そこで、signed int型について考えてみます。
まず、signed int型の頭2桁は21です。
signed int型を分解して使う場合、頭1桁は0～2までしか使えません。
さらに2桁目を0～9まで使うなら頭1桁は0か1のみとなりフラグくらいしか使い道はありません。
あとはプラス値、マイナス値で条件を判断することができますが美しくありません。
したがって0～9まで自由に使えるのは9桁となります。
9桁で表現できる最大は次のようになります。

|桁数|値|
|-|:-|
|表記|HHHHHMMSS|
|最大値|999995959|

99999時間59分59秒でほぼ100,000時間です。
単純計算すると `100,000時間 ÷ 1日24時間 ÷ 1年365日 = 11.4年` となります。
オーバースペックですね（笑

[++C++; // 未確認飛行 C について 経過日数の計算]:https://ufcpp.net/study/algorithm/o_days.html
[◇Windowsのバッチファイルで日時を好みのフォーマットで作成]:https://qiita.com/aromatibus/items/da51080676937a46a2fa

# おわりに

Ｇｏを使ってみたかったのでＧｏでの移植を考えていましたが、Ｑｉｉｔａの記事になると思いバッチで作りました。
他言語への移植や改変も容易な作りになっていますので、もしも移植されたらコメントで教えていただけると嬉しいです。

他のバッチファイルもお見せできるレベルに修正できたら掲載したいです。
お粗末様でした。

https://github.com/Aromatibus/


[⧉Windowsのバッチファイルのテクニックをご紹介します]: https://qiita.com/aromatibus/items/12fa4775f38c9889a686