僕も13桁のISBNコードを10桁に変換するコードを（実質）1行で書いてみた

## はじめに

Qiitaを見ていたらこちらの記事が目に止まりました。

[Rubyでisbn13からisbn10へ1行で変換する \- Qiita](https://qiita.com/110416/items/9c627b2c06ae192ce586)

上の記事ではこんな感じで、13桁のISBNコードを10桁に変換するプログラムの例が紹介されていました。

```
13桁 => 10桁
'9784063842760' => "4063842762" 
'9784774183619' => "477418361X"
'9784797386295' => "4797386290"
```

isbn13からisbn10に変換する仕様についてはこちらのブログ記事が参考になりました。

[ISBN13をISBN10に変換するロジックの解説 · DQNEO起業日記](http://dqn.sakusakutto.jp/2013/10/isbn13isbn10.html)

> 手順１：まず、 ISBN13の先頭3文字と末尾1文字を捨てます。
> すると9桁の数字が得られます。
> "9784063842760" => "406384276"
> 
> あとはこの９桁の数字から、ISBN10のチェックディジットを算出するだけです。
> そう、これは「変換」問題ではなくて、単に「ISBN10のチェックディジットを求める」問題にすぎないのです。
> そして「ISBN10のチェックディジットを求める方法」はWikipediaに書いてあるので、わざわざここでこんな記事を書く必要はなく、あなたもこんなブログ記事を読む必要はないわけです、こんにちは。
>
> 手順２：それぞれの数にについて、下記のように計算します。
> 4\*10 + 0\*9 + 6\*8 + 3\*7 + 8\*6 + 4\*5 + 2\*4 +7\*3 + 6\*2 = 218
>
> 手順３：手順２の結果を11で割った余りを求めます。
> 218 を11で割った余りは9
>
> 手順４：「11 - 手順３の答え」　を計算します。
> 11 - 9 = 2
>
> 手順５：手順４の答えが 11なら0に,10ならxに置き換えます。それ以外ならそのまま。
> 手順４の結果が"2"なのでそのまま"2"
>
> 手順６：手順５の結果がチェックディジットです。これを手順１の末尾に付けます。
> "40638427602" ←これがISBN10です！

（注：一番最後の"40638427602"は、"4063842762"が正だと思われます）

なるほど、これはプログラミングの練習問題としてはなかなか面白そうだと思い、僕もやってみることにしました。

## 僕が書いたコード

僕は最終的にこんなコードを書きました。
冒頭のQiita記事が「1行で書く」という縛りを付けていたので僕も実質1行で書いています（可読性のため改行を入れたが、改行がなくても動く）。

```ruby
def convert_isbn(isbn13)
  (body = isbn13[3..-2])
    .each_char.with_index
    .inject(0) { |sum, (c, i)| sum + c.to_i * (10 - i) }
    .then { |sum| 11 - sum % 11 }
    .then { |raw_digit| { 10 => 'X', 11 => 0 }[raw_digit] || raw_digit }
    .then { |digit| "#{body}#{digit}" }
end
```

ついでに、テストコードも書いています。
（というか、積極的にリファクタリングをするためにはテストコードは必須ですね）

```ruby
require './lib/isbn_converter'
require 'minitest/autorun'

class IsbnConverterTest < Minitest::Test
  def test_convert_isbn
    assert_equal '4063842762', convert_isbn('9784063842760')
  end

  def test_convert_isbn_x
    assert_equal '477418361X', convert_isbn('9784774183619')
  end

  def test_convert_isbn_0
    assert_equal '4797386290', convert_isbn('9784797386295')
  end
end
```

コードはGitHubに置いています。

[JunichiIto/isbn\-converter: Convert ISBN13 to ISBN10\.](https://github.com/JunichiIto/isbn-converter)

## 簡単なコード解説

最初の実装では愚直に前述の「手順」をコードに落としました。

```ruby
def change_isbn(isbn13)
  # 手順１：まず、 ISBN13の先頭3文字と末尾1文字を捨てます。
  body = isbn13[3..-2]

  # 手順２：それぞれの数にについて、下記のように計算します。
  # 4*10 + 0*9 + 6*8 + 3*7 + 8*6 + 4*5 + 2*4 +7*3 + 6*2 = 218
  sum = body.each_char.map.with_index{|a,b| a.to_i * (10-b) }.sum

  # 手順３：手順２の結果を11で割った余りを求めます。
  mod = sum % 11

  # 手順４：「11 - 手順３の答え」　を計算します。
  raw_digit = 11 - mod

  # 手順５：手順４の答えが 11なら0に,10ならxに置き換えます。それ以外ならそのまま。
  digit =
    case raw_digit
    when 11 then '0'
    when 10 then 'X'
    else raw_digit.to_s
    end

  # 手順６：手順５の結果がチェックディジットです。これを手順１の末尾に付けます。
  "#{body}#{digit}"
end
```

### thenを活用して処理を連結する

あとは基本的にこれを全部つなげて1行にしただけですが、「手前で作成した変数を次の処理で使う」という処理が中心だったので、Ruby 2.6で追加された`then`を使ってみました。

参考：[サンプルコードでわかる！Ruby 2\.6の主な新機能と変更点 \- Qiita](https://qiita.com/jnchito/items/ce11cb7fccdf4b66af24)

実は`then`をちゃんと使ったのは今回が初めてなんですが、こうやって使ってみるとなかなか便利ですね。

### ハッシュを利用して0やXの置き換えを実装する

手順5の「手順４の答えが 11なら0に,10ならxに置き換えます。それ以外ならそのまま」というところは、最初はcase文で実装していたのですが、最終的にはハッシュを活用して書きました。

```ruby
.then { |raw_digit| { 10 => 'X', 11 => 0 }[raw_digit] || raw_digit }
```

10と11の場合だけハッシュから値を取得し、それ以外の場合は`nil`になるので、`||`で`raw_digit`（手順4の値）をそのまま返すようにしています。

**追記**

ここは`fetch`メソッドを使っても良かったかもしれませんね。
結果は上と同じになります。

```ruby
.then { |raw_digit| { 10 => 'X', 11 => 0 }.fetch(raw_digit, raw_digit) }
```

### 変数への代入をそのまま式として利用する

前述の手順では最初と最後に「ISBN13の先頭3文字と末尾1文字を捨てた文字列」が登場します。

愚直にDRYを実現しようとすると次のようになりますが、これだと1行になりません。

```ruby 
body = isbn13[3..-2]

body
  .each_char.with_index
  # ...
  .then { |digit| "#{body}#{digit}" }
```

しかし変数を使わずに1行で書こうとすると、`isbn13[3..-2]`のコードがDRYになりません。

```ruby
isbn13[3..-2]
  .each_char.with_index
  # ...
  .then { |digit| "#{isbn13[3..-2]}#{digit}" }
```

そこで今回は変数への代入をそのまま式として後続のメソッド呼び出しで利用するようにしました。

```ruby
(body = isbn13[3..-2])
  .each_char.with_index
  # ...
  .then { |digit| "#{body}#{digit}" }
```

若干技巧的でお行儀がよくない気もしますが、これぐらいなら許容範囲かな〜と思います。

### その他
文字列から先頭3文字と末尾1文字を捨てる処理は、範囲を使って実現できます。
Rubyはマイナスの値で「うしろから何番目」を指定できるので便利ですね。

```ruby
isbn13 = "9784063842760"
isbn13[3..-2]
#=> "406384276"
```

そのあとの2行は拙著「[プロを目指す人のためのRuby入門](http://gihyo.jp/book/2017/978-4-7741-9397-7)」（通称・チェリー本）の知識があれば読めるコードです。

```ruby
.each_char.with_index
.inject(0) { |sum, (c, i)| sum + c.to_i * (10 - i) }
```

持っている方は4.4.4項、4.8.2項、4.8.4項をご覧ください。
持ってない方はお近くの書店でご確認ください😄

ただし、`each_char`だけはチェリー本に登場しないので、知らない方はAPIドキュメントを参照してください。

https://docs.ruby-lang.org/ja/latest/method/String/i/each_char.html

## まとめ

というわけで、この記事では13桁のISBNコードを10桁に変換するプログラムを（実質）1行で実装する例を紹介しました。
あらためて完成形のコードを載せておきますね。

```ruby
def convert_isbn(isbn13)
  (body = isbn13[3..-2])
    .each_char.with_index
    .inject(0) { |sum, (c, i)| sum + c.to_i * (10 - i) }
    .then { |sum| 11 - sum % 11 }
    .then { |raw_digit| { 10 => 'X', 11 => 0 }[raw_digit] || raw_digit }
    .then { |digit| "#{body}#{digit}" }
end
```

今回のコードでは「1行で書く」といっても、手順を上から下へ素直に実装して、できるだけ可読性を維持したいと考えたのですが、いかがでしょうか？
「難しそう、よくわからん」という人も、元の仕様とRubyの言語仕様、それにRubyの標準API仕様を理解すれば、だんだんとコードの仕組みが見えてくるんじゃないかなーと思います。

プログラミングの練習問題としてはなかなか面白い問題だと思うので、みなさんも一度「1行で書くisbnコード変換プログラム」にトライしてみてください！

### あわせて読みたい

こちらのブログ記事には、今回と同じような比較的簡単なプログラミング問題を集めています。

[アウトプットのネタに困ったらこれ！？Ruby初心者向けのプログラミング問題を集めてみた（全10問） \- give IT a try](https://blog.jnito.com/entry/2019/05/03/121235)

また、Railsに依存しない、ピュアなRubyプログラムについて勉強したい方は、拙著「プロを目指す人のためのRuby入門」を読んでみてください。
今回の紹介したような問題を解く上では欠かせないRubyの知識が満載ですので、きっと役に立つと思います！

[プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで：書籍案内｜技術評論社](http://gihyo.jp/book/2017/978-4-7741-9397-7)