Help us understand the problem. What is going on with this article?

バーコードのチェックディジットをLINQでゴリ押してみる

More than 1 year has passed since last update.

バーコードのチェックディジットをLINQでゴリ押してみる

概要という名の感想

  • LINQはあくまでワンライナーネタ(※個人の感想)なので、好き嫌いや得意不得意が出ます。自分は好き
  • しかしデバッグのしやすさにやや難あり。
    • 値がどうなっているかを見るには、同じ処理をするforeach文を書いた方が良い場合も
  • 見た目はすっきりし、モノによってはforeachより高速化(条件など諸説あり)するそうなので、Utilクラスとかに使ってもよさそう
    • 今度のネタにしますが、サブクラスを作ってLINQにまとめるというのも楽しいです
  • まれによくあるんですが、LINQの操作後の返り値がIEnumerable<T>であることを忘れたり、癖で.ToList()するので気をつけましょう。
  • サンプルというより読み物として書いてますので、その辺りご容赦下さい。
    • VB.Netで作っていたものをVSCode上でC#として書いただけなので、未検証部分があります。
    • また、チェックディジットの説明をひねり出しながら書いているので、違うところがあれば編集リクエスト等お願いします

バーコードのチェックディジット

……とは

バーコードが不正に作られたものではないことを保証するための、最後の桁として付与する数字。
いくつもの計算パターンがあるものの、規格ごとに1つに定められているので、バーコードの規格ごとに確認すると良いです。
一般に普及するJANコード(13桁:データ部12桁+チェックディジット1桁)の場合、データ部12桁に対するモジュラス10ウェイト3というパターンを用います。

これは、

  1. 奇数桁の合計に重み付け(ウェイト)で3倍(して偶数桁の合計と足す)
  2. 1.の合計を10のモジュラス:余剰(正確には10から1.の下1桁を引いたもの)で算出する

という意味で、

  1. (奇数桁の合計) * 3 + (偶数桁の合計) = ①
  2. 10 - (①の下1桁) = チェックディジット

という計算になります。(LINQに起こすときに少し付け加えます)

LINQに書き起こす前に

以上の通り、計算アルゴリズムの一種を用いるので、どのようにアルゴリズムを組むかが頭に入っていると良いです。
for文をベースにしてみましょう。

1.桁ごとの合計の計算

変数barcodeでバーコードのデータ部12桁がstring型で渡されているとします。
また、バーコードは数値のみの文字列であると保証されているものとして、ここではParseメソッドの例外は取りません。
(取るならif文でTryParse? VB.NetならIsNumericですね)

for-samprle01.cs
int oddSum = 0;
int evenSum = 0;

for(idx = 0; idx < barcode.Length; idx++){
    if(idx % 2 == 0) { //奇数桁
        var chr = barcode.Substring(idx, 1);
        oddSum += Integer.Parse(chr);
    } else { // 偶数桁
        var chr = barcode.Substring(idx, 1);
        evenSum += Integer.Parse(chr);
    }
}

int digitSum = oddSum * 3 + evenSum;

2.合計からチェックディジットの算出

先ほどのdigitSumを使い、チェックディジットを出します。
理由をあれこれ書いたものの、自信が無いので、早速LINQのメソッドを使います。
文字列をListのように見立てることが出来るので、「下1桁」を取るためにLast()メソッドを使います。

for-samprle02.cs
int lastSumDigit = Integer.Parse(digitSum.ToString().Last());
lastSumDigit = 10 - lastSumDigit;
string checkDigit = lastSumDigit.ToString().Last();

こうすると見た目が悪いですが、メソッドやプロパティとして外出しすれば問題ないと思います。
あるいは、三項演算子が許されていれば、digitSum % 10 = 0のときは最終的にチェックディジットは0になるので、この場合だけ「0」固定にしても良いでしょう。

for-samprle03.cs
int lastSumDigit = digitSum % 10;
lastSumDigit = lastSumDigit == 0 ? 0 : 10 - lastSumDigit;
string checkDigit = lastSumDigit.ToString();

とすることが出来ます。(正直こっちの方が好き)

LINQ版にしてみる

さて、LINQに書き換えるにあたって、注意することがあります。
LINQに書き換えられるのは、foreachでも扱えるIEnumerable<T>に属する型(例えばList)です。
しかし、このIEnumerable<T>系列の型は、「処理する順序は基本的に保証されない」ため、今回のように「奇数桁・偶数桁に分けて処理する」には前準備が必要になります。

それは、あらかじめindexのリストを「奇数リスト・偶数リスト」に分けておくことです。
つまり、最初の手順にこれを加え、

  1. 奇数桁のindexと偶数桁のindexの各リストを作成する
  2. (奇数桁の合計) * 3 + (偶数桁の合計) = ①
  3. 10 - (①の下1桁) = チェックディジット

という手順になります。(③はLINQは関係無くなるので割愛します)

①奇数桁のindexと偶数桁のindexの各リストを作成する

最初にして一番の肝のところです。手順をさらに細分化します。
元ネタは、for-samprle01.csのfor文の()内、およびif文になります。

  1. 0 ~ barcode.Length - 1のindexリストを作成する
  2. 1.のリストから、index % 2 == 0の条件で奇数桁リストを作る
  3. 1.のリストから、index % 2 == 1の条件で偶数桁リストを作る

サクッとサンプルを出しましょう。

LINQ-sample01.cs
// indexのリストを作り、奇数と偶数に仕分けする
IEnumerable<int> idxList = Enumerable.Range(0, barcode.Length - 1); 
IEnumerable<int> oddList = idxList.Where(x => x % 2 == 0);
IEnumerable<int> evenList = idxList.Where(x => x % 2 == 1); 

先ほどの1.~3.が各行に対応しています。

  • Enumerable.Range(0, barcode.Length - 1);でindexリストが生成される
  • idxList.Where(x => x % 2 == 0);のWhereメソッドで、この条件に合うindexが抽出される
  • idxList.Where(x => x % 2 == 1);も同様

idxList.Where(x => x % 2 == 1);idxList.Expect(oddList);としても問題はないですが、else文くらいの感覚で使いましょう。(詳しいことは割愛)

「x => x % 2 == 0」の「=>」(ラムダ式)

詳しいことはQiitaにたくさん記事があるので、ちゃんとした説明は概ね省きますが、ここでは「書き換え方」についてざっくりまとめます。
ラムダ式は、「簡単なメソッドを、(引数 => 引数を使った式やメソッドの返り値)の形式で書いたもの」くらいの認識で(自分は)書いています。
最初に「ワンライナーネタ」と書きましたが、「簡単なメソッドをワンライナーの中に埋め込んで、リストアップなどを楽にする」という感覚です。

LINQ-sample02.cs
// このWhereメソッドは
idxList.Where(x => x % 2 == 0);

// このようなメソッドと同じ(実際の返り値の型はIEnumerable<int>)
List<int> Where(List<int> idxList)
{
    var newList = new List<int>();
    foreach(var x In idxList)
    {
        if(x % 2 == 0){newList.Add(x);}
    }
    return newList;
}

これは多くのLINQメソッドでも同じで、bool型を扱うメソッドでよく使うところだと、

  • list.Any(x => x % 2 == 0)
    • x % 2 == 0がtrueになるxが1つでもあればtrue」(OR)
    • ちなみに、Any()Count() > 0の代用にもなる
  • list.All(x => x % 2 == 0)
    • 「すべてx % 2 == 0がtrueであれば結果もtrue」(AND)
  • list.Count(x => x % 2 == 0)
    • x % 2 == 0がtrueになるxの数をカウント」

という扱いになります。

②(③)①のリストから、奇数桁(偶数桁)リストを作る

あとはそれぞれ、同じように処理するので、まとめて書きます。

LINQ-sample03.cs
// 奇数/偶数のリストごとに、Selectで値を加工
// (ここでは文字列.Substringにindexを渡して取り出し、数値リストに加工)
// 下記のサブメソッドEncodeToIntに分けてもよいかも
IEnumerable<int> oddDigit = EncodeToInt(oddList, barcode);
IEnumerable<int> evenDigit = EncodeToInt(evenList, barcode);

---
// 面倒になってメソッドにまとめました
IEnumerable<int> EncodeToInt(IEnumerable<int> list, string barcode){
    // チェックディジットは数値であることが前提なので、例外は起こします
    // 必要に応じて例外処理を加えて下さい。
    return list.Select(idx => barcode.Substring(idx, 1))        // 文字列を切り出して
               .Select(chr => Integer.Parse(chr));              // 改めて数値リストに変換
}

後半のメソッド部分が本題です。
これはfor-samprle01.csで書いた、ifブロック内の値の加工をまとめたものです。

  • 1行目: indexのリストのアイテム1つずつ(idx)を引数に、barcode.Substring(idx, 1)で1文字ずつに加工(引数int→返り値string)
  • 2行目: 1文字ずつに切り出したもの(chr)を、Integer.Parse(chr)でint型に変換(引数string→返り値int)

.Select()を繋ぐことで、for-samprle02.csでやったような値の加工を、リストの値それぞれに行えます。

(奇数桁の合計) * 3 + (偶数桁の合計)

LINQ-sample04.cs
// それぞれSumで合計、奇数桁目は3倍の重み付け(ウェイト)
int sumOdd = oddDigit.Sum(x => x * 3);  // 一旦Selectで(x => x * 3)、あるいは次で3倍してもOK
int sumEven = evenDigit.Sum();          // こちらはそのまま

// 合計をstringに変換
int sumAll = sumOdd + sumEven;

.Sum()メソッドも、Select()と同じような形で、引数=>返り値のラムダ式を作ることが出来ます。
ラムダ式を入れない場合は、単にリストのすべてを合計した形です。

※補足:LINQ-sample03.csの時点で.Select(x => x * 3)としてもよいですが、どちらでやってもよく、デバッグ時のリストの確認やメソッドへの共通化のため、Sumでの加工にしました。

あとはLINQを使わない形になるので割愛しますが、最後に全文を載せて終わりにします。

LINQ版チェックディジット全文

LINQ-sampleAll.cs
bool CalcCheckDigit(string barcode){
    //最終の桁がチェックディジットなので省く(呼び出し時に省いていたら不要)
    barcode = barcode.Substring(0, barcode.Length - 1); 

    // indexのリストを作り、奇数と偶数に仕分けする
    IEnumerable<int> idxList = Enumerable.Range(0, barcode.Length); 
    IEnumerable<int> oddList = idxList.Where(x => x % 2 == 0);     // 奇数桁目はindex=0,2,...
    IEnumerable<int> evenList = idxList.Where(x => x % 2 == 1);    // 偶数桁目はindex=1,3,...

    // 奇数/偶数のリストごとに、Selectで値を加工
    // (ここでは文字列.Substringにindexを渡して取り出し、数値リストに加工)
    // 下記のサブメソッドEncodeToIntに分けてもよいかも
    IEnumerable<int> oddDigit = EncodeToInt(oddList, barcode);
    IEnumerable<int> evenDigit = EncodeToInt(evenList, barcode);

    // それぞれSumで合計、奇数桁目は3倍の重み付け(ウェイト)
    int sumOdd = oddDigit.Sum(x => x * 3);  // 一旦Selectで(x => x * 3)、あるいは次で3倍してもOK
    int sumEven = evenDigit.Sum();          // こちらはそのまま

    // 合計をstringに変換
    int sumAll = sumOdd + sumEven;
    string sumStr = sumAll.ToString();

    // 10から「合計の下1桁」を引く
    int lastDigit = Integer.Parse(sumStr.Last());
    int checkDigit = lastDigit == 0 ? 0 : 10 - lastDigit;

    return checkDigit;
}

IEnumerable<int> EncodeToInt(IEnumerable<int> list, string barcode){
    // チェックディジットは数値であることが前提なので、例外は起こします
    // 必要に応じて例外処理を加えて下さい。
    return list.Select(idx => barcode.Substring(idx, 1))        // 文字列を切り出して
               .Select(chr => Integer.Parse(chr));              // 改めて数値リストに変換
}
urahito_solution
仕事ではC#を本業としていますが、趣味のためにPythonを練習し始めました。最近は「同人活動×プログラミング」というテーマで色々計画しています。
http://urahito-solution.hatenablog.com/
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