Excel&VBA解説サイト「エクセルの神髄」様出題の問題集、
「VBA100本ノック」に対する私の回答と解説のページです。
100本ノックの出題リストはこちらから
excel-ubara.com
出題:番号リストを簡潔にした文字列で返す
#VBA100本ノック 58本目
配列と数値nを受け取り、配列の番号リストを簡潔にした文字列で返すFunctionを作成します。
n連続以上の場合は「開始-終了」に変換した上でカンマ区切りで結合。
配列={1,2,3,5,8,9,11,12,13,14,15,17,19,20,21,22}
n=2→"1-3,5,8-9,11-15,17,19-22"
※結果は画像を参考に

◇ 出題ページはこちら
ソースコード
Option Explicit ' 100本ノック058:番号リストを簡潔にした文字列で返す Function 番号リストを連続部分をまとめた文字列に変換(Arr番号リスト As Variant, 連続条件 As Long) As String Dim 結果文字列 As String ' 連続が開始された値を記憶しておき、連続が途切れた際に記憶した開始値からそこまでを文字列化する Dim 現在の値 As Long, ひとつ前の値 As Long, 連続開始値 As Long ' 初項を初期値に代入 ひとつ前の値 = Arr番号リスト(LBound(Arr番号リスト)) 連続開始値 = Arr番号リスト(LBound(Arr番号リスト)) ' 2番目以降をループ Dim i As Long For i = LBound(Arr番号リスト) + 1 To UBound(Arr番号リスト) 現在の値 = Arr番号リスト(i) ' 連続が途切れたらひとつ前までを出力し、現在の値を開始値として記憶 If 現在の値 <> ひとつ前の値 + 1 Then 結果文字列 = 結果文字列 & "," & Get連続部分の文字列(連続開始値, ひとつ前の値, 連続条件) 連続開始値 = 現在の値 End If ひとつ前の値 = 現在の値 Next ' 最後の残りを出力 結果文字列 = 結果文字列 & "," & Get連続部分の文字列(連続開始値, ひとつ前の値, 連続条件) ' 最初のカンマを消して結果を返す 番号リストを連続部分をまとめた文字列に変換 = Mid(結果文字列, 2) End Function ' 連続部分の文字列化 Private Function Get連続部分の文字列(開始 As Long, 終了 As Long, 連続条件 As Long) As String ' 1-3 If 終了 - 開始 + 1 >= 連続条件 Then Get連続部分の文字列 = 開始 & "-" & 終了 ' 1,2,3 Else Dim i As Long For i = 開始 To 終了 Get連続部分の文字列 = Get連続部分の文字列 & "," & i Next Get連続部分の文字列 = Mid(Get連続部分の文字列, 2) End If End Function
解説
数値が連続で登場した部分を「1-3」のようにつなげて返す問題です。
いろいろなやり方があると思いますが、本解答ではコメントの通り、
- 連続が途切れた部分を開始値として記憶しておく
- 連続が途切れた際に、記憶した開始値からそこまでの値を文字列化する
というロジックで処理を組んでいます。
この手のロジックは「初項」「最終項」の扱いが少々面倒で、
- 初項をループに入れるとひとつ前の値がない
- 最終項をループに入れると次の値がない
という問題が起き、If文がわかりにくくなってしまいます。
ということで、本解答ではどちらもループの外で処理するコードになっています。
※ 最終項はFor文の中に入っていますが、これは連続判定のためであり、
出力はひとつ前の値までしか行っていません。
こうするとコード自体は結構すっきりしてくれるのですが、
ループが終わった後で最後にもう一度変換をしなければいけなくなるため、
どうしてもコードが重複する部分が出てきてしまいます。
' ループ中の出力部分 If 現在の値 <> ひとつ前の値 + 1 Then 結果文字列 = 結果文字列 & "," & Get連続部分の文字列(連続開始値, ひとつ前の値, 連続条件) ' ↑ このコードを最終項に実施することができない End If Next ' ループの外でもう一度実行する必要が出てしまう。 結果文字列 = 結果文字列 & "," & Get連続部分の文字列(連続開始値, ひとつ前の値, 連続条件)
もどかしいですが、これをどうにかしようとしても今度は「i > UBound(Arr)」のような最終項を超えてしまった用の判定をループ内に書く必要がでてしまいますからね。
個人的にはここを関数化することでせめて1行にし、
ループ内とループ終了後に2度記述するのが一番わかりやすいかなと思います。
絶対的な正解はないと思いますので、一つの参考にしてください。
一旦コレクションにするバージョン
上記の解答ではループ内で順次文字列を作っていきましたが、
「番号リストの構造を一旦配列やコレクションにして整理し、最後に文字列化を行う」
という方法もあります。
例えば「1,2,3,6,7,10,11,12」というリストを、
- 「1,2,3」
- 「6,7」
- 「10,11,12」
という3ブロックに分けて格納し、最後に各ブロックを文字列化するという処理です。
このような整理を行う場合は「配列の配列」いわゆるジャグ配列にしたり、
コレクションの中にコレクションを入れ子にする構造をとることになります。
神髄先生の解答にジャグ配列が使われていましたので、
こちらでは「コレクション In コレクション」を使った解答を置いておきます。
参考にしてみてください。
ちなみに配列を用いるかコレクションを用いるかは好みですが、
ExcelVBAの配列は機能が貧弱なためコレクションにするのがおすすめです。
VBAの配列にはCountもAddもないため、
UBound-LBound+1、ReDim Preserveを連発する羽目になりますからね。
コレクションにすることで、
- UBound-LBound+1 ⇒ Count
- ReDim Preserveしてから第Ubound項へ代入 ⇒ Add
で済むようになりコードが相当簡潔になります。
特に後者のコードに圧倒的な差がありますので見比べてみてください。
Option Explicit ' 100本ノック058:番号リストを簡潔にした文字列で返す_コレクション版 Function 番号リストを連続部分をまとめた文字列に変換_コレクション版(Arr番号リスト As Variant, 連続条件 As Long) As String ' 番号リストを連続部分ごとに分けた コレクション In コレクション に格納する Dim Clct連続番号リスト As New Collection Dim 現在の値 As Long Dim ひとつ前の値 As Long: ひとつ前の値 = WorksheetFunction.Max(Arr番号リスト) + 1 ' 番号リストをループ Dim i As Long For i = LBound(Arr番号リスト) To UBound(Arr番号リスト) 現在の値 = Arr番号リスト(i) ' 連続が途切れたら新規コレクションを追加 If 現在の値 <> ひとつ前の値 + 1 Then Clct連続番号リスト.Add New Collection End If ' 最後のコレクションに現在の値を追加 Clct連続番号リスト(Clct連続番号リスト.Count).Add 現在の値 ひとつ前の値 = 現在の値 Next ' 出来上がったコレクションを文字列へ変換 Dim 結果値 As String Dim 各コレクション As Collection For Each 各コレクション In Clct連続番号リスト 結果値 = 結果値 & "," & Get連続部分の文字列(各コレクション, 連続条件) Next ' 最初のカンマを消して結果を返す 番号リストを連続部分をまとめた文字列に変換_コレクション版 = Mid(結果値, 2) End Function ' 連続部分の文字列化 Private Function Get連続部分の文字列(Clct連続部分 As Collection, 連続条件 As Long) As String ' 1-3 If Clct連続部分.Count >= 連続条件 Then Get連続部分の文字列 = Clct連続部分(1) & "-" & Clct連続部分(Clct連続部分.Count) ' 1,2,3 Else Dim 数値 For Each 数値 In Clct連続部分 Get連続部分の文字列 = Get連続部分の文字列 & "," & 数値 Next Get連続部分の文字列 = Mid(Get連続部分の文字列, 2) End If End Function