前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。
ガラケー文字入力問題
英語のガラケーでは「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]+
と書けます。この正規表現は3
や555
、888888
、881144
などにマッチします。同じ数宇の繰り返し以外にもマッチしますが、今回は、0と0で挟まれた部分は同じ数字になっているので、こちらの正規表現でも問題ないでしょう。同じ数字の繰り返しを表現したい場合はこちらの記事を参照してください。
さて、アルファベットに変換する部分の繰り返しは[1-9]+
で表現されることがわかったので、今度はマッチした部分を抜き出します。
RubyのString#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はいちいち変数に入れるまでもないと思ったので最終的には冒頭のコードとしました。
あとがき
はじめ、同じ数字の連続したものを抽出しようとして試行錯誤しようとして時間を無駄にしてしまいました。同じ数の連続のことは以下の記事にしました。
プログラミングっぽい問題だったのでブログに書くのに緊張しましたが、シンプルに書けたと思います。
出題者の伊藤淳一さんの書籍はこちら↓