バーコードのチェックディジットをLINQでゴリ押してみる
概要という名の感想
- LINQはあくまでワンライナーネタ(※個人の感想)なので、好き嫌いや得意不得意が出ます。自分は好き
- しかしデバッグのしやすさにやや難あり。
- 値がどうなっているかを見るには、同じ処理をするforeach文を書いた方が良い場合も
- 見た目はすっきりし、モノによってはforeachより高速化(条件など諸説あり)するそうなので、Utilクラスとかに使ってもよさそう
- 今度のネタにしますが、サブクラスを作ってLINQにまとめるというのも楽しいです
- まれによくあるんですが、LINQの操作後の返り値が
IEnumerable<T>
であることを忘れたり、癖で.ToList()
するので気をつけましょう。 - サンプルというより読み物として書いてますので、その辺りご容赦下さい。
- VB.Netで作っていたものをVSCode上でC#として書いただけなので、未検証部分があります。
- また、チェックディジットの説明をひねり出しながら書いているので、違うところがあれば編集リクエスト等お願いします
バーコードのチェックディジット
……とは
バーコードが不正に作られたものではないことを保証するための、最後の桁として付与する数字。
いくつもの計算パターンがあるものの、規格ごとに1つに定められているので、バーコードの規格ごとに確認すると良いです。
一般に普及するJANコード(13桁:データ部12桁+チェックディジット1桁)の場合、データ部12桁に対するモジュラス10ウェイト3
というパターンを用います。
これは、
- 奇数桁の合計に重み付け(ウェイト)で3倍(して偶数桁の合計と足す)
- 1.の合計を10のモジュラス:余剰(正確には10から1.の下1桁を引いたもの)で算出する
という意味で、
- (奇数桁の合計) * 3 + (偶数桁の合計) = ①
- 10 - (①の下1桁) = チェックディジット
という計算になります。(LINQに起こすときに少し付け加えます)
LINQに書き起こす前に
以上の通り、計算アルゴリズムの一種を用いるので、どのようにアルゴリズムを組むかが頭に入っていると良いです。
for文をベースにしてみましょう。
1.桁ごとの合計の計算
変数barcode
でバーコードのデータ部12桁がstring型で渡されているとします。
また、バーコードは数値のみの文字列であると保証されているものとして、ここではParseメソッドの例外は取りません。
(取るならif文でTryParse? VB.NetならIsNumericですね)
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()メソッドを使います。
int lastSumDigit = Integer.Parse(digitSum.ToString().Last());
lastSumDigit = 10 - lastSumDigit;
string checkDigit = lastSumDigit.ToString().Last();
こうすると見た目が悪いですが、メソッドやプロパティとして外出しすれば問題ないと思います。
あるいは、三項演算子が許されていれば、digitSum % 10 = 0
のときは最終的にチェックディジットは0になるので、この場合だけ「0」固定にしても良いでしょう。
int lastSumDigit = digitSum % 10;
lastSumDigit = lastSumDigit == 0 ? 0 : 10 - lastSumDigit;
string checkDigit = lastSumDigit.ToString();
とすることが出来ます。(正直こっちの方が好き)
LINQ版にしてみる
さて、LINQに書き換えるにあたって、注意することがあります。
LINQに書き換えられるのは、foreachでも扱えるIEnumerable<T>
に属する型(例えばList)です。
しかし、このIEnumerable<T>
系列の型は、「処理する順序は基本的に保証されない」ため、今回のように「奇数桁・偶数桁に分けて処理する」には前準備が必要になります。
それは、あらかじめindexのリストを「奇数リスト・偶数リスト」に分けておくことです。
つまり、最初の手順にこれを加え、
- 奇数桁のindexと偶数桁のindexの各リストを作成する
- (奇数桁の合計) * 3 + (偶数桁の合計) = ①
- 10 - (①の下1桁) = チェックディジット
という手順になります。(③はLINQは関係無くなるので割愛します)
①奇数桁のindexと偶数桁のindexの各リストを作成する
最初にして一番の肝のところです。手順をさらに細分化します。
元ネタは、for-samprle01.cs
のfor文の()内、およびif文になります。
-
0 ~ barcode.Length - 1
のindexリストを作成する - 1.のリストから、
index % 2 == 0
の条件で奇数桁リストを作る - 1.のリストから、
index % 2 == 1
の条件で偶数桁リストを作る
サクッとサンプルを出しましょう。
// 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にたくさん記事があるので、ちゃんとした説明は概ね省きますが、ここでは「書き換え方」についてざっくりまとめます。
ラムダ式は、「簡単なメソッドを、(引数 => 引数を使った式やメソッドの返り値)
の形式で書いたもの」くらいの認識で(自分は)書いています。
最初に「ワンライナーネタ」と書きましたが、「簡単なメソッドをワンライナーの中に埋め込んで、リストアップなどを楽にする」という感覚です。
// この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の数をカウント」
- 「
という扱いになります。
②(③)①のリストから、奇数桁(偶数桁)リストを作る
あとはそれぞれ、同じように処理するので、まとめて書きます。
// 奇数/偶数のリストごとに、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 + (偶数桁の合計)
// それぞれ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版チェックディジット全文
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)); // 改めて数値リストに変換
}