kintone 日付 to 日付の重複チェック

#２つの日付の期間でレコード重複を確認する。
期間内に重なる日付がある時にエラーを検知して警告したい。
**この記事はコメントを頂き、有効な内容は記事最下部と致しました。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/511519/bafb24c6-0da5-83f3-c591-b02813cdc544.png)

<details><summary>記事内容</summary><div>

**例)**
図書館の本の借用を例として考える。
>Aさんが9/1~7にとある本を既に借りていて、
同じ本を借りようとした場合にエラーを起こしたい。

レコード１が既に登録されていて、レコード２を登録されるとき。

>**レコード１**
借用者：Ａ
借用本：kintone認定 アソシエイト試験対策テキスト
本番号：2203456903
**開始日：9/1**
**終了日：9/7**

>>**新規レコード２**
借用者：Ａ
借用本：kintone認定 アソシエイト試験対策テキスト
本番号：2203456903
**開始日：9/3**
**終了日：9/9**

レコード１
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/511519/092428b4-1b22-1c0e-812f-b7e52a59bc0e.png)

レコード２を追加しようとするとエラー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/511519/e3820023-bb1a-005a-9373-f6ee11a2c9b1.png)


# 解決策
結論、**JavaScriptでの日付計算はほとんど利用しない**。
テーブルを用意し、`２フィールドの日付間の日数分`、日付データを格納する。
重複チェックでレコードを参照する際にテーブルの日付データをクエリに指定する。
尚、js初心者向けに全記載のコードで説明していきます。

## フィールド
|フィールドコード|フィールドタイプ|
|:--:|:--:|
|開始日|日付フィールド|
|終了日|日付フィールド|
|本番号|文字列一行(数値でもok)|
|借用者|文字列一行|
|テーブル|テーブル|
|日付|テーブル --> 日付フィールド|
|借用本|文字列一行(今回は不使用)|

## テーブルへ日付格納

日付フィールドのみのテーブルを作成し、
保存イベントで日付間の全ての日付を格納。
### ソースコード Ver0.1
```js
(() => {
    'use strict';

    //与えられた日付型から文字列の日付を返す関数(yyyy-MM-dd)0埋め日付
    const getDateString = (date) => {
        return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}`;
    }

    //二つの日付フィールドの差を計算して日数で返す関数
    //1を足す理由はその日を含める計算のため9/1~9/7は合計7日間
    //ミリ秒単位の差分計算の結果 / 1000ms * 60sec * 60 min * 24hour = 差分日数
    const getDifference = (dateFrom, dateTo) => {
        return 1 + Number((new Date(dateTo) - new Date(dateFrom)) / (1000 * 60 * 60 * 24));
    }

    kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => {
        const record = event.record;

        //入力必須フィールド空チェック
        const mandatoryFields = [
            '開始日',
            '終了日',
            '本番号',
            '借用者'
        ];

        //必須チェック
        const result = mandatoryFields.reduce((acc, field) => {
            record[field].error = null;
            if (!record[field].value) {
                record[field].error = '必須です';
                acc.push(field);
            }
            return acc;
        }, []);

        //一つでも空であればエラー文と共にreturn
        if (result.length > 0) return event;

        const totalDays = getDifference(record['開始日'].value, record['終了日'].value);

        //テーブル初期化
        record['テーブル'].value = [];

        let date = new Date(record['開始日'].value);
        let i = 0;

        //合計日数分繰り返し
        do {

            //テーブルに日付を格納
            record['テーブル'].value.push({
                value: {
                    "日付": {
                        type: "DATE",
                        value: getDateString(date)
                    }
                }
            });

            //日付インクリメント(繰り上げ)
            date.setDate(date.getDate() + 1);
            //繰り返し用のインクリメント(繰り上げ)
            i++;

        } while (i < totalDays);

        return event;

    });
})();
```

## 重複チェック
kintone.apiで条件付きでレコードを取得する。
**条件：**
借用者がAで2203456903の本番号の本を9/1、9/2、9/3. . . 9/7に借りているレコードが
`編集中のレコードを除き`、存在するか。

**kintone.api、GETの際クエリ指定が今回の肝になる。**

上記の条件をクエリ記法でどのように表すか
### クエリをどうするか
`and` と `or` の使い分け。
最初の例の場合で考えるとすると、

```
借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-01"  or 
借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-02"  or
.........
借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-07"
```
と日付単位で絞り込んでいく。

### ダメな例
```
借用者 = "A" and 本番号 = "2203456903" and 日付 = "2021-09-01"  or 日付 = "2021-09-02 or ...."
```
こうなると、最初の9/1日だけ、借用者と本番号の指定があり、
9/2以降は**日付が9/2のすべてのレコードを参照**することになる。

### ソースコード Ver1.0

```js
(() => {
    'use strict';

    //与えられた日付型から文字列の日付を返す関数(yyyy-MM-dd)0埋め日付
    const getDateString = (date) => {
        return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}`;
    }

    //二つの日付フィールドの差を計算して日数で返す
    //1を足す理由はその日を含める計算のため9/1~9/7は合計7日間
    //ミリ秒単位の差分計算の結果 / 1000ms * 60sec * 60 min * 24hour = 差分日数
    const getDifference = (dateFrom, dateTo) => {
        return 1 + Number((new Date(dateTo) - new Date(dateFrom)) / (1000 * 60 * 60 * 24));
    }

    kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => {
        const record = event.record;

        //入力必須フィールド空チェック
        const mandatoryFields = [
            '開始日',
            '終了日',
            '本番号',
            '借用者'
        ];

        //必須チェック
        const result = mandatoryFields.reduce((acc, field) => {
            record[field].error = null;
            if (!record[field].value) {
                record[field].error = '必須です';
                acc.push(field);
            }
            return acc;
        }, []);

        //一つでも空であればエラー文と共にreturn
        if (result.length > 0) return event;

        const totalDays = getDifference(record['開始日'].value, record['終了日'].value);

        //テーブル初期化
        record['テーブル'].value = [];

        let date = new Date(record['開始日'].value);
        //****************************以下追記******************************//
        let query = ``;
        //****************************以上追記******************************//
        let i = 0;

        //合計日数分繰り返し
        do {

            //テーブルに日付を格納
            record['テーブル'].value.push({
                value: {
                    "日付": {
                        type: "DATE",
                        value: getDateString(date)
                    }
                }
            });

            //****************************以下追記******************************//
            //基本のクエリ
            let basicQuery = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 日付 in ("${getDateString(date)}")`;

            //クエリ記法繰り返し二回目以降は or を追記(三項演算子を使用)
            query += i === 0 ? basicQuery : ` or ` + basicQuery;

            //新規レコードの場合と編集レコードの場合で自レコードを含むか含まないかクエリ分岐
            query += event.recordId ? ` and $id != "${event.recordId}"` : '';
            //****************************以上追記******************************//
            

            //日付インクリメント(繰り上げ)
            date.setDate(date.getDate() + 1);
            //繰り返し用のインクリメント(繰り上げ)
            i++;

        } while (i < totalDays);

        //****************************以下追記******************************//
        //最後にソート文
        query += ' order by レコード番号 desc limit 100 offset 0';

        const param = {
            app: event.appId,
            query: query
        }

        return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', param).then((resp) => {
            if (resp.records.length > 0) {
                event.error = '日付の範囲内に、既に記録された日付が含まれています。';
                record['開始日'].error = '確認';
                record['終了日'].error = '確認';
            }
            return event;
        }).catch((error) => {
            console.log(error);
            event.error = '既存レコードの取得に失敗しました。';
            return event;
        });
        //****************************以上追記******************************//
        //return event; 削除
    });
})();
```

# 今回の肝
## テーブルに指定期間の日付をすべて格納
絞り込み条件に一致させるため、9/1、9/7の間の日付を準備すると、
標準機能の絞り込みでも使用できるようになるため、便利。

## テーブル内の日付を含めた条件で重複チェック
標準機能の絞り込みも使用できるということは、
apiで該当レコードを取得することも容易ですね。
**jsで日付計算は複雑になりがちなので避けてみました**。
`逆にテーブルの設置無し、jsのみで対応が難しかったから`、が本音なので、
方法があれば教えてほしいです。。

# フィールドコード書き換えれば使えますというやつ
Ver1.0のコメントアウトを取っただけです。
<details><summary>JS</summary><div>

```js
(() => {
    'use strict';

    const getDateString = (date) => {
        return `${date.getFullYear()}-${("0" + (date.getMonth() + 1)).slice(-2)}-${("0" + date.getDate()).slice(-2)}`;
    }

    const getDifference = (dateFrom, dateTo) => {
        return 1 + Number((new Date(dateTo) - new Date(dateFrom)) / (1000 * 60 * 60 * 24));
    }

    kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => {
        const record = event.record;

        const mandatoryFields = [
            '開始日',
            '終了日',
            '本番号',
            '借用者'
        ];

        const result = mandatoryFields.reduce((acc, field) => {
            record[field].error = null;
            if (!record[field].value) {
                record[field].error = '必須です';
                acc.push(field);
            }
            return acc;
        }, []);

        if (result.length > 0) return event;

        const totalDays = getDifference(record['開始日'].value, record['終了日'].value);
        record['テーブル'].value = [];

        let date = new Date(record['開始日'].value);
        let query = ``;
        let i = 0;

        do {

            record['テーブル'].value.push({
                value: {
                    "日付": {
                        type: "DATE",
                        value: getDateString(date)
                    }
                }
            });

            let basicQuery = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 日付 in ("${getDateString(date)}")`;
            query += i === 0 ? basicQuery : ` or ` + basicQuery;
            query += event.recordId ? ` and $id != "${event.recordId}"` : '';

            date.setDate(date.getDate() + 1);
            i++;

        } while (i < totalDays);

        query += ' order by レコード番号 desc limit 100 offset 0';

        const param = {
            app: event.appId,
            query: query
        }

        return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', param).then((resp) => {
            if (resp.records.length > 0) {
                event.error = '日付の範囲内に、既に記録された日付が含まれています。';
                record['開始日'].error = '確認';
                record['終了日'].error = '確認';
            }
            return event;
        }).catch((error) => {
            console.log(error);
            event.error = '既存レコードの取得に失敗しました。';
            return event;
        });

    });
})();
```
</div></details>

</div></details>

# コメントで一蹴していただきました。(こちらがシンプルです。)
>@rgn212さん
こんな感じでは駄目でしょうか？
>
```js
// 既存貸し出し期間
const existing = {
  start: 20210901,
  end  : 20210907,
};
// 申請期間
const request = {
  start: 20210903,
  end  : 20210909,
};
if(request.end >= existing.start && existing.end >= request.start) {
  console.log('重複しています');
}
else {
  console.log('重複していません');
}
```

応用して、
開始日が`新規終了日`より小さく
終了日が`新規開始日`より大きいものを取得。以上。

```js
let query = ``;
const includeOwn = event.recordId ? ` and $id != "${event.recordId}"` : '';
query = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 終了日 >= "${record['開始日'].value}" and 開始日 <= "${record['終了日'].value}"${includeOwn}`
query += ' order by レコード番号 desc limit 100 offset 0';
```


```js
(() => {
    'use strict';
    kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => {
        const record = event.record;

        const mandatoryFields = [
            '開始日',
            '終了日',
            '本番号',
            '借用者'
        ];

        const result = mandatoryFields.reduce((acc, field) => {
            record[field].error = null;
            if (!record[field].value) {
                record[field].error = '必須です';
                acc.push(field);
            }
            return acc;
        }, []);

        if (result.length > 0) return event;

        let query = ``;
        const includeOwn = event.recordId ? ` and $id != "${event.recordId}"` : '';
        query = `借用者 = "${record['借用者'].value}" and 本番号 = "${record['本番号'].value}" and 終了日 >= "${record['開始日'].value}" and 開始日 <= "${record['終了日'].value}"${includeOwn}`
        query += ' order by レコード番号 desc limit 100 offset 0';

        const param = {
            app: event.appId,
            query: query
        };

        return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', param).then((resp) => {
            console.log(resp);
            if (resp.records.length > 0) {
                event.error = '日付の範囲内に、既に記録された日付が含まれています。';
                record['開始日'].error = '確認';
                record['終了日'].error = '確認';
            }
            return event;
        }).catch((error) => {
            console.log(error);
            event.error = '既存レコードの取得に失敗しました。';
            return event;
        });

    });
})();
```