【PHP】GTINのチェックディジットで頭の体操

  • 2020年11月13日
  • 2020年11月16日
  • PHP

 GTIN とは次の引用のもので、要はバーコードです。

Number Item Trade Global の略で、GS1 が定める国際標準の商品識別コードの総称。JANコード、U.P.C.、集合包装用コードが含まれる。
はじめてのバーコードガイド 登録事業者・一般用

GTIN のチェックディジットは次にある様にバーコードの符号誤り検知数字のことです。大体チェックサムみたいなもので、おそらくバーコードリーダーがピッとやる時の成否判定が主な活躍の場です。
JANコード、集合包装用商品コード(ITFコード)、U.P.C.、SSCCのチェックデジットの計算方法 | GS1 Japan(一般財団法人流通システム開発センター)
 符号誤り検知数字ということもありチェックディジットはチェックディジット以外の数字から計算して求められる値となっております。アルゴリズムは次です。

  1. 末尾から奇数桁に3、偶数桁に1を掛けてから総和を求める
  2. 結果を10で割った余りを求める
  3. 10からその剰余を引いた値がチェックディジットである

 ちなみにこの計算方法はモジュラス 10 / ウェイト 3(英語で modulus 10 weight 3)という名でも知られています。
 これを次のJANコード、集合包装用商品コード(ITFコード)、U.P.C.、SSCCのチェックデジットの計算方法 | GS1 Japan(一般財団法人流通システム開発センター)
から引用した画像の様に素朴な計算をしてもよいのですが、せっかくなので色々工夫してみます。

 テストは上記 JANコード、集合包装用商品コード(ITFコード)、U.P.C.、SSCCのチェックデジットの計算方法で出した添付画像の結果と関数の実行結果が一致するかのみを見ました。

 まず次のコードです。

/**
 * GS1 公式サイトのプログラムのコードを大いに参考にしたコードです。
 * foreach で 1 桁ずつ読み進める
 * ループごとに係数である $k を 3, 1, 3, 1,... と往復させる
 * 奇数偶数が交互になるとわかっているので 4-1=3 と 4-3=1 で係数を往復できる
 * @param  string $codePayload チェックディジットを除いたコード
 * @return int    チェックディジットの値
 */
function getCheckDigitFlip(string $codePayload)
{
    $sum = 0;
    $k   = 3;
    // strrev で文字列を反転
    // str_split で配列化
    foreach (str_split(strrev($codePayload)) as $digit) {
        $sum += $digit * $k; // 3, 1 を交互にかけます
        $k = 4 - $k;
    }

    // 56桁のコードの総和の最大は 9 * (3 + 1) / 2 * 56 = 1008
    // 55桁のコードの総和の上界は 9 * (3 + 1) / 2 * 54 + 27 = 999
    // 1008 > 1000 > 999 なので55桁のコードまでは (1000 - 総和) の末尾1桁は自然数となり、
    // その値は(10 - 末尾1桁) の末尾1桁と等しい(ここ感覚的なのでもうちょっと詰めたい。補数関連?)

    // 10 で割った余りは割られる値の 1 の位に等しいので
    // % 10 で末尾1桁( 1 の位)の値を取り出せる
    return (1000 - $sum) % 10;
}

 初期値 a の変数 k を使ってk = (a + b) - kを繰り返すことでk の値に a, b を往復させられます。奇数の次は偶数、偶数の次は奇数、となっているため、この往復処理で奇数桁、偶数桁に対する係数を作れます。ちなみにこれのコメントを削除すると読みやすさを保ったまま次の様に中身を 6 行にまとめられます。

function getCheckDigitFlip(string $codePayload)
{
    $sum = 0; $k = 3;
    foreach (str_split(strrev($codePayload)) as $digit) {
        $sum += $digit * $k;
        $k = 4 - $k;
    }
    return (1000 - $sum) % 10;
}

 また、都度、奇数偶数を判定させることで次の様にさらに短くできます。

function getCheckDigitThree(string $codePayload)
{
    $sum = 0;
    foreach (str_split(strrev($codePayload)) as $index => $digit) {
        $sum += $index % 2 === 0 ? $digit * 3 : $digit;
    }
    return (1000 - $sum) % 10;
}

 大変読みにくくなることが予想できますが array_reduce と先述の反復を使って更に少ない行数にまとめることもできそうです。

 1 と 3 を切り替えるのが嫌なら次の様に while を使うこともできます。

function getCheckDigitWhile(string $codePayload)
{
    $codeArr = str_split(strrev($codePayload));
    $codeLen = strlen($codePayload);
    $sum     = 0;
    $i       = 0;
    while ($i < $codeLen) {
        // 1ループ中に2回 $i++ することで係数の分岐も条件分岐もなくせます
        $sum += $codeArr[$i++] * 3; // 0, 2,... 番目
        if ($i === $codeLen) {
            // 存在しない添え字を使って参照することがあるので Notice を無視しないなら break 必須です。
            break;
        }
        $sum += $codeArr[$i++]; // 1, 3,... 番目
    }

    return (1000 - $sum) % 10;
}

 if も三項演算子も嫌だというならば計算するコードを固定長にすることで対応可能です。

function getCheckDigitPadFor(string $codePayload)
{
    // 常に十分な長さの偶数桁数にします。総和に影響を与えずに長さを変えればよいので単純な0埋めです。
    $codeLen = 20;
    $codePayload = str_pad($codePayload, $codeLen, '0', STR_PAD_LEFT);

    $codeArr     = str_split(strrev($codePayload));

    $sum = 0;
    // コード長は常に 20 桁なので添え字エラーは決して起きません
    for ($i = 0; $i < $codeLen; $i += 2) {
        $sum += $codeArr[$i] * 3 + $codeArr[$i + 1];
    }

    return (1000 - $sum) % 10;
}

 奇数偶数を交互に扱うことから離れつつ、奇数偶数を判定する必要をなくすこともできます。

function getCheckDigitBinFor(string $codePayload)
{
    $codeArr = str_split(strrev($codePayload));
    $codeLen = strlen($codePayload);

    $sum     = 0;
    for ($i = 0; $i < $codeLen; $i += 2) {
        // 偶数添え字を順に見ていきます
        $sum += $codeArr[$i] * 3;
    }
    for ($i = 1; $i < $codeLen; $i += 2) {
        // 奇数添え字を順に見ていきます
        $sum += $codeArr[$i];
    }

    return (1000 - $sum) % 10;
}

 ループの起点と進み方を自由に書けるのは for ループならではです。
 ちなみにそれぞれの関数を雑に(メモリ状況等リソースの条件をろくに揃えずに)13桁JANコード想定を 1e8 回回した結果は次で、break 付き while が最速で、一番最初に挙げた係数反復、最後に挙げた奇数だけforと偶数だけfor、が次いで高速でした。また固定長forは固定長を適したものに変えて無用なループ数を減らせば、かなり高速でした。

関数名 実行時間(秒)
getCheckDigitFlip 128.7208599
getCheckDigitThree 151.0854922
getCheckDigitWhile 113.8678158
getCheckDigitPadFor $codeLen=20 189.9463536
getCheckDigitPadFor $codeLen=12 124,7300957
getCheckDigitBinFor 128.4716686
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG