Excel (VBA)

Excel VBAに関するフォーラムです。
  • 解決済みのトピックにはコメントできません。
このトピックは解決済みです。
質問

 
(Windows 11 Home : Excel 2019)
ANDとOrの使い方
投稿日時: 23/07/01 13:39:43
投稿者: shimoichimabu

下記の通り、1つのブックに1か月分のシートを作ろうとしています。
シート名は日付そのものです。
例. 1,2,3,4,・・・・・・・、31
但し、
・土・日曜日に該当する日付のシートは作成しない
・盆休(8/13・8/14・8/15)と正月休(12/29〜1/3)該当する日付のシートも作成しない
という条件でコードを書きましたが、
8月、12月、1月の場合はうまくいきましたが、それ以外の月は全くシートが作成されませんでした。
ANDとOrの使い方に間違いがあると思いますが、ご教授お願い致します。
 
For N = 1 To 月末
     
    年月日 = 元号 & 年次 & "年" & 月次 & "月" & N & "日"
     
    If Weekday(年月日, 2) < 6 And _
       (Month(年月日) = 8 And Not (N = 13 Or N = 14 Or N = 15)) Or _
       (Month(年月日) = 12 And Not (N = 29 Or N = 30 Or N = 31)) Or _
       (Month(年月日) = 1 And Not (N = 1 Or N = 2 Or N = 3)) Then
     
        ActiveSheet.Copy after:=ActiveSheet
        ActiveSheet.Name = N
         
    End If
     
Next

回答
投稿日時: 23/07/01 15:37:59
投稿者: simple

1.
  論理演算子はOrよりもAndのほうが優先度は高いので、

If Weekday(年月日, 2)< 6 And (Month(年月日)=8 And Not (N = 13 Or N = 14 Or N = 15)) _
   Or (Month(年月日) = 12 And Not (N = 29 Or N = 30 Or N = 31)) _
   Or (Month(年月日) = 1 And Not (N = 1 Or N = 2 Or N = 3)) Then
      ActiveSheet.Copy after:=ActiveSheet
      ActiveSheet.Name = N
End If
と解釈されます。
   上記は、3つの要素を Or で結んでいますが、
   1,8,12以外の月は、どの3つの要素でもTrueになり得ないことは明らかです。
   (しかも、1月,12月は 日曜も対象になっていると思います。)
 
2.
    いわゆる"正記帳"的に、シートを作成するときの条件を列挙しても勿論OKですが、
    "逆記帳"的に、シートを作成しない条件を列挙することもできるでしょう。
    例えばこんな書き方です。
    For N = 1 To 月末
        年月日 = 元号 & 年次 & "年" & 月次 & "月" & N & "日"
        flag = True
        If Weekday(年月日, 2) = 7 Then
            flag = False
        Else
            Select Case Month(年月日)
                Case 8:     If (N = 13 Or N = 14 Or N = 15) Then flag = False
                Case 12:    If (N = 29 Or N = 30 Or N = 31) Then flag = False
                Case 1:     If (N = 1 Or N = 2 Or N = 3) Then flag = False
            End Select
        End If
        If flag Then
            ActiveSheet.Copy after:=ActiveSheet
            ActiveSheet.Name = N
        End If
    Next

 なお、投稿にあたっては変数の宣言等も漏らさず書かれたほうがよいでしょう。
 そのこともあり、上記は実際に動かして確認はしていません。(その前提で受け止めて下さい。)

回答
投稿日時: 23/07/01 18:19:19
投稿者: WinArrow

コメント
  
 simpleさんから素晴らしいレスがありましたので、
  
コード作成時のアドアイスとして
  
AND ORを使って、1つのセンテンスに纏めようと考えているようですが、
コードの可読性を優先に考えることを推奨します。
頑張って 1つのセンテンスに纏めると、すっきりした気分になりますが、
後日、読みなしてみると、読み解くのに多くの時間をかけています。(自己反省)
後日の変更時(メンテナンス)に苦労します。
多分、自分でも苦労すると思いますが、他人(業務継承者)は、もっと苦労すると思います。
可読性を上げること=すっきりすることと考えた方がよいです。
  
それから、これは、simpleさんから教えた頂いたことですが、
最初の1つの条件で成立したとしても、全ての条件がチェックされる(VBAの特徴)ということを
覚えておいた方がよいです。つまり、処理時間に影響するということです。

回答
投稿日時: 23/07/01 22:27:03
投稿者: simple

分かりにくかったですか。
そのコードは、
以下の3つの条件をORで結んだものと解釈されるということです。
 

(Weekday(年月日, 2)< 6 And ( Month(年月日) = 8 And Not (N = 13 Or N = 14 Or N = 15))) 
 OR 
 (Month(年月日) = 12 And Not (N = 29 Or N = 30 Or N = 31))
 OR
 (Month(年月日) = 1 And Not (N = 1 Or N = 2 Or N = 3))
↑は、_ もなく、現実のコードとは違うので注意して下さい。
 
■言及のあった以下の点、少し敷衍しておきます。
混乱するようなスキップしてください(情報提供としてメモしておきます)。
>最初の1つの条件で成立したとしても、全ての条件がチェックされる(VBAの特徴)ということを
>覚えておいた方がよいです。つまり、処理時間に影響するということです。

Sub test()
    Dim a&, b&, c&
    a = 1
    b = 2
    c = 3
    If a = 0 And b = 2 And c = 3 Then
        Debug.Print "OK"
    Else
        Debug.Print "NG"
    End If
End Sub
この例では、
最初のa = 0 はFalseなので、第二項、第三項は計算するまでもなく結果はFalseと分かります。
このように「途中で結果が分かった場合に、それ以降の処理を行わない評価の仕方」を"短絡評価"といいます。
(いくつかのOR条件で、ひとつでもTrueになれば、それ以降は計算は不要となるというケースも同じです)
 
VBAは、このような"短絡評価"は行わず、
無駄ではあっても、すべての要素を実直に計算する方式です。
 
したがって、こうした処理が多数ある場合は、(無駄な評価をしているので)効率が悪いとも言えます。
(少件数であれば影響はさほどなく、分かりやすい面があるとも言えます。使い分けるのがよいでしょう。)
 
処理効率を優先するなら、以下のような書き方にしたほうがよいでしょう。
Sub test2()
    Dim a&, b&, c&
    a = 1
    b = 2
    c = 3
    If a = 0 Then
        If b = 2 Then
            If c = 3 Then
                Debug.Print "OK"
            End If
        End If
    End If
End Sub
これであれば、aが0でないと分かった段階で、直ぐに脱出するからです。
 
■(参考情報)
色々なプログラム言語がありますが、
・自動的に"短絡評価"を行うもの
・"短絡評価"と"非短絡評価"とそれぞれの評価を別の演算子で持つもの
など言語によって異なります。
VBA(VB6)は、"非短絡評価"しか備えておらず、比較的珍しい部類に属するかもしれません。
 
ちなみに、test1で、本当に三つの要素を実際に評価しているかどうかは、
ステップ実行してもわかりません。
しかし、下記のようなことをすると確かめることができます。
・以下のコードで、test3を実行すると、
・immidiateウインドウに、
    bb executed
    cc executed
  が表示されることで分かります。
Sub test3()
    Dim a&
    a = 1
    If a = 0 And bb() = 2 And cc() = 3 Then
        Debug.Print "OK"
    End If
End Sub
Function bb()
    Debug.Print "bb executed"
    bb = 2
End Function
Function cc()
    Debug.Print "cc executed"
    cc = 3
End Function

投稿日時: 23/07/02 00:22:34
投稿者: shimoichimabu

simpleさん、WinArrowさん、回答・貴重な助言有難うございます。
下記回答を送信しようと、思い、simpleさんの2回目目の回答を拝見しました。
if (Weekday(年月日, 2)< 6 And ( Month(年月日) = 8 And Not (N = 13 Or N = 14 Or N = 15))) OR _
 (Month(年月日) = 12 And Not (N = 29 Or N = 30 Or N = 31)) OR _
 (Month(年月日) = 1 And Not (N = 1 Or N = 2 Or N = 3)) Then
これを実行すると、希望通りの結果となりました。
私が最初に書いたコードとは異なり、かつ予想外のコードになっていました。やはり難解です。
理解するのに時間がかかりそう。
 
最初は冗長なコードを避けるため、なんとかトライしてみましたが、色々な場合を理論立てて考えないとダメで、上記のように、私には難解でした。Flag、Select Case を使う方法は思いつかなかったです。コード自体は少し長くなりますが、わかり易く、修正・追加があっても簡単にできますね。
ご指摘の通り、下記コードに書き換え、希望通り実行することができました。
何か、変数宣言など、追加する点があれば、ご助言の程、宜しくお願い致します。
 
Dim 月初め As Date, 月末 As Long,N As Long, 年月日 As Date, flag As Boolean
 
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
月初め = 元号 & 年次 & "年" & 月次 & "月" & "1日"
月末 = Format(DateSerial(Year(月初め), Month(月初め) + 1, 0), "d")
     
For N = 1 To 月末
     
    年月日 = 元号 & 年次 & "年" & 月次 & "月" & N & "日"
     
    flag = True
     
    If Weekday(年月日, 2) > 5 Then '土・日曜日はシート作成しない
        flag = False
    Else
        Select Case Month(年月日)
            Case 8: If (N = 13 Or N = 14 Or N = 15) Then flag = False '8月13〜15日はシート作成しない
            Case 12: If (N = 29 Or N = 30 Or N = 31) Then flag = False '12月29〜31日はシート作成しない
            Case 1: If (N = 1 Or N = 2 Or N = 3) Then flag = False '1月1〜3日はシート作成しない
        End Select
    End If
     
    If flag = True Then
        ActiveSheet.Copy after:=ActiveSheet
        ActiveSheet.Name = N
    End If
     
Next

投稿日時: 23/07/02 00:51:17
投稿者: shimoichimabu

訂正です。
 
(Month(年月日) = 12 And Not (N = 29 Or N = 30 Or N = 31)) OR _
 (Month(年月日) = 1 And Not (N = 1 Or N = 2 Or N = 3)) Then
これを実行すると、希望通りの結果となりました。
 
8月、12月、1月はうまくいきましたが、他の月はシート作成されませんでした。
はやとちりでした。

回答
投稿日時: 23/07/02 01:37:50
投稿者: たらのり

こんばんは(おはようございます)
 
# 安定の酒気帯びです
 
初期の If文は次のような構造をしていて:

If 条件A And 条件B Or 条件C Or 条件D Then

次のように書いても同じ結果になると思います:
If (条件A And 条件B) Or 条件C Or 条件D Then

条件Aは「土、日でないこと」、
条件Bは「8月の休暇でないこと」、
条件Cは「12月の休暇でないこと」、
条件Dは「1月の休暇でないこと」をそれぞれ意味しています。
 
上の文(後者)をみると、土日を除外する「条件A」は「条件B」にしか
係っていませんね。
「条件C」と「条件D」は曜日を問わず単独で成立します(土日を除外する
「条件A」が係っていない)。
 
8月分は正しく出力されるかもしれませんが、12月と 1月分については
土日も出力されているのではないでしょうか。
 
# 勘違いだったらごめんなさい
 

回答
投稿日時: 23/07/02 01:53:53
投稿者: たらのり

すみません,12月と 1月の週末が除外されていなことについては
simple さん からすでに指摘がありましたね……
 

回答
投稿日時: 23/07/02 07:44:21
投稿者: hatena
投稿者のウェブサイトに移動

コードを書くときには、可読性とメンテナンス性を考慮するというのは他の人からも指摘があります。
 
休日判定は、どこかに休日日付を格納しておいて、それをみて判定するというのがメンテナンス性が高いと思います。 例えばシートに休日を列挙しておいてMatch関数などで判定するとか。
シートをつくるまでもないようなことなら、下記のように定数で宣言しておくということをよくします。
 

    Const Holidays = "08/13 08/14 08/15 12/9 12/30 12/31 01/01 01/02 01/03"

    If  Holidays Like "*" & Format(年月日, "mm/dd") & "*" Then
        MsgBox "休日です"
    End If

 
休日日程が変更になった場合、定数を修正するだけですみますのでメンテナンス性も高いし、可読性も高いと思います。
今回の要件なら速度は重視しなくてもいいでしょう。
 
休日判定は1行ですみますので、条件式もOrでまとめても可読性は落ちないと思いますので、下記のようなコードでどうでしょう。
 
Public Sub Sample()
    Const Holidays = "08/13 08/14 08/15 12/9 12/30 12/31 01/01 01/02 01/03"

    Dim 元号 As String, 年次 As Long, 月次 As Long
    元号 = "令和"
    年次 = 5
    月次 = 1

    Dim 月初め As Date, 月末 As Date
    月初め = CDate(元号 & 年次 & "年" & 月次 & "月" & "1日")
    月末 = DateSerial(Year(月初め), Month(月初め) + 1, 0)
     
    Dim 年月日 As Date
    For 年月日 = 月初め To 月末
        
        If Weekday(年月日, 2) > 5 _
           Or Holidays Like "*" & Format(年月日, "mm/dd") & "*" Then
        Else
'            ActiveSheet.Copy after:=ActiveSheet
'            ActiveSheet.Name = Day(年月日)
             Debug.Print  Day(年月日)
        End If
        
    Next

End Sub

 

回答
投稿日時: 23/07/02 08:05:39
投稿者: WinArrow

AND OR を使わない方法に拘ってみました。
  
日数分の日付変換をしないようにして、使用する変数を削っています。
発生頻度の多い順にチェックするようンしています。
例えば、
月〜金の頻度と土日の頻度
これは大したことではないが、
2〜7月、9〜12月について
1,8,12を除外しているが、
先にチェックして除外ステップをスキップ

Dim 月初め As Date, 月末 As Date, 年月日 As Date, flag As Boolean

    月初め = 元号 & 年次 & "年" & 月次 & "月" & "1日"
    月末 = WorksheetFunction.EoMonth(月初め, 0)
    
    For 年月日 = 月初め To 月末
        flag = True
        Select Case Weekday(年月日)
            Case vbMonday To vbFriday
                Select Case Month(年月日)
                    Case 2 To 7, 9 To 11
                    Case 1
                        Select Case Day(年月日)
                            Case 1 To 3
                                flag = False
                        End Select
                    Case 8
                        Select Case Day(年月日)
                            Case 13 To 16
                                flag = False
                        End Select
                    Case 12
                        Select Case Day(年月日)
                            Case 29 To 31
                                flag = False
                        End Select
                End Select
            Case Else
                flag = False
        End Select
        If flag = True Then
            ActiveSheet.Copy after:=Sheets(ActiveSheet.Parent.Sheets.Count)
            ActiveSheet.Name = Day(年月日)
        End If
    Next


 

回答
投稿日時: 23/07/02 08:21:53
投稿者: MMYS

私なら、下記のように作成します。
 

Sub Sample()
    Dim 年 As Integer
    Dim 月 As Byte
    Dim 月末 As Byte
    Dim 年月日 As Date
    Dim N      As Byte
    Dim BF As Boolean

    年 = 2023
    月 = 7
    月末 = Day(DateSerial(年, 月 + 1, 1) - 1)

    For N = 1 To 月末
        年月日 = DateSerial(年, 月, N)

        BF = True
        BF = BF And (Weekday(年月日) <> vbSaturday)
        BF = BF And (Weekday(年月日) <> vbSunday)
        BF = BF And (年月日 <> DateSerial(年, 8, 13))
        BF = BF And (年月日 <> DateSerial(年, 8, 14))
        BF = BF And (年月日 <> DateSerial(年, 8, 15))
        BF = BF And (年月日 <> DateSerial(年, 12, 29))
        BF = BF And (年月日 <> DateSerial(年, 12, 30))
        BF = BF And (年月日 <> DateSerial(年, 12, 31))
        BF = BF And (年月日 <> DateSerial(年, 1, 1))
        BF = BF And (年月日 <> DateSerial(年, 1, 2))
        BF = BF And (年月日 <> DateSerial(年, 1, 3))

        Debug.Print BF, Format(年月日, "yyyy/mm/dd(aaa)")
    Next

End Sub

 
上記コードは、ANDで演算してますから、BFがFalseになると、後の判定は関係ありません。
 
VBAではWinArrowさん、simpleさんの指摘のとおり、"短絡評価"は行われません。処理速度は不利ですが、速度を求める内容でもないと思いますし、なりより可読性を優先すべきだと思います。
 

回答
投稿日時: 23/07/02 08:27:55
投稿者: MMYS

ところで祝日はどうするのですか。
 
私なら1年分の休日リストを作って、それに一致すれば休日と扱います。
休日リストに、土日・祝日・夏休暇・年末年始を含む、1年分の休日をリストにします。日付が休日リストと一致したら、休日と扱います。この手法なら、夏休暇・年末年始などは考えなくて良いのでなどの処理がシンプルになります。
 

Sub Sample2()
    Dim 年      As Integer
    Dim 月      As Byte
    Dim 月末    As Byte
    Dim 年月日  As Date
    Dim N       As Byte
    Dim cnt     As Long
    Dim rngHoliday  As Range

    Set rngHoliday = Worksheets("休日リスト").Range("A:A")
    年 = 2023
    月 = 7
    月末 = Day(DateSerial(年, 月 + 1, 1) - 1)

    For N = 1 To 月末
        年月日 = DateSerial(年, 月, N)
        cnt = Application.WorksheetFunction.CountIf(rngHoliday, 年月日)
        Debug.Print cnt, Format(年月日, "yyyy/mm/dd(aaa)")
    Next

    Set rngHoliday = Nothing
End Sub

上記は、「休日リスト」シートのA列に、年間の休日を記載。
 
※土曜、日曜は、オートフルで作成。祝日・夏休暇・年末年始のみ個別に作成すればよく、手間はかからない。

回答
投稿日時: 23/07/02 08:58:27
投稿者: simple

何かうまくコミュニケーションがとれませんが、
私が23/07/01 22:27:03の冒頭で書いたのは、なぜNGになるかの追加説明です。
改善案なんかじゃないです。そんなものでうまくいくわけがありません。
 
除外日(祝日等)を別シートに一覧にしておくのが普通でしょうね。
最初から、AND ORの話は放っておいて、そちらで回答したほうがよかったかもですね。
皆さんから適切な回答をいただいています。

投稿日時: 23/07/02 17:27:36
投稿者: shimoichimabu

hatenaさんの
Const Holidays = "08/13 08/14 08/15 12/9 12/30 12/31 01/01 01/02 01/03"
 For 年月日 = 月初め To 月末
        If Weekday(年月日, 2) > 5 _
           Or Holidays Like "*" & Format(年月日, "mm/dd") & "*" Then
        Else ・・・・・・・
こういう方法もあるのですね。勉強になります。
WinArrowさんの
「日数分の日付変換をしないようにして、使用する変数を削っています。」の貴重な方法を提示して頂き、ありがとうございます。
MMYSさんの「ところで祝日はどうするのですか。」ですが、実は頼んだ人が、祝祭日は自分で削除するからと言われたので、最低限の機能を持たせたものにしました。やはり、多くの方が休日マスターを使用した方がいいと言われているように、その方がいいと思いました。
別件ですが、祝祭日(春分の日、秋分の日、5/6の休日なども)、振替休日、盆休、正月休等を含む休日マスターを作成し、これを自分の勤務表に利用したことがあるので、この方法もトライしてみます。
このたび、多くの貴重なご意見頂き、有難うございました。