Microsoft Access 掲示板

営業日ベースの日付の羅列から連続する開始日と終了日を計算

15 コメント
views
4 フォロー

下記のような素データから結果を取得したいです。

具体例には、土日祝日が抜けた、営業日ベースの休暇取得日の羅列データから連続した期間(開始日、終了日)を計算したいです。

結果

氏名  開始日 終了日
あああ 2022/8/4 2022/8/10
あああ 2022/8/29 2022/8/30
あああ 2022/9/6 2022/9/12


素データ

氏名 休暇取得日 
あああ 2022/8/4
あああ 2022/8/5
あああ 2022/8/8
あああ 2022/8/9
あああ 2022/8/10
あああ 2022/8/29
あああ 2022/8/30
あああ 2022/9/6
あああ 2022/9/7
あああ 2022/9/8
あああ 2022/9/9
あああ 2022/9/12

もっちー
作成: 2022/09/16 (金) 01:06:09
通報 ...
1

現状、どこまでできていますか。まったく手付かずですか。

祝日はどのように判断するつもりでしょうか。
休日テーブルに格納するのでしょうか。

まずは、どこまでできているか、やったことを提示してもらえますか。
手付かずでも、どのようにするのか考えていることを提示してもらえますか。

2
hiroton 2022/09/16 (金) 17:13:13 b1aad@f966d

例えば

Sub test()
Dim rs As Recordset
Dim 氏名 As String
Dim 終了日 As Date

Set rs = CurrentDb.OpenRecordset("SELECT 氏名, 休暇取得日 FROM 素データ ORDER BY 氏名, 休暇取得日;")
Debug.Print rs!氏名, rs!休暇取得日, ;
氏名 = rs!氏名
終了日 = rs!休暇取得日
rs.MoveNext

Do Until rs.EOF
    If rs!氏名 <> 氏名 Then
        Debug.Print 終了日
        Debug.Print rs!氏名, rs!休暇取得日, ;
        氏名 = rs!氏名
        終了日 = rs!休暇取得日
        rs.MoveNext
    ElseIf rs!休暇取得日 = 終了日 + 1 Then
        終了日 = 終了日 + 1
        rs.MoveNext
    Else
        If Is休日(終了日 + 1) Then
            終了日 = 終了日 + 1
        Else
            Debug.Print 終了日
            Debug.Print rs!氏名, rs!休暇取得日, ;
            終了日 = rs!休暇取得日
            rs.MoveNext
        End If
    End If
Loop
Debug.Print 終了日
rs.Close
Set rs = Nothing

End Sub

Function Is休日(mydate As Date) As Boolean
    If Weekday(mydate) = 1 Or Weekday(mydate) = 7 Then
        Is休日 = True
    Else
        Is休日 = Is祝日(mydate)
    End If
End Function

Function Is祝日(mydate As Date) As Boolean
    '(略)
End Function

その日が祝日かどうかを判断する方法はいろいろあるのでひとまず省略します


「存在しないデータ」を評価するのは大変です。処理中に適切に作り出す必要があります

上記例ではVBAで終了日として作り出してみました。処理の状況によってレコードを進めたり、足踏みしていたりと注意が必要です

「休日テーブル」のような実データを用意する方向であれば、単純にテーブルをマージすれば日付が連続するデータになるのでクエリでもなんとかなるのかな?と思います。(そのために土・日の日付まで実データで保存するのはどうかと思いますが)

3
もっちー 2022/09/16 (金) 23:09:10 ce1e4@39a0e

どうもありがとうございます。
hirotonさまにご教授いただいたコードで期待通りの結果が得られたことを確認させていただきました。

一度にあの結果セットまでできなくても良いと考えていまして、
土日祝日の存在しないデータを埋められれば、キーブレイク処理等でできないことはないかなという感じでした。

素データの対象期間内の土日はWeekday関数を使って生成し、祝日はネットでデータを取得して作成していました。
これを、対象期間のうち開始と終了で挟まれた期間のみを、土日祝日の存在しないデータを埋められれば、
と苦戦していたところでした。

まずは、いただきましたロジックをじっくり勉強させていただきます。

5
もっちー 2022/09/17 (土) 00:11:38 ce1e4@39a0e

私の理解を超えていまして、直しどころが全然分かりませんでした。

例えば、
2022/9/16(金)休暇取得、2022/9/19(月)休暇取得があれば、9/17、9/18は休暇期間中と考え、
2022/9/16(金)休暇取得、2022/9/19(月)休暇取得がなければ、9/17、9/18は休暇期間中と考えない、
としたいと思っています。

6
もっちー 2022/09/17 (土) 00:25:20 ce1e4@39a0e

あまり現実的ではないかもしれませんが、私の理解できる範囲では、
1.氏名が空欄のところは、開始と終了に挟まれた期間の土日祝日のデータを補完し、
2.連番を振り、
3.連番最小値が開始日、連番最大値が終了日として認識
ここまで段階的にできれば何とかなりそうなのですが、土日、祝日の実テーブルを用意するにしても、
データの補完方法が分かりません。

アドバイスいただけましたら幸いです。

氏名 休暇取得日 連番  連番最小値 連番最大値
あああ 2022/8/4   1      True
あああ 2022/8/5   2
       2022/8/6   3
       2022/8/7   4
あああ 2022/8/8   5
あああ 2022/8/9   6            
あああ 2022/8/10  7                 True
あああ 2022/8/29  1     True
あああ 2022/8/30  2                 True
あああ 2022/9/6   1     True
あああ 2022/9/7   2
あああ 2022/9/8   3
あああ 2022/9/9   4
       2022/9/10  5
       2022/9/11  6
あああ 2022/9/12  7                 True

7
名前なし 2022/09/17 (土) 01:13:41 c564b@0e907

 結果テーブルを用意して、全部データ移行すれば。素データテーブルにこだわる意味はありますか?
 迷走している様子を見ていると、そもそも何がやりたいのかをきちんと説明できるのか疑問です。

8
hiroton 2022/09/17 (土) 07:39:39 f0ac9@2ee8f

肝となるのは

    ElseIf rs!休暇取得日 = 終了日 + 1 Then
        終了日 = 終了日 + 1
        rs.MoveNext
    Else
        If Is休日(終了日 + 1) Then
            終了日 = 終了日 + 1
        Else
            Debug.Print 終了日
            Debug.Print rs!氏名, rs!休暇取得日, ;
            終了日 = rs!休暇取得日
            rs.MoveNext
        End If
    End If

この部分ですね

変数のつくり方(数・名前)は非常に大事で、コードが複雑化する場合は、変数が適切か見直すと良いです。今回の終了日もコード上では実際の終了日を指すものではなく、一時的なデータとなっています(処理の結果、正式な終了日になる仮データ)
コードの処理に即して名前を付けるのであれば「終了日となりうる日付」で、もっと具体的に言えば「休日を考慮した連続した日付の最終日」です

これを使ってコードを組みなおすと

'//前のレコードと比較して「次の日か?」
    ElseIf rs!休暇取得日 = 休日を考慮した連続した日付の最終日 + 1 Then
    '//「次の日」なので連続した日付と終了日を更新する
        休日を考慮した連続した日付の最終日 = 休日を考慮した連続した日付の最終日 + 1
        終了日となりうる日付 = 休日を考慮した連続した日付の最終日
        rs.MoveNext
    Else
    '//日付が飛んだので休日を挟んでいないかチェック
        If Is休日(休日を考慮した連続した日付の最終日 + 1) Then
        '//休日だったので連続した日付と終了日を更新して、再チェック
        '//(レコードは進めずにループの先頭からやりなおし、更新した日付と現在のレコードの休暇取得日で再チェックする)
            休日を考慮した連続した日付の最終日 = 休日を考慮した連続した日付の最終日 + 1
            終了日となりうる日付 = 休日を考慮した連続した日付の最終日
        Else
        '//休日でなかった=現在のレコードは新しい休暇の区間なので、前の区間の終了日を決定する
            終了日 = 終了日となりうる日付

        '//前の区間の終了処理
            Debug.Print 終了日

        '//新しい区間の開始処理
            Debug.Print rs!氏名, rs!休暇取得日, ;
            終了日 = rs!休暇取得日
            rs.MoveNext
        End If
    End If

処理を丁寧に書けばこんな感じです

9
hiroton 2022/09/17 (土) 08:05:55 修正 f0ac9@2ee8f

さて、本題です

2022/9/16(金)休暇取得、2022/9/19(月)休暇取得があれば、9/17、9/18は休暇期間中と考え、
2022/9/16(金)休暇取得、2022/9/19(月)休暇取得がなければ、9/17、9/18は休暇期間中と考えない、

「休日を挟んで連続していたら連続とみなす。そうでなかったら前のレコードの日付を終了日とする」ですね。上記丁寧版では無意味だった「終了日となりうる日付」と「休日を考慮した連続した日付の最終日」をきっちり分けて考えてあげればいいですね

Do Until rs.EOF
    If rs!氏名 <> 氏名 Then
        Debug.Print 終了日
        Debug.Print rs!氏名, rs!休暇取得日, ;
        氏名 = rs!氏名
        終了日 = rs!休暇取得日
'//--処理が一本化できたので削除
'//      rs.MoveNext
'//--ここまで
'//同じチェックをあとでやるので削除・一本化
'//  ElseIf rs!休暇取得日 = 終了日 + 1 Then
'//      終了日 = 終了日 + 1
'//      rs.MoveNext
'//--ここまで
    Else
    '//休日分、判定する日付をずらす
        Dim 休暇期間の翌日 As Date
        休暇期間の翌日 = 終了日 + 1
        Do While(Is休日(休暇期間の翌日))
            休暇期間の翌日 = 休暇期間の翌日 + 1
        Loop

        If rs!休暇取得日 = 休暇期間の翌日 Then
            終了日 = 休暇期間の翌日
        Else
            Debug.Print 終了日
            Debug.Print rs!氏名, rs!休暇取得日, ;
            終了日 = rs!休暇取得日
        End If
    End If
'//レコード移動処理も一本化
    rs.MoveNext
Loop
10
もっちー 2022/09/17 (土) 09:11:29 ce1e4@39a0e

hirotonさま、どうもありがとうございます。
結果セットとしては完璧でして、感動しています。

最後にコメントさせていただいた方法ですが、私が投稿前に考えたイメージになりますが、
段階的にでも、基幹システムから取得した素データに対して、休日データが補完できて、にフラグを立てられれば、
あとは表示の問題だけと考えておりました。

一気に結果を表示させるよりは、素データにクエリでフラグを立てて、表示変換をするほうが、
私には現実的と考えています(そもそも一気にそこまでできるのだと感動すらしているくらいでして)。
戻ってしまいますが、例えば、クエリとしてフラグなどの作業データを追加したりする方法についても、
少しアドナイスいただけましたら幸いです。

11
もっちー 2022/09/17 (土) 10:47:58 ce1e4@39a0e

詳細な解説ありがとうございました。理解するのに1時間以上かかりました。

フォームで表示対象者を変更させたり、休職者全件レポートなども動的に生成する必要がありますので、
テーブルに書き出して、これらの機能もいちおう実装できました。

12
hiroton 2022/09/20 (火) 16:14:13 20947@f966d

レコードの前後を比較するのはデータベースにとって苦手な処理です。複雑な処理を組むのも、その処理速度もデータベースにやらせるメリットはありません

このようなデータは、「計算で求められるデータ」であっても実データとして保存してしまうほうがシンプルに済みます

ただし、実データとして保存する場合は、それが常に最新のデータであると保証する必要があります。データの更新タイミングと、その内容を理論的に説明できるかということですね

カレンダーテーブルを作る

翌営業日の登録があるデータは連続休暇の最終日ではないので、翌営業日は何日か?という基礎データを作成します
内容は、全日付のカレンダーで、休日フラグ(土日祝日)を設けます。これで、翌営業日は「休日を除いて、対象の日付より大きく、最小の日付」として求められます。

翌営業日:DMin("日付", "カレンダー", "休日フラグ = False And 日付>#" & 休暇取得日 & "#")

Q素データ改

SELECT 氏名, 休暇取得日, DMin("日付","カレンダー","休日フラグ = False And 日付>#" & [休暇取得日] & "#")+0 AS 翌営業日
FROM 素データ;

翌営業日が求められたら、翌営業日が存在するレコードは終了日ではないので、それを取り除くことにより終了日のリストを作成できます

Q終了日リスト

SELECT Q素データ改.氏名, Q素データ改.休暇取得日
FROM Q素データ改 LEFT JOIN 素データ ON Q素データ改.翌営業日 = 素データ.休暇取得日
WHERE 素データ.休暇取得日 Is Null;

終了日のリストができたら、「自身の日付以上で最小の日付」が休暇期間の終了日になるので、素データのすべてのレコードに対して終了日を設定し、グループ化して休暇取得日の最小を取れば求めるデータになります

SELECT 氏名, Min(休暇取得日) AS 開始日, DMin("休暇取得日","クエリ15","氏名='" & 氏名 & "' And 休暇取得日>=#" & 休暇取得日 & "#") AS 終了日
FROM 素データ
GROUP BY 氏名, DMin("休暇取得日","クエリ15","氏名='" & 氏名 & "' And 休暇取得日>=#" & 休暇取得日 & "#");

急に祝日が変わった(東京オリンピックのときとか)などに注意すれば変更することのないデータが元になるのでシンプルでしょう
また、「翌営業日」を求める計算は何度計算しても結果が変わらないのでカレンダーテーブルにフィールドを設けて保存してしまってもいいと思います

13
hiroton 2022/09/20 (火) 16:38:24 20947@f966d

もう一つ、「休暇取得日」登録のタイミングをデータの更新タイミングとする方法を

素データに開始日を登録する

いままで「終了日」を求める方向でやっていましたが、「開始日」を求めるようにします。最終的なデータの出し方は「開始日でグループ化し、休暇取得日は最大を取る(=終了日になる)」です

開始日を記録する理由は、問題の性質上「休暇取得日」が登録されると前のレコードにわたって終了日が変わる可能性があり、終了日を管理するのは手間が多いからです


「休暇取得日」の登録がしっかり管理されていれば(そのタイミングでしっかり更新処理ができれば)独立したデータ(カレンダーテーブル)の管理もいらないシンプルなシステムが作れます

14
もっちー 2022/09/20 (火) 21:51:15 ce1e4@39a0e

hirotonさま

どうもありがとうございます。なるほどです、大変勉強になります。
カレンダーの実データを作ってしまうのですね。

開始日基準ですか、おっしゃる通りですね。
終了日はデータの追加更新の都度、変わる可能性があります。

ひとまず、いただい情報をもとに、実カレンダーで同様の結果が得られるようにトライしてみます。

15
もっちー 2022/09/20 (火) 22:10:41 ce1e4@39a0e

ありがとうございます。クエリを順番に作成していったらできました。
感動です。じっくり勉強させていただきます。