仕事やプライベートでの定期的な予定で、「毎月第3日曜」など「第○ △曜日」といったパターンはありがちです。しかし、その具体的な日付は、すぐにはわからないものですよね。予定表を作成するなどの際、いちいちカレンダーをたどり、手作業で調べている方が多いのではないでしょうか。

そこで今回はExcel VBAで、指定した年・月の第○ △曜日の日付を求める方法を紹介します。

Excel VBAで今回やろうとしていること

今回は、上記の機能を備えたFunctionプロシージャを作成するとします。名前は「getDateWeekNum」とします。書式は次の通りとします。

getDateWeekNum(myYear,  myMonth,  myWeekday, weekNum)

引数は下記4つとします。

  • myYear
    求めたい年を整数として指定します。
     
  • myMonth
    求めたい月を整数として指定します。
     
  • myWeekday
    求めたい曜日の番号を整数として指定します。曜日の番号とは、日曜を「1」、土曜を「7」とする連番です。Excel VBAでは一般的に、曜日をこのような体系の番号で扱います。曜日の各番号には、それぞれ定数が割り当てられています。これらはExcel VBAに最初から用意されている定数であり、コード中にいきなり書いて使えます。厳密には、「VbDayOfWeek列挙型」と呼ばれる定数になります。

    定数 曜日
    vbSunday 日曜 1
    vbMonday 月曜 2
    vbTuesday 火曜 3
    vbWednesday 水曜 4
    vbThursday 木曜 5
    vbFriday 金曜 6
    vbSaturday 土曜 7


  • weekNum
    週目(「第○」の「○」の部分)を整数として指定します。 たとえば、第3日曜なら「3」と指定します。

このように引数を指定すると、目的の日付をDate型で返すとします。たとえば、「2012年6月の第3日曜」を求めたいなら、次のように指定します。第3引数は、もちろん、vbSundayの値である「1」を直接指定しても構いません。

getDateWeekNum(2012, 6, vbSunday, 3)

実行すると、「2012/06/17」という日付が戻り値として得られます。下記画面は、上記のコードで求めた日付をMsgBox関数でメッセージボックスに表示した例です(コードは「MsgBox getDateWeekNum(2012, 6, vbSunday, 3)」になります)。

カレンダーで確認すると、確かに2012年6月の第3日曜は6/17となっていますね。こりゃラクだ!

なお、getDateWeekNumはFunctionプロシージャなので、単体ではマクロとして実行できません。実際に使うには、他のSubプロシージャ内に記述し、そのSubプロシージャを実行して呼び出すかたちになります。また、実行結果の表示形式は日付の書式設定によって変わります。getDateWeekNumが返すのは、あくまでも日付データになります。

コード

Functionプロシージャ「getDateWeekNum」のコードは下記の通りです。記述場所は標準モジュールになります。

Function getDateWeekNum(myYear As Integer, myMonth As Integer, _
          myWeekday As VbDayOfWeek, weekNum As Integer) As Date
  Dim firstWeekday As Integer  ' 指定した年月の1日の曜日番号
  Dim Sabun As Integer      ' 曜日番号の差分

   ' 指定した年月の1日の曜日番号求める
  firstWeekday = weekday(DateSerial(myYear, myMonth, 1))

   '1日の曜日番号と求めたい曜日番号の差分を求める
  Sabun = myWeekday - firstWeekday
  If myWeekday < firstWeekday Then
    Sabun = Sabun + 7
  End If

   ' 目的の日付データを求めて返す
   getDateWeekNum = DateSerial(myYear, myMonth, 1 + Sabun + 7 * (weekNum - 1))
End Function




では、このとコードの考え方・処理手順とポイントを順に解説していきます。以下、解説中に登場するコードの行番号は、空白行を抜いてカウントしたものになっています(1行丸ごとのコメントもカウント)空白行も入れてカウントしています。

指定した年・月の第○ △曜日を求める考え方・処理手順

まずはコードの解説を行う前に、どのような方法で、指定した年・月の第○ △曜日を求めているのか、全体像を解説しますね。少々わかりにくい方法を採用していますので。

いくつか方法は考えられますが、今回は次のような考え方・手順にしました。よりわかりやすくするため、2012年6月の第3土曜の日付を求めたいと仮定して説明します。

第3土曜が何日になるかは、第1土曜の日さえわかれば求められます。一体なぜでしょうか? 具体的に2012年6月の第1土曜の日は2日(6/2)になります。第1土曜が6/2とわかれば、第3土曜なら7日後の6/9、第3土曜なら14日後の6/16、第4土曜なら21日後の6/23といったように、6/2に一週間の日数である7の倍数を足していけば求められます。

ここまではご理解いただけたでしょうか? 問題は、そもそも第1土曜の日をどうやって求めればよいかです。今回は次の方法を採りました。

  • 【STEP1】 6/1(6月の最初の日)の曜日を求める
    これはVBA関数のWeekdayを使えば簡単に求められます。具体的には金曜になります。

  • 【STEP2】 6/1の曜日から、第1土曜の日を求める
    そのためには、曜日が番号で管理されていることを利用します。求めたい曜日である土曜の番号は、定数vbSundayのところで紹介したように7です。 一方、6/1の曜日である金曜の番号はSTEP1で6とわかります。すると、土曜の金曜の番号の差は7-6で1であり、言い換えると1日後です。したがって、第1土曜の日は6/1の1日後である6/2とわかります。

少々強引かもしれませんが、この方法なら第1土曜の日が求められます。あとは第3土曜なので14日後ということで、6/2に14日を足すだけです。以上から、第3土曜の日は16日(6/16)とわかります。手順をまとめると、次のようになります。

  • 【STEP1】 6/1の曜日を求める
  • 【STEP2】 6/1の曜日から、第1土曜の日を求める
  • 【STEP3】 第1土曜の日から、第3土曜の日を求める

考えた処理手順をコードに落とし込む

今回は、指定した年・月の第○ △曜日を求める機能は、Functionプロシージャとして作成します。他のSubプロシージャなどで呼び出して使うことになります。念のため、Functionプロシージャの書式を提示しておきます。

Function プロシージャ名(引数名1 As データ型, 引数名2 As データ型,・・・) As 戻り値のデータ型
  処理の内容
      :
      :
  プロシージャ名 = 戻り値
End Function

getDateWeekNum では、上記書式の1行目「Function」以降は下記のように記述しています。引数宣言の部分は長いので、「 _」で改行しています。

Function getDateWeekNum(myYear As Integer, myMonth As Integer, _
             myWeekday As VbDayOfWeek, weekNum As Integer) As Date

引数は本エントリ冒頭で提示したように指定します。基本的にすべて整数なので、データ型はInteger型にしています。第3引数myWeekdayのみ、先ほど紹介したvbSundayなど曜日の定数のみを入れるので、VbDayOfWeek列挙型としています。この型にしておくと、引数指定時にvbSundayなどの定数が一覧表示され、選択して入力できるようになります。戻り値は日付データを返したいので、Data型を指定します。

7行目では、【STEP1】の処理を行っています。具体的には、指定した年・月(今回の例では2012年6月)の1日の曜日を求め、変数firstWeekday に代入しています。

firstWeekday = weekday(DateSerial(myYear, myMonth, 1))

1日の曜日を求めているのは、「=」の右辺である「weekday(DateSerial(myYear, myMonth, 1))」の部分です。この部分はWeekday関数の引数に、DateSerial関数を指定した入れ子構造になっています。

DateSerial関数は第1引数に年、第2引数に月、第3引数に日の数値を指定すると、その年月日の日付データを返します。今回は「DateSerial(myYear, myMonth, 1)」と、第1引数にはgetDateWeekNum の引数myYear、第2引数には同じくgetDateWeekNum の引数myMonth、第3引数には1日ということで数値の1を直接指定しています。これで、指定した年・月の1日の日付データが得られます。

その日付データをWeekday関数の引数に入れています。Weekday関数は引数に指定された日付データから、曜日の番号を求めて返す関数です。よって、「weekday(DateSerial(myYear, myMonth, 1))」と記述することで、指定した年・月の1日の日付データが得られます。たとえば、myYearが2012、myMonthが6と指定されたなら、「weekday(DateSerial(2012, 6, 1))」となり、2012年6月1日は金曜なので、その番号である6が得られます。

繰り返しになりますが、この5行目で、指定した年・月の1日の曜日の番号が変数firstWeekday に入りました。

求めたい曜日と1日の曜日の差分を求める

10行目から、【STEP2】の処理が始まります。6行目では、求めたい曜日の番号(getDateWeekNum の引数myWeekday)と、その年・月の1日の曜日の番号(変数firstWeekday)の差を求め、変数Sabunに代入しています。

Sabun = myWeekday – firstWeekday

たとえば、求めたい曜日の番号が土曜日の7であり、2012年6月1日の曜日の番号が金曜の6なら、変数Sabunの値は1となります。この値さえわかれば、求めたい曜日である土曜日の第1週の日は、2012年6月1日の1日後とわかるようになります。

ただし、ここで1つ問題があります。先ほどから説明に用いている例のように、求めたい曜日の番号が、指定した年・月の1日の曜日の番号よりも小さい場合は問題ありません。しかし、逆に大きいとなると、不都合が生じてしまうのです。たとえば、求めたい曜日の番号が水曜の4だとします。すると、「myWeekday – firstWeekday」は4 – 6となり、-2という負の値になってしまいます。本来なら、6月1日が金曜なので、第1水曜は5日後の6月6日と求めたいのに、6月1日の-2日後というおかしな状態なってしまいます。

そこで、求めたい曜日の番号が、指定した年・月の1日の曜日の番号よりも大きい場合にも対応可能とする処理を加えてやります。その場合は翌週の該当曜日の日が求められるよう、6行目で得られた変数Sabunの値を、1週間の日数である7だけ増やします。この処理がミソです。コードとしては、11~12行目になります。

If myWeekday < firstWeekday Then
  Sabun = Sabun + 7
End If

これで、求めたい曜日の番号と、指定した年・月の1日の曜日の番号の差分である変数Sabunが正しく求められるようになりました。その月の1日から、この変数Sabunの値だけ増えた日が、求めたい曜日の第1週の日とわかります。

第1週の日をもとに、第3週の日を求める

最後の12行目は少々複雑です。【STEP2】の処理の残りと、【STEP3】の処理がミックスされているからです。最後の16行目では、今回の説明に用いている例のケースだと、2012年6月の第1土曜の日を求め、その値から第3土曜の日を求めます。その日付データをFunctionプロシージャの戻り値として返すよう、プロシージャ名に代入しています。

では、細かく見ていきましょう。「=」の右辺は次のようになっています。

DateSerial(myYear, myMonth, 1 + Sabun + 7 * (weekNum – 1))

DateSerial関数は先ほども登場したように、年月日の数値から日付データを求める関数です。ここでポイントとなるのが、日の数値を指定する第3引数です。第3引数は次のように指定しています。

1 + Sabun + 7 * (weekNum – 1)

前半の「1 + Sabun」の部分では、その月の1日の日である1に、求めたい曜日の番号と1日の曜日の番号の差分である変数Sabunを加えることで、求めたい曜日の第1週の日を算出しています。今回の例のように、求めたい曜日が土曜(番号7)であり、6月1日が金曜(番号6)なら、差分である変数Sabun は1となり、「1 + Sabun」は2となるため、求めたい曜日の第1週の日は2日(6月2日)と算出されます。

後半の「+ 7 * (weekNum – 1)」の部分では、第○週の日を求めるため、1週間の日数である7の倍数を足しています。たとえば第3週なら、第1週から14日(7日 × 2)を足せばよいことになります。そのような処理を行えるよう、「+ 7 * (weekNum – 1)」と記述しています。

コードの解説は以上です。実際に処理を行っているコードは正味6行しかないにもかかわらず、ずいぶん長々とした解説になってしまいましたね。それだけ、解説が必要なコードかと思います。特に7~10行目は少々わかりにくいでしょう。実際にプログラムを書いて動作させ、4つの引数をどう指定すれば、どのような結果が得られるか、コード内の変数や式はどのような値なのか、デバッグ機能などで追っていけば、より理解が深まるでしょう。

ワークシート上でもオリジナルの関数としても使える!

今回のgetDateWeekNumはFunctionプロシージャとして作成したので、VBAのコード内のみならず、通常のセルなどにて、オリジナルの関数としても使えます。たとえば、2012年6月の第3日曜をA1セルに表示するには、次のように指定します。

ただ、ワークシート上では残念ながら、vbSundayなどの定数は使えないので、曜日の番号を直接指定してください。上記例では日曜日の番号である1を第3引数に直接指定しています。また、上記例では、A1セルは表示形式をあらかじめ日付の「*2001/3/14」に設定しています。

ちなみに、ワークシート上で使う通常の関数は、VBA関数と区別するため、「ワークシート関数」と呼ばれるケースもあります。

エラー処理も本来なら欲しいが・・・

 今回のgetDateWeekNumは、すべての引数に適切な値が指定されるという前提になっており、エラー処理を一切設けていないのでご注意ください。たとえば、2012年6月の第5水曜の日付を求めようと、第4引数に「5」と指定し、「getDateWeekNum(2012,6,vbWednesday,5)」と記述したとします。カレンダーを見ればわかるのですが、2012年6月の第5水曜は存在しないため、「2012/07/04」と翌月の日付が得られてしまいます。

そのようなエラーへの対処は、たとえば次のようにします。

‘目的の日付の日付データを求めて返す
myDate = DateSerial(myYear, myMonth, 1 + Sabun + 7 * (weekNum – 1))
If month(myDate) = myMonth Then
  getDateWeekNum = myDate
Else
  ’エラー時の処理
End If

Date型変数myDateを用意し、目的の日付の日付データを一旦代入します。そして、myDateの月をMonth関数で取得し、myMonthと一致しなければ、その月には存在しない日付とわかるので、エラー時の処理を実行するようにします。

同様に本来、myYearやmyMonth、weekNumといった他の引数についても、不適切な値が指定された際のエラー処理が必要ですが、今回は割愛しています。

蛇足: 最後に少々宣伝を

今回のお題は、拙Excel VBAセミナーの受講生さんからの質問が契機となっています。というわけで、最後に少々宣伝させて下さい(汗)

Excel VBAのキホンの「キ」から学びたい方は、下記の拙著をどうぞ。

  

また、前者の拙著をテキストとしたセミナー「Excel VBAがゼッタイにわかる1日セミナー」を東京・名古屋・大阪で開催しています。まったくの初心者だったのに、1日受講しただけで、自分の手でExcel VBAのプログラムを書けるようになったなど、受講者の皆さまからご好評いただいております。

seminar_logo

ビギナーコースステップアップコースの2コースを用意しております。Excelを使った仕事を効率化したい方、よろしければどうぞご受講ください!

また、同拙著はVBAエキスパート推薦図書にもなったので(2012年6月より)、資格試験対策の一環にもなりますよ。

初心者卒業後のオススメ書籍等は下記の過去エントリを参照下さい。

Excel VBA初心者を卒業した人にオススメの厳選書籍&Web