[Django]4桁数字で日付入力するFieldを作った

## なんか4桁入力したいと言われた

昔作ったDjangoの予約日管理する職場用アプリです。
古いやつなのでPythonは2系です。
Djangoも古くて1.6。

一応8桁入力には対応していたけど......

## なんで数字で日付を入力するのか

Dateピッカーみたいな
マウスでポチポチやって日付入力するのがUI的には格好いいし、
ユーザー的にわかりやすいけど、
連続で入力が必要だったりするとけっこう面倒。
みんながマウスを使わなくていいエディタが大好きなのと同じです。

## 問題点

デフォルトのDateFieldはキーワード引数のinput_fieldsに
"%Y-%m-%d","%Y/%m/%d"とかを指定して、テキスト入力が可能になっていますが、
ハイフンとかスラッシュの入力はけっこう面倒。
できればテンキーの数字だけで完結したい。
(Enterでタブ移動みたいにすればなおよし)
かと言って"%Y%m%d"を指定すると、8桁入れた時は問題ないけど、
桁が足りてなかった場合、どこまでが年でどこまでが月でどこまでが日なのか
実際の挙動がユーザーにはわかりにくいことになります。

また"%m%d"を指定すると、年が1900年固定になります。
そして、"%Y%m%d"の時と同じで年月日の境目の問題も残ります。

解決するにはFieldの入力値の解釈を変える必要があります。
実際には、DateFieldのto_python関数をoverrideすればOKです。

## まずは8桁入力から

とりあえず、8桁の数字であることを確認してから

```python

def to_python(self, value):
    #省略
    #他のところのコードは最後に書きます
    year_month_day = value[:4], value[4:6] , value[6:]
    result = date(*map(int, year_month_day))
    return result
```
的な感じで4桁2桁2桁で日付にするだけです。

## それを踏まえて4桁入力

まず、4桁の数字だけにしぼることで、境目問題をクリアします。

4桁だと日付しかないので、おおまかに
去年、今年、来年
の3パターンの可能性があります。

どうやって、決めるかが重要ですが
* 今日の日付から *一番近い日付* 
* 今日の日付から *一番近い未来日*
* 今日の日付から *一番近い過去日*

くらいを実装しておけばとりあえず十分ではないかと。
予約日管理なら一番近い未来日を使うことになります。

というわけで、キーワード引数で初期化の時に日付セレクターを決めます。
イメージ的には

```python
    DateField("未来日", selecter="nearest_fortune")
    DateField("過去日", selecter="nearest_past")
    DateField("近い日", selecter="nearest")
```

上記のような感じです。

まとめると以下のようなコードになります。

```python
class DateField(forms.DateField):

    def __init__(self, *args, **kwargs):

        #selecterの名前はある程度、幅をもたせておく
        nearest = ["nearest_selecter", "nearest"]
        fortune = ["nearest_fortune_selecter",
                   "nearest_fortune",
                   "fortune"]
        past = ["nearest_past_selecter",
                "nearest_past",
                "past"]

        #popにしないと、スーパークラスの__init__呼んだ時に「未定義の引数」で怒られる
        selecter_name = kwargs.pop("selecter", None)

        #selecterキーワードによって年の選択方法を変える
        if selecter_name in nearest:
            self.selecter = self.nearest_selecter
        if selecter_name in past:
            self.selecter = self.nearest_past_selecter
        if selecter_name in fortune:
            self.selecter = self.nearest_fortune_selecter
        else:
            self.selecter = self.nearest_selecter

        forms.DateField.__init__(self, *args, **kwargs)

    #近い過去日を選択するやつ
    def nearest_past_selecter(self, days):
        today = date.today()
        get_days = lambda d: abs((today - d).days)
        nearest = min(filter(lambda d: d <= today, days), key=get_days)
        return nearest

    #近い未来日を選択するやつ
    def nearest_fortune_selecter(self, days):
        today = date.today()
        get_days = lambda d: abs((today - d).days)
        nearest = min(filter(lambda d: d >= today, days), key=get_days)
        return nearest

    #近い日を選択するやつ
    def nearest_selecter(self, days):
        today = date.today()
        get_days = lambda d: abs((today - d).days)
        nearest = min(days, key=get_days)
        return nearest

    @override
    def to_python(self, value):
        value = value.strip()

        #4桁の場合
        if len(value) == 4 and value.isdigit():
            month, day = int(value[:2]), int(value[2:4])
            today = date.today()
            prev_year = date(today.year - 1, month, day)
            this_year = date(today.year    , month, day)
            next_year = date(today.year + 1, month, day)
            days = [prev_year, this_year, next_year]
            return self.selecter(days)

        #8桁の場合
        elif len(value) == 8 and value.isdigit():
            year_month_day = value[:4], value[4:6] , value[6:]
            result = date(*map(int, year_month_day))
            if result.year < 2000 or result.year > 2100:
                message = u'年は2001～2099の間で入力してください。'
                self.error_messages['invalid'] = message
                raise ValidationError

        else:
            result = forms.DateField.to_python(self, value)

        return result


```