yucatio@システムエンジニア

趣味で作ったものいろいろ

もととなるペンタミノのピースの準備 (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

ではプログラムを書いていきます。 はじめにディレクトリとhtmlファイルを用意しましょう。

ファイルとディレクトリの準備

KATAMINO-SOLVERという名前のフォルダを作成し、その下に KATAMINO-SOLVER-preparationという名前のフォルダを作成してください。 その下に index.htmlファイルを作成し、以下の内容を書き込んで保存します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>KATAMINO Preparation</title>
  </head>
  <body>
    <script src="js/katamino-org-arr.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>

KATAMINO-preparationフォルダの下に、jsフォルダを作成し、 その下にkatamino-org-arr.jsmain.jsという名前の空のファイルを作成します。

ディレクトリ構成はこのようになります。

KATAMINO-SOLVER/
  |_ KATAMINO-SOLVER-preparation/
      |_ index.html
      |_ js/
          |_ katamino-org-arr.js
          |_ main.js

もととなるペンタミノのピースの準備

katamino-org-arr.jsにKATAMINOの12個のピースを記載します。

以下の記事で、ピースのデータ構造は、ピースの場所のリストと決めたのですが、元となる12個のピースは、転置や反転のしやすさを考えて、2次元配列で表すことにします。

yucatio.hatenablog.com

katamino-org-arr.jsに以下をコピー&ペーストしてください。

const KATAMINO_ORG_ARRAY =
[
  [
    [1, 1, 1, 1, 1]
  ],
  [
    [1, 1, 1, 1],
    [1, 0, 0, 0]
  ],
  [
    [1, 1, 1, 1],
    [0, 1, 0, 0]
  ],
  [
    [1, 1, 1, 0],
    [0, 0, 1, 1]
  ],
  [
    [1, 1, 1],
    [1, 0, 0],
    [1, 0, 0]
  ],
  [
    [1, 1, 1],
    [1, 1, 0]
  ],
  [
    [1, 1, 1],
    [1, 0, 1]
  ],
  [
    [1, 1, 0],
    [0, 1, 0],
    [0, 1, 1]
  ],
  [
    [1, 1, 0],
    [0, 1, 1],
    [0, 1, 0]
  ],
  [
    [1, 1, 1],
    [0, 1, 0],
    [0, 1, 0]
  ],
  [
    [1, 1, 0],
    [0, 1, 1],
    [0, 0, 1]
  ],
  [
    [0, 1, 0],
    [1, 1, 1],
    [0, 1, 0]
  ],
]

12個の0と1のかたまりは、以下のそれぞれにピースに対応しています。

f:id:yucatio:20190621081852p:plain

はじめに疎通確認をします。 さて、main.jsを開いて、下記を入力してください。

console.log("hello")
console.log("KATAMINO_ORG_ARRAY", KATAMINO_ORG_ARRAY)

index.htmlをブラウザで開きましょう。今回はGoogle Cromeを使用します。

ディベロッパーツールを表示します。Windowsの場合はF12キー、Macの場合はCommandOptionIキーで開きます。

コンソールを開きます。helloと上で作成した配列が表示されていれば成功です。

f:id:yucatio:20190702225552p:plain

以上でもととなるペンタミノのピースの準備ができました。次回はこれを転置・反転していきます。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

ペンタミノの回転、反転をするためのアルゴリズム (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

KATAMINOのピース(ペンタミノ)は以下の12種類です。

f:id:yucatio:20190621081852p:plain

KATAMINOを解いていくときに、それぞれのピースを回転させたり裏返したりする必要があります。

この時の方針として、

  1. あらかじめ回転させたり裏返したりしたものを用意しておく
  2. 解きながら回転させたり裏返したりする

とありますが、全体的な実装のしやすさから、1の方法を選択します。

KATAMINOのピースは12個あります。それぞれ、回転させたり裏返したりすると、1つのピースにつき最大で8パターンの形になります。回転や裏返したものを違う形として数えると、全部で63パターンあります。

f:id:yucatio:20190621083130p:plain

63パターン全てを手で入力するのは少し面倒ですので、12ピースのもとの配列から、残りのパターンを作成することにします。

まず、1つのピースから回転・反転させて全8パターンを作成します。

8パターンはこのようになります。

f:id:yucatio:20190621084539p:plain

さて、回転・反転と言葉で言うのは簡単ですが、どのようにプログラミングしていけばよいでしょうか。

いくつか方法はありますが、今回は転置(transpose)と反転(reverse)を使います。

転置とは、行列の行と列を入れ替えることです。配列で書くと以下のような変換が転置です。

[
  [1,2,3]      [1,4,7]
  [4,5,6]  ->  [2,5,8]
  [7,8,9]      [3,6,9]
]

転置すると、以下の図のように斜めにひっくり返されます。

f:id:yucatio:20190702083100p:plain

0番の形を転置をすると、7番の形になります。 次に、転置したものを上下反転させます。そうすると1番の形になります。 さらにこの形を転置すると6番の形になり、さらに上下反転すると2番の形になります。 これを繰り返すと8種類全ての形を作成することができます。

f:id:yucatio:20190702083723p:plain

全てのピースについて、回転、反転を繰り返したのがこちらになります。

f:id:yucatio:20190621121150p:plain

さて、対称性があるピースは形が重複しているので、重複を取り除きます。 実装方法については以降で扱います。

f:id:yucatio:20190621083130p:plain

以上で63パターンのピースの位置を出すことができました。次回はコーディングしていきます。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

データ構造を考える (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

具体的なプログラミングに入る前に、データ構造を考えておきましょう。

KATAMINOを解くのに必要なものは、KATAMINOを入れるフィールドとKATAMINOのピースです。

フィールドのデータ構造

フィールドは 5 x {ピースの数} のマス目です。

f:id:yucatio:20190619224647p:plain:w250

これは2次元配列で表現できそうです。 まだピースが置かれていない場所は-1にしておきます。フィールドの幅が3つの時は以下のようになります。

[
  [-1, -1, -1],
  [-1, -1, -1],
  [-1, -1, -1],
  [-1, -1, -1],
  [-1, -1, -1]
]

ピースが置かれた場所は、そのピースの番号(ID)で置き換えます。

f:id:yucatio:20190620082653p:plain:w250

上の図のようにピースが置かれたときのデータは以下のようになります。(このT字のピースのIDは9です)

[
  [ 9,  9,  9],
  [-1,  9, -1],
  [-1,  9, -1],
  [-1, -1, -1],
  [-1, -1, -1]
]

以上がフィールドのデータ構造です。

ピースのデータ構造

ピースの表現のしかたは、2次元配列とピースのある場所のリストの2パターン考えられます。

例えば、このピースの場合

f:id:yucatio:20190620083647p:plain:w250

このピースを2次元配列で表現すると以下のようになります。ピースの場所を1, ピースでない場所を0で表現しています。

[
  [1, 1, 0],
  [0, 1, 0],
  [0, 1, 1]
]

リストで表現する場合は、上記の配列のピースの場所のみを抜き出して配列にします。

f:id:yucatio:20190620084008p:plain:w250

[
  {x:0, y:0},
  {x:0, y:1},
  {x:1, y:1},
  {x:2, y:1},
  {x:2, y:2}
]

2次元配列でも埋まっている場所のリストでも解くことはできますが、 ピースを置いたり、重なっている部分の判定がリストの方が効率よくできるので、今回はリストを採用します。

以上がピースのデータ構造です。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

KATAMINOを解くアルゴリズム (KATAMINOを解くプログラムを作成する)

KATAMINOとは

KATAMINOとは、Gigamic社が発売するパズルゲームです。 ペンタミノという、5個の正方形をつなげてできる形をしたピースを使い、 長方形の枠にぴったり収まるように組み合わせて遊ぶゲームです。


KATAMINOを解くアルゴリズム

KATAMINOを解くアルゴリズムとして、総当たりの方法を採用します。

以下のように、左上から右に向かって順番に番号を振ります。

f:id:yucatio:20190619224647p:plain:w250

ピースが置かれていないマスのうち、一番小さい番号を埋めるようにピースを置いていきます。 全てのピースが置ければおしまい、全ての組み合わせを試してみてどの組み合わせでもマスを全て埋めることができなければ解けない組み合わせです。

総当たりの手順は以下のようになります。

  1. 使用するピースを決めます。今回は以下の3つのピースを使います。 f:id:yucatio:20190619230048p:plain
  2. 番号1のマスを埋めるように1つピースを置きます。
    • ピースは枠からはみ出して置くことはできません。

    f:id:yucatio:20190619225500p:plain:w250 f:id:yucatio:20190619225520p:plain:w250

  3. 残ったピースのうち、一番小さい番号(今回の例では3)を埋めるようにピースを置きます。
    • ピースは枠からはみ出して置くことはできません。
    • ピース同士は重なってはいけません。

    f:id:yucatio:20190619230949p:plain:w250 f:id:yucatio:20190619231009p:plain:w250

  4. 2を繰り返します。(今回の例では繰り返せないので下の手順へ進んでください)
  5. どのピースも置けなくなったら、ピースを回転または反転させます。2に戻ります。

    全ての回転・反転を試した場合は、ピースを1つ戻して、まだ試していない別のピースを置きます。2に戻ります。

    全てのピースの全ての回転を試したならば、1つ前のピースを回転または反転させます。

    f:id:yucatio:20190619231333p:plain:w250

  6. 全てのピースが置ければ終了です。

    f:id:yucatio:20190619231422p:plain:w250

解く過程を図にすると、以下のような木構造になります。

f:id:yucatio:20190619231652p:plain

KATAMINOを解くには、この木をもれなくたどっていけば良いことがわかります。 木をたどるには、幅優先探索深さ優先探索と2つのアルゴリズムがあり、 上のアルゴリズム深さ優先探索を用いています。

プログラムを書きやすくするために、もう少しプログラミングしやすい言葉を使って手順を書きます。 深さ優先探索を採用するので、スタックを使用します。

  1. スタックに、空のフィールドと次に置くピースの組み合わせを入れる
  2. スタックが空になるまで以下を繰り返す
    1. スタックからフィールドと次に置くピースの組み合わせを1つ取り出す
    2. ピースが置けるか判定する
      1. 置けなかったら繰り返しの先頭に戻る
    3. 置けたら、置いた状態のフィールドと次に置くピースをスタックに入れる
    4. 残っているピースがなければ完成
  3. スタックが空になった場合は解けない組み合わせだった

スタックのイメージ↓

f:id:yucatio:20190619231821p:plain:w250

以上がKATAMINOを解くプログラムの基本的なアルゴリズムです。次回はデータ構造について考えます。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

KATAMINOを解くプログラムを作成する【目次】

f:id:yucatio:20190616200409p:plain

KATAMINOとは

KATAMINOとは、Gigamic社が発売するパズルゲームです。 ペンタミノという、5個の正方形をつなげてできる形をしたピースを使い、 長方形の枠にぴったり収まるように組み合わせて遊ぶゲームです。


作るもの

KATAMINOを解くプログラムを作成します。言語はJavaScriptです。

完成したものがこちらです。↓

KATAMINO SOLVER

カタミノのピースをドラッグアンドドロップで選択してスタートボタンを押します。

f:id:yucatio:20190617084121p:plain:w300

解く過程も表示されます。

f:id:yucatio:20190617084135p:plain:w620

完成したら、「できたよ」と表示されます。

f:id:yucatio:20190617084151p:plain:w300

KATAMINOを解くプログラム 目次

STEP 1: KATAMINOを解くプログラムを作成する

  1. KATAMINOを解くアルゴリズム
  2. データ構造を考える
  3. ペンタミノの回転、反転をするためのアルゴリズム
  4. もととなるペンタミノのピースの準備
  5. 最終的に出来上がる配列の確認と関数の準備
  6. JavaScriptで転置を実装する
  7. JavaScriptで2次元配列のコピーと配列の反転
  8. 8パターンのスピンを作成する
  9. 2次元配列の重複を取り除く
  10. 2次元配列からピースがある場所のリストを作成する
  11. KATMINOを解くメインのプログラムの準備
  12. フィールドの初期化
  13. スタックへの初期データの投入
  14. スタックからの情報の取り出し
  15. ピースを置く場所の算出
  16. フィールドにピースが置けるかどうかの判定
  17. フィールドの更新
  18. JavaScriptで値を指定して削除する(置かれていないピースの更新)
  19. 最小の空白マスの更新
  20. 次に置くピースの投入
  21. 表示用メソッドの作成
  22. セルに色をつける
  23. JavaScriptのsetTimeoutを使用して途中経過を表示する
  24. ピースを画面から選べるようにする
  25. フィールドが分割された時に探索をやめる

STEP 2: UIを整える

  1. STEP2での成果物
  2. bootstrapとjQueryの設定
  3. ロゴの作成とbootstrapのnavbarの設定
  4. KATAMINOピースの用意
  5. ピースの選択エリアの作成
  6. ピースをドラッグ&ドロップさせる(jQuery-UI使用)
  7. スタートボタンの用意
  8. KATAMINOフィールドの用意
  9. 一時停止・再開ボタン、速度調節用スライダーの用意
  10. プログラムの構造を考える
  11. 画面の状態を考える
  12. 使うピースの選択を実装する
  13. KATAMINOを解くのをスタートさせる
  14. KATAMINOを解いているときの表示
  15. KATAMINOを解くのが終了したときの遷移
  16. 解いた結果をダイアログで表示する
  17. ピースの再選択を実装する
  18. 一時停止、再開機能を実装する
  19. 速度調整機能を実装する
  20. ピースが置かれないフィールドをマスクする
  21. フィールドに置かれたピースを選択エリアから消す
  22. ピースを戻す表示を追加する

ソースコード

githubソースコード公開しています : https://github.com/yucatio/KATAMINO-SOLVER

依存ライブラリ

動作環境

カレンダー作成問題の解答

前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。元記事では、解答を見ないで解くこととなっていましたが、ある程度自分で考えた後に他の人の解答を見ました。それでもオリジナルなコードになったと思います。

blog.jnito.com

カレンダー作成問題

「たのしいRuby」に載っている、オーソドックスなカレンダー作成問題です。
DateクラスのAPIさえわかれば、あとは基礎的なプログラミング知識だけでコードが書けると思います。 Date クラスを使って、今月の1日と月末の日付と曜日を求め、次のような形式でカレンダーを表示させてください

      April 2013
 Su Mo Tu We Th Fr Sa
     1  2  3  4  5  6
  7  8  9 10 11 12 13
 14 15 16 17 18 19 20
 21 22 23 24 25 26 27
 28 29 30

解答

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|
        if day.month == target_date.month
          print ' %2d' % day.day
        else
          print '   '
        end
      }
      puts
    }
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

解説

ブログに解答を載せるに際して、以下のような方針を立てました。

  • Railsで実装することを前提とする
    • HTMLのテーブルを使用する前提で実装する
    • フォーマットを(あまり)がんばらない

Railsで実装することを前提とする、とは、コンソール出力する用にコードを書くのではなく、Railsでhtmlを(erbなどで)出力する場合にこのように書く、という意味です。 特に今回はHTMLのテーブル形式で出力するとよさそうです。以下のテーブルを出力するイメージで実装を進めます。

f:id:yucatio:20190531085309p:plain

HTMLで出力を考えるとき、フォーマットはCSSで行うため、フォーマットについては最低限にします。

まずはクラスとメソッドを作成します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    puts target_date
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

問題では今月のカレンダーを表示する、となっていましたが、メソッド内部に現在時刻が入ってしまうとバグに気付きにくくデバッグもしづらいので、対象とする日にちは引数で渡すことにします。デフォルトで引数を現在日時にすることにより問題の意図通りに動くようにします。

実行します。本日の日付と引数で渡した日付が表示されました。 this monthの方は現在の日付なので実行ごとに結果が変わります。

--- this month ---
2019-05-29
--- 2013 04 ---
2013-04-24

実装を進めます。まずは月の初めの日付と月の終わりの日付のオブジェクトを作成します。 月初めは、現在の年月の1日なので、Date(target_date.year, target_date.month, 1)で作成します。これをstart_of_monthという変数に格納します。 月終わりは、次の月の1日の、1日前なので、これをそのままプログラムに直します。start_of_monthを使用して、start_of_month.next_month.prev_dayでよさそうです。ここまでの実装は以下になります。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)  # 追加
    end_of_month = start_of_month.next_month.prev_day  # 追加
    puts start_of_month
    puts end_of_month
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。月の初めと終わりの日付が取得できました。

--- this month ---
2019-05-01
2019-05-31
--- 2013 04 ---
2013-04-01
2013-04-30

次に、曜日を求めます。曜日のぶん出力をずらすためです。Date#wdayが使用できます。一旦出力を確認しておきましょう。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    puts start_of_month.wday
    puts end_of_month.wday
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。曜日の対応は以下なので、曜日に対応する数値が取れました。

0 1 2 3 4 5 6
--- this month ---
3
5
--- 2013 04 ---
1
2

次に曜日のぶんだけ日付を右にずらします。今回は、"ずらす"というという発想より、カレンダー(テーブル)の最初の日付と最後の日付を求める、という発想で解決します。

カレンダーの左上と右下、この2つは(当月の日付でなければ)空白で出力しますが、ここに該当する前月と次月の日付を求めます。

f:id:yucatio:20190531091237p:plain

カレンダーの左上は、上で求めたstart_of_month.wdayぶんstart_of_monthから過去の日付です。 カレンダーの右下は、end_of_month.wdayから土曜までの日数を足せば良いので、6 - end_of_month.wdaぶん未来の日付がカレンダーの右下の日付になります。ここまでをコードにします。日付を足したり引いたりするにはDate#prev_dayDate#next_dayを使用します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)  # 追加
    cal_end = end_of_month.next_day(6 - end_of_month.wday)  # 追加
    puts cal_start
    puts cal_end
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。カレンダーの左上と右下の日付が出力されました。

--- this month ---
2019-04-28
2019-06-01
--- 2013 04 ---
2013-03-31
2013-05-04

ここまでで事前準備は終了なので、本番の出力部分を実装していきます。まずは April 2013 の部分を出力します。時間を文字列にフォーマットするには、Date#strftime( class Date (Ruby 2.6.0) ) を使用します。Aprilのような英語の月名表示は%B、年表示は%Yで表します。センタリングはString#center( class String (Ruby 2.6.0) ) で行います。ここまでのコードです。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)  # 追加
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。月名と年がセンタリングして表示されました。

--- this month ---
      May 2019       
--- 2013 04 ---
     April 2013      

次に曜日のヘッダーSu Mo Tu We Th Fr Saを表示します。これはそのまま表示すれば良いでしょう。コードです。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'  # 追加
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。曜日名が表示されました。

--- this month ---
      May 2019       
 Su Mo Tu We Th Fr Sa
--- 2013 04 ---
     April 2013      
 Su Mo Tu We Th Fr Sa

日にちを表示していきます。まず、カレンダーの左上の日付からカレンダーの右下の日付までの範囲オブジェクトを作成します。(cal_start..cal_end)です。これを7日ごとに分割します。Enumerable#each_slice( each_slice (Enumerable) - Rubyリファレンス )を使用します。ここまでの実装です。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|  # 追加
      days  # 仮実装
    }
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

ここから各日付を出力します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|  # 追加
        print day.day  # 仮実装
      }
      puts
    }
  end
end
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

日付をフォーマットしていきます。フォーマットを、数値2桁にし、先頭に空白を入れます。 フォーマットには、String#%を使用します。 https://docs.ruby-lang.org/ja/latest/class/String.html#I_--25

先頭に空白1つ、数値2桁に揃えてフォーマットは、%2dで表します。

ここまでのコードです。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|
         print ' %2d' % day.day  # 変更
      }
      puts
    }
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。列が揃っています。

--- this month ---
      May 2019       
 Su Mo Tu We Th Fr Sa
 28 29 30  1  2  3  4
  5  6  7  8  9 10 11
 12 13 14 15 16 17 18
 19 20 21 22 23 24 25
 26 27 28 29 30 31  1
--- 2013 04 ---
     April 2013      
 Su Mo Tu We Th Fr Sa
 31  1  2  3  4  5  6
  7  8  9 10 11 12 13
 14 15 16 17 18 19 20
 21 22 23 24 25 26 27
 28 29 30  1  2  3  4

最後に、当月でない部分の日付は表示しないようにします。当月でない場合は空白3つを表示します。 当月かどうかの判定は、出力しようとしている日付と、引数で与えられた日付の月を比較します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|
        if day.month == target_date.month  # if-else 追加
          print ' %2d' % day.day
        else
          print '   '
        end
      }
      puts
    }
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。問題で求められた形式のカレンダーを作成することができました。

--- this month ---
      May 2019       
 Su Mo Tu We Th Fr Sa
           1  2  3  4
  5  6  7  8  9 10 11
 12 13 14 15 16 17 18
 19 20 21 22 23 24 25
 26 27 28 29 30 31   
--- 2013 04 ---
     April 2013      
 Su Mo Tu We Th Fr Sa
     1  2  3  4  5  6
  7  8  9 10 11 12 13
 14 15 16 17 18 19 20
 21 22 23 24 25 26 27
 28 29 30            

あとがき

最初の日付をずらして空白を入れる実装に迷いましたが、 わりと分かりやすく書けたかと思います。

Railsで実装する時に、

  • 前後の月の日付を表示してほしい(薄い文字で)
  • "今日"をハイライトしてほしい

といった追加の要望にも簡単に応えられる実装にしました。

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


連続した文字を抽出する正規表現とガラケー文字入力問題(応用)の解答

前回の記事の問題を解いている過程で、"連続した数字を取り出さなければ"と思い、凝った正規表現を作ってしまったので、それを使える問題を作成しました。

★前回の記事

yucatio.hatenablog.com

こちらの記事のガラケー文字入力問題の応用編を作成しました。

blog.jnito.com

問題

英語のガラケーでは「2」キーを2回押すと「b」になり、「3」キーを3回押すと「f」になります。

文字を確定するには、'0'または別の数字を入れることで確定されます。

たとえば、このプログラムに対して"443355505556660"を入力すると、"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])\2*)/).map {|digits, |
      button_values = BUTTONS[digits[0]]
      button_values[(digits.length - 1) % button_values.length]
    }.join
  end
end

テストコードです。

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

describe FeaturePhone do
  describe '#digits_to_alphabet_non_zero' 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回押され、毎回0で確定された場合' do
      let(:digits) { '440330555055506660' }
      it { is_expected.to eq 'hello'}
    end

    context '1, 4, 3, 5, 6, 7, 9のボタンが押され、途中に0がない場合' do
      let(:digits) { '4433555055566611011111090666077755531110' }
      it { is_expected.to eq 'hello, world!'}
    end

    context '0が複数回登場し、5のボタンが8回押されたケースが含まれる場合' do
      let(:digits) { '000555555550000330000444000080000200004440000' }
      it { is_expected.to eq 'keitai'}
    end

    context '0が複数回登場し、5のボタンが8回押されたケースが含まれる場合' do
      let(:digits) { '0005555555533444824440000' }
      it { is_expected.to eq 'keitai'}
    end
  end
end

解説

1から9の数字で同じ数字が連続している部分を抜き出します。[1-9]+では、443355などにマッチしてしまい、同じ数字の連続を取り出すことができません。

同じ数字の連続を取り出すのには、後方参照を使用します。後方参照は、カッコでキャプチャした部分を、\1の形式で正規表現中で参照できる機能です。同じ数字が2回連続で出現する正規表現([0-9])\1で表すことができます。今回は、1から9の数字の1回以上の連続を取り出したいので、正規表現([1-9])\1*となります。

String.scan( scan (String) - Rubyリファレンス )で正規表現にマッチする部部分を繰り返し取得します。

ここまでをコードにしていきます。

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

入力が0005555555533444824440000の場合の実行結果です。連続した部分の先頭の文字列が取得できました。これはカッコでキャプチャした部分です。

[["5"], ["3"], ["4"], ["8"], ["2"], ["4"]]

マッチする部分全体を取得するため、キャプチャのカッコを追加します。後方参照の\1\2に変更します。

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

入力が0005555555533444824440000の場合の実行結果です。連続した数字が取得できました。

[["55555555", "5"], ["33", "3"], ["444", "4"], ["8", "8"], ["2", "2"], ["444", "4"]]

連続した数字列と最初のの数字の2つをmapに渡します。 多重代入を使用して、配列の1番目と2番目をそれぞれ別の変数に代入します。

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])\2*)/).map {|digits, digit|
      button_values = BUTTONS[digit]
      button_values[(digits.length - 1) % button_values.length]
    }.join
  end
end

そのほかの部分は前回の記事と一緒ですので、参考にどうぞ。

yucatio.hatenablog.com

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