VBA100本ノックチャレンジ:51~60本目

ますます苛酷になっていく。

51本目。

Public Sub VBA100本ノック_051()

   Const WSNAME As String = "目次"

   Dim wsResult As Worksheet
   On Error Resume Next
   Set wsResult = ThisWorkbook.Worksheets(WSNAME)
   If Err.Number > 0 Then
       Set wsResult = ThisWorkbook.Worksheets.Add(ThisWorkbook.Worksheets(1))
       wsResult.Name = WSNAME
   End If
   wsResult.Range("A:B").Clear
   wsResult.Range("A1:B1").Value = Array("シート名", "総ページ数")
   On Error GoTo 0
   
   Dim ws As Worksheet
   For Each ws In ThisWorkbook.Worksheets
       With wsResult
           With .Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0)
               .Value = "'" & WorksheetFunction.Clean(ws.Name)
               If ws.Visible = xlSheetVisible Then
                   wsResult.Hyperlinks.Add Anchor:=.Cells, Address:="", SubAddress:="'" & ws.Name & "'!A1"
                   .Offset(0, 1).Value = ws.PageSetup.Pages.Count
               End If
           End With
       End With
   Next

End Sub

やっていることは概ね外していない(Hyperlinksを使うなど)ものの、「シート名にはシングルコーテーション(')が使える」という点を失念しているため、これだとシート名に「'」使ってると参照不可になっちゃうのでReplaceでシート名中の「'」を「''」に置き換える必要があります。
(全角の「’」も引っかかるのでこれも同じように対応)

52本目。

Public Sub VBA100本ノック_052()

   Dim wb As Workbook
   Set wb = ThisWorkbook
   
   Dim arSht() As String
   Dim cnt As Long
   cnt = -1
   
   Dim ws As Worksheet
   For Each ws In wb.Worksheets
       If ws.Name Like "*印刷*" Then
           cnt = cnt + 1
           ReDim Preserve arSht(cnt)
           arSht(cnt) = ws.Name
       End If
   Next
   
   If cnt > -1 Then wb.Sheets(arSht).PrintPreview
   
End Sub

※arShtへの格納方法については後半の設問で別のやり方を学ぶものの、現時点ではこの通りで記載。

「非表示シートを含めるとエラーになる」ということをすっかり忘れているので、非表示シートが含まれていると止まっちゃう中途半端プロシージャ。
この「非表示シート」周りの処理にいつもすっころんでいる気がする……学習していない……由々しき問題。凹みです。

53本目。

Public Sub VBA100本ノック_053()

   Dim ws As Worksheet
   Set ws = ActiveSheet
   
   Dim oList As ListObject
   Set oList = ws.ListObjects(1)
   
   With oList
       .Range.AutoFilter Field:=2, Criteria1:="男"
       .Range.AutoFilter Field:=3, Criteria1:="<=" & DateSerial(Year(Date) - 35, 12, 31)
       .Range.AutoFilter Field:=4, Criteria1:="東京都"
       
       .ListColumns(5).Range.Resize(.ListRows.Count - 1, 1).Offset(1, 0).Value = "対象"
       .ShowAutoFilter = False
   End With
   
End Sub

これでもできなくはないですが、「DataBodyRange」使った方がField指定や範囲指定がずっとやりやすそうですよねやっぱり。あれこれするにもやっぱり見出し行除いた範囲になるもの……。

ListObject.DataBodyRange プロパティ (Excel)
テーブルの見出しの行を除く、値の範囲を表す Range オブジェクトを返します。 読み取り専用です。

54本目。

Private Sub Worksheet_Change(ByVal Target As Range)
   
   With Target
       If .Count > 1 Then Exit Sub
       If .Row = 1 Then Exit Sub
       If Me.Cells(1, .Column).Value <> "商品コード" Then Exit Sub
       
       Application.EnableEvents = False
       .NumberFormatLocal = "@"
       
       Dim rngMasta As Range
       Set rngMasta = ThisWorkbook.Worksheets("マスタ").Range("A1").CurrentRegion
           
       On Error Resume Next
       .Font.ColorIndex = xlAutomatic
       .Offset(0, 1).Value = WorksheetFunction.VLookup(.Value, rngMasta, 2, 0)
       .Offset(0, 3).Value = WorksheetFunction.VLookup(.Value, rngMasta, 3, 0)
       If Err.Number > 0 Then
           .Offset(0, 1).ClearContents
           .Offset(0, 3).ClearContents
           .Font.Color = vbRed
       End If
       On Error GoTo 0
       
       Application.EnableEvents = True
   End With
   
End Sub

B列に1セルずつ手入力する想定でしたけど、他のところから複数行分をB列にコピペしてくる場合とかも考えたら「If .Count > 1 Then Exit Sub」で弾かないでTargetに対してFor Eachで処理した方が確実ですよね。
この辺りは「設計」みたいな話もちょっとずつ関わってくるんだろうか。ユーザーの動作について考えなきゃいけない的な……。

※余談※
実務でちょうど「A列へ契約番号をユーザーに入力させるが、重複があるとその後の処理でエラーになるため入力時に重複がある場合はユーザーへ視覚的に通知、修正させる」みたいな入力シートを作成しました。
某ゼミならぬ「100本ノックでやったところだ!」となりつつユーザーの入力や削除が複数行になったときの対応は一応組めましたが、ユーザーに分かりやすいようになっているかは分からないです……。

55本目。

Public Sub VBA100本ノック_055()
   
   Dim pathTest As String
   pathTest = ThisWorkbook.Path & "\test.xlsm"
   If Dir(pathTest) = "" Then Exit Sub
   
   On Error Resume Next
   Application.EnableEvents = False
   Application.ScreenUpdating = False
   
   Dim wbTest As Workbook
   Set wbTest = Application.Workbooks.Open(pathTest)
   
   Dim Ret As Variant
   Ret = Application.Run("'" & wbTest.Name & "'!mult", 2, 10)
   
   wbTest.Close 0
   MsgBox Ret
   
   Application.EnableEvents = True
   Application.ScreenUpdating = True
   
End Sub

まったく同じではないものの、大体解答と同じになっているはず?
表示すべき「結果」がエラーも含むのであれば、MsgBox IIf(Err.Number = 0, Ret, Err.Description)にするとかでしょうか。

56本目。

Public Sub VBA100本ノック_056()
   
   Dim myCalc As XlCalculation
   myCalc = Application.Calculation
   Application.Calculation = xlCalculationManual
       
   Dim oRE As Object
   Set oRE = CreateObject("VBScript.RegExp")
   oRE.Global = True
   
   On Error Resume Next
   Dim rngArea As Range
   Dim ws As Worksheet
   For Each ws In ThisWorkbook.Worksheets
       oRE.Pattern = "('" & ws.Name & "'!|" & ws.Name & "!)"
       
       Set rngArea = Nothing
       Set rngArea = ws.UsedRange.SpecialCells(xlCellTypeFormulas)
       
       If Not rngArea Is Nothing Then
           Dim r As Range
           For Each r In rngArea
               If oRE.Test(r.Formula) Then
                   If Not r.HasArray And Not r.HasSpill Then
                       r.Formula = oRE.Replace(r.Formula, "")
                   End If
               End If
           Next
       End If
   Next
   
   Application.Calculate
   Application.Calculation = myCalc

End Sub

死ぬほど頭を抱えた。しばらく頭を抱えっぱなしだった。

シート名をどうやって抽出するかなという……悩みの末にRegExpでシングルコーテーションで囲まれてるパターンとないパターンを使ったわけですが。
そして解答を見てもたらされる「シート名にvbTab使えるんだ!?」という驚き。使ってみたらなるほど使えるし、問答無用でシート名にシングルコーテーションつくから表記を統一できますね。
自力で気づける範疇なのかこれは……。

57本目。

Public Sub VBA100本ノック_057()
   
   Dim oFSO As Object
   Set oFSO = CreateObject("Scripting.FileSystemObject")
       
   Dim sPath As String
   sPath = ThisWorkbook.Path & "\BACKUP"
   
   Dim oDicTime As Object
   Set oDicTime = CreateObject("Scripting.Dictionary")
   
   Dim oDicPath As Object
   Set oDicPath = CreateObject("Scripting.Dictionary")
   
   Dim f As Object
   For Each f In oFSO.GetFolder(sPath).Files
       Dim dt As Date
       Dim tm As Date
       dt = CDate(Format(f.DateLastModified, "yyyy/mm/dd"))
       tm = CDate(Format(f.DateLastModified, "hh:mm:ss"))
       
       If oDicTime.Exists(dt) Then
           If oDicTime.Item(dt) < tm Then
               oFSO.DeleteFile oDicPath.Item(dt)
               oDicPath.Item(dt) = f.Path
               oDicTime.Item(dt) = tm
           Else
               oFSO.DeleteFile f.Path
           End If
       Else
           oDicTime.Add dt, tm
           oDicPath.Add dt, f.Path
       End If
   Next
   
End Sub

これも解答と同じくDictionary使ったやつですね。その場で比較して削除していくので、削除失敗時(ファイル使用中等)に対応できていないからそこは勿論甘い。
あと2つDictionary使いましたけど、Itemには解答のように配列も指定できるのでそうすれば1つで済んでもっとすっきりしますね。やればやるほど自分の至らなさが浮き彫りで落ち込んでくるな100本ノック……。

58本目。

Public Function VBA100本ノック_058(ByVal ary As Variant, ByVal n As Long) As String
   
   Dim buf As String
   buf = ary(LBound(ary))
   
   Dim i As Long
   For i = LBound(ary) + 1 To UBound(ary)
       buf = buf & IIf(ary(i - 1) + 1 = ary(i), "-", ",") & ary(i)
   Next
   
   Dim arTemp As Variant
   arTemp = Split(buf, ",")
   
   For i = LBound(arTemp) To UBound(arTemp)
       Dim a As Variant
       a = Split(arTemp(i), "-")
       
       If UBound(a) >= n - 1 Then
           arTemp(i) = a(0) & "-" & a(UBound(a))
       Else
           arTemp(i) = Replace(arTemp(i), "-", ",")
       End If
   Next
   
   VBA100本ノック_058 = Join(arTemp, ",")
   
   ' ジャグ配列 http://wisdom.sakura.ne.jp/programming/cs/cs37.html
   
End Function

ジャグ配列とか知りませんでした。
なので力業で、とりあえず前の値+1と今の値が一致したら「-」、それ以外は「,」で区切りつつ並べ、それを「,」でSplitした配列に対して1つずつ「-」で区切ったときに要素数がn以上となれば最小値と最大値を「-」で結んだものを値とし、それ未満の配列は「-」を「,」に置換、最後にJOINで文字列化。
お恥ずかしい話、JOINは前回のノックでちゃんとやったはずなのにしっかりばっちり忘れていました……暗記が死ぬほど苦手を自認しているんですけど単にやる気がないだけなんだろうかもしかして……。

ジャグ配列
配列の中に配列を収めるという概念が存在します
(中略)
この「配列の配列」のことをジャグ配列と呼びます

59本目。

Public Sub VBA100本ノック_059()
   
   On Error Resume Next
   Application.ScreenUpdating = False

   Dim wb As Workbook
   Set wb = ThisWorkbook
   
   Dim arSht(1 To 4) As Variant
   Dim iQ As Long
   Dim ws As Worksheet
   For Each ws In wb.Worksheets
       Select Case Month(CDate(ws.Name & "1日"))
           Case 4 To 6:   iQ = 1
           Case 7 To 9:   iQ = 2
           Case 10 To 12: iQ = 3
           Case 1 To 3:   iQ = 4
       End Select
       
       If Not IsArray(arSht(iQ)) Then
           arSht(iQ) = Array(ws.Name)
       Else
           Dim arTemp As Variant
           arTemp = arSht(iQ)
           ReDim Preserve arTemp(UBound(arTemp) + 1)
           arTemp(UBound(arTemp)) = ws.Name
           arSht(iQ) = arTemp
       End If
   Next
   
   Dim i As Long
   For i = 1 To 4
       wb.Worksheets(arSht(i)).Copy
       
       Dim sFile As String
       Dim cnt As Long: cnt = 0
       Do
           cnt = cnt + 1
           sFile = wb.Path & "\" & i & "Q" & IIf(cnt = 1, "", "(" & cnt & ")") & ".xlsx"
       Loop Until Dir(sFile) = ""
       
       ActiveWorkbook.SaveAs sFile
       ActiveWorkbook.Close 0
   Next
   
   Application.ScreenUpdating = True

End Sub

せっかくなので直前にもやったジャグ配列を使いました!すぐに復習できるのは良いこと。
日付シートがちゃんとしてない場合とか他にもシートがある場合とかいろいろもろもろ考えられていない部分はありますが、もうどこからどこまでが想像力で補えるところなのか、思いつくエラー対策を100本ノックでどんなに長くなってもちゃんと記述するべきなのか、とかちょっと悩んできました。

60本目。

Public Function VBA100本ノック_060(ByVal Arg As String) As String
   
   Const STR_BEFORE As String = "●"
   Const STR_AFTER  As String = "株式会社"
   
   If Len(Arg) = 0 Then Exit Function
   
   Dim i As Long
   For i = 1 To Len(Arg)
       Select Case WorksheetFunction.Unicode(Mid(Arg, i, 1))
           Case 12849, 13183
               Mid(Arg, i, 1) = STR_BEFORE
       End Select
   Next
   
   Dim oRE As Object
   Set oRE = CreateObject("VBScript.RegExp")
   oRE.Pattern = ".{0,1}株.{0,1}"
   oRE.Global = True
       
   Dim m As Object
   For Each m In oRE.Execute(Arg)
       If StrConv(Left(m.Value, 1), vbNarrow) = "(" Or StrConv(Right(m.Value, 1), vbNarrow) = ")" Then
           Arg = Replace(Arg, m.Value, STR_BEFORE)
       End If
   Next

   VBA100本ノック_060 = Replace(Arg, STR_BEFORE, STR_AFTER)
   
   Set oRE = Nothing

End Function

別に使い慣れてもいないのですがRegExpで頭捻って書きました。
Replace関数の引数でvbTextCompare指定するの(大小・全半区別なし)紹介してましたっけ、すっかり忘れている……というかできないかググったのに見つけられなかったから探し方も良くないということに。つらい。

「㍿」はツイに記載があったものの、「㏍」「㊑」はそんな表記があるの知らなかったのですけど、「かぶしきがいしゃ」と打って変換すれば出てくるのでちゃんと探さなかったのが悪い……のか?

ChrW関数はともかくとして、解答で「ChrW(&H337F)」とかなっている引数の値をどうやって求めたらいいんだろう、というのが気になりました。ググってみると16進数。なるほど、じゃあ10進数を16進数にするには……とググったらありました。

Hex
指定された値を 16 進数で表した文字列で返します。

VBScript関数リファレンスだけど、Excelの関数にあったりしないかなーと適当なセルに「=hex」と入力してみると色々候補が……そしてやっぱりありました!

DEC2HEX 関数
10 進数を 16 進数に変換します。

というわけでちゃんと自力で対象の文字列のコード値を確認することができました!

画像1

これはちょっと嬉しい。

■60本目を終えて。

難易度が増すごとに自己肯定感が下がっていく恐ろしい100本ノック。(※ごくごく個人の感想です)
無論大変勉強にはなるのですが、問題を通して自分に内在する「ExcelVBAでプログラミングしていく上での課題」を掘り起こされることでもあり、「なるほどー!こんなやり方があるんだー!」という前向きな気持ちの隙間からその暗雲たる思いが覗いてきてつらい。
でも最後までやり切りたいなという気持ちはずっとあるので、しんどかろうがもう少し頑張ります。

あと40本!