4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【初心者向け】Ruby のまずいコード 25 本Advent Calendar 2021

Day 14

【Ruby のまずいコード】回文素数を表示

Last updated at Posted at 2021-12-13

お題

3 桁の回文素数をすべて表示してください。

回文素数とは,回文数になっている素数のことです。
回文数とは,数字列で書いたときにそれが回文になっている数のことです。
(一般の $N$ について,$N$ 進法の回文数を考えることができますが,ここでは 10 進法で表記した場合のこととします)

たとえば,101 という数は回文になっていて,しかも素数なので回文素数です。
121 は回文数ですが,合成数なので($11^2 = 121$)回文素数ではありません。

コード

require "prime"

def put_palindromic_prime_number(num)
  str = num.to_s
  puts str if num.prime? && str == str.reverse
end

(100..999).each do |num|
  put_palindromic_prime_number(num)
end

問題点

コードは合っています。101 から 929 まで,15 個の回文素数が表示されます。

素数かどうかの判定は,自分でアルゴリズムを実装するより,このコードのように標準添付ライブラリーの prime を使うほうがいいですね。
回文かどうかの判定も str == str.reverse でいいでしょう。
また,機能の一部をメソッドに切り出したことも悪くないと思います。

まずいのは,メソッド内で puts している点です。
こういうコードを Qiita の記事で非常によく見かけます。
何がまずいのでしょうか。

つぶしが効かない

このメソッドは柔軟性に欠けます。
標準出力でなくファイルに書き出したくなったらどうするのでしょうか?
回文素数ごとに改行するのではなく,空白で区切って表示したくなったら?

そのたびにメソッドを書き換えるか,新設することになります。
一つのプログラムで複数の出力方法に対応したければ,メソッドをいくつも作るか,出力方法を指定する引数を設ける,といったことになります。
スジの悪いやり方です1

一つのメソッドに,

  • 回文素数かどうかを判定する機能
  • 回文素数だった場合にそれを表示する機能

という異質なものを詰め込んだ設計が失敗なのでした。
前者は普遍的であり,したがって安定しています。
後者は普遍性があまりなく,上で見たように多様で変わりやすいものです。

テストしづらい

回文素数のような,判定が単純なものならまだいいのですが,もう少し複雑になってくると,アルゴリズムやその実装が本当に合っているか組織的に検証する必要があります。
そのためにソフトウエアテストを行います。プログラムでプログラムをテストするのです。
たとえば,101121 を与えて,正しく動作するかどうかを検証するプログラムを書くのです。

しかし,我らが put_palindromic_prime_number メソッドは,テストに向いていません。
標準出力に何が表示されたかをプログラムで拾うのは面倒なのです。

改善

問題点が理解できたら,改善は容易ですね。
要するに,メソッドの役目を「回文素数かどうか判定する」ことに留めればいいのです。

require "prime"

def palindromic_prime?(num)
  str = num.to_s
  num.prime? && str == str.reverse
end

(100..999).each do |num|
  puts num if palindromic_prime?(num)
end

出力方式を変えたくなってもメソッドには手をつけなくてすみます。

また,この palindromic_prime? メソッドは容易にテストできます。
標準添付ライブラリーの test-unit を使うと,以下のようなテストコードが書けます2

require "prime"
require "test/unit"

def palindromic_prime?(num)
  str = num.to_s
  num.prime? && str == str.reverse
end

class TestPalindromic_prime < Test::Unit::TestCase
  test "回文素数" do
    assert palindromic_prime?(2)
    assert palindromic_prime?(7)
    assert palindromic_prime?(101)
    assert palindromic_prime?(15451)
  end

  test "回文数だが素数でない" do
    refute palindromic_prime?(9)
    refute palindromic_prime?(22)
    refute palindromic_prime?(121)
    refute palindromic_prime?(12321)
  end

  test "素数だが回文数でない" do
    refute palindromic_prime?(13)
    refute palindromic_prime?(97)
    refute palindromic_prime?(1987)
  end

  test "回文数でも素数でもない" do
    refute palindromic_prime?(24)
    refute palindromic_prime?(9876)
  end
end

簡単のためテスト対象とテストコードを一つのファイルにまとめて示しましたが,ふつうは別ファイルにします。

テストコードがしっかり書けていれば,メソッドをリファクタリングするときにバグの混入が防げます。

機能の一部をメソッドに括り出す際,「そのメソッドはテストしやすいか?」という観点も持つとよいでしょう。

  1. 引数によって出力方法を分けること自体は悪くありませんが,多様な用途に対応しようとすると設計が複雑になる恐れがあります。

  2. 同じく標準添付ライブラリーの minitest を使っても,これと似たコードでテストできます。

4
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?