「シェリルの誕生日」の問題作成

年始に作成した『シェリルの誕生日』の処理。
https://qiita.com/hnkyi/items/dbd937a3e11ef7682804

ふと「逆も行けるんじゃね？」と思って、入力された誕生日から問題となる候補日を作成してみました。

```py:create_question_in_sheryl.py
"""

『シェリルの誕生日』の問題作り

- 「m/d形式」で誕生日を受け取り、問題を作成
- 誕生日を基点に「前後４ヶ月、左右６日」を取得
- 「ユニークな日を持つ月」を選定
    - ユニークな日が誕生日の場合、バーナードが初見で正解できてしまう
- 「重複する日を持つ月」を選定
    - 重複する日が誕生日の場合、アルバートの言葉を聞いてもバーナードは特定できない

"""

import sys
import random
import re
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

# 引数チェック
if len(sys.argv) < 2 or not re.search(r"^[0-9]{1,2}/[0-9]{1,2}$", sys.argv[1]):
    print("usage: python create_question_in_sheryl.py mm/dd")
    exit()

birthday = datetime.strptime(sys.argv[1], "%m/%d")
birth_month = birthday.strftime("%m")
birth_day   = birthday.strftime("%d")

# 前後４ヶ月、左右６日のテーブル作成
months = []
start_month = random.choice(list(range(-3, 1)))
for add_val in range(start_month, start_month + 4):
    month = birthday + relativedelta(months=add_val)
    months.append(month.strftime("%m"))
days = []
start_day = random.choice(list(range(-5, 1)))
for add_val in range(start_day, start_day + 6):
    day = birthday + timedelta(days=add_val)
    days.append(day.strftime("%d"))

# ユニークな日を選定
unique_days = []
while len(unique_days) < 2:
    choice = random.choice(days)
    if choice != birth_day and choice not in unique_days:
        unique_days.append(choice)

# ユニークな日を持つ月を選定
unique_months = []
while len(unique_months) < 2:
    choice = random.choice(months)
    if choice != birth_month and choice not in unique_months:
        unique_months.append(choice)

# 重複する日を持つ月を選定
duplicate_month = [month for month in months if month != birth_month and month not in unique_months][0]

# 重複させる日を選定
duplicate_day = ""
while True:
    choice = random.choice(days)
    if choice != birth_day and choice not in unique_days:
        duplicate_day = choice
        break

# XXX 20180627 modified ダミー日を使うよう修正
# # 重複させない日を選定
# unduplicate_days = []
# while len(unduplicate_days) < 2:
#     choice = random.choice(days)
#     if choice != duplicate_day and choice not in unique_days and choice not in unduplicate_days:
#         unduplicate_days.append(choice)
#
# # 重複する日を持つ月を選定
# duplicate_month = [month for month in months if month != birth_month and month not in unique_months][0]
#
# # ユニークでない日を選定
# ununique_days = [day for day in days if day not in unique_days and (day == duplicate_day or day in unduplicate_days)]
dummy_days = {birth_day: 1}
for dummy_day in [day for day in days if day not in unique_days and day != birth_day and day != duplicate_day]:
    dummy_days[dummy_day] = 2

# 候補日を作成
candidates = []
candidates.append("{}/{}".format(birth_month, birth_day))
candidates.append("{}/{}".format(birth_month, duplicate_day))

candidates.append("{}/{}".format(duplicate_month, duplicate_day))
# XXX 20180627 modified ダミー日を使うよう修正
# for d in unduplicate_days:
#     candidates.append("{}/{}".format(duplicate_month, d))
for dummy_day in random.sample([day for day in dummy_days.keys() if dummy_days[day] != 0 and day != birth_day], k=2):
    candidates.append("{}/{}".format(duplicate_month, dummy_day))
    dummy_days[dummy_day] -= 1

for m, d in zip(unique_months, unique_days):
    candidates.append("{}/{}".format(m, d))
for idx in range(len(unique_months)):
    unique_month = unique_months[idx]
    # XXX 20180627 modified ダミー日を使うよう修正
    # if 0 == idx:
    #     candidates.append("{}/{}".format(unique_month, random.choice(ununique_days)))
    # else:
    #     choices = []
    #     while True:
    #         choices = random.choices(ununique_days, k=2)
    #         if choices[0] != choices[1]:
    #             break
    #     candidates.append("{}/{}".format(unique_month, choices[0]))
    #     candidates.append("{}/{}".format(unique_month, choices[1]))
    if 0 == idx:
        dummy_day = random.choice([day for day in dummy_days.keys() if dummy_days[day] != 0])
        candidates.append("{}/{}".format(unique_month, dummy_day))
        dummy_days[dummy_day] -= 1
    else:
        for dummy_day in random.sample([day for day in dummy_days.keys() if dummy_days[day] != 0], k=2):
            candidates.append("{}/{}".format(unique_month, dummy_day))
            dummy_days[dummy_day] -= 1

# 結果出力
print(sorted(candidates))
```

３回ほど実行してみたけど、毎度違う値が生成されてるのが分かる。
それぞれ解いてみて正解まで行き着くから、とりあえず試験もOK。

```sh
$ python create_question_in_sheryl.py 9/17
['08/17', '08/21', '09/17', '09/18', '10/17', '10/18', '10/19', '11/17', '11/19', '11/20']

$ python create_question_in_sheryl.py 9/17
['07/13', '07/14', '07/17', '08/14', '08/18', '09/14', '09/17', '10/14', '10/15', '10/17']

$ python create_question_in_sheryl.py 9/17
['08/18', '08/19', '08/22', '09/17', '09/22', '10/18', '10/20', '10/22', '11/20', '11/21']
```

試験も兼ねて、別の誕生日でも試してみた。
とりあえず最近世話になってるとある４人の誕生日でも試してみた。
それぞれ解いて正解まで辿り着く事は確認した。

```sh
$ python create_question_in_sheryl.py 10/30
['08/01', '08/30', '08/31', '09/02', '09/30', '10/29', '10/30', '11/01', '11/29', '11/30']

$ python create_question_in_sheryl.py 4/14
['03/11', '03/13', '03/14', '04/13', '04/14', '05/11', '05/12', '06/10', '06/11', '06/14']

$ python create_question_in_sheryl.py 1/12
['01/11', '01/12', '02/10', '02/12', '02/15', '11/12', '11/14', '12/10', '12/11', '12/12']

$ python create_question_in_sheryl.py 10/5
['08/05', '08/07', '08/09', '09/04', '09/06', '10/05', '10/09', '11/04', '11/05', '11/09']
```

これ使って候補日を割り出して、酒の席とかで使ったら盛り上がるね🍺🍺

以上。

##追記：20190627
@shiracamus さんにご指摘いただきまして、ソースコードを修正しました。
コメントアウトして新しいソースコードを書き足す、古臭い修正方法してますが、ご確認下さい、、、
なお、「内包表記」と「ジェネレーター」も採用すると、全取っ替えになるので、問題箇所のみの修正としております。

- 指摘内容
    - 元のソースコードで出力されたリストを、[シェリルの誕生日](https://qiita.com/hnkyi/items/dbd937a3e11ef7682804)の方に食わせると、正解しない
- 原因
    - 問題に関わらないダミー的な立ち位置の`day`の設定誤り
    - [シェリルの誕生日](https://qiita.com/hnkyi/items/dbd937a3e11ef7682804)には原因なし
- 改修内容
    - ダミー日として__「`ユニークな日`と`重複する日`以外」__を設定
    - 誕生日(`birth_day`)となる日は「`重複する日`」には用いない
- 試験結果
    - 私の誕生日である「9/17」で３回試して、３回とも正解
    - すでに上げてる「４人の誕生日」でそれぞれ１回ずつ試して、それぞれ正解

以上です。