Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

色々な言語でマイナンバーのチェックデジットを計算したかった

More than 3 years have passed since last update.

はじめに

内容としては今更ですが、2016年から自分のブログで書いてた内容を含め今回作ろうとしたものも含めてQiitaに全部持ってこようと思いました。

元記事のあるブログ
沖の雑記帳(カテゴリ:マイナンバー)

元々は仕事上、バッチファイルで少し複雑な処理があって勉強がてら何か書いてみようかなと思っていたときに下記の記事を見つけたことです。
マイナンバーのチェックデジットを計算する - Qiita

Windowsバッチファイル、awk、Fortranは全て自身のブログからの一部文章を修正したものです。
Brainfuck版のみ今年作成したものですが、現状バグが残っています。
内容に間違い等あるかもしれないのとあまり効率的なコードにはなっていないので、何か気になることや気づかれたことがありましたら指摘いただけると助かります。

Windowsバッチファイル

作成日:2016-03-04
確認環境:
Windows 7 32-bit
コマンドプロンプト

マイナンバーのチェックデジット計算

validate_mynumber.bat
@echo off
REM validate_mynumber.bat
REM マイナンバーのチェックデジットを検証する
REM 参考:http://qiita.com/qube81/items/fa6ef94d3c8615b0ce64
setlocal enabledelayedexpansion
call :calc_strlen %1
if %strlen% neq 12 echo requires 12-digit input & goto end

set mynumber=%1
set check_digit=%mynumber:~-1%
set /a sum=0
set temp=%mynumber:~0,-1%

for /l %%I in (1,1,11) do (
  if %%I leq 6 (
    set /a q=%%I+1
  ) else (
    set /a q=%%I-5
  )
  set p=!temp:~-1!
  set /a sum+=!p!*!q!
  set temp=!temp:~0,-1!
)
set /a remainder=!sum!%%11

if !remainder! leq 1 (
  set check=0
) else (
  set /a check=11-!remainder!
)
if !check_digit! equ !check! (
  echo true
) else (
  echo false
)
goto end

:calc_strlen
set str=%1
set strlen=0
:loop_calc
if defined str (
  set str=%str:~1%
  set /a strlen+=1
  goto :loop_calc
)
exit /b

:end
endlocal

検証用バッチファイル

check_mynum.bat
@echo off
setlocal enabledelayedexpansion

set number=12345678901
for /l %%I in (0,1,9) do (
  echo input:%number%%%I
  call validate_mynumber %number%%%I
)

endlocal

検証結果

$ check_mynum.bat
input:123456789010
false
input:123456789011
false
input:123456789012
false
input:123456789013
false
input:123456789014
false
input:123456789015
false
input:123456789016
false
input:123456789017
false
input:123456789018
true
input:123456789019
false

動作解説

文字数の計算について

Batファイルにはそもそも文字列長を計算してくれるような機能はありません
なので少々強引に文字列長の計算をする処理を:calc_strlenラベル以下に記述しています。

:calc_strlen
set str=%1
set strlen=0
:loop_calc
if defined str (
  set str=%str:~1%
  set /a strlen+=1
  goto :loop_calc
)
exit /b

今回は処理を簡略化するために第1引数以外を無視しています。
:calc_strlenが文字列長を計算し、strlenという変数に文字列長を代入します。
%1は第1引数
if defined strはバッチファイルの仕様上、空でなければ定義されているという扱いになるため変数strが空になるまでループさせ、変数strlenをインクリメントする処理になっています。
%str:~1%:これは、strの1文字目から最後までを取得という意味。0文字目開始なので、ループ中で空になるまで1文字ずつシフトしています。
if %strlen% neq 12はそのままstrlenが12じゃなかったらという意味ですね。
今回扱いたい文字列長は12桁の数値なので12文字以外は処理を行わないようにしています。
バッチファイルで使用できる比較演算子については、コマンドプロンプトでif /?などと入力していただければ確認できるので省略します。

初期化処理

set mynumber=%1
set check_digit=%mynumber:~-1%
set /a sum=0
set temp=%mynumber:~0,-1%

特に解説すべきところはなさそうですが、2行目についてset check_digit=%mynumber:~-1%の部分は、最後から1文字目から最後まで文字列を取得
また、set temp=%mynumber:~0,-1%の部分は、0文字目から最後から1文字目までの文字列を取得しています。
バッチファイルの文字列は0文字目を起点とするため、変数名:の後に0以上の数値を指定することで指定した位置からの文字を取得することができます。
同様に、変数名:(開始位置),(終了位置)とすることで開始位置から終了位置までの文字列を取得することができます。
また0以上の数値を指定すると開始位置からの文字数、負の値を指定すると終了位置からの文字数を指定することができます。

これらのことを利用して検証用のチェックデジットと計算に使用するための数字を分割しています。

計算・検証部分

for /l %%I in (1,1,11) do (
  if %%I leq 6 (
    set /a q=%%I+1
  ) else (
    set /a q=%%I-5
  )
  set p=!temp:~-1!
  set /a sum+=!p!*!q!
  set temp=!temp:~0,-1!
)

まず前半部分です。FOR文の使用方法についてもfor /?とすることで確認可能なので省略
今回、!(変数名)!という表記が出てきていますがこちらは変数の遅延展開*1を使用しています。
この辺りから遅延展開というものを使わないとどうやらうまく計算されなくてドハマリしてました。
ただ計算内容自体は参考にしたRuby版と同様で、i≦6の場合はi+1を、i>7の場合はi-5をqとして、検証用に計算しているだけです。

set /a remainder=!sum!%%11

if !remainder! leq 1 (
  set check=0
) else (
  set /a check=11-!remainder!
)
if !check_digit! equ !check! (
  echo true
) else (
  echo false
)

最後はあまり解説することが見当たりません。
少しわかりにくいところとしてはバッチファイルを書く場合の注意点が1点含まれているところでしょうか?
剰余の計算が他の言語同様に%として使用できるのですが、バッチファイルでは%を表す場合は%%と重ねて書かなければなりません。
なので%%と二重に書いています。

さて解説は以上のとおりとなります。

いかがでしたでしょうか?バッチファイルは本来計算が得意な言語(?)ではありませんが工夫次第で色々なことができそうですね。
間違えていることを書いているかもしれないのでその際はコメント等でご連絡ください。

awk版

作成日:2017-03-07
確認環境:
Windows 7 32-bit
GNU Awk 3.1.8

マイナンバーのチェックデジット計算

validate_mynumber.awk
# サマリ出力するデータの初期化
BEGIN {
    invalid_count = 0
    valid_count = 0
    unexpected_count = 0
}

# 全入力データを保存
{
    mynumber[FNR] = $0
}

# 数値のみの行にマッチ
/^[0-9]+$/ {
    sum = 0
    if (length($0) == 12) {
        print "mynumber : " $0
        digit = calc_checkdigit(substr($0,1,11))
        print "digit    : " digit
        if (digit == substr($0,12,1)) {
            print "validate : true"
            valid_count++
            result[FNR] = 1
        }
        else {
            print "validate : false"
            invalid_count++
            result[FNR] = 0
        }
    }
    else {
        print $0 " :requires 12-digit."
        unexpected_count++
    }
    print ""
    next
}

# 非数を1つでも含む行にマッチ
/[^0-9]+/ {
    print $0 " : Not be used non-numeric.\n"
    unexpected_count++
    next
}

# どのパターンにもマッチしてこなかった場合は不正データ
# 改行のみの場合などはここに来る
{
    print "Unexpected data.\n"
    unexpected_count++
}

# 検証結果のサマリ出力
END {
    print "Summary:"
    print "  Number of data : " FNR
    print "  Valid data     : " valid_count
    for (i = 1; i <= FNR; i++) {
        if (result[i]) {
            print "\t" mynumber[i]
        }
    }
    print "  Invalid data   : " invalid_count
    print "  Illegal data   : " unexpected_count
# 不正入力を出したい時だけ
#    for (i = 1; i <= FNR; i++) {
#        if (!result[i]) {
#            print "\t" mynumber[i]
#        }
#    }
}

# チェックデジットの計算
# 入力は11桁の数値列
function calc_checkdigit(number){
    for (i = 1; i < 12; i++) {
        p = substr(number,12-i,1)
        if (i <= 6) {
            q = i + 1
        }
        else {
            q = i - 5
        }
        sum += p * q
    }
    remainder = sum % 11
    if (remainder <= 1) {
        return 0
    }
    else {
        return 11 - remainder
    }
}

解説はコメント内にほぼほぼ書いたので省略。

検証用データ(awk版)

checkdata.txt
1234567890123
123456789010
123456789011
123456789012
123456789013
123456789014
123456789015
123456789016
123456789017
123456789018
123456789010
023456789013
1234567890

abcdefghijkl
12b45671b999
abcd111
486855818850
874413748700
700971250770
789947831400

検証結果(awk版)

1234567890123 :requires 12-digit.

mynumber : 123456789010
digit    : 8
validate : false

mynumber : 123456789011
digit    : 8
validate : false

mynumber : 123456789012
digit    : 8
validate : false

mynumber : 123456789013
digit    : 8
validate : false

mynumber : 123456789014
digit    : 8
validate : false

mynumber : 123456789015
digit    : 8
validate : false

mynumber : 123456789016
digit    : 8
validate : false

mynumber : 123456789017
digit    : 8
validate : false

mynumber : 123456789018
digit    : 8
validate : true

mynumber : 123456789010
digit    : 8
validate : false

mynumber : 023456789013
digit    : 3
validate : true

1234567890 :requires 12-digit.

Unexpected data.

abcdefghijkl : Not be used non-numeric.

12b45671b999 : Not be used non-numeric.

abcd111 : Not be used non-numeric.

mynumber : 486855818850
digit    : 0
validate : true

mynumber : 874413748700
digit    : 4
validate : false

mynumber : 700971250770
digit    : 3
validate : false

mynumber : 789947831400
digit    : 5
validate : false

Summary:
  Number of data : 21
  Valid data     : 3
    123456789018
    023456789013
    486855818850
  Invalid data   : 12
  Illegal data   : 6

awk版作成で気をつけたこと

配列や、文字数の開始は1からで0ではないこと。この辺は他の言語と違っています。(あくまでC言語系に慣れているため他にも1開始は多々ありますが)
変数は全てグローバル。
関数や、各パターンマッチで同じ変数名使うと書き換わる。
パターンマッチした時の処理にnextとか書いとかないと次に一致したパターンも延々処理していく。
と、こんなところかな?

Fortran

作成日:2016-03-08
確認環境:
Windows 7 32-bit
GNU Fortran 0.5.25

十数年ぶりにFORTRANなんて書いたんで大分汚いコードになっています。
あと、Fortran90じゃ動きません。Fortran77となっています。

マイナンバーのチェックデジット計算(Fortran版)

c----------------------------------------------------------------
c マイナンバーの検証を行なう
c----------------------------------------------------------------
      program valid_mynumber
      implicit none             ! 変数は宣言必須
      logical validate_chkdigit ! 関数の事前宣言も必要

      character*13 numstr       ! 12にしちゃうと多い時判別できないから
      integer l
      logical b
      call getarg(1,numstr)   ! 引数の取得(getargはサブルーチン)
      l = len_trim(numstr)    ! 空白を除去した文字数
      ! 文字数のチェック
      if (l.gt.12) then
        write (*,*) 'Input is over 12-digits.'
        stop
      else if(l.lt.12) then
        write (*,*) 'Input is less than 12-digits.'
        stop
      end if
      ! ここまで来たらサブルーチンまかせ
      b = validate_chkdigit(numstr)
      if (b) then
        write (*,*),'Valid number.'
      else
        write (*,*),'Invalid number.'
      end if
      stop
      end program

      ! 入力された番号が正しいか計算・検証する
      logical function validate_chkdigit(str)
        implicit none
        character*12 str    ! 引数の型宣言必要
        character c
        integer chkdigit,validdigit
        integer i,q,p,nsum,remainder
        integer atoi        ! atoiもどき
        atoi(c) = ichar(c) - ichar('0')
        nsum = 0
        !chkdigit = ichar(str(12:12)) - ichar('0')
        chkdigit = atoi(str(12:12))
        validate_chkdigit = .false.
        do i = 1, len_trim(str) - 1, 1
          !q = ichar(str(12-i:12-i)) - ichar('0')
          q = atoi(str(12-i:12-i))
          if (i.le.6) then
            p = i + 1
          else
            p = i - 5
          end if
          nsum = nsum + (p * q) ! +=とか使えなかったっけ…?
        end do
        remainder = mod(nsum,11)
        if (remainder.le.1) then
          validdigit = 0
        else
          validdigit = 11 - remainder
        end if
        if (chkdigit.eq.validdigit) then
          validate_chkdigit = .true.
        else
          validate_chkdigit = .false.
        end if
      end

検証結果(Fortran版)

$ check.bat
input:1234567890123
 Input is over 12-digits.
input:1234567890
 Input is less than 12-digits.
input:123456789010
 Invalid number.
input:123456789011
 Invalid number.
input:123456789012
 Invalid number.
input:123456789013
 Invalid number.
input:123456789014
 Invalid number.
input:123456789015
 Invalid number.
input:123456789016
 Invalid number.
input:123456789017
 Invalid number.
input:123456789018
 Valid number.
input:123456789019
 Invalid number.

Fortran版解説

解説を書きたかったのですが、自身が書くのが十数年振りなのもありドハマリしたので解説らしい解説はありません。
とりあえず、ハマったところを下記に列挙します

  • implicit noneを入れないと変数宣言しなくても1文字目で型が決まるのを忘れていた
  • 関数の事前宣言の仕方を忘れてた
  • ブロック関数って便利な機能の存在を失念していた
  • +=みたいな書き方ができたような気がしたんだけどできなかった(Fortran90ではできたかも?)
  • サブルーチンはcallじゃないと呼べない
  • 配列ってあふれた部分は切り捨てられるし、文字列は不足してると空白を埋められちゃう。だからlen(str)はちょうどのサイズだと12文字ちょうどかどうか判別できない

このあたりもう既に他の人が書いていない言語で書こうと思って書いていただけなので、段々とコード自体も雑になってきてます。

Brainfuck版(バグあり)

作成日:2017-04-07
確認環境:
ideone.com

事前に

Brainfuckの言語仕様がすっ飛んでたのと検証用のデータのおかげでsum値が255を超える可能性を考慮できていませんでした。
そのため、途中の総計と余剰を求める処理にバグがあり検証用数字以外で普通のマイナンバーを入れても正しく結果が出ないです。
この点については、後日修正できたら修正版をあげようと思っています。

途中の合計値を求める処理と余剰の計算処理は修正しました。
ただし、もう1点最後の判定処理にバグがあることがわかりました。
チェックデジットが0, 1になる場合に正しく判定できていません。
この点は再度修正版をあげます。
また、処理についても無駄が多く最適化されていないのでこの辺はバグの修正が完了したらその後考えたいと思います

マイナンバーのチェックデジット計算と検証

1行に全て詰め込むとわかりにくいのである程度処理毎に分割して書いています。
本来は1行に全て詰め込んでいます。

validate_mynumber.bf
,>,>,>,>,>,>,>,>,>,>,>,
>++++++++[<------<------<------<------<------<------<------<------<------<------<------<------>>>>>>>>>>>>-]
++<<[->>>+<<<]>>[->[->+>+<<]>>[-<<+>>]<[-<<<<+>>>>]<<]>[-]<
+++<<<[->>>>+<<<<]>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<+>>>>>]<<]>[-]<
++++<<<<[->>>>>+<<<<<]>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<+>>>>>>]<<]>[-]<
+++++<<<<<[->>>>>>+<<<<<<]>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<+>>>>>>>]<<]>[-]<
++++++<<<<<<[->>>>>>>+<<<<<<<]>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<+>>>>>>>>]<<]>[-]<
+++++++<<<<<<<[->>>>>>>>+<<<<<<<<]>>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<<+>>>>>>>>>]<<]>[-]<
++<<<<<<<<[->>>>>>>>>+<<<<<<<<<]>>>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<<<+>>>>>>>>>>]<<]>[-]<
+++<<<<<<<<<[->>>>>>>>>>+<<<<<<<<<<]>>>>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<<<<+>>>>>>>>>>>]<<]>[-]<
++++<<<<<<<<<<[->>>>>>>>>>>+<<<<<<<<<<<]>>>>>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<<<<<+>>>>>>>>>>>>]<<]>[-]<
+++++<<<<<<<<<<<[->>>>>>>>>>>>+<<<<<<<<<<<<]>>>>>>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<<<<<<+>>>>>>>>>>>>>]<<]>[-]<
++++++<<<<<<<<<<<<[->>>>>>>>>>>>>+<<<<<<<<<<<<<]>>>>>>>>>>>>[->[->+>+<<]>>[-<<+>>]<[-<<<<<<<<<<<<<<+>>>>>>>>>>>>>>]<<]>[-]<
<<<<<<<<<<<<[->+<]>[->+<]>[->+<]>[->+<]>[->+<]
>[<<<<<+>>>>>-]<<<<+++++++++++>>+[<<<[->>>>+>+<<<<<]>>>>>[-<<<<<+>>>>>]<<<<[->>>>+>>>>>>>+<<<<<<<<<<<]>>>>>>>>>>>[-<<<<<<<<<<<+>>>>>>>>>>>]<<<<<<<<[>[->>>>>>>+>+<<<<<<<<]>>>>>>>>[-<<<<<<<<+>>>>>>>>]<[[-]<<<<<<<->>>>>>>]<<<<<<<<-]+>[->>>>>>>+>+<<<<<<<<]>>>>>>>>[-<<<<<<<<+>>>>>>>>]<[[-]<<<<<<<<->>>>>>>>]<<<<<<<[[-]<<->>]<[[-]<<+<[->>>>>>>>>>>+>+<<<<<<<<<<<<]>>>>>>>>>>>>[-<<<<<<<<<<<<+>>>>>>>>>>>>]<[-<<<<<<<<<<<<->>>>>>>>>>>>]<<<<<<<<]<]<<[-]<[->+<]>>[-<<+>>]<<[-]>[>>>>+<<<<-]>>>
>[->+<]>[->+<]>[->+<]>[->+<]>[->+<]>[-<<<<<<<<<<+>>>>>>>>>>]
<<<<<<<<<+++++++++++>>+[<<<[->>>>+>+<<<<<]>>>>>[-<<<<<+>>>>>]<<<<[->>>>+>+<<<<<]>>>>>[-<<<<<+>>>>>]<<<<<>>>[>[->+>+<<]>>[-<<+>>]<[[-]<->]<<-]+>[->+>+<<]>>[-<<+>>]<[[-]<<->>]<[[-]<<->>]<[[-]<<+<[->>>>>+>+<<<<<<]>>>>>>[-<<<<<<+>>>>>>]<[-<<<<<<->>>>>>]<<]<]<<[-]<[->+<]>>[-<<+>>]
+<[-[>[-]>++++++++++<<[->>-<<]>]]
>[->>>>>>>>-<<<<<<<<]
>>>>>>>+>[<[-]<<<<+++++++[>+++++++++++<-]>+.-------.>>>]<[[-]>++++++++[>++++++++++<-]>-.----.<]

解説(Brainfuck版)

1行目:
12桁の数値を入力してもらうだけ(数値かどうかのチェックはしてないから信じるしかない)

2行目:
もっと効率良い方法ありそうだけどとりあえず、12桁を文字コードから10進数に変換しています。
各アドレスから48を引いていく処理を続けて書いています。

3~13行目:
pnを計算するために、qnに固定値入れてその値と事前に入力した値の乗算を繰り返しています
この時点で1~11桁目までは入力した値がそのままpnの値に置き換わる形になっています
※破壊的変更してるから元の数値は残らない、考慮すると一時領域用意してとか面倒だったんで省いてしまいました

14行目:
上で計算した結果を合計していってます
そして、ここにバグの大元である255を超えると0に戻る原因が入ってます
p[0]~p[5]までの合計を求めています。
p[0]~p[5]までの和は最大でも255を超えることはないため処理を簡略化するためにチェックもしていません。

15行目:
14行目の修正に伴って、p[0]~p[5]までの合計値を一旦11で除算しています。
この余りを再度16行目からの処理に使用するように変更することで対処しました。

16行目:
15行目の余りとp[6]~p[10]の合計を求めています。
ここでの合計値も255を超えることが無いことから処理を簡略化するためにチェックは入れていません。

15行目:
17行目:
除算です。
商と余剰を求めています
14行目のバグが原因で既に正しい答えにならない場合があります。
14行目で桁上りの処理とこちらで桁上りしている場合の処理を書かなきゃいけないのですが、既に出来上がってからバグに気がついたため修正は別途考え中です。

16行目:
18行目:
12桁目が正しいか比較するための値を17行目で求めたものと比較するための値を計算しています
要するにチェックデジットの計算です

17行目:
19行目:
最後に合っているかどうかを判定するために、12桁目から18行目の値を引き算して0になったら正しい、それ以外は正しくないという処理をするための計算処理です

18行目:
20行目:
19行目の結果が0ならOK、非0ならNGかを出力しています

検証

https://ideone.com/jEN5Cy
Input:123456789018
Output:OK
http://ideone.com/HpO65m
Input:999999999996
Output:OK

https://ideone.com/qAvzVg
Input:123456789012
Output:NG
http://ideone.com/HpO65m
Input:987654321093
Output:OK

下記は本来正しいチェックデジットですがNGが出力されています
https://ideone.com/47id0V
Input:723456789120
Output:NG
http://ideone.com/uDKVNV
Input:723456789120
Output:NG

最後に

そもそも最初は業務で使うためと面白い題材をとバッチファイルで作り始めたのですが、途中から他の人が使っていなくて自分が使える言語で作ろうとか方向性が狂ってきました。
awkは既に他の方がもっと良いコードを書かれていますし、Fortran77はそもそも既に需要がほぼない言語です。
Brainfuckはもう完全に何がしたいのかよくわからないです
目的がどこかにいってしまっています
完全に私の趣味が暴走しています

それでも、作り始めたからには、Brainfuck版のバグが取れるまでは続けようかなと思っています。
他3言語版は各2時間程度でできているもののBrainfuck版のみ2時間×3週間かかっています…バグが取れてないのでまだまだ戦いは続きます

[追記]
Brainfuck版に新たなバグが潜んでいました。
最後の判定処理そもそもr=0になったときにOKルートに入っていません。
再度して更新しますので少々お待ち下さい。

h_oki
本業は組込・制御系SE。プログラム歴3x年の他称年齢詐称のおじさんです。 社内の便利屋、仕様不明なデータ解析やツール解析、多種多様な言語から他言語への移植という名の不毛な作業を生業としています。
http://oki.hateblo.jp/
vsn
IT、メカトロニクス・エレクトロニクス、バイオ・ケミストリー分野における無期雇用型派遣事業を行っています。技術力とコンサル力でお客さま事業に革新をもたらすべく、約4,000名のエンジニアが活躍中です。
https://www.modis-vsn.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away