VBA100本ノックチャレンジ:41~50本目
久々に着手。ようやく折り返しですけど正直めっちゃしんどい!!
41本目。
#VBA100本ノック 41本目
— エクセルの神髄 (@yamaoka_ss) December 7, 2020
暗算練習アプリを作成します。
・整数2個と+-*/の演算子をランダムに選ぶ
・問題をInputBoxに表示
・入力値を採点
・全10問、最後に10点満点で点数をMsgBox表示
・キャンセルや未回答は次の問題に進む
※整数の範囲については暗算できる範囲で随意
※添付GIFを参考に pic.twitter.com/6XyE5GgRFB
Public Sub VBA100本ノック_041()
Dim cntCorrected As Long
Dim i As Long
For i = 1 To 10
Dim intL As Long
Dim intR As Long
Dim Operator As String
Dim Result As Long
Select Case getRandomInteger(3)
Case 0
intL = getRandomInteger(100, 1)
intR = getRandomInteger(100, 1)
Operator = "+"
Result = intL + intR
Case 1
intL = getRandomInteger(100, 10)
intR = getRandomInteger(intL - 1, 1)
Operator = "-"
Result = intL - intR
Case 2
intL = getRandomInteger(100, 1)
intR = getRandomInteger(10, 2)
Operator = "×"
Result = intL * intR
Case 3
intR = getRandomInteger(10, 2)
intL = intR * getRandomInteger(10, 2)
Operator = "÷"
Result = intL / intR
End Select
Dim Answer As Variant
Answer = Val(InputBox("第" & i & "問目" & vbCrLf & vbCrLf & intL & Operator & intR))
If Result = Answer Then cntCorrected = cntCorrected + 1
Next
Dim Message As String
Message = "発表!" & vbCrLf & vbCrLf & "正解数:" & cntCorrected & vbCrLf & vbCrLf
Select Case cntCorrected
Case 0: Message = Message & "計算をもっとがんばろう!"
Case 1 To 3: Message = Message & "もう少しがんばってみよう!"
Case 4 To 6: Message = Message & "いい感じ!まだまだがんばろう!"
Case 7 To 9: Message = Message & "惜しい!あとちょっと!"
Case 10: Message = Message & "よくできました!"
End Select
MsgBox Message
End Sub
Private Function getRandomInteger(intMax As Long, Optional intMin As Long = 0) As Long
getRandomInteger = Int(Rnd() * (intMax - intMin + 1)) + intMin
End Function
長い!ごくシンプルに「Evaluate」を知りませんでした。あるといいのになあと思っていたらあるんですねこういうの。
あとRnd()は知ってたんですけど、「Randomizeステートメント」も知らなかったです。
・Evaluateメソッド(文字列の数式を実行します)
・Randomizeステートメント
42本目。
#VBA100本ノック 42本目
— エクセルの神髄 (@yamaoka_ss) December 8, 2020
画像1のように「階層」シートに階層を表したデータがあります。
これを画像2のように「階層DB」シートにデータベース形式に変換して出力してください。 pic.twitter.com/WdxKggsXi3
Public Sub VBA100本ノック_042()
Dim wsKaiso As Worksheet
Set wsKaiso = ThisWorkbook.Worksheets("階層")
Dim arKaiso As Variant
arKaiso = wsKaiso.Range("A1").CurrentRegion.Value
Dim arKaisoDB() As Variant
ReDim arKaisoDB(LBound(arKaiso, 1) To UBound(arKaiso, 1), LBound(arKaiso, 2) To UBound(arKaiso, 2))
Dim cnt As Long
Dim rw As Long
Dim cl As Long
For rw = 2 To UBound(arKaiso, 1)
For cl = 1 To 3
If arKaiso(rw, cl) = "" Then
arKaiso(rw, cl) = arKaiso(rw - 1, cl)
End If
Next
If arKaiso(rw, 4) <> "" Then
cnt = cnt + 1
For cl = 1 To UBound(arKaisoDB, 2)
arKaisoDB(cnt, cl) = arKaiso(rw, cl)
Next
End If
Next
Dim wsKaisoDB As Worksheet
Set wsKaisoDB = ThisWorkbook.Worksheets("階層DB")
With wsKaisoDB
.Cells.ClearContents
wsKaiso.Rows(1).Copy .Rows(1)
Application.CutCopyMode = False
.Range("A2").Resize(UBound(arKaisoDB, 1), UBound(arKaisoDB, 2)).Value = arKaisoDB
End With
End Sub
これは完全に手癖。何かというと配列で済まそうとしちゃうから、やっぱり長くなっちゃいますね。
こればっかりは駄目だと思って、以前「空白セルを選択して一個上のセル参照すればいいよ」というのを思い出して別解も記載。
Public Sub VBA100本ノック_042_Other()
Dim wsKaiso As Worksheet
Set wsKaiso = ThisWorkbook.Worksheets("階層")
Dim wsKaisoDB As Worksheet
Set wsKaisoDB = ThisWorkbook.Worksheets("階層DB")
wsKaisoDB.Cells.ClearContents
Dim rngBlank As Range
Set rngBlank = wsKaiso.Range("A1").CurrentRegion.Resize(, 3)
Set rngBlank = rngBlank.SpecialCells(xlCellTypeBlanks)
rngBlank.Formula2R1C1 = "=R[-1]C"
wsKaiso.Range("A1").AutoFilter 4, "<>"
wsKaiso.Range("A1").CurrentRegion.Copy wsKaisoDB.Range("A1")
Application.CutCopyMode = False
wsKaiso.AutoFilterMode = False
rngBlank.ClearContents
End Sub
なお正式な回答はさらにシンプルで、こういうのが考えつかないんだよなーと悔しい思いをします。厄介なタイプの負けず嫌い……。
43本目。
#VBA100本ノック 43本目
— エクセルの神髄 (@yamaoka_ss) December 9, 2020
表範囲をCSV出力してください。
・A列:yyyy-mm-dd
・B列:カンマなし整数
・C列:カンマなし少数2桁
・D列:文字列 (適宜ダブルクォートで囲む)
※画像はあくまで1例です。
CSVファイルはブックと同一フォルダに出力(SJIS)。
※出力ファイルおよびブック・シートの指定は任意 pic.twitter.com/TeWiEL214Y
Public Sub VBA100本ノック_043()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("CSV")
With ws
.Columns(1).NumberFormatLocal = "yyyy-mm-dd"
.Columns(2).NumberFormatLocal = "0"
.Columns(3).NumberFormatLocal = "0.00"
.Columns(4).NumberFormatLocal = "@"
End With
Dim sBase As String
sBase = ThisWorkbook.FullName
sBase = Left(sBase, InStrRev(sBase, ".") - 1)
Dim cnt As Long
Dim sPath As String
Do Until Dir(sPath) = ""
sPath = sBase & IIf(cnt = 0, "", "(" & cnt & ")") & ".csv"
cnt = cnt + 1
Loop
ws.Copy
ActiveWorkbook.SaveAs Filename:=sPath, FileFormat:=xlCSV, Local:=True
ActiveWorkbook.Close
End Sub
実務ではあんまりCSV化しないので、マクロ記録から。前回覚えたIIFを使いたくてちょくちょく挟んでいます。便利で好き。
44本目。
#VBA100本ノック 44本目
— エクセルの神髄 (@yamaoka_ss) December 11, 2020
ブック内の全シート全テーブルについて、以下の情報をシートに出力してください。
・A列にテーブル名(テーブル1)
・B列にシート名(Sheet1)
・C列にセル範囲($B$2:$F$12)
・D列にリスト行数(10)
・E列にリスト列数(5)
※()内は画像の出力例
※対象ブック及び出力シートは任意 pic.twitter.com/EvCR2xfUCU
Public Sub VBA100本ノック_044()
Dim wsResult As Worksheet
Set wsResult = ThisWorkbook.Worksheets("044")
With wsResult
.Cells.ClearContents
.Range("A1").Value = "テーブル名"
.Range("B1").Value = "シート名"
.Range("C1").Value = "セル範囲"
.Range("D1").Value = "リスト行数"
.Range("E1").Value = "リスト列数"
End With
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
If ws.Name <> wsResult.Name Then
Dim oList As ListObject
For Each oList In ws.ListObjects
With wsResult
With .Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0)
.Offset(0, 0).Value = oList.Name
.Offset(0, 1).Value = oList.Parent.Name
.Offset(0, 2).Value = oList.Range.Address
.Offset(0, 3).Value = oList.ListRows.Count
.Offset(0, 4).Value = oList.ListColumns.Count
End With
End With
Next
End If
Next
End Sub
テーブル使ったことありません!由々しき事態。VBA以前にテーブルの使い方を覚えるべき…。
これも使ったことがなかったので、マクロ記録で「どうやら【ListObject】で管理されてるらしい」と判断、変数で宣言して自動メンバからいろいろプロパティを選び、Debug.Printしてみて何のプロパティか確認したりしました。ググった方が勿論早いけど。
本筋には関係ないんですけど、今まで1行目に項目名反映するときは上のコードみたいに書いてたんですが、
Range("A1:C1").Value = Array("ID", "氏名", "年齢")
みたいに書いても同じって、配列を値に代入するのよくやってるくせに今まで全然気づかなくて「ウワッ!!!」てなりました。こういうところがシンプル化ポイント……!
45本目。
#VBA100本ノック 45本目
— エクセルの神髄 (@yamaoka_ss) December 12, 2020
シートにB2から始まる5列(列1,列2,列3,列4,列5)のテーブルあります。
・列3の後ろに列挿入して列1から列3の合計列を作成、見出しは"合計列1"
・テーブルの右端に列4から列5の合計列を作成、見出しは"合計列2
出来上がりは画像を参照してください。
※シートは任意 pic.twitter.com/vxd3iGjwHs
Public Sub VBA100本ノック_045()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("045")
Dim oList As ListObject
Set oList = ws.ListObjects(1)
With oList
On Error Resume Next
.ListColumns("合計列1").Delete
.ListColumns("合計列2").Delete
On Error GoTo 0
.ListColumns.Add(6).Range(1) = "合計列2"
.ListColumns.Add(4).Range(1) = "合計列1"
.ListColumns(4).Range(2) = "=SUM([@列1]:[@列3])"
.ListColumns(7).Range(2) = "=SUM([@列4]:[@列5])"
End With
End Sub
普通にググりました。ほぼコピペ。
列を追加/削除する
そして公式回答がものすごくすっきり……ちょっと凹んじゃうくらい……綺麗に分かりやすくコード書くのって本当に難しい。
46本目。
#VBA100本ノック 46本目
— エクセルの神髄 (@yamaoka_ss) December 14, 2020
1行目の見出しをそのセルの名前定義(ブック範囲)に設定してください。
・記号や空白は"_"に置換
・先頭使用不可文字の場合は先頭に"_"を補う
エラー回避できない場合はイミディエイトに出力
※見出しは重複しないように入力
※既存の名前定義は無視して良い
※シートは任意 pic.twitter.com/8r5IZ3burQ
Public Sub VBA100本ノック_046()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("名前定義")
Dim rngArea As Range
Set rngArea = ws.Range("A1").CurrentRegion
Dim myName As String
Dim r As Range
For Each r In rngArea
If Len(r.Value) = 0 Then GoTo Next_Handle
If LenB(r.Value) > 255 Then
Call ps_DebugPrint046(r.Value, "定義できるのは半角で数えて255文字まで")
GoTo Next_Handle
End If
If StrConv(r.Value, vbNarrow + vbLowerCase) Like "[cr]" Then
Call ps_DebugPrint046(r.Value, "r,R,c,Cいずれか1文字のみの場合は使用できない")
GoTo Next_Handle
End If
Dim i As Long
myName = ""
For i = 1 To Len(r.Value)
Dim buf As String
buf = myName & Mid(r.Value, i, 1)
buf = Replace(buf, " ", "_")
buf = Replace(buf, " ", "_")
If buf = "." Or IsNumeric(buf) Then buf = "_" & buf
On Error Resume Next
If ps_ExistsName(buf) Then
myName = buf
Else
ThisWorkbook.Names.Add buf, ws.Name & "!" & r.Address
myName = IIf(Err.Number > 0, myName & "_", buf)
ThisWorkbook.Names(buf).Delete
End If
On Error GoTo 0
Next
If ps_ExistsName(myName) Then
Call ps_DebugPrint046(myName, "すでに同名が定義されている")
GoTo Next_Handle
End If
On Error Resume Next
ThisWorkbook.Names.Add myName, ws.Name & "!" & r.Address
If Err.Number > 0 Then
Call ps_DebugPrint046(myName, "(元:" & r.Value & ")" & "/" & Err.Description)
End If
On Error GoTo 0
Next_Handle:
Next
End Sub
Private Sub ps_DebugPrint046(sName As String, Description As String)
Debug.Print sName; ":"; Description
End Sub
Private Function ps_ExistsName(sName As String) As Boolean
ps_ExistsName = False
Dim n As Name
For Each n In ThisWorkbook.Names
If UCase(n.Name) = UCase(sName) Then
ps_ExistsName = True
Exit Function
End If
Next
End Function
これ本当に頭抱えました。使える文字・使えない文字はググったんですけど……。
Excel2010-2016:名前に使える文字・使えない文字
どう再現したらいいのか途方に暮れて、結局「1文字ずつ実際に名前登録してみて、エラーが起こったらアンダーバーにする」みたいなやり方になりました。
まあ公式回答がものすごくすっきりして読みやすいのはいつものこととして、その中で知らなかったことが2点。
WorksheetFunction.Clean メソッド (Excel)
使ったことなくて知りませんでした。Worksheet関数なんで、そういえばExcelシート上で関数ベタ打ちしてるときに「CLEAN」というのは見たことがあったけど。
あと、Mid関数でそのまま代入すればその文字だけ入れ替わるのも知りませんでした。へー!!めっちゃ便利!!
Sub TestMid()
Dim buf: buf = "123456789"
Dim i
For i = 1 To Len(buf)
If Mid(buf, i, 1) Mod 3 = 0 Then
Mid(buf, i, 1) = "a"
End If
Next
Debug.Print buf ' -> 12a45a78a
End Sub
47本目。
#VBA100本ノック 47本目
— エクセルの神髄 (@yamaoka_ss) December 15, 2020
ブックの全ウィンドウの全シートに対して以下の処理を行ってください。
・A1セルを選択しA1セルが見える状態にする
・ズームを85%
・枠線を非表示
・表示を標準
・印刷の向き「横」
※全ウィンドウという点を忘れずに
※ブックは任意
Public Sub VBA100本ノック_047()
ThisWorkbook.Activate
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
With ws
.Activate
.Range("A1").Select
.PageSetup.Orientation = xlLandscape
End With
With ActiveWindow
.Zoom = 85
.DisplayGridlines = False
.View = xlNormalView
End With
Next
End Sub
これもマクロ記録から。でも実行しててちょっと重ためだと思っていたら、「Application.PrintCommunication」でプリンター設定の同期を一時停止させられるんですね。なるほど……。
あと「Application.Goto」、VBA勉強であれこれ見てる時に見かけた記憶はあったんですがすっかり忘れてました。スクロールはされるが非表示はそのまま……普通にSelectしか選択肢がないとハマっちゃう場面があるのか。
・Application.PrintCommunication
・Application.Goto
48本目。
#VBA100本ノック 48本目
— エクセルの神髄 (@yamaoka_ss) December 16, 2020
引数が1次元または2次元配列の場合、以下の処理を行いVariantで返すFunctionを作成。
xxx (v as Variant) As Variant
・数値型は整数部のみにする(1.5→1, -1.5→-1)
・数値型以外(文字列、日付、その他)はそのまま
※配列以外と3次元配列以上もそのまま返してください。
Public Function VBA100本ノック_048(v As Variant) As Variant
If Not IsArray(v) Then GoTo End_Handle
Dim tmp As Variant
Dim dimension As Long
On Error Resume Next
tmp = UBound(v, 3)
If Err.Number = 0 Then GoTo End_Handle
Err.Clear
tmp = UBound(v, 2)
dimension = IIf(Err.Number = 0, 2, 1)
On Error GoTo 0
Dim rw As Long, cl As Long
Dim buf As Variant
Select Case dimension
Case 1
For rw = LBound(v) To UBound(v)
If VarType(v(rw)) = vbSingle Or VarType(v(rw)) = vbDouble Then
v(rw) = WorksheetFunction.RoundDown(v(rw), 0)
End If
Next
Case 2
For rw = LBound(v, 1) To UBound(v, 1)
For cl = LBound(v, 2) To UBound(v, 2)
If VarType(v(rw, cl)) = vbSingle Or VarType(v(rw, cl)) = vbDouble Then
v(rw, cl) = WorksheetFunction.RoundDown(v(rw, cl), 0)
End If
Next
Next
End Select
End_Handle:
VBA100本ノック_048 = v
End Function
あんまりよくない。あと、Function指定だったからか、なぜか「なるべく1つのプロシージャに収めた方がいいか?」みたいな発想になってしまって、公式回答見たときに「分かったはずなのに分からなかった……」みたいな凹み方をしました。何次元かなんて外に出しちゃってもよかったんですよね。
Private Function pf_GetCountDimension(v As Variant) As Long
Dim cnt As Long
If Not IsArray(v) Then GoTo End_Handle
On Error GoTo End_Handle
Do Until Err.Number <> 0
If IsNumeric(UBound(v, cnt + 1)) Then
cnt = cnt + 1
End If
Loop
End_Handle:
pf_GetCountDimension = cnt
End Function
この問題は他にも反省点多くて、VarTypeを思い出せたのは良かったものの、ここもなぜか「Select Case使わない方がいいのかな…」とか思いこんじゃってIFで記述したため、結局CurrencyとかDecimalを見逃してます。
あと知らなかったFix関数。RoundDown使っちゃいましたが、半端に知ってるせいで他のやり方を知らないままっていうのは良くないですね……。
Fix
49本目。
#VBA100本ノック 49本目
— エクセルの神髄 (@yamaoka_ss) December 18, 2020
D列には以下の条件付き書式が設定されています。
・文字色(赤)
・塗りつぶし(赤,黄)
条件が適用されている行を別シートに値を転記し、同じ文字色と塗りつぶしをセルの書式に設定してください。
※元セルは書式設定されていません。
※転記元と転記先シートは任意 pic.twitter.com/XITlRR1xX1
Public Sub VBA100本ノック_049()
Dim wsIn As Worksheet
Set wsIn = ThisWorkbook.Worksheets("49In")
Dim wsOut As Worksheet
Set wsOut = ThisWorkbook.Worksheets("49Out")
wsOut.Cells.Clear
wsIn.Rows(1).Copy wsOut.Rows(1)
Application.CutCopyMode = False
Call ps_CopyPaste(wsIn, RGB(255, 0, 0), xlFilterCellColor, wsOut)
Call ps_CopyPaste(wsIn, RGB(255, 255, 0), xlFilterCellColor, wsOut)
Call ps_CopyPaste(wsIn, RGB(255, 0, 0), xlFilterFontColor, wsOut)
End Sub
Private Sub ps_CopyPaste(wsSrc As Worksheet, myColor As Long, myOperator As XlAutoFilterOperator, wsDst As Worksheet)
With wsSrc
.AutoFilterMode = False
.Range("A1").AutoFilter Field:=4, Criteria1:=myColor, Operator:=myOperator
Dim rngCopy As Range
Set rngCopy = .Range("A1").CurrentRegion.Offset(1, 0)
End With
With wsDst
Dim rowTarget As Long
rowTarget = .Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0).Row
rngCopy.Copy .Cells(rowTarget, 1)
Application.CutCopyMode = False
With .Range(.Cells(rowTarget, 4), .Cells(.Rows.Count, 4).End(xlUp))
Select Case myOperator
Case xlFilterCellColor: .Interior.Color = myColor
Case xlFilterFontColor: .Font.Color = myColor
End Select
End With
End With
wsSrc.AutoFilterMode = False
End Sub
同じことの繰り返しなので、何とか関数にできないかなあと試行錯誤したやつ。せっかくなので、これは別案も記述しました。
Public Sub VBA100本ノック_049_Other()
Dim wsIn As Worksheet
Set wsIn = ThisWorkbook.Worksheets("49In")
Dim wsOut As Worksheet
Set wsOut = ThisWorkbook.Worksheets("49Out")
wsOut.Cells.Clear
Call ps_CopyPaste_Other(wsIn.Rows(1), wsOut.Rows(1))
With wsIn
Dim myInteriorColor As Long
Dim myFontColor As Long
Dim myRow As Long
Dim rw As Long
For rw = 2 To .Cells(.Rows.Count, 1).End(xlUp).Row
myRow = wsOut.Cells(wsOut.Rows.Count, 1).End(xlUp).Offset(1, 0).Row
myInteriorColor = .Cells(rw, 4).DisplayFormat.Interior.ColorIndex
Select Case myInteriorColor
Case 3, 6
Call ps_CopyPaste_Other(.Rows(rw), wsOut.Rows(myRow))
wsOut.Cells(myRow, 4).Interior.ColorIndex = myInteriorColor
End Select
myFontColor = .Cells(rw, 4).DisplayFormat.Font.ColorIndex
Select Case .Cells(rw, 4).DisplayFormat.Font.ColorIndex
Case 3
Call ps_CopyPaste_Other(.Rows(rw), wsOut.Rows(myRow))
wsOut.Cells(myRow, 4).Font.ColorIndex = myFontColor
End Select
Next
End With
End Sub
Private Sub ps_CopyPaste_Other(src As Range, dst As Range)
src.Copy dst
Application.CutCopyMode = False
End Sub
公式回答は、あえてどちらかと言えば別解寄り(一行ずつDisplayFormatでチェックして反映)。
もちろん「DisplayFormat」とか知らなかったので、これもググった奴です。
【VBA】条件付き書式の色を取得【DisplayFormatを使う】
50本目。
#VBA100本ノック 50本目
— エクセルの神髄 (@yamaoka_ss) December 19, 2020
記念すべき50本目は数学です。
直前の三項の和として各項が定まるトリボナッチ数列を出力してください。
0,0,1,1,2,4,7,13,24,44,81,149,274,…
最初の0,0,1は固定です。
※エクセルの限界まで出力してみましょう。
※出力先は任意
Public Sub VBA100本ノック_050()
Dim arSequence As Variant
arSequence = Array(0, 0, 1)
On Error Resume Next
Do Until Err.Number <> 0
ps_tribonacci_series ary:=arSequence
Loop
On Error GoTo 0
Dim ws As Worksheet
Set ws = ActiveSheet
ws.Cells.ClearContents
Dim rw As Long
Dim elm As Variant
For Each elm In arSequence
rw = rw + 1
ws.Cells(rw, 1).Value = "'" & elm
Next
End Sub
Private Sub ps_tribonacci_series(ByRef ary As Variant)
Dim iMax As Long: iMax = UBound(ary)
ReDim Preserve ary(iMax + 1)
ary(iMax + 1) = WorksheetFunction.Sum(ary(iMax - 2), ary(iMax - 1), ary(iMax))
End Sub
トリボナッチ数列って何!?(ちゃんと説明されてます)
32bit版なので知らなかったんですが、64bitならlonglongなんてあるんですね。
ps_tribonacci_seriesで、最初は変数long型で宣言して足してたら割とすぐ止まっちゃったんで、Sum関数使ったらどうかな?と試してみたら1000行超えたのでいけるのでは!?となったんですけど、解答見るとLongLong型でも100行程度だったので、数値として認識できないのであればこれはやっぱり誤りってことでしょうか……。
■50本目を終えて。
普通に難しいし、これでまだ折り返しだし!
問題を提供くださっているエクセルの神髄さんは、リプライなりくれたらどんなコードでも見ますということをおっしゃってくださってますが、これ投げても……うーん……。
でも取り組んでいて思うのが、私はやっぱり独りよがりなコードしか書けてないなってことなので、ガツンと言ってもらった方がいいのかなとも思います。
まだもうちょっと頑張ります。