バッチファイルでタイピング練習

タイピング練習のバッチファイルができたので投稿します。
タイピング練習のソフトを作ることになった経緯については[前の記事](https://qiita.com/kyunashige/items/248aa000dd68290284b8 "９月１３日は、プログラマーの日")に載っています。


## バッチファイルを選んだ理由

> GUIがしたくて、JavaやPythonに挑戦してみる
　　　　　↓
早くプログラム書きたいな...
　　　　　↓
バッチファイルにも if とか for ってあるんだ！
　　　　　↓
まずはバッチファイルで作ってみるか

　小学校の頃、コマンドプロンプトの黒い画面をかっこいいな～と思っていました。そのコマンドプロンプトに助けを求める（「`help`」と入力する）と、利用できるコマンドの一覧を返してくれて、それで一行のコマンドに慣れていったり。「`tree /f`」というコマンドを実行して眺めるのが好きでした。
　また、「.bat」で検索して当時の自分のパソコン内のバッチファイルの中身を片っ端から見たりしていました。

　そしてこの前、バッチファイルの記事を偶然見て、if と for があることを知り、バッチファイルでも作れそうだなと思い、バッチファイルを選びました。


## いざやってみると

　遅延環境変数の扱いが大変でした。
　また、プログラミング自体が初めてなので、変数の名前の付け方がよくわからず、また、変数の量も増えてしまって、これでいいのかな...と思いながらも、なんとか作りました。

> 　遅延環境変数については、他の方の説明が素晴らしいので、気になる方は検索してみてください。


## 全体の概要

>　:scroll:　TYPING.bat　　 ← これが本体
　:open_file_folder:　setting　　　　 ← 初回起動時に作成されます
　┣　:page_facing_up:　ini.txt（前回の起動時の情報を管理）
　┗　:page_facing_up:　record.txt（成績を管理）
　:file_folder:　data　　　　　 ← 任意（後述）

### 機能と仕様
　自分で作ったテキストファイルでタイピング練習することができます。
　成績に残るのは、１分以上タイピング練習を行った上位の10回分で、ユーザー毎に保存されます。
　このとき、100分以上タイピング練習をすることや、日時をまたぐことは想定していません。タイピング練習を行うことはできますが、成績に正しく保存されません。
　一度入力を間違えるとビープ音がなり、入力を受け付けない時間が生まれます。
　バッチファイル（TYPING.bat）を起動した後は、すべての操作をキーボードで行うことを想定しています。たとえば、初期起動時の後に、

 - 1を入力すると、ユーザー（ゲストを含めて３人分）を選択する画面に移動する
 - 2→bと連続して入力すると、dataフォルダに入っているファイルを選択する画面に移動する
 - 3→bと連続して入力することで、問題をループして、練習をし続けることができるようにする
 - yを入力すると、タイピング練習開始
 - 4またはnを入力すると、バッチファイルの終了

　また、dataフォルダには、テキストファイルまたはCSVファイルを90個まで入れることが可能です（91個以上入ることを想定して作っていないため。入れること自体はもちろん可能です）。dataフォルダに関する詳しい説明は次の項で述べます。

### 外部ファイルについて
　対応しているファイルの形式は、テキストファイル（.txt）とCSVのカンマ区切り（.csv）の２種類です。どちらもバッチファイル（TYPING.bat）と同じ階層にdataというフォルダを作って、そこに保存してください。

```text:example01.txt
abcdefghijklmnopqrstuvwxyz
123567890

aiueo
123 567890
```
　上記のようなテキストファイルをdataファイルに置いておき、バッチファイルを実行して適切に選択すると、
　　１問目「abcdefghijklmnopqrstuvwxyz」、２問目「123567890」、…
のように、自分で作ったテキストファイルでタイピング練習をすることができます。
　しかし、いくつか条件がありまして、

 - 使える文字は4を除く半角英数字〔アルファベット26文字と4を除く9つの数字〕（上記の1,2行目を参照）
 - 改行は無視される
 - 文頭のスペースは禁止
 - 文中にスペースを入れた場合、スペース以下は無視される（上記5行目は「123」と同じ扱いになります）

　「、」や「。」、「!」や「?」の入力に対応していないため、実用性は低いかと思います...（この不便な仕様は、キー入力の制限を`choice`に頼っているためです）
　さらに、ファイル名についても約束事がありまして、

 - 拡張子（「.txt」または「.csv」）を除いて、半角のみで14文字以内
 - スペースの使用禁止

　また、次のようなカンマ区切りのファイル（「.txt」または「.csv」）も読み込むことができます。

```txt:example02.txt
Japanese：乃木坂46 19thシングル
いつかできるから今日できる,itukadekirukarakyoudekiru
不眠症,huminsyou
まあいいか？,maaiika
失恋お掃除人,siturenosouzinin
My rule,myrule
僕の衝動,bokunosyoudou
新しい花粉 ～ミュージカル「見知らぬ世界」より～,atarasiikahun
```
　カンマ区切りのファイルかどうかは、1行目の文頭に「Japanese」があるかどうかで認識しています。そのため、文頭に「Japanese」さえあれば、その後は上記のように何が書かれてあっても構いません。
　2行目以降はカンマの左に日本語、右にローマ字のようにすることで、
　　１問目「itukadekirukarakyoudekiru」、２問目「huminsyou」
のように実行されます。この時の注意事項は次の２つです。

 - 左には「,」を使用しないこと
 - 右は4を除く半角英数字のみで、スペース等は禁止


## こちらがそのバッチファイル（全文）になります

　約500行です。単体でも簡単なタイピング練習をすることができます（全２問）。
　可読性の高いプログラムを作る方法も知らない未熟者ではありますが、不満や改善案などがあればコメントしていただけると嬉しいです。

```bat:TYPING.bat
@echo off
setlocal enabledelayedexpansion
cd /d %~dp0
color 07
:: 2017.9.10～29
mode con:cols=63 lines=23
if not exist .\setting mkdir %~dp0\setting

@rem おおまかな変数の用意
:initialize
title タイピング練習
set user[1]=Taro  &rem 半角6文字
set user[2]=Jiro  
set user[3]=Saburo
set user[4]=Guest 
set record=.\setting\record.txt
if not exist %RECORD% (
    for /l %%i in (1,1,4) do (
        echo !user[%%i]!>>%RECORD%
        for /l %%j in (1,1,10) do echo 0,>>%RECORD%
    )
)
if exist .\setting\ini.txt (
    for /f "tokens=3 delims=," %%i in (.\setting\ini.txt) do (
        set user=%%i
        set user=!user:~5!
    )
) else set user=%user[4]%
set mode=Mode             &rem 半角17文字
set openfname=[demo]&rem 半角14文字以内(.txtを除く)
if exist .\setting\ini.txt (
    for /f "tokens=4 delims=," %%i in (.\setting\ini.txt) do (
        set openfname=%%i
        set openfname=!openfname:~5!
    )
)
set s=0
set yn=0
set y_l= &set y_r= 
set n_l= &set n_r= 
set status=off
set final_q=off
if exist .\setting\ini.txt (
    for /f "tokens=1,2 delims=," %%i in (.\setting\ini.txt) do (
        set para_a=%%i&set para_b=%%j
    )
) else set para_a=0&set para_b=0
if %para_a%==1 goto main

@rem 最初の画面
:start
set sc_user=[1]
set sc_mode=[2]
set bar=│[3] setting │[4] exit
cls&call :bar ┴ ┴ ┴&echo.
echo    %y_l% [y]: タイピングをはじめる      %y_r%
echo    %n_l% [n]: このウィンドウを閉じる %n_r%
echo.
echo    ※ ユーザーを変更すると、Modeは[demo]に設定されます
echo    ※ ユーザー、Modeを変更すると、最初からになります
if %yn%==0 choice /c 1234yns /n
if %yn%==no1 choice /c 1234ynsed /n /t 1 /d e
if %yn%==no2 choice /c 1234ynsed /n /t 1 /d d
if %yn%==no3 choice /c 1234ynsed /n /t 1 /d 4
if %yn%==yes choice /c 1234yns /n /t 1 /d s
call :menu
if %command%==5 set y_l=[&set y_r=]&set n_l= &set n_r= &set yn=yes&goto start
if %command%==6 set y_l= &set y_r= &set n_l=[&set n_r=.  ]&set yn=no1&goto start
if %command% equ 8 set n_r=.. ]&set yn=no2&goto start
if %command% equ 9 set n_r=...]&set yn=no3&goto start
if errorlevel 7 goto main

@rem [1] ユーザー選択画面
:user
cls&call :bar ┼ ┴ ┴
for /l %%i in (1,1,4) do if !user!==!user[%%i]! (set cursor_%%i=←) else set cursor_%%i=　
echo  [a] %user[1]% %cursor_1%│
echo  [b] %user[2]% %cursor_2%│ ※ Userを選択すると
echo  [c] %user[3]% %cursor_3%│   １秒後にタイピング練習が始まります。
echo  [d] %user[4]% %cursor_4%│   その間に、[2]を押すことでModeが選べます。
echo ───────┘
choice /c 1234abcd /n
call :menu
if %errorlevel% geq 5 set y_l=[&set y_r=]&set n_l= &set n_r= &set yn=yes
for /l %%i in (1,1,4) do (
    set /a j=4+%%i
    if !command!==!j! set user=!user[%%i]!
)
set openfname=[demo]&set s=0
call :write
goto start

@rem [2] モード選択（練習するor成績を見る）
:mode
cls&call :bar ┼ ┼ ┴
set head=*             
	::1行目
set /p<nul=%head%│[a] %openfname%
set exte=aaaaaaaaaaaaaaaaaa│
set len_exte=19
call :getLength %openfname%
set /a m=%len_exte%-%errorlevel%
:exte
if %len_exte% gtr %m% (
    set exte=%exte:~1%
    set /a len_exte-=1
    goto exte
)
echo %exte:a= %
	::2行目
echo %head%│[b] 外部ﾌｧｲﾙを読み込む│
	::3行目以降
if not %user%==%user[4]% (
    echo %head%├───────────┤
    echo %head%│[c] 成績              │
)
echo %head%└───────────┘
choice /c 1234abc /n
call :menu
if %command%==5 goto main
if %command%==6 goto read
if %command%==7 goto record

@rem [2-b] 外部ファイルを選択する画面
:read
set /a start=1
for /l %%i in (1,1,16) do set line[%%i]=%blank%
set /a num_file=0
for %%I in (.\data\*.txt,.\data\*.csv) do set /a num_file+=1
:restart
set mode=ﾌｧｲﾙを選択中...  
cls&call :bar ┴ ┴ ┴
if %num_file%==0 (
    echo.
    echo ファイルがありません。スタート画面に戻ります。
    timeout /t -1
    set mode=Mode             
    goto start
)
set /a end=%start%+9
set /a last=%num_file%-%end%
if %start%==1 (
    set start=01
    if %num_file% gtr 10 (
        set line[14]=┃                               [l] 次の10件を表示する→   ┃
    )
    set kl=0l
) else (
    set start=%start%
    if %last% leq 0 (
        set line[14]=┃ [k] ←前の10件を表示する                                 ┃
        set kl=k0
    ) else (
        set line[14]=┃ [k] ←前の10件を表示する      [l] 次の10件を表示する→   ┃
        set kl=kl
    )
)
set /a pause=%start%+10
set /a l=1
set ab=abcdefghij
for %%I in (.\data\*.txt,.\data\*.csv) do (
    if !l!==!pause! goto loopend
    set fname=%%~nxI
    set blankline=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa┃
    call :getLength !fname!
    set len_!l!=!errorlevel!
    for /l %%i in (1,1,!errorlevel!) do set blankline=!blankline:~1!
    set /a m=!l!%%10+2
    if !m!==2 set m=12
    set sc=!ab:~0,1!
    set line[!m!]=┃ [!sc!] !fname!!blankline:a= !
    set /a l_mod10=!l!%%10
    set /a l+=1
    if !l_mod10!==0 (set ab=%ab%) else set ab=!ab:~1!
    set /a n=!l!+6
)
:loopend
set atoj=abcdefghij
set /a rest=13+%last%
if %last% lss 0 (
    for /l %%k in (12,-1,%rest%) do (
        set line[%%k]=%blank%
        set atoj=!atoj:~0,-1!
    )
)
set line[1]=┃以下のリストからファイルを選択してください (%start%～%end%)       ┃
set status=on
set statusbar=* 対象ﾌｧｲﾙ：.txt .csv   ※ ﾌｧｲﾙ名は半角。ｽﾍﾟｰｽは使用不可
call :window
set mode=Mode             
choice /c 1234%kl%%atoj% /n
call :menu
if %command%==5 set /a start-=10&goto restart
if %command%==6 set /a start+=10&goto restart
for /l %%i in (1,1,10) do (
    set /a num=6+%%i
    set /a m=2+%%i
    if %command%==!num! (
        for /l %%j in (1,1,!m!) do set openfname=!line[%%j]:~6%!
        for /l %%j in (!len_%%i!,1,53) do set openfname=!openfname:~0,-1!
        goto main
    )
)

@rem [2-c] 成績画面
:record
set uname=%user%
:RELOAD
set mode=成績画面         
cls&call :bar ┴ ┴ ┴
for /l %%i in (1,1,16) do set line[%%i]=%blank%
set cnt=0
set line[1]=┃  #     登録日時       文字数  計測時間   使用したﾌｧｲﾙ    ┃
set line[2]=┠─────────────────────────────┨
for /f "tokens=1-3,* delims=," %%A in (%RECORD%) do (
    if !cnt! geq 3 (
        set /a rank=!cnt!-2
        if !rank! leq 9 set rank= !rank!
        if %%A==0 (
            goto skip
        ) else if %%A leq 9 (set type=  %%A) else if %%A leq 99 (set type= %%A) else set type=%%A
　　　　set /a rank_t=%%B/100
        set /a rank_m=!rank_t!/60
        if !rank_m! leq 9 set rank_m= !rank_m!
        set /a rank_s=!rank_t!%%60
        if !rank_s! leq 9 set rank_s=0!rank_s!
        call :getLength %%C
        if errorlevel 1 set exte=               ┃
        if errorlevel 2 set exte=              ┃
        if errorlevel 3 set exte=             ┃
        if errorlevel 4 set exte=            ┃
        if errorlevel 5 set exte=           ┃
        if errorlevel 6 set exte=          ┃
        if errorlevel 7 set exte=         ┃
        if errorlevel 8 set exte=        ┃
        if errorlevel 9 set exte=       ┃
        if errorlevel 10 set exte=      ┃
        if errorlevel 11 set exte=     ┃
        if errorlevel 12 set exte=    ┃
        if errorlevel 13 set exte=   ┃
        if errorlevel 14 set exte=  ┃
        set line[!cnt!]=┃ !rank!  %%D    !type!   !rank_m!分!rank_s!秒   %%C!exte!
        :skip
        if !cnt!==12 (set cnt=0) else set /a cnt+=1
    )
    if %%A==!uname! set cnt=3
)
if "%line[3]%"=="%blank%" (
    set line[3]=┃  データがありません。                                    ┃
)
set line[13]=┠─────────────────────────────┨
for /l %%i in (1,1,4) do if !uname!==!user[%%i]! (set cursor_%%i=￣￣￣) else set cursor_%%i=　　　
set line[15]=┃     [A] %user[1]%       [B] %user[2]%       [C] %user[3]%         ┃
set line[16]=┃         %cursor_1%           %cursor_2%           %cursor_3%         ┃
set status=on
set statusbar=* ミスタイプ数をカウントできないため、正解率等は表示できません
call :window
choice /c 1234abc /n
set mode=Mode             
call :menu
for /l %%i in (1,1,3) do (
    set /a j=4+%%i
    if !command!==!j! set uname=!user[%%i]!
)
goto RELOAD

@rem [3] 設定画面
:setting
cls&call :bar ┴ ┴ ┴
for /l %%i in (1,1,11) do set line[%%i]=%blank%
for /l %%i in (13,1,16) do set line[%%i]=%blank%
if %para_a%==0 (set setting_a= y or[n]) else set setting_a=[y]or n 
set line[2]=┃  [A] バッチファイル実行時に、           %setting_a%         ┃
set line[3]=┃      すぐにタイピング練習を始める                        ┃
if %para_b%==0 (set setting_b= y or[n]) else set setting_b=[y]or n 
set line[5]=┃  [B] 問題をループする                   %setting_b%         ┃
set line[8]=┠─────────────────────────────┨
set line[10]=┃  [C] 一時ファイルを消去する           %setting_c%   ┃
set line[15]=┃  [Z] 過去のデータをすべてリセットする                    ┃
call :window
if %final_q%==off (
    choice /c 1234abcz /n
) else (
    choice /c 1234yn /n /m "* settingフォルダを消去後、このウィンドウは閉じます"
)
call :menu
if %final_q%==off (
    if %command%==5 (
        set /a para_a=1-%para_a%
        call :write
    )
    if %command%==6 (
        set /a para_b=1-%para_b%
        call :write
    )
    if %command%==7 (
        if exist .\setting\ini.txt del .\setting\ini.txt
        set setting_c=* 完了しました *
        set line[12]=┃     〔 ini.txt の消去を実行しました    〕                ┃
    )
    if %command%==8 (
        set final_q=on
        set status=on
        set statusbar=* 本当にデータをリセットしてよろしいですか？（Y/N）
    )
    goto setting
) else (
    if %command%==5 rmdir /s /q setting&exit
    if %command%==6 goto start
)

@rem ここがメインのタイピング練習部分
:main
title 4を押すとスタート画面に移動します
if %openfname% neq [demo] (
    set je=JPN&set s=1
    findstr /b Japanese .\data\%openfname%||set je=ENG&set s=0
    set /a k=0
    set /a num_q=0
    for /f "tokens=1,2 delims=," %%i in (.\data\%openfname%) do (
        set a_!k!=%%i
        set b_!k!=%%j
        set /a num_q+=1
        set /a k+=1
    )
) else (
    set je=ENG&set s=0
    set num_q=2&set a_0=kakikukekeyaki&set a_1=naninunenogizaka
)
set /a num_q-=1
if exist .\setting\ini.txt (
    for /f "tokens=3,4,5 delims=," %%i in (.\setting\ini.txt) do (
        set uname=%%i
        set uname=!uname:~5!
        set fname=%%j
        set fname=!fname:~5!
        set l=%%k
    )
    if "!uname!"=="%user%" if "!fname!"=="%openfname%" set /a s=!l!
)
set count=0
set t0=0
set tl=0
set v_t=0
:mainloop
for /l %%i in (%s%,1,%num_q%) do (
    if %je%==ENG (set b_%%i=!a_%%i!) else set J=!a_%%i!
    set Q=!b_%%i!
    set oriQ=!Q!
    set ans=*  ＞ 
    if %%i leq 9 (set m=0%%i) else set m=%%i
    call :getLength !Q!
    for /l %%j in (1,1,!errorlevel!) do (
        cls
        if %je%==ENG (
            echo.
            echo   !m!: !oriQ!
            echo.
        ) else (
            echo.
            echo   !m!: !J!
            echo.
            echo       !oriQ!
        )
        set /p<nul=!ans!
        set chr=!Q:~0,1!
        set ans=!ans!!chr!
        choice /c !chr!4 /n
        if !t0!==0 (
            call :getTime
            set d0=%date%
            set t0=!t!
        )
        set /a count+=1
        set Q=!Q:~1!
        if errorlevel 2 (
            echo    もう一度押すことでスタート画面に戻ります。
            set s=%%i&goto exit
        )
    )
    call :getTime
    set t1=!t!
    set /a tl=!t1!-!t0!
    if !tl! geq 6000 (
        set /a v_t=!count!*6000/!tl!
        title !v_t! 文字／分
    ) else (
        title 測定中：!count! 文字入力  ※１分以上で成績に残ります
    )
)
if %je%==ENG (set s=0) else set s=1
if %para_b%==1 goto mainloop
echo.
if %openfname%==[demo] (
    echo  デモは以上です。スタート画面に戻ります。
) else (
    echo  問題は以上です。スタート画面に戻ります。
)
:exit
pause >nul
if %d0%==%date% (
    if %tl% geq 6000 (
        set cnt=1&set new_r=OFF
        for /f "tokens=1-2,* delims=:," %%A in ('findstr /n "." %RECORD%') do (
            if %%A==1 call>%RECORD%
            if !cnt!==12 set cnt=1
            if !cnt!==1 (
                if "%%B"=="%user%" (set rew=ON) else set rew=OFF
                echo %%B>>%RECORD%
            ) else if !cnt! leq 11 (
                if "!rew!"=="ON" (
                    if "!new_r!"=="OFF" (
                        if %v_t% geq %%B (
                            set new_r=ON
                            set t_end=!time:~0,5!
                            echo %v_t%,%tl%,%openfname%,%d0% !t_end: =0!>>%RECORD%
                            if not !cnt!==11 (
                                echo %%B,%%C>>%RECORD%
                            )
                        ) else (
                            echo %%B,%%C>>%RECORD%
                        )
                    ) else (
                        if not !cnt!==11 (
                            echo %%B,%%C>>%RECORD%
                        )
                    )
                ) else (
                    echo %%B,%%C>>%RECORD%
                )
            )
            set /a cnt+=1
        )
    )
) else (
    echo.
    echo ●日付が変わってしまったため、正常に記録できませんでした。
    echo   ↑日付が変わった時の処理を実装していないためです。
    pause
)
call :write
set mode=Mode             &set yn=0&set y_l= &set y_r= &title タイピング練習
goto start

@rem 以下、６つのサブルーチン（bar, window, menu, getLength, getTime, write）
:bar
::echo yn=%yn%&echo command=%command%&echo errorlevel=%errorlevel%
echo  %sc_user%【%user%】│%sc_mode% %mode% %bar%
for /l %%i in (1,1,7) do set /p<nul=─
set /p<nul=%1
for /l %%i in (1,1,11) do set /p<nul=─
set /p<nul=%2
for /l %%i in (1,1,6) do set /p<nul=─
set /p<nul=%3
set /a n=(63/2)-3-7-11-6
for /l %%i in (1,1,%n%) do set /p<nul=─
echo.&exit /b

:window
set blank=┃                                                          ┃
echo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
for /l %%i in (1,1,16) do (
    if not defined line[%%i] set line[%%i]=%blank%
    echo !line[%%i]!
)
echo ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
if "%mode%"=="成績画面         " (
    echo * 100分を越えることは想定していません
) else if "%mode%"=="ﾌｧｲﾙを選択中...  " (
    echo * ファイル数が91個以上あることは想定していません
)else echo.
if %status%==off set statusbar=*
echo %statusbar%
exit /b

:menu
set command=%errorlevel%
if %command%==1 goto user
if %command%==2 goto mode
if %command%==3 (
    set setting_c=                
    if exist .\setting\ini.txt (
        set line[12]=┃     〔 一時ファイル：.\setting\ini.txt 〕                ┃
    ) else (
        set line[12]=┃     〔 一時ファイルはありません        〕                ┃
    )
    set final_q=off
    set status=off
    goto setting
)
if %command%==4 exit
exit /b

:getLength
set len=0
set str=%1
:LOOP
if not "%str%"=="" (
    set str=%str:~1%
    set /a len+=1
    goto LOOP
)
exit /b %len%

:getTime
for /f "tokens=1,2,3,4 delims=:." %%a in ("%time%") do (
    set hour=%%a
    set min=%%b
    set sec=%%c
    set ms=%%d
    set /a t=hour*360000+min*6000+sec*100+ms
)
exit /b

:write
echo !para_a!,!para_b!,user:!user!,file:!openfname!,!s!>>.\setting\ini.txt
exit /b
```