0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Outlook上で指定したユーザー、指定した期間での共通の空き時間を抽出するコード

Last updated at Posted at 2025-08-18
Option Explicit

' ==========================================
' 共通空き時間 抽出:フォーム自動生成 or 自動フォールバック版
' ==========================================

' ===== 営業時間(必要に応じて変更) =====
Private Const BUSINESS_START_HOUR As Long = 9
Private Const BUSINESS_START_MIN  As Long = 0
Private Const BUSINESS_END_HOUR   As Long = 18
Private Const BUSINESS_END_MIN    As Long = 0

' ===== Free/Busy ステータス =====
Private Enum OlFreeBusyStatus
    fbFree = 0
    fbTentative = 1
    fbBusy = 2
    fbOutOfOffice = 3
    fbWorkingElsewhere = 4
End Enum

' 参加者(フォーム/フォールバック両用)
Private mRecips As Outlook.Recipients

' 祝日キャッシュ
Private mHolidayCache As Object ' Dict: Year -> Dict(Date->True)

' ──────────────────────────────
' エントリポイント
' ──────────────────────────────
Public Sub Run_CommonFreeTimes()
    If TryShowAutoForm() Then
        Exit Sub
    Else
        ' フォーム不可環境 → フォールバックUIで実行
        FindCommonFreeTimes_Fallback
    End If
End Sub

' ──────────────────────────────
' 1) フォーム自動生成(VBOM可) or False
' ──────────────────────────────
Private Function TryShowAutoForm() As Boolean
    On Error Resume Next

    ' VBOMにアクセスできるか(信頼設定ON かつ アンチウイルス等がブロックしていないか)
    Dim vbProj As Object
    Set vbProj = Application.VBE.ActiveVBProject
    If Err.Number <> 0 Or vbProj Is Nothing Then
        Err.Clear
        TryShowAutoForm = False
        Exit Function
    End If

    ' 生成
    Dim frmName As String: frmName = "frmCommonFreeTimes"
    EnsureRemoveVBComponentIfExists frmName

    ' 追加できるか事前テスト
    Dim vbComp As Object
    Set vbComp = vbProj.VBComponents.Add(3) ' 3 = vbext_ct_MSForm
    If vbComp Is Nothing Then
        TryShowAutoForm = False
        Exit Function
    End If
    vbProj.VBComponents.Remove vbComp ' 一旦消し、改めて正規生成
    BuildUserForm frmName

    ' 表示
    VBA.UserForms.Add(frmName).Show
    TryShowAutoForm = True
End Function

' ── フォーム構築(安定版) ──
Private Sub BuildUserForm(ByVal frmName As String)
    Dim vbProj As Object, vbComp As Object, designer As Object, ctl As Object
    Set vbProj = Application.VBE.ActiveVBProject
    Set vbComp = vbProj.VBComponents.Add(3) ' vbext_ct_MSForm
    vbComp.Name = frmName
    Set designer = vbComp.Designer

    ' タイトル
    Set ctl = designer.Controls.Add("Forms.Label.1", "lblHeader", True)
    With ctl
        .Caption = "共通空き時間 抽出"
        .Left = 12: .Top = 8: .Width = 240
        .Font.Bold = True
    End With

    ' 開始・終了
    designer.Controls.Add "Forms.Label.1", "lblStart", True
    With designer.Controls("lblStart"): .Caption = "開始": .Left = 12: .Top = 36: .Width = 36: End With
    designer.Controls.Add "Forms.TextBox.1", "txtStart", True
    With designer.Controls("txtStart")
        .Left = 60: .Top = 32: .Width = 160
        .Value = Format(Date + TimeSerial(9, 0, 0), "yyyy/m/d HH:nn")
    End With

    designer.Controls.Add "Forms.Label.1", "lblEnd", True
    With designer.Controls("lblEnd"): .Caption = "終了": .Left = 232: .Top = 36: .Width = 36: End With
    designer.Controls.Add "Forms.TextBox.1", "txtEnd", True
    With designer.Controls("txtEnd")
        .Left = 280: .Top = 32: .Width = 160
        .Value = Format(DateAdd("d", 7, Date) + TimeSerial(18, 0, 0), "yyyy/m/d HH:nn")
    End With

    ' 粒度・会議分
    designer.Controls.Add "Forms.Label.1", "lblSlot", True
    With designer.Controls("lblSlot"): .Caption = "粒度(分)": .Left = 12: .Top = 64: .Width = 56: End With

    designer.Controls.Add "Forms.ComboBox.1", "cboSlot", True
    With designer.Controls("cboSlot")
        .Left = 72: .Top = 60: .Width = 60
        .AddItem "5": .AddItem "10": .AddItem "15": .AddItem "30": .AddItem "60"
        .Value = "30"
    End With

    designer.Controls.Add "Forms.Label.1", "lblDur", True
    With designer.Controls("lblDur"): .Caption = "会議(分)": .Left = 148: .Top = 64: .Width = 56: End With

    designer.Controls.Add "Forms.TextBox.1", "txtDuration", True
    With designer.Controls("txtDuration"): .Left = 204: .Top = 60: .Width = 48: .Value = "60": End With

    ' オプション
    designer.Controls.Add "Forms.CheckBox.1", "chkTentative", True
    With designer.Controls("chkTentative"): .Caption = "仮の予定(1)も空き": .Left = 264: .Top = 60: .Width = 120: .Value = False: End With

    designer.Controls.Add "Forms.CheckBox.1", "chkExcludeWeekends", True
    With designer.Controls("chkExcludeWeekends"): .Caption = "土日を除外": .Left = 12: .Top = 88: .Width = 90: .Value = True: End With

    designer.Controls.Add "Forms.CheckBox.1", "chkExcludeHolidays", True
    With designer.Controls("chkExcludeHolidays"): .Caption = "祝日を除外": .Left = 110: .Top = 88: .Width = 90: .Value = True: End With

    designer.Controls.Add "Forms.CheckBox.1", "chkExcludeLunch", True
    With designer.Controls("chkExcludeLunch"): .Caption = "昼休み(12-13)除外": .Left = 208: .Top = 88: .Width = 120: .Value = True: End With

    designer.Controls.Add "Forms.CheckBox.1", "chkIncludeMe", True
    With designer.Controls("chkIncludeMe"): .Caption = "自分を含める": .Left = 332: .Top = 88: .Width = 100: .Value = True: End With

    ' 参加者選択
    designer.Controls.Add "Forms.CommandButton.1", "btnPickRecipients", True
    With designer.Controls("btnPickRecipients"): .Caption = "参加者を選択": .Left = 12: .Top = 116: .Width = 100: End With

    designer.Controls.Add "Forms.Label.1", "lblRecipients", True
    With designer.Controls("lblRecipients"): .Caption = "(未選択)": .Left = 120: .Top = 120: .Width = 320: End With

    ' 実行系
    designer.Controls.Add "Forms.CommandButton.1", "btnRun", True
    With designer.Controls("btnRun"): .Caption = "抽出": .Left = 12: .Top = 148: .Width = 60: End With
    designer.Controls.Add "Forms.CommandButton.1", "btnCopy", True
    With designer.Controls("btnCopy"): .Caption = "コピー": .Left = 80: .Top = 148: .Width = 60: End With
    designer.Controls.Add "Forms.CommandButton.1", "btnMail", True
    With designer.Controls("btnMail"): .Caption = "メール作成": .Left = 148: .Top = 148: .Width = 80: End With
    designer.Controls.Add "Forms.CommandButton.1", "btnClose", True
    With designer.Controls("btnClose"): .Caption = "閉じる": .Left = 234: .Top = 148: .Width = 60: End With

    ' 結果
    designer.Controls.Add "Forms.ListBox.1", "lstResults", True
    With designer.Controls("lstResults"): .Left = 12: .Top = 180: .Width = 428: .Height = 220: End With

    ' フォームサイズ
    With designer: .Width = 460: .Height = 420: End With

    ' フォームのイベントコードを注入
    Dim cm As Object: Set cm = vbComp.CodeModule
    Dim code As String
    code = "Option Explicit" & vbCrLf & _
           "Private Sub btnPickRecipients_Click(): SetRecipientsViaDialog Me: End Sub" & vbCrLf & _
           "Private Sub btnRun_Click(): ExecuteExtraction Me: End Sub" & vbCrLf & _
           "Private Sub btnCopy_Click(): CopyResults Me: End Sub" & vbCrLf & _
           "Private Sub btnMail_Click(): CreateMailFromResults Me: End Sub" & vbCrLf & _
           "Private Sub btnClose_Click(): Unload Me: End Sub"
    cm.InsertLines 1, code
End Sub

Private Sub EnsureRemoveVBComponentIfExists(ByVal name As String)
    On Error Resume Next
    Dim vbComp As Object
    Set vbComp = Application.VBE.ActiveVBProject.VBComponents(name)
    If Not vbComp Is Nothing Then Application.VBE.ActiveVBProject.VBComponents.Remove vbComp
    Err.Clear
End Sub

' ──────────────────────────────
' 2) フォームから呼ばれる処理
' ──────────────────────────────
Public Sub SetRecipientsViaDialog(frm As Object)
    Dim ns As Outlook.NameSpace: Set ns = Application.Session
    Dim snd As Outlook.SelectNamesDialog: Set snd = ns.GetSelectNamesDialog
    With snd
        .Caption = "参加者を選択"
        .NumberOfRecipientSelectors = OlRecipientSelectors.olShowTo
        .ToLabel = "参加者"
        .ForceResolution = True
        .AllowMultipleSelection = True
        If Not .Display Then Exit Sub
    End With
    Set mRecips = snd.Recipients
    frm.Controls("lblRecipients").Caption = IIf(mRecips Is Nothing Or mRecips.Count = 0, "(未選択)", RecipientsToString(mRecips))
End Sub

Public Sub ExecuteExtraction(frm As Object)
    On Error GoTo EH

    If mRecips Is Nothing Or mRecips.Count = 0 Then
        MsgBox "参加者を選択してください。", vbExclamation: Exit Sub
    End If

    Dim startDt As Date, endDt As Date
    If Not TryParseDate(frm.Controls("txtStart").Value, startDt) Then MsgBox "開始日時が不正です。", vbExclamation: Exit Sub
    If Not TryParseDate(frm.Controls("txtEnd").Value, endDt) Then MsgBox "終了日時が不正です。", vbExclamation: Exit Sub
    If endDt <= startDt Then MsgBox "終了は開始より後にしてください。", vbExclamation: Exit Sub

    Dim slotMin As Long, needMin As Long
    If Not IsNumeric(frm.Controls("cboSlot").Value) Then MsgBox "粒度(分)は数値です。", vbExclamation: Exit Sub
    slotMin = CLng(frm.Controls("cboSlot").Value)
    If slotMin <= 0 Or 1440 Mod slotMin <> 0 Then MsgBox "粒度は1440を割り切れる整数です。", vbExclamation: Exit Sub

    If Not IsNumeric(frm.Controls("txtDuration").Value) Then MsgBox "会議分は数値です。", vbExclamation: Exit Sub
    needMin = CLng(frm.Controls("txtDuration").Value)
    If needMin <= 0 Then MsgBox "会議分は正の整数です。", vbExclamation: Exit Sub

    Dim treatTentative As Boolean: treatTentative = frm.Controls("chkTentative").Value
    Dim excludeWeekends As Boolean: excludeWeekends = frm.Controls("chkExcludeWeekends").Value
    Dim excludeHolidays As Boolean: excludeHolidays = frm.Controls("chkExcludeHolidays").Value
    Dim excludeLunch As Boolean: excludeLunch = frm.Controls("chkExcludeLunch").Value
    Dim includeMe As Boolean: includeMe = frm.Controls("chkIncludeMe").Value

    If includeMe Then EnsureCurrentUserIncluded Application.Session, mRecips

    Dim totalMin As Long, nSlots As Long, minSlots As Long
    totalMin = DateDiff("n", startDt, endDt)
    nSlots = totalMin \ slotMin
    If nSlots <= 0 Then MsgBox "期間が短すぎます。", vbExclamation: Exit Sub
    minSlots = (needMin + slotMin - 1) \ slotMin

    Dim allFree() As Boolean
    ReDim allFree(1 To nSlots)
    Dim j As Long: For j = 1 To nSlots: allFree(j) = True: Next

    Dim i As Long, iSlot As Long
    For i = 1 To mRecips.Count
        Dim rcp As Outlook.Recipient: Set rcp = mRecips(i)
        If Not rcp.Resolve Then MsgBox "解決できない参加者: " & rcp.Name, vbExclamation: Exit Sub

        Dim fbCache As Object: Set fbCache = CreateObject("Scripting.Dictionary")

        For iSlot = 1 To nSlots
            Dim slotStart As Date, slotEnd As Date
            slotStart = DateAdd("n", (iSlot - 1) * slotMin, startDt)
            slotEnd   = DateAdd("n", iSlot * slotMin, startDt)

            If Not IsSlotBusinessEligible(slotStart, slotEnd, excludeWeekends, excludeHolidays, excludeLunch) Then
                allFree(iSlot) = False
            Else
                Dim dayStart As Date: dayStart = DateSerial(Year(slotStart), Month(slotStart), Day(slotStart))

                Dim fbDay As String
                If fbCache.Exists(dayStart) Then
                    fbDay = fbCache(dayStart)
                Else
                    On Error Resume Next
                    fbDay = rcp.FreeBusy(dayStart, slotMin, True) ' 当日0:00起点
                    On Error GoTo 0
                    If Len(fbDay) = 0 Then allFree(iSlot) = False: GoTo NextSlot
                    fbCache.Add dayStart, fbDay
                End If

                Dim pos As Long: pos = (DateDiff("n", dayStart, slotStart) \ slotMin) + 1
                If pos < 1 Or pos > Len(fbDay) Then
                    allFree(iSlot) = False
                Else
                    Dim ch As String: ch = Mid$(fbDay, pos, 1)
                    If Not IsCharFree(ch, treatTentative) Then allFree(iSlot) = False
                End If
            End If
NextSlot:
        Next iSlot
    Next i

    ' 連続区間
    Dim ranges As Collection: Set ranges = New Collection
    Dim runStart As Long: runStart = 0
    For j = 1 To nSlots
        If allFree(j) Then
            If runStart = 0 Then runStart = j
        ElseIf runStart > 0 Then
            TryAddRange ranges, runStart, j - 1, minSlots: runStart = 0
        End If
    Next j
    If runStart > 0 Then TryAddRange ranges, runStart, nSlots, minSlots

    ' 表示
    Dim lst As Object: Set lst = frm.Controls("lstResults")
    lst.Clear
    If ranges.Count = 0 Then
        lst.AddItem "条件を満たす共通空き時間は見つかりませんでした。"
    Else
        Dim dayDict As Object: Set dayDict = CreateObject("Scripting.Dictionary")
        Dim dayOrder As Collection: Set dayOrder = New Collection
        Dim it As Variant

        For Each it In ranges
            Dim sIdx As Long, eIdx As Long
            sIdx = it(0): eIdx = it(1)
            Dim sTime As Date, eTime As Date
            sTime = DateAdd("n", (sIdx - 1) * slotMin, startDt)
            eTime = DateAdd("n", eIdx * slotMin, startDt)

            Dim dKey As Date: dKey = DateSerial(Year(sTime), Month(sTime), Day(sTime))
            Dim seg As String: seg = Format(sTime, "HH:nn") & " ~ " & Format(eTime, "HH:nn")

            If Not dayDict.Exists(dKey) Then
                dayDict.Add dKey, seg: dayOrder.Add dKey
            Else
                dayDict(dKey) = dayDict(dKey) & "、 " & seg
            End If
        Next it

        Dim k As Variant
        For Each k In dayOrder
            lst.AddItem Format(k, "yyyy/mm/dd") & " (" & WeekdayJpAbbrev(k) & ") " & dayDict(k)
        Next k
    End If
    Exit Sub
EH:
    MsgBox "抽出中エラー: " & Err.Description, vbExclamation
End Sub

Public Sub CopyResults(frm As Object)
    Dim lst As Object: Set lst = frm.Controls("lstResults")
    Dim s As String, i As Long
    For i = 0 To lst.ListCount - 1
        s = s & lst.List(i) & vbCrLf
    Next
    If Len(s) = 0 Then s = "(結果なし)"
    With CreateObject("Forms.DataObject")
        .SetText s: .PutInClipboard
    End With
    MsgBox "結果をコピーしました。", vbInformation
End Sub

Public Sub CreateMailFromResults(frm As Object)
    Dim mail As Outlook.MailItem: Set mail = Application.CreateItem(olMailItem)
    Dim hdr As String
    hdr = "参加者: " & IIf(mRecips Is Nothing, "(未選択)", RecipientsToString(mRecips)) & vbCrLf
    hdr = hdr & "期間: " & frm.Controls("txtStart").Value & " 〜 " & frm.Controls("txtEnd").Value & vbCrLf
    hdr = hdr & "粒度(分): " & frm.Controls("cboSlot").Value & ", 会議(分): " & frm.Controls("txtDuration").Value & vbCrLf
    hdr = hdr & "仮の予定(1): " & IIf(frm.Controls("chkTentative").Value, "空き扱い", "空き扱いしない") & vbCrLf
    hdr = hdr & "営業時間: " & Format(TimeSerial(BUSINESS_START_HOUR, BUSINESS_START_MIN, 0), "HH:nn") & " 〜 " & Format(TimeSerial(BUSINESS_END_HOUR, BUSINESS_END_MIN, 0), "HH:nn") & vbCrLf
    hdr = hdr & "土日除外: " & IIf(frm.Controls("chkExcludeWeekends").Value, "する", "しない") & vbCrLf
    hdr = hdr & "昼休み除外(12:00–13:00): " & IIf(frm.Controls("chkExcludeLunch").Value, "する", "しない") & vbCrLf
    hdr = hdr & "祝日除外: " & IIf(frm.Controls("chkExcludeHolidays").Value, "する", "しない") & vbCrLf
    hdr = hdr & String(40, "-") & vbCrLf

    Dim lst As Object: Set lst = frm.Controls("lstResults")
    Dim body As String, i As Long
    For i = 0 To lst.ListCount - 1: body = body & lst.List(i) & vbCrLf: Next
    If Len(body) = 0 Then body = "(結果なし)"

    mail.Subject = "共通空き時間の抽出結果"
    mail.Body = "【共通空き時間の抽出結果】" & vbCrLf & hdr & body
    mail.Display
End Sub

' ──────────────────────────────
' 3) フォールバック(フォーム不可時)
' ──────────────────────────────
Private Sub FindCommonFreeTimes_Fallback()
    Dim ns As Outlook.NameSpace: Set ns = Application.Session
    Dim snd As Outlook.SelectNamesDialog: Set snd = ns.GetSelectNamesDialog
    With snd
        .Caption = "参加者を選択"
        .NumberOfRecipientSelectors = OlRecipientSelectors.olShowTo
        .ToLabel = "参加者"
        .ForceResolution = True
        .AllowMultipleSelection = True
        If Not .Display Then Exit Sub
    End With
    Set mRecips = snd.Recipients
    If mRecips Is Nothing Or mRecips.Count = 0 Then MsgBox "参加者未選択", vbExclamation: Exit Sub

    Dim startDt As Date, endDt As Date, slotMin As Long, needMin As Long
    If Not TryParseDate(InputBox("開始日時(例: 2025/8/18 09:00)", "開始", Format(Date + TimeSerial(9, 0, 0), "yyyy/m/d HH:nn")), startDt) Then Exit Sub
    If Not TryParseDate(InputBox("終了日時(例: 2025/8/25 18:00)", "終了", Format(DateAdd("d", 7, Date) + TimeSerial(18, 0, 0), "yyyy/m/d HH:nn")), endDt) Then Exit Sub
    If endDt <= startDt Then MsgBox "終了は開始より後に。", vbExclamation: Exit Sub

    Dim s As String: s = InputBox("粒度(分)5/10/15/30/60", "粒度", "30"): If Len(s) = 0 Or Not IsNumeric(s) Then Exit Sub
    slotMin = CLng(s): If slotMin <= 0 Or 1440 Mod slotMin <> 0 Then MsgBox "粒度不正", vbExclamation: Exit Sub

    s = InputBox("会議時間(分)", "会議分", "60"): If Len(s) = 0 Or Not IsNumeric(s) Then Exit Sub
    needMin = CLng(s): If needMin <= 0 Then MsgBox "会議分不正", vbExclamation: Exit Sub

    Dim treatTentative As Boolean: treatTentative = (MsgBox("仮の予定(1)を空き扱いにしますか?", vbYesNo + vbQuestion) = vbYes)
    Dim excludeWeekends As Boolean: excludeWeekends = (MsgBox("土日を除外しますか?", vbYesNo + vbQuestion) = vbYes)
    Dim excludeHolidays As Boolean: excludeHolidays = (MsgBox("祝日を除外しますか?", vbYesNo + vbQuestion) = vbYes)
    Dim excludeLunch As Boolean: excludeLunch = (MsgBox("昼休み(12:00–13:00)を除外しますか?", vbYesNo + vbQuestion) = vbYes)
    Dim includeMe As Boolean: includeMe = (MsgBox("自分も判定対象に含めますか?", vbYesNo + vbQuestion) = vbYes)
    If includeMe Then EnsureCurrentUserIncluded ns, mRecips

    ' 抽出本体(フォーム版と同じ)
    Dim totalMin As Long, nSlots As Long, minSlots As Long
    totalMin = DateDiff("n", startDt, endDt)
    nSlots = totalMin \ slotMin
    If nSlots <= 0 Then MsgBox "期間が短すぎます。", vbExclamation: Exit Sub
    minSlots = (needMin + slotMin - 1) \ slotMin

    Dim allFree() As Boolean
    ReDim allFree(1 To nSlots)
    Dim j As Long: For j = 1 To nSlots: allFree(j) = True: Next

    Dim i As Long, iSlot As Long
    For i = 1 To mRecips.Count
        Dim rcp As Outlook.Recipient: Set rcp = mRecips(i)
        If Not rcp.Resolve Then MsgBox "解決できない参加者: " & rcp.Name, vbExclamation: Exit Sub

        Dim fbCache As Object: Set fbCache = CreateObject("Scripting.Dictionary")

        For iSlot = 1 To nSlots
            Dim slotStart As Date, slotEnd As Date
            slotStart = DateAdd("n", (iSlot - 1) * slotMin, startDt)
            slotEnd   = DateAdd("n", iSlot * slotMin, startDt)

            If Not IsSlotBusinessEligible(slotStart, slotEnd, excludeWeekends, excludeHolidays, excludeLunch) Then
                allFree(iSlot) = False
            Else
                Dim dayStart As Date: dayStart = DateSerial(Year(slotStart), Month(slotStart), Day(slotStart))
                Dim fbDay As String
                If fbCache.Exists(dayStart) Then
                    fbDay = fbCache(dayStart)
                Else
                    On Error Resume Next
                    fbDay = rcp.FreeBusy(dayStart, slotMin, True)
                    On Error GoTo 0
                    If Len(fbDay) = 0 Then allFree(iSlot) = False: GoTo NextSlot2
                    fbCache.Add dayStart, fbDay
                End If

                Dim pos As Long: pos = (DateDiff("n", dayStart, slotStart) \ slotMin) + 1
                If pos < 1 Or pos > Len(fbDay) Then
                    allFree(iSlot) = False
                Else
                    Dim ch As String: ch = Mid$(fbDay, pos, 1)
                    If Not IsCharFree(ch, treatTentative) Then allFree(iSlot) = False
                End If
            End If
NextSlot2:
        Next iSlot
    Next i

    Dim ranges As Collection: Set ranges = New Collection
    Dim runStart As Long: runStart = 0
    For j = 1 To nSlots
        If allFree(j) Then
            If runStart = 0 Then runStart = j
        ElseIf runStart > 0 Then
            TryAddRange ranges, runStart, j - 1, minSlots: runStart = 0
        End If
    Next j
    If runStart > 0 Then TryAddRange ranges, runStart, nSlots, minSlots

    Dim mail As Outlook.MailItem: Set mail = Application.CreateItem(olMailItem)
    Dim body As String
    body = "【共通空き時間の抽出結果】" & vbCrLf & _
           "参加者: " & RecipientsToString(mRecips) & vbCrLf & _
           "期間: " & Format(startDt, "yyyy/mm/dd HH:nn") & " 〜 " & Format(endDt, "yyyy/mm/dd HH:nn") & vbCrLf & _
           "粒度: " & slotMin & " 分/スロット, 会議: " & needMin & " 分" & vbCrLf & _
           "仮の予定(1): " & IIf(treatTentative, "空き扱い", "空き扱いしない") & vbCrLf & _
           "営業時間: " & Format(TimeSerial(BUSINESS_START_HOUR, BUSINESS_START_MIN, 0), "HH:nn") & " 〜 " & Format(TimeSerial(BUSINESS_END_HOUR, BUSINESS_END_MIN, 0), "HH:nn") & vbCrLf & _
           "土日除外: " & IIf(excludeWeekends, "する", "しない") & vbCrLf & _
           "祝日除外: " & IIf(excludeHolidays, "する", "しない") & vbCrLf & _
           "昼休み除外: " & IIf(excludeLunch, "する", "しない") & vbCrLf & _
           String(40, "-") & vbCrLf

    If ranges.Count = 0 Then
        body = body & "条件を満たす共通空き時間は見つかりませんでした。" & vbCrLf
    Else
        Dim dayDict As Object: Set dayDict = CreateObject("Scripting.Dictionary")
        Dim dayOrder As Collection: Set dayOrder = New Collection
        Dim it As Variant
        For Each it In ranges
            Dim sIdx As Long, eIdx As Long
            sIdx = it(0): eIdx = it(1)
            Dim sTime As Date, eTime As Date
            sTime = DateAdd("n", (sIdx - 1) * slotMin, startDt)
            eTime = DateAdd("n", eIdx * slotMin, startDt)
            Dim key As Date: key = DateSerial(Year(sTime), Month(sTime), Day(sTime))
            Dim seg As String: seg = Format(sTime, "HH:nn") & " ~ " & Format(eTime, "HH:nn")
            If Not dayDict.Exists(key) Then
                dayDict.Add key, seg: dayOrder.Add key
            Else
                dayDict(key) = dayDict(key) & "、 " & seg
            End If
        Next it
        Dim k As Variant
        For Each k In dayOrder
            body = body & Format(k, "yyyy/mm/dd") & " (" & WeekdayJpAbbrev(k) & ") " & dayDict(k) & vbCrLf
        Next k
    End If

    mail.Subject = "共通空き時間の抽出結果"
    mail.Body = body
    mail.Display
End Sub

' ──────────────────────────────
' 共通ユーティリティ
' ──────────────────────────────
Private Function TryParseDate(ByVal s As String, ByRef dt As Date) As Boolean
    On Error Resume Next
    dt = CDate(s)
    TryParseDate = (Err.Number = 0)
    Err.Clear
End Function

Private Function IsCharFree(ByVal ch As String, ByVal treatTentative As Boolean) As Boolean
    If ch = "0" Then
        IsCharFree = True
    ElseIf treatTentative And ch = "1" Then
        IsCharFree = True
    Else
        IsCharFree = False
    End If
End Function

' スロットが有効(営業時間/土日/祝日/昼休み)
Private Function IsSlotBusinessEligible( _
    ByVal slotStart As Date, ByVal slotEnd As Date, _
    ByVal excludeWeekends As Boolean, _
    ByVal excludeHolidays As Boolean, _
    ByVal excludeLunch As Boolean) As Boolean

    Dim d As Date: d = DateSerial(Year(slotStart), Month(slotStart), Day(slotStart))

    If excludeWeekends Then
        Dim wd As VbDayOfWeek: wd = Weekday(d, vbSunday)
        If wd = vbSaturday Or wd = vbSunday Then IsSlotBusinessEligible = False: Exit Function
    End If

    If excludeHolidays Then
        If IsJapaneseHoliday(d) Then IsSlotBusinessEligible = False: Exit Function
    End If

    Dim businessStart As Date, businessEnd As Date
    businessStart = d + TimeSerial(BUSINESS_START_HOUR, BUSINESS_START_MIN, 0)
    businessEnd   = d + TimeSerial(BUSINESS_END_HOUR,   BUSINESS_END_MIN, 0)
    If Not (slotStart >= businessStart And slotEnd <= businessEnd) Then
        IsSlotBusinessEligible = False: Exit Function
    End If

    If excludeLunch Then
        Dim lunchStart As Date, lunchEnd As Date
        lunchStart = d + TimeSerial(12, 0, 0)
        lunchEnd   = d + TimeSerial(13, 0, 0)
        If (slotStart < lunchEnd) And (slotEnd > lunchStart) Then
            IsSlotBusinessEligible = False: Exit Function
        End If
    End If

    IsSlotBusinessEligible = True
End Function

Public Function WeekdayJpAbbrev(ByVal d As Date) As String
    Select Case Weekday(d, vbSunday)
        Case vbSunday:    WeekdayJpAbbrev = "日"
        Case vbMonday:    WeekdayJpAbbrev = "月"
        Case vbTuesday:   WeekdayJpAbbrev = "火"
        Case vbWednesday: WeekdayJpAbbrev = "水"
        Case vbThursday:  WeekdayJpAbbrev = "木"
        Case vbFriday:    WeekdayJpAbbrev = "金"
        Case vbSaturday:  WeekdayJpAbbrev = "土"
    End Select
End Function

Private Function RecipientsToString(ByVal recips As Outlook.Recipients) As String
    Dim i As Long, t As String
    For i = 1 To recips.Count
        If Len(t) > 0 Then t = t & "; "
        t = t & recips(i).Name
    Next i
    RecipientsToString = t
End Function

Private Sub EnsureCurrentUserIncluded(ByVal ns As Outlook.NameSpace, ByRef recips As Outlook.Recipients)
    On Error Resume Next
    Dim curName As String: curName = ns.CurrentUser.Name
    On Error GoTo 0
    If Len(curName) = 0 Then Exit Sub

    Dim i As Long
    For i = 1 To recips.Count
        If StrComp(recips(i).Name, curName, vbTextCompare) = 0 Then Exit Sub
    Next i

    Dim r As Outlook.Recipient
    Set r = recips.Add(curName)
    If Not r Is Nothing Then If r.Resolve = False Then r.Delete
End Sub

Private Sub TryAddRange(ByRef ranges As Collection, ByVal sIdx As Long, ByVal eIdx As Long, ByVal minSlots As Long)
    If eIdx >= sIdx Then
        If (eIdx - sIdx + 1) >= minSlots Then
            Dim pair(1) As Long: pair(0) = sIdx: pair(1) = eIdx
            ranges.Add pair
        End If
    End If
End Sub

' ──────────────────────────────
' 祝日(固定/ハッピーマンデー/春秋分/国民の休日/振替/五輪特例)
' ──────────────────────────────
Private Function IsJapaneseHoliday(ByVal d As Date) As Boolean
    Dim y As Long: y = Year(d)
    Dim dict As Object: Set dict = GetHolidayDictForYear(y)
    IsJapaneseHoliday = dict.Exists(DateSerial(Year(d), Month(d), Day(d)))
End Function

Private Function GetHolidayDictForYear(ByVal y As Long) As Object
    If mHolidayCache Is Nothing Then Set mHolidayCache = CreateObject("Scripting.Dictionary")
    If Not mHolidayCache.Exists(y) Then mHolidayCache.Add y, BuildJapaneseHolidays(y)
    Set GetHolidayDictForYear = mHolidayCache(y)
End Function

Private Function BuildJapaneseHolidays(ByVal y As Long) As Object
    Dim H As Object: Set H = CreateObject("Scripting.Dictionary")

    ' 固定祝日
    AddHoliday H, DateSerial(y, 1, 1)
    AddHoliday H, DateSerial(y, 2, 11)
    If y >= 2020 Then AddHoliday H, DateSerial(y, 2, 23) ' 天皇誕生日(令和)
    AddHoliday H, DateSerial(y, 4, 29)
    AddHoliday H, DateSerial(y, 5, 3)
    AddHoliday H, DateSerial(y, 5, 4)
    AddHoliday H, DateSerial(y, 5, 5)
    AddHoliday H, DateSerial(y, 8, 11)
    AddHoliday H, DateSerial(y, 11, 3)
    AddHoliday H, DateSerial(y, 11, 23)

    ' ハッピーマンデー
    AddHoliday H, NthWeekdayOfMonth(y, 1, vbMonday, 2)  ' 成人の日
    AddHoliday H, NthWeekdayOfMonth(y, 7, vbMonday, 3)  ' 海の日
    AddHoliday H, NthWeekdayOfMonth(y, 9, vbMonday, 3)  ' 敬老の日
    AddHoliday H, NthWeekdayOfMonth(y, 10, vbMonday, 2) ' スポーツの日

    ' 春分・秋分(近似)
    Dim shun As Integer, shu As Integer
    shun = Int(20.8431 + 0.242194 * (y - 1980)) - Int((y - 1980) \ 4)
    AddHoliday H, DateSerial(y, 3, shun)
    shu = Int(23.2488 + 0.242194 * (y - 1980)) - Int((y - 1980) \ 4)
    AddHoliday H, DateSerial(y, 9, shu)

    ' 五輪特例 2020/2021
    If y = 2020 Then
        RemoveHoliday H, NthWeekdayOfMonth(y, 7, vbMonday, 3)
        RemoveHoliday H, NthWeekdayOfMonth(y, 10, vbMonday, 2)
        RemoveHoliday H, DateSerial(y, 8, 11)
        AddHoliday H, DateSerial(y, 7, 23)
        AddHoliday H, DateSerial(y, 7, 24)
        AddHoliday H, DateSerial(y, 8, 10)
    ElseIf y = 2021 Then
        RemoveHoliday H, NthWeekdayOfMonth(y, 7, vbMonday, 3)
        RemoveHoliday H, NthWeekdayOfMonth(y, 10, vbMonday, 2)
        RemoveHoliday H, DateSerial(y, 8, 11)
        AddHoliday H, DateSerial(y, 7, 22)
        AddHoliday H, DateSerial(y, 7, 23)
        AddHoliday H, DateSerial(y, 8, 8)
        ' 8/9 は振替処理で付与
    End If

    ' 国民の休日(前後が祝日の平日)
    Dim dt As Date
    For dt = DateSerial(y, 1, 2) To DateSerial(y, 12, 30)
        If Not H.Exists(dt) Then
            If IsWeekday(dt) And H.Exists(DateAdd("d", -1, dt)) And H.Exists(DateAdd("d", 1, dt)) Then
                AddHoliday H, dt
            End If
        End If
    Next dt

    ' 振替休日(祝日が日曜)
    Dim keys As Variant: keys = H.Keys
    Dim i As Long
    For i = LBound(keys) To UBound(keys)
        dt = keys(i)
        If Weekday(dt, vbSunday) = vbSunday Then
            Dim subDay As Date: subDay = DateAdd("d", 1, dt)
            Do While Not IsWeekday(subDay) Or H.Exists(subDay)
                subDay = DateAdd("d", 1, subDay)
                If Year(subDay) <> y Then Exit Do
            Loop
            If Year(subDay) = y Then AddHoliday H, subDay
        End If
    Next i

    Set BuildJapaneseHolidays = H
End Function

Private Function IsWeekday(ByVal d As Date) As Boolean
    Dim wd As VbDayOfWeek: wd = Weekday(d, vbSunday)
    IsWeekday = (wd >= vbMonday And wd <= vbFriday)
End Function

Private Function NthWeekdayOfMonth(ByVal y As Long, ByVal m As Long, ByVal vbWDay As VbDayOfWeek, ByVal n As Long) As Date
    Dim first As Date: first = DateSerial(y, m, 1)
    Dim offset As Long: offset = (vbWDay - Weekday(first, vbSunday) + 7) Mod 7
    NthWeekdayOfMonth = DateSerial(y, m, 1 + offset + 7 * (n - 1))
End Function

Private Sub AddHoliday(ByRef dict As Object, ByVal d As Date)
    d = DateSerial(Year(d), Month(d), Day(d))
    If Not dict.Exists(d) Then dict.Add d, True
End Sub

Private Sub RemoveHoliday(ByRef dict As Object, ByVal d As Date)
    d = DateSerial(Year(d), Month(d), Day(d))
    If dict.Exists(d) Then dict.Remove d
End Sub
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?