EAN-13 と UPC-A で重み 1/3 が入れ替わる理由

バーコードの検査数字(チェックディジット)を自分で計算しようとすると、必ず一度は同じところで詰まる。EAN-13 と UPC-A で、重みの 1 と 3 が入れ替わっているからだ。

どちらもモジュラス10で、どちらも「1桁おきに3倍する」と説明される。それなのに実装をコピーして流用すると、片方だけ答えが合わない。

この記事では、ハシトシステムのチェックディジット計算ツールが実際に使っている実装をそのまま追いながら、なぜ入れ替わるのか、どこを間違えやすいのかを整理する。最後に Node.js で既知の番号を使って検算するスクリプトを置く。

実装は3行しかない

このツールが持っている計算部分は、方式ごとに1関数ずつ、合計3つである。

1
2
3
function weighted13(d){ let s=0; for(let i=0;i<12;i++){ s += (+d[i]) * (i%2===0?1:3); } return (10-s%10)%10; }
function weightedUPC(d){ let s=0; for(let i=0;i<11;i++){ s += (+d[i]) * (i%2===0?3:1); } return (10-s%10)%10; }
function luhnCD(d){ let s=0,dbl=true; for(let i=d.length-1;i>=0;i--){ let n=+d[i]; if(dbl){ n*=2; if(n>9) n-=9; } s+=n; dbl=!dbl; } return (10-s%10)%10; }

ブラウザ内で完結する作りなので、入力した番号がどこかへ送られることはない。計算の実体はこれで全部である。

上の2つを並べると、違いが1か所だけなのが分かる。

  • weighted13i%2===0 のとき 1、それ以外 3
  • weightedUPCi%2===0 のとき 3、それ以外 1

ループの範囲も違う(12 と 11)。EAN-13 は本体12桁、UPC-A は本体11桁だからだ。

なぜ逆になるのか

規格の定義はどちらも同じ言い方をしている。「右端(検査数字)から数えて奇数番目に 3 を掛ける」である。

つまり基準が右端なのだ。左から i=0 で回す実装に直すとき、本体の桁数が偶数か奇数かで、左端が偶数番目に当たるか奇数番目に当たるかが変わる。

  • EAN-13 の本体は 12桁(偶数)。右から数えた重み 3,1,3,1,... を左から並べ直すと 1,3,1,3,... になる
  • UPC-A の本体は 11桁(奇数)。同じことをすると 3,1,3,1,... のまま

規格が違うのではない。桁数の偶奇が違うだけである。 ここを「EAN と UPC は別ルール」と覚えると、他の桁数の方式に出会ったときにまた間違える。覚えるなら「右端基準」のほうを覚える。

ISBN-13 が weighted13 を共有しているのも同じ理由だ。ISBN-13 は EAN-13 の一種で、本体12桁だから式が完全に一致する。ツール側も SPEC の定義で isbn13weighted13 を割り当てているだけで、別実装は持っていない。

Luhn だけ考え方が違う

クレジットカード番号などに使う Luhn は、上の2つとは別の系統である。

1
function luhnCD(d){ let s=0,dbl=true; for(let i=d.length-1;i>=0;i--){ let n=+d[i]; if(dbl){ n*=2; if(n>9) n-=9; } s+=n; dbl=!dbl; } return (10-s%10)%10; }

違いは2つある。

1つ目。掛けるのは 3 ではなく 2 である。 そして 2倍して10以上になったら 9 を引く。これは「10の位と1の位を足す」と同じ意味だ。たとえば 8×2=16 は 1+6=7 で、16-9=7 と一致する。

2つ目。右端から始める。 ford.length-1 から下がっているのはそのためで、dbl=true で始まる。つまり本体の右端が必ず2倍される。だから本体の桁数が何桁でもこの実装は正しく動く。EAN や UPC のように桁数を固定していないのはこの性質による。

ツールの SPEC でも luhn だけ body: null になっていて、桁数の検査をしていない。

実際に試す

Node.js 20 以降があれば依存は要らない。cd.js として保存する。既知の正しい番号で検算する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function weighted13(d){ let s=0; for(let i=0;i<12;i++){ s += (+d[i]) * (i%2===0?1:3); } return (10-s%10)%10; }
function weightedUPC(d){ let s=0; for(let i=0;i<11;i++){ s += (+d[i]) * (i%2===0?3:1); } return (10-s%10)%10; }
function luhnCD(d){ let s=0,dbl=true; for(let i=d.length-1;i>=0;i--){ let n=+d[i]; if(dbl){ n*=2; if(n>9) n-=9; } s+=n; dbl=!dbl; } return (10-s%10)%10; }

const cases = [
["ISBN-13", "9784873119045", weighted13],
["ISBN-13", "9784798142470", weighted13],
["UPC-A", "036000291452", weightedUPC],
["Luhn", "4111111111111111", luhnCD],
["Luhn", "79927398713", luhnCD],
];

for (const [kind, full, fn] of cases) {
const body = full.slice(0, -1);
const expected = Number(full.slice(-1));
const got = fn(body);
console.log(kind.padEnd(8), full.padEnd(18), "計算:", got, expected === got ? "OK" : "NG(期待 " + expected + ")");
}

実行する。

1
node cd.js

出力はこうなる。

1
2
3
4
5
ISBN-13  9784873119045      計算: 5 OK
ISBN-13 9784798142470 計算: 0 OK
UPC-A 036000291452 計算: 2 OK
Luhn 4111111111111111 計算: 1 OK
Luhn 79927398713 計算: 3 OK

入れ替わりを体感するなら、ここで1文字だけ変えてほしい。 weighted13(i%2===0?1:3)(i%2===0?3:1) に変えて実行すると、ISBN-13 の2件だけが NG になる。UPC-A と Luhn は影響を受けない。この差の出方が、そのまま「桁数の偶奇で左右が入れ替わる」ことの現れである。

検算に使った番号の性質も書いておく。4111111111111111 は決済処理のテストで広く使われる番号で、実在のカードではない。79927398713 は Luhn の説明でよく引かれる例である。ISBN-13 の2件は書籍の識別子で、末尾の 5 と 0 が規格どおりの検査数字になっている。

使い分けの目安

自分で書くか、ツールを叩くかの判断は単純だ。

バッチで大量に処理する、あるいは入力検証に組み込むなら自分で書く。 上のとおり実装は3行で、外部ライブラリを入れる理由がない。桁数の固定だけ気をつければよい。

1件だけ確かめたい、あるいは自分の実装が合っているか照合したいならチェックディジット計算ツールのほうが速い。EAN-13/JAN・UPC-A・ISBN-13・Luhn を切り替えられて、本体だけを入れれば検査数字を出し、検査数字まで入れれば妥当かどうかを判定する。ブラウザ内で完結するので、番号が送信されることはない。

実務で効くのは後者の「照合」のほうだと思う。自分の実装を信じきってバッチを流すより、既知の1件で先に突き合わせておくほうが、あとで全件やり直す事故を防げる。

まとめ

  • EAN-13 と UPC-A の重みが逆に見えるのは、規格が違うからではなく本体の桁数の偶奇が違うから。定義はどちらも「右端から数えて奇数番目に3を掛ける」
  • ISBN-13 は EAN-13 と本体12桁で一致するので、実装を共有できる
  • Luhn は 2倍して9を引く方式で、右端から始めるため桁数を固定しなくてよい
  • 実装は3行。ただし自分の実装は、既知の番号で必ず1件照合してから流す

本記事のコードは、ハシトシステムのチェックディジット計算ツールの実装に基づいている。出力例は Node.js 20.17.0 で実際に実行した結果である。