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

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

## 概要という名の感想

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

## バーコードのチェックディジット
### ……とは

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

これは、

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

という意味で、

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

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

## LINQに書き起こす前に

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

### 1.桁ごとの合計の計算

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

```C#: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()メソッドを使います。

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

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

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

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


## LINQ版にしてみる

さて、LINQに書き換えるにあたって、注意することがあります。
LINQに書き換えられるのは、foreachでも扱える``IEnumerable<T>``に属する型（例えばList<T>）です。
しかし、この``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``の条件で偶数桁リストを作る

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

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

```C#: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の数をカウント」

という扱いになります。

#### ②（③）①のリストから、奇数桁（偶数桁）リストを作る

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

```C#: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 + (偶数桁の合計)

```C#: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版チェックディジット全文

```C#: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から「合計の下１桁」を引く
    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));              // 改めて数値リストに変換
}
```