VBA100本ノックチャレンジ:21~30本目
独学なりに何回か作ったことのある構成と、ない構成での差が大きくなってきました。
21本目。
#VBA100本ノック 21本目
— エクセルの神髄 (@yamaoka_ss) November 10, 2020
自身(ThisWorkbook)と同じフォルダに"BACKUP"フォルダがあります。
このフォルダ内に自身のバックアップが以下の名称で多数入っています。
ブック名_yyyymmddhhmm.xlsm
実行日を含め30日分だけ残し、古いファイルは削除してください。
※日付はファイル名で判断
Public Sub VBA100本ノック_021()
Dim sBackupFolder As String
sBackupFolder = ThisWorkbook.Path & "\BACKUP"
If Dir(sBackupFolder, vbDirectory) = "" Then
MkDir sBackupFolder
End If
Dim oFSO As Object
Set oFSO = CreateObject("Scripting.FileSystemObject")
Dim sBaseName As String
sBaseName = oFSO.GetBaseName(ThisWorkbook.Name)
Dim sExtensionName As String
sExtensionName = oFSO.GetExtensionName(ThisWorkbook.Name)
Dim oRE As Object
Set oRE = CreateObject("VBScript.RegExp")
oRE.Pattern = "^" & sBaseName & "_+|\." & sExtensionName & "$"
oRE.Global = True
Dim sName As String
Dim varDate As Variant
sName = Dir(sBackupFolder & "\" & sBaseName & "_*." & sExtensionName)
Do Until sName = ""
varDate = oRE.Replace(sName, "")
varDate = Left(varDate, 4) & "年" & Mid(varDate, 5, 2) & "月" & Mid(varDate, 7, 2) & "日"
If IsDate(varDate) Then
If DateDiff("y", CDate(varDate), Date) > 30 Then
oFSO.DeleteFile oFSO.BuildPath(sBackupFolder, sName)
End If
End If
sName = Dir()
Loop
End Sub
前回(16本目)で正規表現(RegExp)のやり方が自力で発想できなかったので、今回ちょっと無理目ながら使用してみた。文頭と文尾をoRE.Replaceで消してしまえば、取得したいyyyymmddhhmmの部分が取り出せる寸法。
しかし何回やっても文頭しか消せず、何でかと思ってふと「oRE.Global = true」を宣言してみたら正解。身になりました。
(Globalプロパティ:検索対象の文字列内で、文字列全体を検索するにはTrueを設定する※デフォルトではFalse)
22本目。
#VBA100本ノック 22本目
— エクセルの神髄 (@yamaoka_ss) November 11, 2020
たまにはちょっと遊んでみましょう!
A列に1からの連番を出力してください。
ただし、
・3で割り切れる場合はB列に"Fizz"を出力。
・5で割り切れる場合はC列に"Buzz"を出力。
・両者で割り切れる場合はD列に"FizzBuzz"を出力。
※出力する数値範囲は適当に pic.twitter.com/wLjCvsKKFY
Public Sub VBA100本ノック_022()
Dim ws As Worksheet
Set ws = ActiveSheet
Const conFizz = "Fizz"
Const conBuzz = "Buzz"
Dim rw As Long
For rw = 1 To 100
Select Case True
Case rw Mod (3 * 5) = 0
ws.Cells(rw, 4).Value = conFizz & conBuzz
Case rw Mod 5 = 0
ws.Cells(rw, 3).Value = conBuzz
Case rw Mod 3 = 0
ws.Cells(rw, 2).Value = conFizz
Case Else
ws.Cells(rw, 1).Value = rw
End Select
Next
End Sub
わあい遊ぶの大好き!
それはともかく、特筆することもないSelectCaseを使ったコードにしました。
他の方の回答で「IIf」を使っているものがあり、前回(20本目)で目にして存在を知ったつもりではいたけれども、自分の発想の中に組み込むのはそうたやすいことではないのだなと再確認。
23本目。
#VBA100本ノック 23本目
— エクセルの神髄 (@yamaoka_ss) November 12, 2020
ThisWorkbookと同一フォルダに"Book_20201101.xlsx"と"Book_20201102.xlsx"の2ファイルがあります。
シート構成(シート名のみ、位置は不問)が一致しているか確認してください。
「一致」または「不一致」の結果をメッセージボックスで表示。
※不一致の詳細は不要。
Public Sub VBA100本ノック_023()
Dim wb1 As Workbook
Set wb1 = getWorkbook(ThisWorkbook.Path & "\Book_20201101.xlsx")
If wb1 Is Nothing Then
MsgBox "ファイル:Book_20201101.xlsxが見つかりません。"
Exit Sub
End If
Dim wb2 As Workbook
Set wb2 = getWorkbook(ThisWorkbook.Path & "\Book_20201102.xlsx")
If wb2 Is Nothing Then
MsgBox "ファイル:Book_20201102.xlsxが見つかりません。"
Exit Sub
End If
Dim dic As Object
Set dic = CreateObject("Scripting.Dictionary")
Dim ws As Worksheet
For Each ws In wb1.Worksheets
dic.Add ws.Name, "不一致"
Next
For Each ws In wb2.Worksheets
If Not dic.Exists(ws.Name) Then
dic.Add ws.Name, "不一致"
Else
dic.Item(ws.Name) = "一致"
End If
Next
Dim buf As String
Dim key As Variant
For Each key In dic.Keys
buf = buf & vbCrLf & key & ":" & dic.Item(key)
Next
MsgBox wb1.Name & "と" & wb2.Name & "のシート構成:" & buf
End Sub
Private Function getWorkbook(sPath As String) As Workbook
Dim wb As Workbook
For Each wb In Workbooks
If wb.Name = Dir(sPath) Then
Set getWorkbook = wb
Exit Function
End If
Next
If Dir(sPath) <> "" Then
Set getWorkbook = Workbooks.Open(sPath)
End If
End Function
Dictionaryを使用。業務上取り扱うデータの関係で、個人的には割とよく使っているつもりです。やっぱりユニーク化できるのがとっても便利。
あと今回は外部Excelファイルが2つだったので、Excelを開く処理をFunction化。これもよく使う。
24本目。
#VBA100本ノック 24本目
— エクセルの神髄 (@yamaoka_ss) November 13, 2020
引数で受け取った文字列に対して、以下の処理を行い文字列で返す関数(Function)を作成してください。
・英小文字は英大文字にする
・全角の英数文字は半角にする
※英数文字とは:A-Z,a-z,1-9
"あいうABCアイウabc123"
↓
"あいうABCアイウABC123"
Public Function VBA100本ノック_024(sWord As String) As String
Dim oRE As Object
Set oRE = CreateObject("VBScript.RegExp")
oRE.Pattern = "[a-za-z0-9]"
oRE.Global = True
oRE.IgnoreCase = True
Dim ret As String
Dim buf As String
If Len(sWord) > 0 Then
Dim i As Long
For i = 1 To Len(sWord)
buf = Mid(sWord, i, 1)
If oRE.Test(buf) Then
buf = StrConv(buf, vbUpperCase + vbNarrow)
End If
ret = ret & buf
Next
End If
VBA100本ノック_024 = ret
End Function
正規表現やっぱり難しい。
回答にSelectCaseを使ったものがあって、Case n To nで間を指定できるのは知っていたけど、使っていたのは数値だけだったので、Case ”a” To "z"とかが有効なのは初めて知りました。意外と便利!
その分、VBSではCase ToもLikeも使えない惜しさが際立つ。
25本目。
#VBA100本ノック 25本目
— エクセルの神髄 (@yamaoka_ss) November 14, 2020
画像1のように「売上」シートに横に日付と金額が入力されています。
行数・列数(日数)は増減します。
A列はセル結合されています。
画像2のようにデータベース形式に変換して「売上DB」シートに出力してください。
※「売上DB」は既存で見出しも入っています。 pic.twitter.com/3fcmsUgPQC
Public Sub VBA100本ノック_025()
Dim wsRec As Worksheet
Set wsRec = ThisWorkbook.Worksheets("売上")
Dim arRec As Variant
arRec = wsRec.Range("A1").CurrentRegion.Value
Dim arDB() As Variant
ReDim arDB(1 To (UBound(arRec, 1) - 1) * (UBound(arRec, 2) - 2), 1 To 4)
Dim rw As Long
Dim cl As Long
Dim cntRow As Long
For rw = 2 To UBound(arRec, 1)
If arRec(rw, 1) = "" Then
arRec(rw, 1) = arRec(rw - 1, 1)
End If
For cl = 3 To UBound(arRec, 2)
cntRow = cntRow + 1
arDB(cntRow, 1) = arRec(rw, 1)
arDB(cntRow, 2) = arRec(rw, 2)
arDB(cntRow, 3) = arRec(1, cl)
arDB(cntRow, 4) = arRec(rw, cl)
Next
Next
Dim wsDB As Worksheet
Set wsDB = ThisWorkbook.Worksheets("売上DB")
wsDB.Range("A1").CurrentRegion.Offset(1, 0).ClearContents
wsDB.Range("A2").Resize(UBound(arDB, 1), UBound(arDB, 2)).Value = arDB
End Sub
業務あるある。この「売上」シートのような状態のデータ、よくあります。何ならまだマシな方。
だいたい配列で処理してしまうことが多いです。取得するデータ(=貼り付けるデータ)が多くてもすっきり。
26本目。
#VBA100本ノック 26本目
— エクセルの神髄 (@yamaoka_ss) November 16, 2020
フォルダ選択のダイアログでフォルダを指定し、フォルダ内にあるファイルの一覧を「ファイル一覧」シートのA列に出力してください。
・ファイル名,更新日時,サイズ※画像参照
・Excelファイル(xls,xlsx,xlsm)にはハイパーリンクを設定
※サブフォルダは不要です。 pic.twitter.com/wocvbZWtyG
Public Sub VBA100本ノック_026()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("ファイル一覧")
Dim sFolderPath As String
With Application.FileDialog(msoFileDialogFolderPicker)
If .Show = True Then
sFolderPath = .SelectedItems(1)
End If
End With
If sFolderPath <> "" Then
Dim oFSO As Object
Set oFSO = CreateObject("Scripting.FileSystemObject")
ws.Range("A1").CurrentRegion.Offset(1, 0).Clear
Dim f As Object
Dim rw As Long
For Each f In oFSO.GetFolder(sFolderPath).Files
rw = ws.Cells(ws.Rows.Count, 1).End(xlUp).Offset(1, 0).Row
Select Case oFSO.GetExtensionName(f.Name)
Case "xls", "xlsx", "xlsm"
ws.Hyperlinks.Add ws.Cells(rw, 1), f.Path, , , f.Name
Case Else
ws.Cells(rw, 1).Value = f.Name
End Select
ws.Cells(rw, 2).Value = f.DateLastModified
ws.Cells(rw, 3).Value = f.Size
Next
End If
End Sub
フォルダ選択ダイアログ、使ったことは一応あるもののやり方はもちろん忘れているのでググりました。
ハイパーリンクもVBAであんまり使うことがないのでググる。FSOは使い慣れているけれどもDateLastModifiedプロパティとか暗記できていないのでこれも。
ハイパーリンクの指定が無ければWSHでDirコマンドとかWhereコマンド飛ばして取得できるかもとか考えたけどどうだろう。(取得できたとしても結局Excelファイルのみまたハイパーリンク貼らないといけなくて面倒なのでやめました)
27本目。
#VBA100本ノック 27本目
— エクセルの神髄 (@yamaoka_ss) November 17, 2020
WEBページからコピーしてシートに貼り付けたら、セルの文字列にハイパーリンクが付いてきました。
ハイパーリンクが付いているセルについて、
・右隣のセルにリンクのURLを出力
・ハイパーリンクを解除
※図は無視してください。
※対象シートは任意 pic.twitter.com/ogSuFLobCa
Public Sub VBA100本ノック_027()
Dim ws As Worksheet
Set ws = ActiveSheet
Dim h As Hyperlink
For Each h In ws.Hyperlinks
If h.Type = msoHyperlinkRange Then
h.Range.Offset(0, 1).Value = h.Address
h.Delete
End If
Next
End Sub
<失念ポイント>Hyperlink.Typeが分からなかったので、「※図は無視してください」の部分を反映できていなかった。
使わない部分は「HyperlinkはTypeプロパティがある」みたいな発想もないので、こういう抜けが発生しがち。
28本目。
#VBA100本ノック 28本目
— エクセルの神髄 (@yamaoka_ss) November 18, 2020
個人別のシートを個人別のブックに分けまます。
シート名は"部署_氏名"です。
ブックと同一フォルダに"部署"フォルダを作成し、シート名をブック名にして出力してください。
"部署1_日本 太郎"→"部署1"フォルダに"部署1_日本 太郎.xlsx"
※再実行を考慮
※対象ブックは任意 pic.twitter.com/M8UZv3Wt2g
Public Sub VBA100本ノック_028()
Dim wb As Workbook
Set wb = ActiveWorkbook
Dim msgError As String
Dim msgExist As String
Dim ws As Worksheet
For Each ws In wb.Worksheets
Dim sFolderPath As String
sFolderPath = ThisWorkbook.Path & "\" & Split(ws.Name, "_")(0)
If Dir(sFolderPath, vbDirectory) = "" Then
MkDir sFolderPath
End If
Dim sFilePath As String
sFilePath = sFolderPath & "\" & ws.Name & ".xlsx"
If Dir(sFilePath) <> "" Then
Dim iAns As Integer
iAns = MsgBox(Dir(sFilePath) & "はすでに存在しています。上書きしますか?", vbOKCancel, "確認")
If iAns = vbOK Then
On Error Resume Next
Kill sFilePath
If Err.Number > 0 Then
msgError = msgError & vbCrLf & "・" & Dir(sFilePath)
End If
On Error GoTo 0
Else
msgExist = msgExist & vbCrLf & "・" & Dir(sFilePath)
End If
End If
If Dir(sFilePath) = "" Then
ws.Copy
ActiveWorkbook.SaveAs sFilePath
ActiveWorkbook.Close
End If
Next
If msgError <> "" Then
msgError = "以下のファイルで上書きできませんでした。" & msgError
End If
If msgExist <> "" Then
msgExist = "以下のファイルは上書きをしませんでした。" & msgExist
End If
If msgError & msgExist = "" Then
Exit Sub
End If
MsgBox msgExist & vbCrLf & vbCrLf & msgError
End Sub
業務で割と使うものなので、今回はきもちエラー回りも記述。しかしこれでも不足はありそう。
29本目。
#VBA100本ノック 29本目
— エクセルの神髄 (@yamaoka_ss) November 20, 2020
ファイル選択ダイアログで画像ファイルを指定し、その画像をアクティブセルにリンクしない図として貼り付けてください。
貼り付けた画像は、縦横比を維持したままセル内に収めてください。
セル内の位置はなるべく真ん中に。
※選択できる拡張子は適当に。 pic.twitter.com/2RBxaN0N83
Public Sub VBA100本ノック_029()
Dim sFilePath As String
sFilePath = Application.GetOpenFilename("画像ファイル(*.png;*.jpg),*.png;*.jpg")
If Dir(sFilePath) = "" Then
Exit Sub
End If
Dim rng As Range
Set rng = ActiveCell
Dim shp As Shape
Set shp = rng.Parent.Shapes.AddPicture(sFilePath, msoFalse, msoCTrue, 0, 0, 0, 0)
With shp
.ScaleHeight 1, msoTrue
.ScaleWidth 1, msoTrue
.LockAspectRatio = msoCTrue
.Placement = xlMoveAndSize
If .Width > rng.Width Then
.Width = rng.Width
End If
If .Height > rng.Height Then
.Height = rng.Height
End If
.Left = rng.Left + (rng.Width - .Width) / 2
.Top = rng.Top + (rng.Height - .Height) / 2
End With
End Sub
ほんっとうに時間かかった。1時間近くやっていたのでは。
画像の追加をVBAでやることがまず業務でなかったので、ググって例文コピペして動作確認から。何とかそれっぽくなったあとで、解答を見ました。
Placementプロパティのみ、参照した例文に記載がなかったので解答をみてから書き足した部分。
あとはおおむね解答に添う記載ができたかと思ったけれども、なぜか解答では「If .Width > rng.Width -2 Then」と「-2」しており、この-2が何に由来するものなのかは解読できませんでした…。
30本目。
#VBA100本ノック 30本目
— エクセルの神髄 (@yamaoka_ss) November 21, 2020
古臭いですが名札を作ることになりました。
「名簿」シートのB列に役職、C列に名前が入っています。
「名簿」から「名札」を作成してください。
「名札」はレイアウト・書式を作成済みですが、行数は毎回変わるので3行目以降は1,2行目からコピーしてください。
※画像参照 pic.twitter.com/9sy7NHwSlL
Public Sub VBA100本ノック_030()
Dim wsList As Worksheet
Set wsList = ThisWorkbook.Worksheets("名簿")
With wsList
Dim rowEnd As Long
rowEnd = .Cells(.Rows.Count, 1).End(xlUp).Row
Dim rowCnt As Long
rowCnt = WorksheetFunction.RoundUp((rowEnd - 1) / 2, 0) * 2
Dim arList() As Variant
ReDim arList(1 To rowCnt, 1 To 2)
Dim rw As Long
Dim cntRow As Long
For rw = 2 To rowEnd
If rw Mod 2 = 0 Then
cntRow = cntRow + 1
arList(cntRow, 1) = .Cells(rw, 2).Value
arList(cntRow + 1, 1) = .Cells(rw, 3).Value
Else
arList(cntRow, 2) = .Cells(rw, 2).Value
arList(cntRow + 1, 2) = .Cells(rw, 3).Value
cntRow = cntRow + 1
End If
Next
End With
Dim wsPlate As Worksheet
Set wsPlate = ThisWorkbook.Worksheets("名札")
With wsPlate
.Cells.ClearContents
If .UsedRange.Rows.Count > 3 Then
.UsedRange.Offset(2, 0).Clear
End If
With .Range("A1").Resize(UBound(arList, 1), UBound(arList, 2))
.Value = arList
.Resize(2, 2).Copy
.PasteSpecial xlPasteFormats
Application.CutCopyMode = False
End With
End With
End Sub
意外と名札を作る機会はあります。名札じゃなくても、封筒に貼る住所で名札シール使ってたりもするので。
そういうのは大体1ページあたりの名札枚数が決まっているので、数式を組み込んでおいてVBAで行だけずらすみたいなやり方をすることが多いです。なのでフォーマットもいじらない。
今回はフォーマットも臨機応変にということなので、このような感じに。配列を使うとちょっといきいきします。
■30本目を終えて
「あ!これ業務でやったことある!」な設問と、「全然使ったことないわ…」な設問での差が個人的には大きかった。もちろん発想としては不十分な箇所はあるとはいえ、使い慣れているというか実際の使用場面に心当たりがあるのでエラー原因にも思い至りやすいようで。
VBA自体の経験をもっと積めば、使ったことのない動作に関しても「これにはこういうプロパティ/メソッドがあるはず…」とか予測して色々考えられたりできるようになるでしょうか。ううん。奥が深い。