ガラケー文字入力問題の解答

前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。

blog.jnito.com

ガラケー文字入力問題

英語のガラケーでは「2」キーを2回押すと「b」になり、「3」キーを3回押すと「f」になります。
ただし、この問題では特別ルールとして「0」で文字を確定させます。 たとえば、このプログラムに対して"440330555055506660"を入力すると、"hello"が返ってきます。

解答

class FeaturePhone
  BUTTONS = {
    '1' => ['.', ',', '!', '?', ' '],
    '2' => %w[a b c],
    '3' => %w[d e f],
    '4' => %w[g h i],
    '5' => %w[j k l],
    '6' => %w[m n o],
    '7' => %w[p q r s],
    '8' => %w[t u v],
    '9' => %w[w x y z]
  }.freeze

  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      button_values = BUTTONS[digits[0]]
      button_values[(digits.length - 1) % button_values.length]
    }.join
  end
end

テストコード

describe FeaturePhone do
  describe '#digits_to_alphabet' do
    subject { phone.digits_to_alphabet(digits) }
    let(:phone){ FeaturePhone.new }

    context '2のボタンが1回押された場合' do
      let(:digits) { '20' }
      it { is_expected.to eq 'a'}
    end
    context '4, 3, 5, 6のボタンが2回か3回押された場合' do
      let(:digits) { '440330555055506660' }
      it { is_expected.to eq 'hello'}
    end
    context '1, 4, 3, 5, 6, 7, 9のボタンが1回から5回押された場合' do
      let(:digits) { '44033055505550666011011111090666077705550301110' }
      it { is_expected.to eq 'hello, world!'}
    end
    context '0が複数回登場し、5のボタンが8回押されたケースが含まれる場合' do
      let(:digits) { '000555555550000330000444000080000200004440000' }
      it { is_expected.to eq 'keitai'}
    end
  end
end

出題元のサイト ( Keitai Message | Aizu Online Judge )では、はじめに入力数とその数ぶんの入力列が与えられるという使用ですが、その部分は実装していません。

解説

始めに、クラスとメソッドと引数名を定義します。ガラケーは英語では"feature phone"というようです。これをクラス名にします。

メソッド名は数字の列をアルファベットに直すという意味で、digits_to_alphabetにしました。引数は、数字の列ということで、digits_lineにしました。

class FeaturePhone
  def digits_to_alphabet(digits_line)
    0
  end
end

テストを書きます。

require 'rspec'
require File.expand_path(File.dirname(__FILE__) + '/../feature_phone')

describe FeaturePhone do
  describe '#digits_to_alphabet' do
    subject { phone.digits_to_alphabet(digits_line) }
    let(:phone){ FeaturePhone.new }

    context '2のボタンが1回押された場合' do
      let(:digits_line) { '20' }
      it { is_expected.to eq 'a'}
    end
  end
end

テストを実行して落ちるのを確認します。

expected: "a"
     got: 0
# 中略
1 example, 1 failure, 0 passed

では、実装を進めていきます。

まず、0以外の数字が連続している部分を抜き出すことを考えてみます。今回は正規表現を使います。

0以外の数値は、正規表現[1-9]と書けます。正確にいうと、[1-9]は、1から9のうちの1文字を表します。1から9の文字の1回以上の繰り返しは、[1-9]+と書けます。この正規表現3555888888881144などにマッチします。同じ数宇の繰り返し以外にもマッチしますが、今回は、0と0で挟まれた部分は同じ数字になっているので、こちらの正規表現でも問題ないでしょう。同じ数字の繰り返しを表現したい場合はこちらの記事を参照してください。

yucatio.hatenablog.com

さて、アルファベットに変換する部分の繰り返しは[1-9]+で表現されることがわかったので、今度はマッチした部分を抜き出します。 RubyString#scan( scan (String) - Rubyリファレンス )メソッドを使えばよさそうです。このメソッドは、引数に与えられた正規表現にマッチする部分を繰り返し取り出します。

ここまでコードです。

class FeaturePhone
  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/)
  end
end

テスト結果です。

expected: "a"
     got: ["2"]
# 中略
1 example, 1 failure, 0 passed

配列の要素が1つだと動作が見せにくいので、ここでテストケースを追加しておきます。

describe FeaturePhone do
  describe '#digits_line' do
    subject { phone.digits_line(digits_line) }
    let(:phone){ FeaturePhone.new }

    context '2のボタンが1回押された場合' do
      let(:numbers) { '20' }
      it { is_expected.to eq 'a'}
    end
    context '4, 3, 5, 6のボタンが2回か3回押された場合' do
      let(:digits_line) { '440330555055506660' }
      it { is_expected.to eq 'hello'}
    end
  end
end

テストを実行すると、新しいテストも失敗しているのが分かります。ただ、連続する数値が配列の各要素にすることができています。

expected: "hello"
     got: ["44", "33", "555", "555", "666"]
# 中略
2 examples, 2 failures, 0 passed

この配列の各要素をそれぞれアルファベットに変換しましょう。

 ["44", "33", "555", "555", "666"]
    ↓     ↓      ↓      ↓      ↓
 [ "h",  "e",   "l",   "l",   "o"]

このような変換はmapを使うのでした。一旦mapをつなげてみましょう。中の実装は仮です。

class FeaturePhone
  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      digits  # 仮実装
    }
  end
end

テストを実行して、前回と同じ出力になる事を確認してください。

次はどのように"555"を"l"に変換するか考えます。まず、どのボタンが何回押されたかを考えることにしましょう。"555"の場合は"5"のボタンが"3"回というふうにです。 どのボタンが押されたか、は1番目の文字を見ればよさそうです。1番目の文字は、配列と同じように[0]で取得することができるので、今回の場合はdigits[0]で取得することができます。また、何回押されたかは、String#lengthで取得できるので、上記のコードではdigits.lengthで取得することができます。

class FeaturePhone
  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      digit = digits[0]
      length = digits.length
    }
  end
end

ここでガラケーのボタンを定義しましょう。キーを数字(文字列型)にして、値をアルファベットの配列にしました。ボタンは定数として定義しました。 1に対応するアルファベット(というか記号)はスペースを含むため、角カッコ[]を使用して定義していますが、それ以外は、パーセント記法(%w)を使って見やすくしています。最後にfreezeをつけて変更不能にしました。

class FeaturePhone
  BUTTONS = {
    '1' => ['.', ',', '!', '?', ' '],
    '2' => %w[a b c],
    '3' => %w[d e f],
    '4' => %w[g h i],
    '5' => %w[j k l],
    '6' => %w[m n o],
    '7' => %w[p q r s],
    '8' => %w[t u v],
    '9' => %w[w x y z]
  }.freeze

  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      digit = digits[0]
      length = digits.length
    }
  end
end

まずは押されたボタンから、対応するアルファベットの配列を取得しましょう。

class FeaturePhone
  BUTTONS = {
    '1' => ['.', ',', '!', '?', ' '],
    '2' => %w[a b c],
    '3' => %w[d e f],
    '4' => %w[g h i],
    '5' => %w[j k l],
    '6' => %w[m n o],
    '7' => %w[p q r s],
    '8' => %w[t u v],
    '9' => %w[w x y z]
  }.freeze

  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      digit = digits[0]
      length = digits.length
      button_values = BUTTONS[digit]  # 追加
    }
  end
end

テストを実行します。

expected: "hello"
     got: [["g", "h", "i"], ["d", "e", "f"], ["j", "k", "l"], ["j", "k", "l"], ["m", "n", "o"]]

だんだん完成に近づいてきましたね。

配列の中から1つアルファベットを選択すればよさそうです。選択にはlengthを使います。配列の添字は0から始まるので、数字列の長さ(length)から1を引く必要があります。

class FeaturePhone
  BUTTONS = {
    # 中略
  }.freeze

  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      digit = digits[0]
      length = digits.length
      button_values = BUTTONS[digit]
      button_values[digits.length - 1]  # 追加
    }
  end
end

テスト結果です。

expected: "hello"
     got: ["h", "e", "l", "l", "o"]

あとは各配列の要素を結合すれば良いですね。Array.join( join (Array) - Rubyリファレンス )が使えます。

class FeaturePhone
  BUTTONS = {
    # 中略
  }.freeze

  def digits_to_alphabet(digits_line)
    digits_line.scan(/[1-9]+/).map {|digits|
      digit = digits[0]
      length = digits.length
      button_values = BUTTONS[digit]
      button_values[digits.length - 1]
    }.join  # 追加
  end
end

テスト結果です。

2 examples, 0 failures, 2 passed

テストがパスしました。

しかしここで終わりではありません。テストケースを追加します。

describe FeaturePhone do
  describe '#digits_to_alphabet' do
    subject { phone.digits_to_alphabet(digits) }
    let(:phone){ FeaturePhone.new }

   # 中略
   # 追加ここから
    context '1, 4, 3, 5, 6, 7, 9のボタンが1回から5回押された場合' do
      let(:digits) { '44033055505550666011011111090666077705550301110' }
      it { is_expected.to eq 'hello, world!'}
    end
    context '0が複数回登場し、5のボタンが8回押されたケースが含まれる場合' do
      let(:digits) { '000555555550000330000444000080000200004440000' }
      it { is_expected.to eq 'keitai'}
    end
    # 追加ここまで
  end
end

テストを実行します。

expected: "keitai"
     got: "eitai"

最後のテストが失敗してしまいました。5のボタンが8回押されているので、配列の範囲外を参照してしまいました。配列の長さより大きい回数押された場合は、ループするのでした。コードを変更します。button_values.lengthの剰余を取ればよいです。

def digits_to_alphabet(digits_line)
  BUTTONS = {
    # 中略
  }.freeze

  digits_line.scan(/[1-9]+/).map {|digits|
    digit = digits[0]
    length = digits.length
    button_values = BUTTONS[digit]
    button_values[(digits.length - 1) % button_values.length]  # 変更
  }.join
end

テストを実行します。

4 examples, 0 failures, 4 passed

テストが通りました。digitとlengthはいちいち変数に入れるまでもないと思ったので最終的には冒頭のコードとしました。

あとがき

はじめ、同じ数字の連続したものを抽出しようとして試行錯誤しようとして時間を無駄にしてしまいました。同じ数の連続のことは以下の記事にしました。

yucatio.hatenablog.com

プログラミングっぽい問題だったのでブログに書くのに緊張しましたが、シンプルに書けたと思います。

出題者の伊藤淳一さんの書籍はこちら↓