## 経緯
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<p class="link-card-title">予定日後の稼働日に · Issue #9 · tadashi-aikawa/silhouette</p>
<p class="link-card-description">毎月予定日の処理で、その日が稼働日じゃないことも出てきます。その際に、その後の稼働日に日にちをずらして表示することは可能ですか?</p>
</div>
<img src="https://opengraph.githubassets.com/0f6fc281c9f4f3c340ead5ba7b7f07764013acd1faf2f60a1ae8169fd5543501/tadashi-aikawa/silhouette/issues/9" class="link-card-image" />
</div>
<a href="https://github.com/tadashi-aikawa/silhouette/issues/9"></a>
</div>
## メモ
- 稼働日前後だと `wed>1!` や `web<2!` とかになる
- `web>0!` は試したけど無理だった
- やろうと思えばこれでもパースできるけど、どうせするならこれは直感的ではない
- 別の表現の方が望ましいと思う
- **たとえば `web>!` と `web<!`**
## 現行ロジック
たとえば `10d>2!` について考える。これは **10日より2稼働日後**。
`RepetitionTask` の実装 `shoudTry` は **いずれかの繰り返し条件に一致すればOKとしている** (`|` 区切りにこと)。 ここは今回の話には影響しないので `needTaskBy` を考えればよい。
```ts
shouldTry(date: DateTime, holidays: DateTime[]): boolean {
return this._props.repetitions.some((r) =>
needTaskBy(date, holidays, r, this._props.baseDate)
);
}
```
`needTaskBy` について。
```ts
function needTaskBy(
date: DateTime,
holidays: DateTime[],
repetition: Repetition,
baseDate?: DateTime,
): boolean {
let targetDate = date.clone();
if (repetition.dayOffset !== 0) {
targetDate = date.minusDays(repetition.dayOffset);
}
if (repetition.workdayOffset !== 0) {
const targetDates = reverseOffsetWorkdays(
date,
repetition.workdayOffset,
holidays,
);
if (targetDates.length === 0) {
return false;
}
return targetDates.some((d) =>
needTaskBy(
d,
holidays,
repetition.withOffset({
dayOffset: 0,
workdayOffset: 0,
}),
baseDate,
)
);
}
// ここから先は `targetDate` に対する条件を一致するか調べている
```
`repetition.dayOffset` については今回考慮しなくていいので除外する。つまり、今回の焦点は `repetition.workdayOffset` になる。
```ts
if (repetition.workdayOffset !== 0) {
const targetDates = reverseOffsetWorkdays(
date,
repetition.workdayOffset,
holidays,
);
if (targetDates.length === 0) {
return false;
}
return targetDates.some((d) =>
needTaskBy(
d,
holidays,
repetition.withOffset({
dayOffset: 0,
workdayOffset: 0,
}),
baseDate,
)
);
}
```
まずは変数の意味から。
| 変数 | 意味 |
| ------------------------ | -------------------------------- |
| repetition.workdayOffset | 稼働日としてのオフセット. 2ならタスクが2稼働日後を指している |
| targetDates | 後述 |
| holidays | 休日一覧 (ファイルで指定されたもの) |
`targetDates` はちょっと複雑。たとえば
- あるタスクが
- 10d(10日)
- `repetition.workdayOffset` が 2
であるとき、12日の `shoudTry` を実行したら **12日の1営業日前、2営業日前にそれぞれtargetDateを移行し、そこでoffsetなしのshouldTryを行う** という処理になる。
```
// date | holidays | baseDate | repetitionWord | expected
[d("2023-01-02") , holidays , undefined , "non workday<2!" , true],
```
```
// 2023/01/03(火)
// 2023/01/04(水) 祝
// 2023/01/05(木)
```
- 1/4の1稼働日前も1/5の1稼働日前も1/3
- 1/3に対して web<!1 としたとき 1/4も当てはまるが、1/3の1稼働日後としてしまうと1/5でしか判定されない
- だから [1/4, 1/5] と2つ返しているのか...
```ts
if (days === 0) {
dates.push(d);
}
```
の部分は **その日で確定させてもいいのだけど その日が非稼働日の場合(ex: 1/4)はそこも対象に入るから** それを考慮している...と。
やはりこのロジックの使いまわしはできないので新しい概念を作った方がいい。お互いシンプル。
## ロジック
稼働日振替フラグ(前 or 後)をつくる。(以降 wff)
wff後バージョンで一旦考える。
- wffが存在するタスクにフォーカスしたとき
- そのタスクが **稼働日でなかったために** 遂行できなかった翌稼働日である可能性を考える必要がある
- 少なくとも **対象日の前日が稼働日であれば可能性はゼロ** と言える
- ここでearly return
- 対象日の前日が稼働日ではないとき
- 対象タスクが
- **wffを除き** 遂行すべき条件であるなら **対象日のタスクとしてshoudTryはtrueになる**
- そうでなければ無視
- ↑の結果にかかわらず **この試行は前日が稼働日になるまで繰り返される**
- 日付を遡ってiterate & 稼働日でなければ続行
- wffを除きshoudTryがtrue なら targetDateで引き受ける
- `mon>!`:
- `mon<!`
## 使い捨て
```ts
export function reverseOffsetWorkday(
dst: DateTime,
days: number,
holidays: DateTime[],
): DateTime {
let d = dst.clone();
if (days > 0) {
while (days > 0) {
d = d.minusDays(1);
if (isWorkday(d, holidays)) {
days -= 1;
}
}
} else {
while (days < 0) {
d = d.plusDays(1);
if (isWorkday(d, holidays)) {
days += 1;
}
}
}
return d;
}
```
```ts
parameterizedTest<
[
Parameters<typeof reverseOffsetWorkday>[0],
Parameters<typeof reverseOffsetWorkday>[1],
Parameters<typeof reverseOffsetWorkday>[2],
ReturnType<typeof reverseOffsetWorkday>,
]
>(
"reverseOffsetWorkday",
// deno-fmt-ignore
// prettier-ignore
// dst | days | holidays | expected
[
[d("2023-01-03") , 1 , holidays , d("2023-01-02")],
[d("2023-01-03") , 2 , holidays , d("2022-12-30")],
[d("2023-01-03") , 3 , holidays , d("2022-12-29")],
[d("2023-01-03") , -1 , holidays , d("2023-01-05")],
[d("2023-01-03") , -2 , holidays , d("2023-01-06")],
[d("2023-01-03") , -3 , holidays , d("2023-01-09")],
],
([dst, days, holidays, expected]) => {
assertEquals(reverseOffsetWorkday(dst, days, holidays), expected);
},
);
```