kintoneで相対日付のバリデーションを行う


先日[kintoneで宣言的にフィールド値の検証を行う](https://qiita.com/shellyln/items/cebe1b0ca4f1ae4d9559)という記事を投稿しました。[Tynder](https://github.com/shellyln/tynder)というライブラリを使い、型宣言を書くことで、（手続き的ではなく）宣言的に入力値の検証を行いました。

今回はその応用編として、日付や日付時刻型のフィールドを相対日付（例えば、今月、来月、今年、今年度）で検証します。
さらに、フィールドのエラーに合ったカスタムエラーメッセージを表示するようにします。

![kintone-custom-dt-validation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185253/5ee4fe4a-750b-b34c-e3fd-430cf6365568.png)

# コード

* [tynder.min.js](https://github.com/shellyln/tynder/releases)
* [lib.js](https://qiita.com/shellyln/items/cebe1b0ca4f1ae4d9559#%E8%A8%AD%E5%AE%9A%E6%96%B9%E6%B3%95) (前回記事)
* app.js (下述)

```javascript
// アプリのレコード型を定義します
// interfaceの各フィールドは、kintoneアプリのフィールドコードと一致させてください
const definition = `
/** サブテーブル */
interface Table {
    itemName: string;
    itemValue: number;
}

/** アプリ */
interface App {
    /** 名称 */
    @minLength(2) @maxLength(16)
    @match(/^[A-Z][0-9]{3}-.+/)
    name: string;

    /** 数量 (非必須) */
    amount?: number;

    /** 期日 */
    @stereotype('lcdate') // ローカルタイムゾーン日付
    @range('=today first-date-of-mo', '=today last-date-of-mo')
    @msg({
        valueRangeUnmatched: '当月の日付を入力してください',
    })
    dueDate: string;

    /** 最終接点日時 */
    @stereotype('lcdatetime') // ローカルタイムゾーン日付時刻
    @minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')
    @msg({
        valueRangeUnmatched: '当月の日付時刻を入力してください',
    })
    contactDt: string;

    /** サブテーブル */
    table: Table[];
}
`;

// イベントハンドラ
kintone.events.on([
    'app.record.create.submit',
    'mobile.app.record.create.submit',
    'app.record.edit.submit',
    'mobile.app.record.edit.submit',
    'app.record.index.edit.submit',
    ], function(event) {
    event.record = removeBlankTableRow(event.record, 'table');

    // スキーマ検証を行います
    const schema = tynder.compile(definition);
    const ctx = {
        checkAll: true,
        schema,
        stereotypes: new Map(tynder.stereotypes),
        // 共通のカスタムエラーメッセージ
        errorMessages: {
            required:             '必須です',
            valueLengthUnmatched: '値は%{minLength}から%{maxLength}の長さで入れてください',
            valueRangeUnmatched:  '値は%{minValue}から%{maxValue}の間で入れてください',
            typeUnmatched:        '値の形式が間違っています',
        },
    };

    const unknownInput = mapRecord(event.record);
    const validated = tynder.validate(unknownInput, tynder.getType(schema, 'App'), ctx);
    if (! validated) {
        const errText = JSON.stringify(ctx.errors, null, 2);
        console.error(errText);

        // エラーを表示します
        displayValidationErrorMessages(event, ctx);
    }
    return event;
});
```

# 日付時刻数式

* 今月(日付)
    * `@range('=today first-date-of-mo', '=today last-date-of-mo')`
* 今月(日付時刻)
    * `@minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day')`
* 来月(日付)
    * `@range('=today first-date-of-mo +1mo', '=today @1day +1mo last-date-of-mo')`
* 来月(日付時刻)
    * `@minValue('=today first-date-of-mo +1mo') @lessThan('=today @1day +1mo last-date-of-mo +1day')`
* 今年(日付)
    * `@range('=today first-date-of-yr', '=today last-date-of-yr')`
* 今年(日付時刻)
    * `@minValue('=today first-date-of-yr') @lessThan('=today last-date-of-yr +1day')`
* 来年(日付)
    * `@range('=today first-date-of-yr +1yr', '=today @1day +1yr last-date-of-yr')`
* 来年(日付時刻)
    * `@minValue('=today first-date-of-yr +1yr') @lessThan('=today @1day +1yr last-date-of-yr +1day')`
* 今年度(date)
  * `@range('=today first-date-of-fy(4)', '=today first-date-of-fy(4) +1yr -1day')`
      * 4月始まり
* 今年度(datetime)
  * `@minValue('=today first-date-of-fy(4)') @lessThan('=today first-date-of-fy(4) +1yr')`
      * 4月始まり
* 来年度(日付)
  * `@range('=today first-date-of-fy(4) +1yr', '=today first-date-of-fy(4) +2yr -1day')`
      * 4月始まり
* 来年度(日付時刻)
  * `@minValue('=today first-date-of-fy(4) +1yr') @lessThan('=today first-date-of-fy(4) +2yr')`
      * 4月始まり



詳しくは https://github.com/shellyln/tynder#date--datetime-stereotypes を参照してください。

# 型宣言のテストについて
記述した型宣言が文法的に正しいかどうかは以下のサイトで確認できます。
https://shellyln.github.io/tynder/playground2.html