文字列と日付の操作:VBA のヒントとコツ

David Shank
Microsoft Corporation

2000 年 8 月 3 日

ソリューションを開発するとき、いくつかの操作を何度も繰り返し実行しなければならないことがあります。たとえば、ファイル パスの解析や、2 つの日付のインターバルの計算といったことです。どのような関数が利用でき、それらがどのように動作するか分かったら、頻繁に行う処理についてはカスタム プロシージャのコレクションを作成し、それらを再利用するとよいでしょう。

Visual Basic for Applications(VBA)コードを記述する開発者が最も頻繁に行う作業に、文字列の操作と日付の操作の 2 つです。VBA にはこれらの操作を行うための強力な関数群が用意されていますが、そのすべてが簡単で使い方も分かりやすいというわけではありません。今月のコラムでは、VBA の強力な組み込み関数を最大限に活用した、文字列や日付の操作方法について解説します。

文字列の操作

以下では、文字列操作に関するいくつかの一般的な質問を挙げ、それらの質問への答えとなるカスタム プロシージャやサンプル コードを紹介します。

文字列の長さを調べる最もよい方法は ?

Len 関数を使って文字列の長さを計算します。

Dim lngLen As Long
lngLen = Len(strText)

VBA では、必ず文字列の先頭に文字列の長さが Long 型の整数として保存されます。Len 関数はこの値を取得してくるため、その動作はきわめて高速です。たとえば、文字列が長さ 0 の文字列("")かどうかを調べる場合、対象となる文字列を長さ 0 の文字列と比較するよりも、次のようなコードで文字列の長さが 0 かどうかを調べたほうが高速です。

If Len(strText)> 0 Then
   ' Perform some operation here.
End If

ある文字列の中に指定した部分文字列が含まれているかどうかを調べる方法は ?

InStr 関数、または InStrRev 関数を使って文字列を検索できます。InStr 関数は 2 つの文字列どうしを比較し、2 番目の文字列が 1 番目の文字列の内部に含まれていれば、その部分文字列の開始位置を返します。InStr 関数が部分文字列を見つけられない場合には、0 を返します。部分文字列の開始位置がわかれば、Mid 関数などほかの VBA 関数を使って部分文字列を取り出すことができます。

' Use InStr to search a string to determine whether
' a substring exists. In the following example InStr
' returns 4, which is the character position where the
' substring begins:
Dim intSubStringLoc As Integer
intSubStringLoc = InStr("My dog has fleas", "dog")

InStr 関数では、start という引数に検索の開始位置を指定することもできます。

' In the following example, InStr returns 0 because
' the search starts at character position 5, which
' has the effect of changing the search string to
'  "og has fleas":
Dim intSubStringLoc As Integer
intSubStringLoc = InStr(5, "My dog has fleas", "dog")

この引数を省略した場合、InStr 関数は文字列の先頭から検索を開始します。

InStrRev 関数は、文字列の先頭からではなく末尾から検索を開始します。InStr 関数と同じように開始位置を指定することもできます。この場合、InStrRev 関数はその開始位置から先頭に向かって文字列を検索します。検索しようとしている部分文字列が文字列の末尾にあることがわかっている場合は、InStr 関数よりも InStrRev 関数を選択したほうがよいでしょう。たとえば、ファイル パスからファイル名を返す場合に InStrRev 関数を使います。次のプロシージャ呼び出しは "Coverletter.doc" を返します。

Dim strFileName As String
StrFileName = GetNameFromPath("C:\Letters\New\Coverletter.doc")

Function GetNameFromPath(strPath As String) As String
    Dim strTemp As String
    Dim intPos As Integer
    ' If string contains a "\" then return that portion
    ' of the string after the last "\":
    If InStr(strPath, "\")> 0 Then
        intPos = InStrRev(strPath, "\") + 1
        strTemp = Mid$(strPath, intPos)
        GetNameFromPath = strTemp
    Else
        ' Invalid path submitted so return 0.
        GetNameFromPath = 0
    End If
End Function

どちらの関数も、部分文字列の開始位置について同じ値を返します。たとえば、文字列 "C:\Temp" の中で部分文字列 "C:\" を検索すると、InStr または InStrRev のどちらの関数を呼び出しても、1 を返します。ただし、部分文字列が複数出現し、かつ start 引数の値を指定しなかった場合、InStr 関数は部分文字列が最初に現れた位置を返すのに対して、InStrRev 関数は最後に現れた位置を返します。

次のプロシージャは、特定の 1 文字または文字の集合が文字列の中に出現する回数を数えます。このプロシージャを呼び出すには、被検索文字列、検索する部分文字列、そして検索で大文字小文字を区別するかどうかを示す定数を渡します。すると、プロシージャは InStr 関数を使って指定されたテキストを検索し、そのテキストが最初に出現する位置の値を返します。たとえば出現位置が文字列中の 3 番目の文字であれば、InStr 関数は 3 を返します。この値は、次の InStr 関数呼び出しの後も使用できるように、一時変数に格納されます。そして、プロシージャは出現回数を記録するカウンタ変数をインクリメントして、次の InStr 関数呼び出しの開始位置を設定します。新しい開始位置は、検索テキストが見つかった位置に検索文字列の長さを加えた値です。このようにして開始位置を設定することにより、2 文字以上の長さのテキストを検索するときに同じ部分文字列が 2 回検索されることがなくなります。

Function CountOccurrences(strText As String, _
                          strFind As String, _
                          Optional lngCompare _
                          As VbCompareMethod) As Long

   ' Count occurrences of a particular character or characters.
   ' If lngCompare argument is omitted, procedure performs binary comparison.

   Dim lngPos       As Long
   Dim lngTemp      As Long
   Dim lngCount     As Long

   ' Specify a starting position. We don't need it the first
   ' time through the loop, but we'll need it on subsequent passes.
   lngPos = 1
   ' Execute the loop at least once.
   Do
      ' Store position at which strFind first occurs.
      lngPos = InStr(lngPos, strText, strFind, lngCompare)
      ' Store position in a temporary variable.
      lngTemp = lngPos
      ' Check that strFind has been found.
      If lngPos> 0 Then
         ' Increment counter variable.
         lngCount = lngCount + 1
         ' Define a new starting position.
         lngPos = lngPos + Len(strFind)
      End If
   ' Loop until last occurrence has been found.
   Loop Until lngPos = 0
   ' Return the number of occurrences found.
   CountOccurrences = lngCount
End Function

この関数をたとえば次のように Visual Basic Environment(VBE)の[イミディエイト]ウィンドウから呼び出すと、3 を返します。

? CountOccurrences("This is a test", "t", vbTextCompare)

文字列中に含まれる単語の数を調べる方法は ?

Split 関数は、1 つの文字列を複数の文字列からなる配列に変換します。標準ではスペース文字が区切り文字として使われます。別の区切り文字を指定するにはその文字を delimiter 引数に渡します。1 つの英文を Split 関数に渡すと、配列の各要素に単語が収められます。たとえば次の文字列を渡します。

"This is a test"

すると、次の 4 つの要素からなる配列が得られます。

"This"
"is"
"a"
"test"

次の例は文字列中の単語の数を数えます。配列内の要素の数を調べるだけで簡単に単語の数が分かります。これには、配列の上限値から下限値を引いた値に 1 を加えます。

Function CountWords(strText As String) As Long
   ' This procedure counts the number of words in a string.

   Dim astrWords() As String

   astrWords = Split(strText)
   ' Count number of elements in array -- this will be the
   ' number of words.
   CountWords = UBound(astrWords) - LBound(astrWords) + 1
End Function

文字列から余分なスペース文字を取り除く方法は ?

次のプロシージャは、Split 関数と Join 関数を使って文字列から余分なスペース文字を取り除きます。まず、渡された文字列を配列に分割します。文字列中に 2 文字以上の連続するスペース文字がある場合、それに対応する配列要素は必ず長さ 0 の文字列になります。これらの長さ 0 の文字列要素を探して削除することで、余分なスペース文字を文字列から取り除くことができます。

長さ 0 の文字列要素を配列から削除するには、長さが 0 でない文字列要素を別の 2 つ目の配列にコピーする必要があります。そして、Join 関数を使い、2 つ目の配列を 1 つの文字列に連結します。

2 つ目の配列は Split 関数が作成したものではないため、手動でサイズを調整する必要がありますが、その方法は簡単です。まず 2 つ目の配列のサイズを 1 つ目の配列と同じにしておき、長さが 0 でない文字列をコピーした後にサイズ変更します。

Function TrimSpace(strInput As String) As String
   ' This procedure trims extra space from any part of
   ' a string.

   Dim astrInput()     As String
   Dim astrText()      As String
   Dim strElement      As String
   Dim lngCount        As Long
   Dim lngIncr         As Long

   ' Split passed-in string.
   astrInput = Split(strInput)

   ' Resize second array to be same size.
   ReDim astrText(UBound(astrInput))

   ' Initialize counter variable for second array.
   lngIncr = LBound(astrInput)
   ' Loop through split array, looking for
   ' non-zero-length strings.
   For lngCount = LBound(astrInput) To UBound(astrInput)
      strElement = astrInput(lngCount)
      If Len(strElement)> 0 Then
         ' Store in second array.
         astrText(lngIncr) = strElement
         lngIncr = lngIncr + 1
      End If
   Next
   ' Resize new array.
   ReDim Preserve astrText(LBound(astrText) To lngIncr - 1)

   ' Join new array to return string.
   TrimSpace = Join(astrText)
End Function

TrimSpace プロシージャをテストするために、次のような文字列を使って[イミディエイト]ウィンドウから呼び出してみます。

? TrimSpace("  This   is    a    test  ")
' This function returns, "This is a test"

部分文字列を別の部分文字列に置き換える方法は ?

Replace 関数を使えば、文字列の中に出現する部分文字列をすべて検索して置換できます。Replace 関数は、被検索文字列、文字列中で検索するテキスト、置換テキスト、開始文字、置換数、そして文字列の比較方法を示す定数の、全部で 6 つの引数を受け取ります。Replace 関数は 1 回の呼び出しで該当するテキストをすべて自動的に置換してくれるため、ループを記述する必要すらありません。

たとえば、アプリケーションの中で何らかの条件に基づいて SQL ステートメントの条件を変更したいとします。このような場合、SQL ステートメントを作成し直す代わりに、Replace 関数を使って次のようなコードを記述することで文字列の条件部分だけを置換できます。

strSQL = "SELECT * FROM Products WHERE ProductName Like 'M*' ORDER BY ProductName;"
strFind = "'M*'"
strReplace = "'T*'"

strNewString = Replace(strSQL, strFind, strReplace)

このコードを実行すると、strNewString の値が次のようになります。

SELECT * FROM Products WHERE ProductName Like 'T*' ORDER BY ProductName;

次に示すプロシージャは、被検索文字列、文字列中で検索する単語、そして置換テキストの 3 つの引数を受け取ります。このプロシージャを呼び出すときは、strFind 引数に渡す文字列でワイルドカード文字を含めることができます。たとえば、次のようなパラメータで ReplaceWord プロシージャを呼び出します。

StrNewString = ReplaceWord("There will be a test today", "t*t", "party")

プロシージャは strText 引数を配列に分割したあと、Like 演算子を使って配列の各要素と strFind とを比較し、ワイルドカードの指定に一致する要素を置換します。

Function ReplaceWord(strText As String, _
                     strFind As String, _
                     strReplace As String) As String

   ' This function searches a string for a word and replaces it.
   ' You can use a wildcard mask to specify the search string.

   Dim astrText()    As String
   Dim lngCount      As Long

   ' Split the string at specified delimiter.
   astrText = Split(strText)

   ' Loop through array, performing comparison
   ' against wildcard mask.
   For lngCount = LBound(astrText) To UBound(astrText)
      If astrText(lngCount) Like strFind Then
         ' If array element satisfies wildcard search,
         ' replace it.
         astrText(lngCount) = strReplace
      End If
   Next
   ' Join string, using same delimiter.
   ReplaceWord = Join(astrText)
End Function

日付と時刻の操作

日付と時刻の操作は単純明快です。VBA にはこの作業を楽にしてくれる優れた組み込み関数がいくつか用意されています。しかし、その中にはややもするとコードの記述を厄介にするような罠が潜んでいます。

次に紹介する日付の操作に関する質問は、そうした罠に注目したもので、それらの罠を避けてカスタム関数を作成し、多数のカスタム Office ソリューションで使用できるようにする方法を示します。

現在の日付または時刻を調べる方法は ?

VBA では、現在の日付や時刻を正確に調べるための関数として NowDateTime の 3 つの関数が提供されています。Now 関数は、Date 変数のうち日付と時刻の両方の部分を返します。たとえば Now 関数は次のような値を返します。

08/01/2000 10:18:33 AM

Date 関数は、時刻のない現在の日付を返します。

08/01/2000

Time 関数は、日付のない現在の時刻を返します。

10:18:33 AM

FormatDateTime 関数を呼び出すと、定義済みの書式を使って日付の書式を設定できます。次のプロシージャは両方の組み込み書式を使って日付の書式を設定します。

' Print date using built-in formats.
Debug.Print FormatDateTime(Date, vbGeneralDate)
' Returns: 08/01/2000

Debug.Print FormatDateTime(Date, vbLongDate)
' Returns: Tuesday, August 01, 2000

Debug.Print FormatDateTime(Date, vbShortDate)
' Returns: 08/01/2000

Debug.Print FormatDateTime(Time, vbLongTime)
' Returns: 10:26:31 AM

Debug.Print FormatDateTime(Time, vbShortTime)
' Returns: 10:27

FormatDateTime 関数の第 1 引数では必要に応じて Date 関数または Time 関数が使用できます。Now 関数はあらゆる場合において使用できます。

Format 関数を使ってカスタムの日付または時刻の書式を作成することもできます。

' Print date using built-in formats.
Debug.Print Format$(Now, "ddd, mmm d, yyyy")
' Returns: Tue, Aug 1, 2000

Debug.Print Format$(Now, "mmm d, H:MM am/pm")
' Returns: Aug 1, 10:31 am

VBA はなぜ誤った値を日付変数に格納するのか ?

コードの中で日付リテラルを操作するときは、値が日付であることを VBA に伝える必要があります。そうしないと、VBA が減算または浮動小数点除算を実行しているものと解釈することがあります。

たとえば次のコードでは、VBA が Date 変数に割り当てている値は April 5, 1999 ではなく、4 割る 5 割る 99 です。この値を Date 変数に代入しているので、VBA はその数値を日付に変換します。

Dim dteDate As Date
dteDate = 4 / 5 / 99
Debug.Print dteDate
' Returns: 12:11:45 AM

この問題を回避するためには、日付を区切り文字で囲む必要があります。VBA の日付用区切り文字はシャープ記号(#)です。文字列と同じようにダブルクォートを使うこともできますが、その場合は VBA が文字列を日付に変換するためにさらに余分な手順を実行する必要が生じます。次の例では、日付用区切り文字 # を使って正しい値を返しています。

Dim dteDate As Date
dteDate = #4/5/99#
Debug.Print dteDate
' Returns: 04/05/1999

日付または時刻の構成要素を操作する方法は ?

コードの中で日付を操作するために、しばしば日付を日、月、および年に分解しなければならないことがあります。その後、いずれかの要素について計算を実行し、再度日付のかたちに結合します。日付を日、月、および年の個々の構成要素に分解するには、DayMonth、および Year の各関数を使います。これらの関数はいずれも日付を受け取り、それぞれ日、月、年の部分を返します。

Dim dteDate As Date
dteDate = #4/5/99#
Debug.Print Day(dteDate)
' Returns: 4

Debug.Print Month(dteDate)
' Returns: 5

Debug.Print Year(dteDate)
' Returns: 1999

日付を個々の構成要素から再結合するには、DateSerial 関数を使います。この関数は、日、月、年をそれぞれ表す引数を受け取り、再結合された日付を含む Date 値を返します。

日付を分解し計算を実行して再結合するまでのすべての処理を、1 回の手順で行うこともできます。たとえば、指定された任意の日付の月の最初の日を求める次のような関数を記述できます。

Function FirstOfMonth(Optional dteDate As Date) As Date

   ' This function calculates the first day of a month, given a date.
   ' If no date is passed in, the function uses the current date.

   If CLng(dteDate) = 0 Then
      dteDate = Date
   End If

   ' Find the first day of this month.
   FirstOfMonth = DateSerial(Year(dteDate), Month(dteDate), 1)
End Function

dteDate 引数に #2/23/00# を指定してこのプロシージャを呼び出すと、"2/1/2000" という値が返されます。

次のプロシージャも同じ手法を使い、指定された日付の月の最後の日を返します。

Function LastOfMonth(Optional dteDate As Date) As Date

   ' This function calculates the last day of a month, given a date.
   ' If no date is passed in, the function uses the current date.

   If CLng(dteDate) = 0 Then
      dteDate = Date
   End If

   ' Find the first day of the next month, then subtract one day.
   LastOfMonth = DateSerial(Year(dteDate), Month(dteDate) + 1, 1) - 1
End Function

同じ手法で時刻値の分解と再結合を行う関数群も VBA に用意されています。HourMinute、および Second の各関数は、時刻値の対応する個々の部分を返します。TimeSerial 関数は、時、分、秒の値を受け取り、完全な時刻値を返します。

指定された日付の四半期、週、曜日など、日付に関するその他の情報も同様に取得できます。Weekday 関数は日付を受け取り、その日付の曜日を示す定数を返します。

次のプロシージャは、日付を受け取り、その日付が就業日(月曜日~金曜日)ならば True を返し、週末ならば False を返します。

Function IsWorkday(Optional dteDate As Date) As Boolean
   ' This function determines whether a date
   ' falls on a weekday.

   ' If no date passed in, use today's date.
   If CLng(dteDate) = 0 Then
      dteDate = Date
   End If

   ' Determine where in week the date falls.
   Select Case Weekday(dteDate)
      Case vbMonday To vbFriday
         IsWorkday = True
      Case Else
         IsWorkday = False
   End Select
End Function

日付の個々の部分を返す YearMonthDayWeekday などの関数のほかにも、VBA には DatePart という関数が用意されています。この関数は日付の任意の部分を返すことができます。このような機能は冗長に思われるかもしれませんが、DatePart 関数では週の最初の日と年の最初の日を指定するオプションが付いているため、返される日付値をより細かく制御することが可能です。そのため、自国以外の国や地域のシステム上で実行される可能性のあるコードを書くときに便利なことがあります。また、日付がどの四半期に属しているかという情報を返すために使用できるのは、DatePart 関数だけです。

日付の加算および減算の方法は ?

日付に値を加算するには DateAdd 関数を使います。Date 変数に整数値を加算することも日付の加算と同じことになります。これは、Date 変数の整数部分が 1899 年 12 月 30 日からの経過日数を表しているためです。

DateAdd 関数を使えば、任意の日付に年、月、日、週、または四半期の任意のインターバルを加算できます。次のプロシージャは、任意の日付の記念日を求めます。この年の記念日が既に過ぎている場合、プロシージャはその次の年の記念日の日付を返します。

Function Anniversary(dteDate As Date) As Date
   ' This function finds the next anniversary of a date.
   ' If the date has already passed for this year, it returns
   ' the date on which the anniversary occurs in the following year.

   Dim dteThisYear As Date

   ' Find corresponding date this year.
   dteThisYear = DateSerial(Year(Date), Month(dteDate), Day(dteDate))
   ' Determine whether it's already passed.
   If dteThisYear <Date Then
      Anniversary = DateAdd("yyyy", 1, dteThisYear)
   Else
      Anniversary = dteThisYear
   End If
End Function

2 つの日付の間のインターバルを調べるには DateDiff 関数を使います。返されるインターバルは、日、週、月、年、時など、いくつかの時間単位で表現できます。

次の例は、DateDiff 関数を使い、ある年の特定の日に対応する番号を返します。この値は、その日がその年の何番目であるかを表す 1~365 までの数値です。プロシージャは DateSerial 関数を使い、指定された日付の前の年の最終日を調べたあと、プロシージャに渡された日付からその日付を減算します。

Function DayOfYear(Optional dteDate As Date) As Long

   ' This function takes a date as an argument and returns
   ' the day number for that year. If the dteDate argument is
   ' omitted, the function uses the current date.

   ' If the dteDate argument has not been passed, dteDate is
   ' initialized to 0 (or December 30, 1899, the date
   ' equivalent of 0).
   If CLng(dteDate) = 0 Then
      ' Use today's date.
      dteDate = Date
   End If

   ' Calculate the number of days that have passed since
   ' December 31 of the previous year.
   DayOfYear = Abs(DateDiff("d", dteDate, _
      DateSerial(Year(dteDate) - 1, 12, 31)))
End Function

#10/23/00# という値を指定してこのプロシージャを呼び出すと、"297" という値が返されます。

人の年齢を計算する方法は ?

DateAdd 関数と DateDiff 関数を使うと、2 つの日付の間に経過した時間を計算できます。また、コードを少し追加するだけで、その時間を望みの書式で出力できます。たとえば、人の年齢を年単位で計算する次のプロシージャは、現在の年でその人の誕生日が既に過ぎているかどうかを考慮に入れています。

今日の日付と誕生日との間の年数を DateDiff 関数を使って調べても、必ずしも有効な答えが得られるとは限りません。これは、DateDiff 関数が翌年への繰り上げを行うためです。誕生日がまだ来ていない場合、DateDiff 関数による答えはその人の実際の年齢よりも 1 年多くなります。

これを解決するため、以下のプロシージャではその人の誕生日が今年になって既に過ぎているかどうかを調べ、まだ過ぎていなければ答えから 1 を減算して正しい年齢を返します。

Function CalcAge(dteBirthdate As Date) As Long

   Dim lngAge As Long

   ' Make sure passed-in value is a date.
   If Not IsDate(dteBirthdate) Then
      dteBirthdate = Date
   End If

   ' Make sure birthdate is not in the future.
   ' If it is, use today's date.
   If dteBirthdate> Date Then
      dteBirthdate = Date
   End If

   ' Calculate the difference in years between today and birthdate.
   lngAge = DateDiff("yyyy", dteBirthdate, Date)
   ' If birthdate has not occurred this year, subtract 1 from age.
   If DateSerial(Year(Date), Month(dteBirthdate), Day(dteBirthdate))> Date Then
      lngAge = lngAge - 1
   End If
   CalcAge = lngAge
End Function

参考資料

今回紹介したコード例は、文字列や日付を操作する独自のカスタム関数のライブラリを作成する上で十分役に立つと思います。その他の詳細情報については次の各リンク先をご覧ください。

David Shank は、Office チームにおいて開発者向けのドキュメンテーションを専門とするプログラマーでありライターでもあります。彼はレドモンドの東に位置する山の上で生活していて、ノースウエストには残り少ないネイティブであるという噂です。