ガラケー文字入力問題の解答
前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。
ガラケー文字入力問題
英語のガラケーでは「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はいちいち変数に入れるまでもないと思ったので最終的には冒頭のコードとしました。
あとがき
はじめ、同じ数字の連続したものを抽出しようとして試行錯誤しようとして時間を無駄にしてしまいました。同じ数の連続のことは以下の記事にしました。
プログラミングっぽい問題だったのでブログに書くのに緊張しましたが、シンプルに書けたと思います。
出題者の伊藤淳一さんの書籍はこちら↓
電話帳作成問題の解答
前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。
電話帳作成問題
カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成する問題です。
# INPUT ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'] # OUTPUT [ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]
詳しい仕様はこちらにあります。
解答
class NameIndex NAME_INDEX_HASH = { 'ア' => %w(ア イ ウ エ オ ヴ), 'カ' => %w(カ キ ク ケ コ ガ ギ グ ゲ ゴ), 'サ' => %w(サ シ ス セ ソ ザ ジ ズ ゼ ゾ), 'タ' => %w(タ チ ツ テ ト ダ ヂ ヅ デ ド), 'ナ' => %w(ナ ニ ヌ ネ ノ), 'ハ' => %w(ハ ヒ フ ヘ ホ バ ビ ブ ベ ボ パ ピ プ ペ ポ), 'マ' => %w(マ ミ ム メ モ), 'ヤ' => %w(ヤ ユ ヨ), 'ラ' => %w(ラ リ ル レ ロ), 'ワ' => %w(ワ ヲ ン) }.freeze def self.create_index(names) names.sort.group_by {|name| NAME_INDEX_HASH.find(['Others']) {|_, value| value.include?(name[0])}.first }.to_a end end
テストです。
describe NameIndex do describe '#create_index' do specify {expect(NameIndex.create_index([])).to eq []} specify {expect(NameIndex.create_index(['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'])).to eq [['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']]]} specify {expect(NameIndex.create_index(['サトウ', 'スズキ', 'タカハシ', 'イケガミ', 'アラキ', 'デグチ', 'ヌマタ'])).to eq [['ア', ['アラキ', 'イケガミ']], ['サ', ['サトウ', 'スズキ']], ['タ', ['タカハシ', 'デグチ']], ['ナ', ['ヌマタ']]]} end end
元記事では、解答を見ないで解くこととなっていましたが、ある程度自分で考えた後に解答を見てから解きました。それでもオリジナルなコードになったと思います。
解説
問題を見たときに以下の4つを考えました。
- 名前の先頭のカタカナを取り出す
- カタカナが属する行のア段のカタカナを求める(ただし濁音、半濁音は清音にする)(例: キ → カ, バ →ハ)
- ア段のカタカナでグループ化する
- ソートする
これらをクリアにしていけば解けそうです。
- 名前の先頭のカタカナを取り出すには
name[0]
のように、[0]
を使用します。 - カタカナが属する行のア段のカタカナを求める を行う標準のメソッドは存在しないので、自身で実装する必要があります。
- ア段のカタカナでグループ化する は
Array.group_by
( group_by (Enumerable) - Rubyリファレンス )を使用します。 - ソートする は
Array.sort
( sort (Enumerable) - Rubyリファレンス )が使用できます。
まず先頭のカタカナを取り出すところまで実装しましょう。
class NameIndex def self.create_index(names) names.map { |name| name[0] } end end
テストは一旦以下の2ケースのみ書いておくことにします。また、以下実行結果として書くのは2つ目のテストケースについてです。
describe NameIndex do describe '#create_index' do specify { expect(NameIndex.create_index([])).to eq [] } specify { expect(NameIndex.create_index(['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ'])).to eq [ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ] } end end
実行結果です。先頭の文字列を取り出すことができました。
["キ", "イ", "バ", "カ", "ワ", "ハ"]
次にア段の音に変換していきます。範囲オブジェクトを使用する方法もありますが、
50音表の順番で並んでいるわけではないので、注意して使用する必要があります。(
片仮名 (Unicodeのブロック) - Wikipedia
)
(カキクケコガギグゲゴ
と並ぶわけでなく、カガキギクグケゲコゴ
と並んでいます。)
今回はわかりやすさのため、名前の先頭に使用できるカタカナを全て書き出しました。(ヲ ンから始まる名前があるかどうか微妙ですが)
どのカタカナがどのア段のカタカナに対応するか求めます。まず全てのカタカナをキー、対応するア段のカタカナを値とする方法が思いつきます。
NAME_INDEX_HASH = {'ア' => 'ア', 'イ' => 'ア', 'ウ' => 'ア', ..., }
これは確実な方法ですが、コードが長くなるため今回は採用しませんでした。
代わりに、ア段のカタカナをキー、そのア段のカタカナが含むカタカナを値とするハッシュを定義しました。この場合だとハッシュの逆引きが必要なのですが、コードがわかりやすくなります。
NAME_INDEX_HASH = { 'ア' => %w(ア イ ウ エ オ ヴ), 'カ' => %w(カ キ ク ケ コ ガ ギ グ ゲ ゴ), 'サ' => %w(サ シ ス セ ソ ザ ジ ズ ゼ ゾ), 'タ' => %w(タ チ ツ テ ト ダ ヂ ヅ デ ド), 'ナ' => %w(ナ ニ ヌ ネ ノ), 'ハ' => %w(ハ ヒ フ ヘ ホ バ ビ ブ ベ ボ パ ピ プ ペ ポ), 'マ' => %w(マ ミ ム メ モ), 'ヤ' => %w(ヤ ユ ヨ), 'ラ' => %w(ラ リ ル レ ロ), 'ワ' => %w(ワ ヲ ン) }.freeze
ス→サ
のような変換を行うには、"ス"が含まれる配列を見つけ、それのキーを取得することで実現できます。
Enumerable#find
(
find, detect (Enumerable) - Rubyリファレンス
)
を使用すると、ブロックが最初に真になったときのキーと値を返します。名前の始めの文字が見つからなかったときの挙動は特に問題には示されていなかったので、"Other"というキーを返すようにしました。
class NameIndex NAME_INDEX_HASH = { 'ア' => %w(ア イ ウ エ オ ヴ), 'カ' => %w(カ キ ク ケ コ ガ ギ グ ゲ ゴ), 'サ' => %w(サ シ ス セ ソ ザ ジ ズ ゼ ゾ), 'タ' => %w(タ チ ツ テ ト ダ ヂ ヅ デ ド), 'ナ' => %w(ナ ニ ヌ ネ ノ), 'ハ' => %w(ハ ヒ フ ヘ ホ バ ビ ブ ベ ボ パ ピ プ ペ ポ), 'マ' => %w(マ ミ ム メ モ), 'ヤ' => %w(ヤ ユ ヨ), 'ラ' => %w(ラ リ ル レ ロ), 'ワ' => %w(ワ ヲ ン) }.freeze def self.create_index(names) names.map { |name| NAME_INDEX_HASH.find(['Others']) {|_, value| value.include?(name[0])} # 変更 } end end
実行結果です。名前の最初のカタカナが含まれている配列と対応するキーが取り出せています。
[ ["カ", ["カ", "キ", "ク", "ケ", "コ", "ガ", "ギ", "グ", "ゲ", "ゴ"]], ["ア", ["ア", "イ", "ウ", "エ", "オ", "ヴ"]], ["ハ", ["ハ", "ヒ", "フ", "ヘ", "ホ", "バ", "ビ", "ブ", "ベ", "ボ", "パ", "ピ", "プ", "ペ", "ポ"]], ["カ", ["カ", "キ", "ク", "ケ", "コ", "ガ", "ギ", "グ", "ゲ", "ゴ"]], ["ワ", ["ワ", "ヲ", "ン"]], ["ハ", ["ハ", "ヒ", "フ", "ヘ", "ホ", "バ", "ビ", "ブ", "ベ", "ボ", "パ", "ピ", "プ", "ペ", "ポ"]] ]
必要なのはキーだけなんで、配列の最初の要素を取り出します。Array#first
(
first (Array) - Rubyリファレンス
)を使用します。
class NameIndex NAME_INDEX_HASH = { 'ア' => %w(ア イ ウ エ オ ヴ), 'カ' => %w(カ キ ク ケ コ ガ ギ グ ゲ ゴ), 'サ' => %w(サ シ ス セ ソ ザ ジ ズ ゼ ゾ), 'タ' => %w(タ チ ツ テ ト ダ ヂ ヅ デ ド), 'ナ' => %w(ナ ニ ヌ ネ ノ), 'ハ' => %w(ハ ヒ フ ヘ ホ バ ビ ブ ベ ボ パ ピ プ ペ ポ), 'マ' => %w(マ ミ ム メ モ), 'ヤ' => %w(ヤ ユ ヨ), 'ラ' => %w(ラ リ ル レ ロ), 'ワ' => %w(ワ ヲ ン) }.freeze def self.create_index(names) names.map { |name| NAME_INDEX_HASH.find(['Others']) {|_, value| value.include?(name[0])}.first # 変更 } end end
実行結果です。ア段のカタカナを取り出すことができました。
["カ", "ア", "ハ", "カ", "ワ", "ハ"]
次に、ア段のカタカナごとに名前をグループ化します。Array#group_by
を使うのでした。
class NameIndex # 中略 def self.create_index(names) names.group_by { |name| # 変更 NAME_INDEX_HASH.find(['Others']) {|_, value| value.include?(name[0])}.first } end end
実行結果です。ア段ごとにグループ化されました。
{ "カ"=>["キシモト", "カネダ"], "ア"=>["イトウ"], "ハ"=>["ババ", "ハマダ"], "ワ"=>["ワダ"] }
個人的には、データ形式は上記のように{ア段のカタカナ => 含まれる名前の配列}
がよいと思うのですが、問題ではア段のカタカナを0番目の要素、名前の配列を1番目の要素とする配列の配列を作成することになっているので変換します。この変換はHash#to_a
(
to_a (Hash) - Rubyリファレンス
)で行うことができます。
class NameIndex # 中略 def self.create_index(names) names.group_by { |name| NAME_INDEX_HASH.find(['Others']) {|_, value| value.include?(name[0])}.first }.to_a # 変更 end end
実行結果です。配列に変更できました。
[ ["カ", ["キシモト", "カネダ"]], ["ア", ["イトウ"]], ["ハ", ["ババ", "ハマダ"]], ["ワ", ["ワダ"]] ]
最後にソートをかけます。ア段のカタカナ、それに対応する名前の配列それぞれにソートをかける必要があります。今回は最後にソートかけるのではなく、最初にソートをかけるように変更します。それによって自然に最終的な配列もソートされます。
1つ気にしておくべきことは、Rubyの(というよりほぼ全てのプログラミング言語での)ソートは文字コード順に並ぶということです。例えば、['ゴトウ', 'コニシ', 'コバヤシ']
は、国語辞典ではこの順に並びますが、Rubyでは['コニシ', 'コバヤシ', 'ゴトウ']
の順に並びます。もし国語辞典と同じ順番で並べたい場合はArray#sort
に比較を行うブロックを渡し独自のソートを行う必要があります。
今回は問題文中の例として示されているケース(['ハ', ['ハマダ', 'ババ']]
)から、文字コード順で並び替えることが類推できます(あと初心者向けと銘打っていることからも)。コードは以下になります。
class NameIndex # 中略 def self.create_index(names) names.sort.group_by { |name| # 変更 NAME_INDEX_HASH.find(['Others']) {|_, value| value.include?(name[0])}.first }.to_a end end
実行結果です。期待通りの結果が得られました。
[ ["ア", ["イトウ"]], ["カ", ["カネダ", "キシモト"]], ["ハ", ["ハマダ", "ババ"]], ["ワ", ["ワダ"]] ]
あとがき
こちらも意外と考えることの多い問題でした。自分が初心者の時に解けただろうか。全てのことを一度にやろうとするのではなく、問題を細かいステップに分けるのがカギなのですが、初心者の頃は全てのことを一度にやりがちでした。
名前の先頭のカタカナからア段のカタカナを求める部分がキモなのですが、その部分は今回はわりと読みやすいコードになったかとは思います。
テストが書きにくかったです。数ケースはコードが書いた人が書けばよいと思いますが、チームで働いてる場合はテストケースは他の人が追加で書いた方がよい気がします(特に今回の場合は)。考慮漏れが発生しやすいプログラミング問題だと感じました。
テストケースの名字は名字由来netを参考にしました。
出題者の伊藤淳一さんの書籍はこちら↓
国民の祝日.csv パースプログラムの解答
前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。
国民の祝日.csv パースプログラム
平成28年(2016年),,平成29年(2017年),,平成30年(2018年), 名称,月日,名称,月日,名称,月日 元日,2016/1/1,元日,2017/1/1,元日,2018/1/1 成人の日,2016/1/11,成人の日,2017/1/9,成人の日,2018/1/8 建国記念の日,2016/2/11,建国記念の日,2017/2/11,建国記念の日,2018/2/11 春分の日,2016/3/20,春分の日,2017/3/20,春分の日,2018/3/21 # 中略 文化の日,2016/11/3,文化の日,2017/11/3,文化の日,2018/11/3 勤労感謝の日,2016/11/23,勤労感謝の日,2017/11/23,勤労感謝の日,2018/11/23 天皇誕生日,2016/12/23,天皇誕生日,2017/12/23,天皇誕生日,2018/12/23 ,,,,, 月日は表示するアプリケーションによって形式が異なる場合があります。,,,,,
これをいい感じにパースして、以下のようなデータ構造(Rubyのハッシュオブジェクト)に変換しよう、というプログラミング問題です。
{ 2016 => { # 実際のキーは文字列ではなくDateオブジェクト '2016/01/01' => '元日', '2016/01/11' => '成人の日', # ... '2016/11/23' => '勤労感謝の日', '2016/12/23' => '天皇誕生日', }, 2017 => { '2017/01/01' => '元日', '2017/01/09' => '成人の日', # ... '2017/11/23' => '勤労感謝の日', '2017/12/23' => '天皇誕生日', }, 2018 => { '2018/01/01' => '元日', '2018/01/08' => '成人の日', # ... '2018/11/23' => '勤労感謝の日', '2018/12/23' => '天皇誕生日', }, }
解答
class SyukujitsuParser CSV_PATH = File.expand_path('../../resource/syukujitsu.csv', __FILE__) def self.parse(csv_path = CSV_PATH) self.new.parse(csv_path) end def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: 'Shift_JIS:UTF-8') .join(',') .scan(%r!([^,]+),([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})!) .group_by{|_, year| year.to_i} .map { |year, holiday_arr| holiday_hash = holiday_arr.map {|holiday, *date| [Date.new(*date.map(&:to_i)), holiday] }.to_h [year, holiday_hash] }.to_h end end
RSpecは本家のgithubに記載されているので割愛します。 https://github.com/JunichiIto/parse-syukujitsu/blob/master/test/syukujitsu_parser_test.rb
解説
RubyでCSVファイルを読み込むには、CSV
クラスを使用するのが定石のようですが、CSVクラスを知らなかったので、通常のファイルとして読み込みました。
まず、どのように祝日名と日付部分を取り出すか考えます。公式ドキュメントのString#scan
(
scan (String) - Rubyリファレンス
)の例に以下のようなものがあるので、同様の処理を行います。
s = "Hokkaido:Sapporo, Aomori:Aomori, Iwate:Morioka" p s.scan(/(\w+):(\w+)/) # => [["Hokkaido", "Sapporo"], ["Aomori", "Aomori"], ["Iwate", "Morioka"]]
祝日名と日付の部分は、
(平仮名か漢字),(年/月/日)
というペアになっており、上記のscanの例と同じようになっています。
正規表現にしていきます。まず、(平仮名か漢字)
の部分ですが、これを満たす正規表現が無くはないようですが、一筋縄ではいかないようなので今回は別の方法にします。カンマより前のすべての(カンマでない)文字を取れば良いので、[^,]+
という正規表現で表します。
日付部分の正規表現は
[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}
にします。ここまでで、マッチさせる正規表現は以下のようになります。
([^,]),([0-9]{4}/[0-9]{1,2}/[0-9]{1,2})
先々の過程で、日付の年月日が一つの変数に入っているよりも、年・月・日で別々の方が扱いやすいので、カッコでのキャプチャの位置を変更します。
([^,]+),([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})
それでは、ファイルを読み込んでString#scan
で一致した部分を抜き出していきます。
ファイルは1行ごとに読むことが多いのですが、今回は一度にすべての行を読み込みます。IO.readlines
は指定されたファイルを全て読み込んで、その各行を要素としてもつ配列を返すメソッドです。
(
singleton method IO.readlines (Ruby 2.6.0)
)
class SyukujitsuParser CSV_PATH = File.expand_path('../../resource/syukujitsu.csv', __FILE__) def self.parse(csv_path = CSV_PATH) self.new.parse(csv_path) end def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: "Shift_JIS:UTF-8") end end
readlinesの引数のchomp: true
は各行の末尾の改行を取り除くオプションです。今回の場合、指定しなくても出力は変わらないのですが、あると将来的なバグの温床になるので取り除いておきます。読み込むCSVファイルのエンコーディングがShift-JIS、内部的にはUTFで扱いたいので、encoding: "Shift_JIS:UTF-8"
を指定しています。
実行結果です。
[ "平成28年(2016年),,平成29年(2017年),,平成30年(2018年),", "名称,月日,名称,月日,名称,月日", "元日,2016/1/1,元日,2017/1/1,元日,2018/1/1", "成人の日,2016/1/11,成人の日,2017/1/9,成人の日,2018/1/8", "建国記念の日,2016/2/11,建国記念の日,2017/2/11,建国記念の日,2018/2/11", "春分の日,2016/3/20,春分の日,2017/3/20,春分の日,2018/3/21", "昭和の日,2016/4/29,昭和の日,2017/4/29,昭和の日,2018/4/29", # 中略 "秋分の日,2016/9/22,秋分の日,2017/9/23,秋分の日,2018/9/23", "体育の日,2016/10/10,体育の日,2017/10/9,体育の日,2018/10/8", "文化の日,2016/11/3,文化の日,2017/11/3,文化の日,2018/11/3", "勤労感謝の日,2016/11/23,勤労感謝の日,2017/11/23,勤労感謝の日,2018/11/23", "天皇誕生日,2016/12/23,天皇誕生日,2017/12/23,天皇誕生日,2018/12/23", ",,,,,", "月日は表示するアプリケーションによって形式が異なる場合があります。,,,,," ]
この各配列の要素に対してscan
をかけてもよいのですが、今回は簡単のためすべての行をカンマで結合してからscan
をかけます。CSVの各行を1行にまとめるのは普通はやらないのですが、今回は元となるデータの形式がイマイチなのでこのようなことをしてもしょうがないという気持ちです。配列の各要素をjoin
で連結します。引数にカンマを与えて、カンマ区切りで結合します。結果的に1行のCSVが出力されます。
class SyukujitsuParser # 中略 def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: "Shift_JIS:UTF-8") .join(',') # 追加 end end
実行結果です。1行にまとまりました。
"平成28年(2016年),,平成29年(2017年),,平成30年(2018年),,名称,月日,名称,月日,名称,月日,元日,2016/1/1,元日,2017/1/1,元日,2018/1/1,成人の日,2016/1/11,成人の日,2017/1/9,成人の日,2018/1/8,建国記念の日,2016/2/11,建国記念の日,2017/2/11,建国記念の日,2018/2/11,春分の日,2016/3/20,春分の日,2017/3/20,春分の日,2018/3/21,昭和の日,2016/4/29,昭和の日,2017/4/29,昭和の日,2018/4/29,(中略)2018/9/17,秋分の日,2016/9/22,秋分の日,2017/9/23,秋分の日,2018/9/23,体育の日,2016/10/10,体育の日,2017/10/9,体育の日,2018/10/8,文化の日,2016/11/3,文化の日,2017/11/3,文化の日,2018/11/3,勤労感謝の日,2016/11/23,勤労感謝の日,2017/11/23,勤労感謝の日,2018/11/23,天皇誕生日,2016/12/23,天皇誕生日,2017/12/23,天皇誕生日,2018/12/23,,,,,,,月日は表示するアプリケーションによって形式が異なる場合があります。,,,,,"
これに対して、上で作成した正規表現で祝日名と日付を抽出します。scan
の引数には正規表現オブジェクトを渡します。今回は%r
を使用して正規表現オブジェクトを作成します。マッチしたい部分に記号がたくさん含まれるので!
で正規表現を囲みます。
class SyukujitsuParser # 中略 def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: "Shift_JIS:UTF-8") .join(',') .scan(%r!([^,]+),([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})!) # 追加 end end
実行結果です。[祝日名, 年, 月, 日]
の配列が作成されました。
[ ["元日", "2016", "1", "1"], ["元日", "2017", "1", "1"], ["元日", "2018", "1", "1"], ["成人の日", "2016", "1", "11"], ["成人の日", "2017", "1", "9"], ["成人の日", "2018", "1", "8"], ["建国記念の日", "2016", "2", "11"], ["建国記念の日", "2017", "2", "11"], # 中略 ["文化の日", "2017", "11", "3"], ["文化の日", "2018", "11", "3"], ["勤労感謝の日", "2016", "11", "23"], ["勤労感謝の日", "2017", "11", "23"], ["勤労感謝の日", "2018", "11", "23"], ["天皇誕生日", "2016", "12", "23"], ["天皇誕生日", "2017", "12", "23"], ["天皇誕生日", "2018", "12", "23"] ]
ここから年でグルーピングしてみましょう。group_by
を使えば簡単です。年は配列の2番目なので、それを整数に変換したものでグルーピングします。
class SyukujitsuParser # 中略 def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: 'Shift_JIS:UTF-8') .join(',') .scan(%r!([^,]+),([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})!) .group_by{|_, year| year.to_i} # 追加 end end
実行結果です。
{ 2016=>[ ["元日", "2016", "1", "1"], ["成人の日", "2016", "1", "11"], # 中略 ["勤労感謝の日", "2016", "11", "23"], ["天皇誕生日", "2016", "12", "23"] ], 2017=>[ ["元日", "2017", "1", "1"], ["成人の日", "2017", "1", "9"], # 中略 ["勤労感謝の日", "2017", "11", "23"], ["天皇誕生日", "2017", "12", "23"] ], 2018=>[ ["元日", "2018", "1", "1"], ["成人の日", "2018", "1", "8"], ["建国記念の日", "2018", "2", "11"], # 中略 ["勤労感謝の日", "2018", "11", "23"], ["天皇誕生日", "2018", "12", "23"] ] }
だいぶ完成に近くなってきました。完成形と違うところは、ハッシュの各値が配列になっていることです。
まず、ハッシュの値(value)を変換する方法として、以下のイディオムがあります。
hash.map {|key, value| [key, convert(value)] }.to_h
こちらを適用します。中の実装は仮のものにしておきます。
class SyukujitsuParser # 中略 def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: 'Shift_JIS:UTF-8') .join(',') .scan(%r!([^,]+),([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})!) .group_by{|_, year| year.to_i} .map { |year, holiday_arr| # 追加 [year, holiday_arr] # 仮実装 }.to_h # 追加 end end
実行すると1つ前で実行したのと同じものになります。
次に
[ ["元日", "2016", "1", "1"], ["成人の日", "2016", "1", "11"], # 中略 ["勤労感謝の日", "2016", "11", "23"], ["天皇誕生日", "2016", "12", "23"] ]
を
{ Date.parse('2016/01/01') => '元日', Date.parse('2016/01/11') => '成人の日', # 中略 Date.parse('2016/11/23') => '勤労感謝の日', Date.parse('2016/12/23') => '天皇誕生日', }
に変換します。配列からハッシュを作成するには、Array.to_h
を使用します。(
instance method Array#to_h (Ruby 2.6.0)
)
このメソッドは[key, value]
のペアの配列を、{key => value}
のハッシュに変換します。
to_h
で変換する前の配列は
[ [Date.parse('2016/01/01'), '元日'], [Date.parse('2016/01/11'), '成人の日'], # 中略 [Date.parse('2016/11/23'), '勤労感謝の日'], [Date.parse('2016/12/23'), '天皇誕生日'], ]
となっている必要があります。
年ごとの配列に対してmap
を使用します。mapで受け取る引数は、["元日", "2016", "1", "1"]
の形式です。多重代入を使用して、holiday, *date = ["元日", "2016", "1", "1"]
のように受け取っています。
この場合だと、holiday = "元日"
, date = ["2016", "1", "1"]
のように代入されます。
date
をDate(
class Date (Ruby 2.6.0)
)
オブジェクトに変換します。Date.new
で作成するので、
はじめにdateの各日付を数値に変換します(date.map(&:to_i)
)これをDate.newの引数にします。配列展開を使います。Date.new(*[2016, 1, 1])
と書くと、Date.new(2016, 1, 1)
と書いたのと同じになります。
class SyukujitsuParser # 中略 def parse(csv_path) IO.readlines(csv_path, chomp: true, encoding: 'Shift_JIS:UTF-8') .join(',') .scan(%r!([^,]+),([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})!) .group_by{|_, year| year.to_i} .map { |year, holiday_arr| holiday_hash = holiday_arr.map {|holiday, *date| # 追加 [Date.new(*date.map(&:to_i)), holiday] # 追加 }.to_h # 追加 [year, holiday_hash] # 変更 }.to_h end end
これで完成です。書いている最中はプログラムの意図を理解していますが、1週間くらいすると各変数やメソッドの使用意図を忘れそうなので、適宜コメントを入れた方がよさそうです。
あとがき
元のCSVがひどいのでコードも力技になったなあと思います。
初心者向けの問題と書かれていながら、なかなか考えることの多い問題でした。
★前回の記事
出題者の伊藤淳一さんの書籍はこちら↓
ボーナスドリンク問題の解答
こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。
ボーナスドリンク問題
「ある駄菓子屋で飲み物を買うと、空き瓶3本で新しい飲み物を1本プレゼントしてくれる。最初に100本購入した場合、トータルで何本飲めるか」
解答
コード
class BonusDrink def self.total_count_for(amount) return 0 if amount.zero? last_amount = (amount - 1) % 2 + 1 (3 * amount - last_amount) / 2 end end
テストコードはこちら。
require 'rspec' require File.expand_path(File.dirname(__FILE__) + '/../bonus_drink') describe BonusDrink do specify { expect(BonusDrink.total_count_for(0)).to eq 0 } specify { expect(BonusDrink.total_count_for(1)).to eq 1 } specify { expect(BonusDrink.total_count_for(2)).to eq 2 } specify { expect(BonusDrink.total_count_for(3)).to eq 4 } specify { expect(BonusDrink.total_count_for(11)).to eq 16 } end
解説
最初にN本買ってきたときに、最終的に飲める本数をf(N)とします。
f(1) = 1 f(2) = 2 f(3) = 4 f(11) = 16
です。
例として問題にある通り、今回はN=100
の場合を考えます。
f(100) = 最初に100本買ってきたときに最終的に飲める本数
まず、100本の中身が入っているジュースがあります。
3本飲んでください。
さっそく3本だけ取り替えに行きましょう。1本もらいました。
手元にあるジュースの本数を確認しましょう。
残ってる97本と取り替えた1本で98本ですね。
98本で飲める量はf(98)
で表されるのでした。
すでに3本のジュースを飲んでいるので、 100本のジュースで飲める量は、
f(100) = 3 + f(98)
で表すことができます。
同様に98本のうち3本を飲んで新しいジュース1本と交換します。
手元には96本のジュースがあります。すでに6本のジュースを飲んでいるので100本で最終的に飲める本数は以下の式で表すことができます。
f(100) = 3 + 3 + f(96)
お気付きのように、ジュースを3本飲むごとに手持ちのジュースが2本減ります。
繰り返していきましょう。
f(100) = 3 + 3 + 3 + f(94) f(100) = 3 + 3 + 3 + 3 + f(92) f(100) = 3 + 3 + 3 + 3 + 3 + f(90) # 中略 f(100) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + f(6) f(100) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + 3 + f(4) f(100) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + 3 + 3 + f(2)
ここで手持ちのジュースが2本になってしまいました。 最後にこれを飲みましょう。
f(100) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + 3 + 3 + 2
では、上の式で3を何個足したか数えましょう。 100から2まで、2ずつ引いていったので、
(100 - 2) / 2 = 49
49個の3を足しています。
なので、上記の式は以下のように書き換えられます。
f(100) = 3 * 49 + 2 = 149
最初に100本買った場合に最終的に飲める本数は149本であることがわかりました。(プログラムで算出するはずなのに、算数的に解いてしまった)
本数がNの時は上記と同様に
f(N) = 3 * (N-2)/2 + 2
となります。しかし、2本ずつ引いていって最後に2本になるのは偶数の時のみです。 奇数の場合を考えてみましょう。例として97本の場合を考えます。
考え方は同様で、3本飲むと、2本手持ちが減ります。これを数式にしていきます。
f(97) = 3 + f(95) f(97) = 3 + 3 + f(91) f(97) = 3 + 3 + 3 + f(89) f(97) = 3 + 3 + 3 + 3 + f(87) # 中略 f(97) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + f(5) f(97) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + 3 + f(3) f(97) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + 3 + 3 + f(1)
このようになり、最後に手持ちの1本を飲んで終わりです。
f(97) = 3 + 3 + 3 + 3 + 3 + ... + 3 + 3 + 3 + 3 + 3 + 1
97から1まで2ずつ引いたので、足した3の数は、
(97 - 1)/2 = 48
48個の3を足したので、
f(97) = 3 * 48 + 1 = 145
最初に97本買った場合に最終的に飲める本数は145本であることがわかりました。
本数がNかつNが奇数の時、最終的に飲めるジュースの本数は
f(N) = 3 * (N-1)/2 + 1
になります。
ここまでをまとめると、
f(N) = 3 * (N-2)/2 + 2 (Nが偶数のとき) 3 * (N-1)/2 + 1 (Nが奇数のとき)
です。
コードにしてみましょう。メソッドの最初にamountが0の時に0を返す処理を書いています。
amount % 2
はamountを2で割ったときの余りという意味です。
class BonusDrink def self.total_count_for(amount) return 0 if amount.zero? case amount % 2 when 0 3 * (amount - 2) / 2 + 2 when 1 3 * (amount - 1) / 2 + 1 end end end
2つの式は似ているので、工夫すればまとめられそうです。 2つの式の違いは、amountから引いている数と最後に足している数です。この数は最後に手元に残る瓶の本数でした。
奇数と偶数で値が異なるので、
last_amount = amount % 2 === 0 ? 2 : 1 3 * (amount - last_amount) / 2 + last_amount
のようなコードを書きたくなりますが、あまりよい方法とは言えません。"3本で1本プレゼント"が"4本で1本プレゼント"になったときに対応できなくなってしまいます。
基本的には剰余(割り算の余り)を考えればよいのですが、割り切れる時には2にします。
last_amount = amount % 2 === 0 ? 2 : amount % 2 3 * (amount - last_amount) / 2 + last_amount
3項演算子を使わなくても、一旦1を引いて剰余を求めてから1を足すと同様の結果が得られます。
last_amount = (amount - 1 ) % 2 + 1 3 * (amount - last_amount) / 2 + last_amount
式を整理して、最終的にこちらのコードになります。
class BonusDrink def self.total_count_for(amount) return 0 if amount.zero? last_amount = (amount - 1 ) % 2 + 1 (3 * amount - last_amount) / 2 end end
完成です!
あとがき
最初に再帰のアルゴリズムを思いついたのですが、最終的に四則演算と剰余だけで求めることができました。 試行錯誤できて楽しい問題でした。
ちなみに5本で1本交換の場合は、以下のようになります。
class BonusDrink def self.total_count_for(amount) return 0 if amount.zero? last_amount = (amount - 1 ) % 4 + 1 (5 * amount - last_amount) / 4 end end
算数としてはこれで答えは出るのですが、プログラミングとしては整除を使用すればもう少し短く書けるかもと思いました。
こちらもどうぞ
JavaScriptのnew Array(n)をmapしたいとき fillをはさむ理由
経緯
配列をオブジェクトで初期化したい場合、
new Array(3).fill({foo: "ふう", bar:"ばあ"})
というコードだと、全てのインデックスが同じオブジェクトを指してしまうので、 調べたら
new Array(3).fill().map(() => ({foo: "ふう", bar: "ばあ"}))
という方法が出てきたので試したらうまく行きました。
疑問
このfill()
要らなくない?new Array(3).map(() => ({foo: "ふう", bar: "ばあ"}))
って書けばよさそうな気がします。
答え
fill()
は必要。
理由はここに書いてありますが、英語なので日本語で&自分で実行しながら書いていきます。
まず、JavaScriptの配列は、実質的には数値をキーとしたオブジェクトです。
例えば、以下のように配列を初期化します。
const array = ['りんご', 'みかん', 'バナナ'] console.log("array", array)
これを実行すると以下のようになります。
{ 0: "りんご" , 1: "みかん" , 2: "バナナ" , length: 3 }
上記配列はこのオブジェクトを宣言したのと同じになります。
さて、今度は今度はArrayのコンストラクタを使った場合をみてみます。
const array = new Array(3) console.log("array", array)
実行結果です。
{ length: 3 }
この場合はlengthのみ含まれているオブジェクトになっています。数値のキーはありません。
コンストラクタで作成された配列に対して、map()
を呼び出してみます。
const array = new Array(3).map(() => ({foo: "ふう", bar: "ばあ"})) console.log("array", array)
実行結果です。
{ length: 3 }
何も設定されていません。mapに渡したコールバックが呼ばれていないことがわかります。
公式ドキュメントのArray.prototype.map()のページには、下記のように書いてあります。
callback は、値が代入されている配列のインデックスに対してのみ呼び出されます(undefined が代入されているものも含みます)。すでに削除されたインデックスや、まだ値が代入されていないインデックスに対しては呼び出されません。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map
つまり、new Array(3)
だけした状態でmapを呼び出しても、インデックス(オブジェクトのキー)がないので1度もコールバックが呼ばれないのです。
この挙動はforEachやreduce、filterなども同様です。
そこで、先人の教えの通り、一旦fill()します
const array = new Array(3).fill() console.log("array", array)
実行結果です。
{ 0: undefined , 1: undefined, 2: undefined, length: 3 }
インデックスのキーが作成されました。fillに引数を渡していないので、各値はundefinedです。
この状態でmap()
をすれば、各キーに対してコールバックが呼ばれそうです。
const array = new Array(3).fill().map(() => ({foo: "ふう", bar: "ばあ"})) console.log("array", array)
実行結果です。
{ 0: {foo: "ふう", bar: "ばあ"}, 1: {foo: "ふう", bar: "ばあ"}, 2: {foo: "ふう", bar: "ばあ"}, length: 3 }
配列に初期値を設定できました。
あとがき
オブジェクトで配列の初期化をする方法は複数ありますが、どれも初心者にとって直感的ではないのでなんとかならないかなと思います。
「5と8を使った和で表すことができない最大の整数を求めよ」を解いてみる
小2が「5と8を使った和で表すことができない最大の整数を求めよ」という大学入試レベルの算数を教えてと聞いてきた
この問題が面白いなと思って自分なりに解いて見たので書いてみます。数学は専門ではないので厳密さに欠けることは先に記しておきます。
問題を書き直すと、
8x + 5y (xとyは0以上の整数) で表される数で、表現できない最大の数はいくつか
になります。
問題文から、ある程度大きい数字であれば5と8の和で表すことができるので、適当な数で確かめます。 49にでもしましょうか。
49から8を順に引いていきます。
さらに5で割った余りを下に書いていきます。
25が5で割り切れますね。25までは49から3回8を引いていますので、
49 = 8 × 3 + 5 × 5
で表すことができます。別の数でもやってみましょう。8を引いていった数字と、それぞれ5で割った数字を書きます。
40が5で割り切れます。40まで1回8を引いているので、
48 = 8 × 1 + 5 × 8 (もちろん、48 = 8 × 6 でもよいです)
15が5で割り切れます。15まで4回8を引いているので、
47 = 8 × 4 + 5 × 3
長くなるので、たくさんのパターンは書けませんが、ある数から8を引いて、それを5で割った余りは、
この順番で現れることがわかります。もう少し大きい数でも確かめてみましょう。97でやってみます。
なりますね。実際に定理になってると思うのですが、何の定理か忘れました。
ある数から8ずつ引いていくと、5回に1回は、5で割った余りが0になる(=5で割り切れる=5の倍数である)ということがわかりました。
ということは、ある数から4回以上8が引ければ、元の数と合わせて5個の数字が並び、その中の1つは5で割り切れます。4回以上8が引ける数の最小は33 (= 8 * 4 + 1 ) です。確かめてみると、
33 = 8 × 1 + 5 × 5
で表すことができます。
32のときは
5で割り切れる数は出てきませんが、8で割り切れるので、
32 = 8 × 4
です。1つずつみていきましょう。
31のときは、
31 = 8 × 2 + 5 × 3
30のときは、
30 = 5 × 6
29のときは、
29 = 8 × 3 + 5 × 1
28のときは、
28 = 8 × 1 + 5 × 4
27のときは、
27から8を引いていっても5の倍数にならないので、27が 8x + 5y で表すことのできない最大の数ということがわかります。
実は、32から1つずつみていかなくても、規則性を見ていけば計算で答えを求められます。
32から25までは8が3回引けるので、元の数と合わせて4つの数が並びます。4つの場合、上の図で、2から始まると 2 → 4 → 1 → 3になり、0が出てこない(=5で割り切れない)ことが分かります。
5で割った余りが2になる33より小さい数は、32ですが、これは8の倍数(32 = 8*4)なので、次に大きい27(=32-5)が答えになります。
あとがき
小学校2年生まで習う知識ではとても解けなさそうでした。
ネット上に同じ解法が載っているかもしれませんが、見つけられませんでした。
この問題自体はフロベニウスの硬貨交換問題として知られているそうです。
RSpecでsubjectを使用して配列を検査時、配列の長さを取得する
RSpecでsubjectを使用して、配列を
subject { user.errors[:name] } # user.errors[:name] は配列
このように検査時、配列の長さを取得する方法が分からなかったので調べました。
結論
rspec-its
のits
を使用する。
its(:size) { is_expected.to eq 3 }
のように書くとsubjectの検査対象の配列の長さを取得できます。
経緯
モデルのテストを書きました。
modelのコード↓
class User < ApplicationRecord validates :nickname, presence: true, length: { maximum: 32 }, format: { with: /\A[-0-9a-zA-Z#$%&()._]*\z/, message: 'には半角英数字と#$%&()-._のみ使用できます。' } end
テストは以下のようになります。(全て書くと長いので一部のみ抜粋)
require 'rails_helper' RSpec.describe User, type: :model do describe '#nickname' do context '空白のとき'do it 'エラーが出ること' do user = User.new(nickname: '') user.valid? expect(user.errors[:nickname]).to include("can't be blank") end end context '許可される文字の場合' do context '英大文字小文字数字#$%&()-._' do it 'エラーが出ないこと' do user = User.new(nickname: 'ARZatz809(_#-$%.&)') user.valid? expect(user.errors[:nickname]).to be_blank end end end context '許可されない文字の場合' do context '許可されていない文字"?"が含まれている場合' do it 'エラーが出ること' do user = User.new(nickname: 'correct?') user.valid? expect(user.errors[:nickname]).to include('には半角英数字と#$%&()-._のみ使用できます。') end end end end end
これを綺麗にすると以下のようになります。(参考: 使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita )
require 'rails_helper' RSpec.describe User, type: :model do describe '#nickname' do subject { user.errors[:nickname] } let(:user) { User.new(params) } let(:params) { {nickname: nickname} } before do user.valid? end context '空白のとき'do let(:nickname) { '' } it { is_expected.to include("can't be blank") } end context '許可される文字の場合' do context '英大文字小文字数字#$%&()-._' do let(:nickname) { 'ARZatz809(_#-$%.&)' } it { is_expected.to be_blank } end end context '許可されない文字の場合' do context '許可されていない文字"?"が含まれている場合' do let(:nickname) { 'correct?' } it { is_expected.to include('には半角英数字と#$%&()-._のみ使用できます。') } end end end end
繰り返しがなくなってすっきりしました。
ここでふと思いました。 このテストコード、対象のエラーが含まれることはチェックできるけど、想定外のエラーが(間違って)含まれることはテストできていないじゃん、と。
ここでの方針としては、
- includeでなく配列同士の比較を行う
- エラーの数をチェックする
などが思いつきます。今回は
- エラーの数をチェックする
方法にします。この方法をとることで、
- エラーの順番は問わず
- 想定外のエラーが間違って入っていることを防ぐ
ことができます。
さて、テストの対象はsubject
に書かれています。これのサイズを取得したい。
これにはits
を使用します。
RSpec3からits
はgemに分離されているのでインストールします。
Gemfile
group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'rspec-rails' gem 'rspec-its' # この行を追加 end
bundle installします。
テストを追加します。
its(:size) { is_expected.to eq 1 }
のように書くとuser.errors[:nickname].size
が1かどうかをテストすることができます。
RSpec.describe User, type: :model do describe '#nickname' do subject {user.errors[:nickname]} # 略 context '空白のとき'do let(:nickname) { '' } it { is_expected.to include("can't be blank") } its(:size) { is_expected.to eq 1 } # 追加 end context '許可される文字の場合' do context '英大文字小文字数字#$%&()-._' do let(:nickname) {'ARZatz809(_#-$%.&)'} it { is_expected.to be_blank} end end context '許可されない文字の場合' do context '許可されていない文字"?"が含まれている場合' do let(:nickname) {'correct?'} it { is_expected.to include('には半角英数字と#$%&()-._のみ使用できます。')} its(:size) { is_expected.to eq 1 } # 追加 end end end end
以上でsubjectに指定した配列のサイズ(長さ)をテストすることができました。