こんにちは、 yu です。前回はインターン生としてブログを書きましたが、今回は内定者アルバイトとしてブログを書いていきます。
STORES レジには、会員バーコードやアイテムのバーコードを読み込むスキャン機能があります。 しかし、そのスキャン機能では 12 桁のバーコードが読めないという問題が発生していました。 今回は、なぜ 12 桁のバーコードが読めなかったのかとその解決法について、バーコードの規格とともに紹介します。
発生した問題と原因
問題
STORES レジに登録する商品には、オーナーさまがお好みのバーコード番号を設定できます。 しかし、アイテムに 12 桁のバーコードを設定し、そのバーコードをレジアプリで読み取っても、アイテムが見つからないというエラーが出てしまい検索ができないという問題が発生しました。
原因
ログを見てみると、 12 桁のバーコードの先頭に 0
が付加され、 13 桁のバーコードとして認識されてしまっていました。
つまり、問題の原因としては、「先頭に 0
が付加されてしまっていた」という大変シンプルなものでした。しかし、これを解決するためには、バーコードの読み取り部分やバーコードの判別方法ついて詳しく調査する必要がありそうです。
バーコードの種類について
諸々の調査へ進む前に、まずは前提知識としてバーコードの種類を紹介します。
- EAN コード (JAN コード)
- 日本や世界の流通商品で使われている標準的なバーコード
- EAN-13 (13 桁) • EAN-8 (8 桁)
- UPC コード
- 主にアメリカで使われているバーコード
- UPC-A (12 桁) • UPC-E (8 桁)
- CODE128
- 英数字・記号・制御コードも表現できるバーコード
など、他にもさまざまな種類が存在します。
今回の問題では、 UPC-A (12 桁) のバーコードが EAN-13 (13 桁) のバーコードと誤って認識されてしまっていたので、それぞれの詳細な仕様を調べました。
EAN-13
- 国コード (3 桁)
- GS1 というフォーマットによって規定されている
- 商品コード
- 自由に決められる
- チェックデジット
- チェックデジット (一番右の桁) を除いた残りの 12 桁の数字を右から数えて、奇数番目を 3 倍、偶数番目を 1 倍したものを足し合わせる (チェックサム)。
- チェックサムと同じか、チェックサムより大きい最も近い 10 の倍数から、チェックサムを引いたものが、チェックデジットとなる。
参考: International Article Number
UPC-A
- 商品コード
- 自由に決められる
- チェックデジット (1 桁)
- 12 桁の数字を左から数えて、奇数番目を 3 倍、偶数番目を 1 倍したものを足し合わせる (チェックサム)。
- チェックサムを 10 で割ってあまりを出す。
- あまりが 0 ならそれがチェックデジットとなり、 0 でなければ 10 からあまりを引いたものが、チェックデジットとなる。
調査
バーコードは、 Apple の Vision フレームワークに含まれている VNDetectBarcodesRequest
を用いて検出しています。
そこでまずは UPC-A にフレームワークが対応しているかどうか確認してみました。
しかし、Apple 公式の対応リスト には EAN-13 や UPC-E の記載はありますが、肝心の UPC-A の記載がありません。
ここで、「公式ライブラリでは、 UPC-A の先頭に 0 を付加させることで、EAN-13として扱っているのかも?」と仮説を立ててみました。この仮説が正しいか、検証した内容を次に示します。
UPC-A バーコードを用意
まず、 UPC-A のバーコードを用意します。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
UPC-A | 7 | 2 | 5 | 2 | 7 | 2 | 7 | 3 | 0 | 7 | 0 | 6 |
一応チェックデジットを計算して、正常なバーコードかを確かめてみます。
この UPC-A のバーコードのチェックデジットは、末尾の 6
です。
末尾のチェックデジットを除いた残りの 11 桁の数字を左から数えて、奇数番目を 3 倍、偶数番目を 1 倍したものを足し合わせる。
(7 + 5 + 7 + 7 + 0 + 0) * 3 + (2 + 2 + 2 + 3 + 7) = 94
94 を 10 で割ってあまりを出す。
94 % 10 = 4
余りが 0 でなければ 10 からあまりを引いたものが、チェックデジットとなる。
10 - 4 = 6
末尾の 6
と一致しているので、この UPC-A のバーコードは正常なバーコードといえます。
UPC-A バーコードに 0 を付けてみる
次に、 UPC-A のバーコードに 0
を付加します。 13 桁となるので EAN-13 のバーコードとして計算してみます。
12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
EAN-13 (UPC-A だったもの) | 0 | 7 | 2 | 5 | 2 | 7 | 2 | 7 | 3 | 0 | 7 | 0 | 6 |
この EAN-13 のバーコードのチェックデジットも、同様に末尾の 6
です。
チェックデジット (一番右の桁) を除いた残りの 12 桁の数字を右から数えて、奇数番目を 3 倍、偶数番目を 1 倍したものを足し合わせる。
(0 + 0 + 7 + 7 + 5 + 7) * 3 + (7 + 3 + 2 + 2 + 2 + 0) = 94
94 より大きく、かつ最も近い 10 の倍数である 100 から 94 を引いたものが、チェックデジットとなる。
100 - 94 = 6
こちらも、末尾の 6
と一致しているので、この EAN-13 のバーコードも正常なバーコードといえます。
結果
つまり、 UPC-A の先頭に 0
を付加すると、 EAN-13 のバーコードとしても正常なバーコードとなることが分かります。
Apple の Vision フレームワークは、 UPC-A のバーコードの先頭に意図的に 0
を付加して、 EAN-13 として認識させているようです。
読み取る段階で 12 桁の UPC-A バーコードかどうかを判定できれば最善なのですが、公式ライブラリではそこまで自由が効きません。
検索は 12 桁の数字で行う必要があるため、読み取った後の処理で EAN-13 になってしまった UPC-A バーコードを判別して、 12 桁に戻す必要があります。
対処法
バーコードの判別
アプリ側で見分けたい!と思っても、先頭に 0
が付いて EAN-13 に化けた UPC-A のバーコードは、先述の通りどちらの場合でも正常なバーコードとなるため、アプリ側で見分けることができません。
しかも、 EAN-13 の規格的に先頭に 0
が付かない仕様なら良いのですが、先頭が 0
の場合は北米のバーコードであると GS1 に規定されています。
そこで、読み取ったバーコードが UPC-A であると断定はできないので、 UPC-A の可能性があるかどうかを判定する関数を作りました。
行っていることとしてはシンプルで、この 3 つです。
- 先頭の
0
を削除 - String を Int の配列に変換
- チェックデジットに一致するか計算
func isUPCACode(barcode: String) -> Bool { var upcaCode = barcode if upcaCode.count == 13 { // UPC-A (12 桁) のバーコードの先頭に 0 が付加されて EAN-13 (13 桁) として認識されている可能性がある。 // 13 桁かつ先頭が 0 の場合のみ、先頭の 0 を削除。 guard let firstDigit = upcaCode.first, firstDigit == "0" else { return false } upcaCode.removeFirst() } // String 形式のバーコードを Int の配列に変換 var digits = [Int]() for digitChar in upcaCode { guard let digit = Int(String(digitChar)) else { return false } digits.append(digit) } // UPC-A である条件 // (12 桁目以外の偶数桁の合計 + 奇数桁の合計の 3 倍) の mod を M として、 M が 0 でなければ Μ = 10 - M とすると、 M が 12 桁目のチェックデジットに一致する。 var oddSum = 0 var evenSum = 0 for (index, digit) in digits.dropLast().enumerated() { // index が 0 オリジンなので偶奇判定が逆になっている。 if index % 2 == 0 { oddSum += digit * 3 } else { evenSum += digit } } let sum = oddSum + evenSum // 条件を確認 guard let lastDigit = digits.last else { return false } let checkDigit = (10 - sum % 10) % 10 return checkDigit == lastDigit }
検索フローの修正
ここまでの流れを軽くおさらいしておきます。
- Vision フレームワークが 12 桁の UPC-A のバーコードを 13 桁の EAN-13 のものと認識する
- EAN-13 になってしまったバーコードは、元々 UPC-A だったかどうかを見分けることができない
- EAN-13 のバーコードから、 UPC-A の可能性があるかどうかを見分ける関数を用意した
ここまで辿り着けば、あとは UPC-A のバーコードで検索できるように処理を修正するだけです。
最終的に、下の図のように UPC-A の可能性がある場合には再検索をするという、この解決策を採用することにしました。
QA で確認したところ検索速度には大きな変化は無く、オーナーさまへの影響も限定的と判断したため、このままリリースしました 🙌
まとめ
今回は 12 桁のバーコードが読めない問題について、バーコードの規格や仕様を調査しながら解決していきました。
私はiOSエンジニアではありますが、Swift のコードがほぼ出てこないというブログになってしまいました。しかし、今まで Swift を触ってきてバーコードに触れる機会は無かったので、 STORES レジならではの良い経験になりました。
(この対応をしてからというもの、バーコードの種類やどの国の製品なのかを見る癖が付いてしまいました 😇)