ラベル ExcelVBA-SampleCode の投稿を表示しています。 すべての投稿を表示
ラベル ExcelVBA-SampleCode の投稿を表示しています。 すべての投稿を表示

2011年12月25日日曜日

[ExcelVBA] 税込み価格への修正


■お題
税込み価格への修正(問題出典:どう書く?org)
- 問題詳細 -
ここにチラシの原稿があります。例えば「ダイコン150円、ハクサイ120円、ジャガイモ30円」のような文字列です。法改正によって商品の値段は税込み表示にしないといけなくなりました。そこで、与えられた文字列の中から税抜き価格を見つけ出し、税込み価格に変更した文字列を返す関数を作ってください。
なお、税抜き価格は半角の数字の連なりで、かつ半角の数字の連なりはすべて税抜き価格だとします。「9,800円」「百円」「100円」「100g80円」などのような記述はないと考えてかまいません。 また税込み価格は税抜き価格の1.05倍で、端数は切り捨てとしてください。

■解答例
正規表現を使う問題ですね。
問題文は、親切でとてもやさしいものとなっています。
他の言語であれば置換する時にマッチしたデータに対し計算を施した結果を置換することができるのに
対しExcelVBAではできないようなので、ちょっと発想の転換が必要なのかもしれません。


では、コードです。
Option Explicit

Function ExchangeValue(Str As String)
    Dim Reg As Object, result As Object
    Set Reg = CreateObject("VBScript.RegExp")
    
    Reg.Pattern = "\d+"
    Reg.Global = True
    
    Set result = Reg.Execute(Str)
    Dim i As Integer
    For i = result.Count - 1 To 0 Step -1
        With result.Item(i)
            Str = Left(Str, .FirstIndex - 1) & Replace(Str, .Value, CInt(.Value) * 1.05, Start:=.FirstIndex)
        End With
    Next
    
    ExchangeValue = Str
End Function
3行目:引数に変更する文章をうけとるStrを用意してあります。
4行目:変数Regは正規表現のオブジェクトを格納するよう。
変数resultは、正規表現の結果を格納するよう。
5行目:正規表現のオブジェクトを生成

7行目:正規表現を指定。今回は「数字が1つ以上続く」を指定
8行目:複数回出てくることを想定してGlobalプロパティをTrue

10行目:正規表現の結果を格納
11行目:変数i For文内で配列のインデックスとして使用
12行目:For文:マッチした正規表現の回数文ループ。
13行目:正規表現の結果のオブジェクトが長かったんで、Withでくくりだしています。
End Withまでは、以降 .で始まる文字列は、result.Item(i)が省略されてます
14行目:Replace関数の引数にStartオプションを使うとそれより前の文字列は返されません。
よって、全文取得するため、Startオプションより前をLeft関数で取得しています。
Replaceする内容は、10行で実行した正規表現の結果をもとに連続する数字を
1.05倍したものと置換していきます。
15行目:Withを終了
16行目:繰り返し

18行目:変換した文字列を返す

実際にこんな感じで使う
Option Explicit

Sub main()
    Dim Str As String
    Str = "「ダイコン150円、ハクサイ120円、ジャガイモ30円」"
    
    Debug.Print ExchangeValue(Str)
End Sub


実行結果
「ダイコン157.5円、ハクサイ126円、ジャガイモ31.5円」

[ExcelVBA] 条件を満たす行を取り除く


■お題
条件を満たす行を取り除く(問題出典:どう書く?org)
- 問題詳細 -
ファイルから1行ずつ読み込み、"#"で始まる行だけを取り除いてファイルに出力するコードを書いてください。
サンプル入力

hello!
# remove this
# don't remove this
bye!
サンプル出力
hello!
# don't remove this
bye!

■解答例
ファイルのオープンの仕方、1文字目の判定ができればよいですね。
読込むファイルデータは、Sample.txtとして上記サンプルを同一フォルダ内に保存しています。

では、コードです。
Option Explicit

Sub main()
    Dim FH As Integer
    FH = FreeFile
    
    Dim OneLine As String
    Open "Sample.txt" For Input As #FH
        Do Until EOF(FH)
            Line Input #FH, OneLine
            If Left(OneLine, "1") <> "#" Then Debug.Print OneLine
        Loop
    Close #FH
End Sub
4行目:変数 FH は、ファイル識別番号を格納する用
5行目:Freefileを使うことでダブりなくファイル識別番号を割り当てる

7行目:変数 Oneline は、ファイルを開き1行のデータを受取る用
8行目:ファイルを開く
9行目:ファイルの最終行でなければループを続ける
10行目:ファイルの1行データをOneLineに格納する
11行目:Left関数で1行の最初の文字が#でなければ出力する
12行目:繰り返し
13行目:ファイル閉じる


実行結果
-----------------------------
hello!
# don't remove this
bye!

-----------------------------

【別の方法】
最初の1文字目を判定する別の方法としてASC関数を使う方法がありますね。
ASC関数は、与えられた文字列の最初の文字のコードを返す関数です。
いちいち、1文字目とか指定する必要がありません。
具体的には・・・
If Asc(OneLine) <> 35 Then Debug.Print OneLine
でOKです。
ASC関数で1行目のデータを受取り1文字目が#の時は35を返すので
35じゃない時は、出力する。という風です。

[ExcelVBA] ウィンドウの表示


■お題
ウィンドウの表示(問題出典:どう書く?org)
- 問題詳細 -
画面中央に幅100ピクセル、高さ75ピクセルのウィンドウを表示してください。
タイトルには「こんにちは、GUI!」と表示してください。
使用するGUIライブラリに名前が付いているならば、それをタグでつけてください。(例えば「Swing」など。)

追記:100x75はあまりに小さすぎるようなので、小さすぎるせいでうまく行かない場合は400x300でもいいということにします。 このお題の意図は「小さいウィンドウを出せ」というわけではないので。

■解答例
ポイントは2つ。
・ディスプレイのサイズはどうやって取得するか?
・ポイントとピクセルの違いは?

ExcelVBAでウィンドウサイズを取得できないようなので、WindowsAPIのGetWindowRect関数を使って取得するとよさそうです。

□ウィンドウズAPIとは?
Windows API(ウィンドウズ エーピーアイ)とは、Microsoft WindowsのAPIのことである。特に32ビットプロセッサで動作するWindows 95以降やWindows NTで利用できるものはWin32 APIと呼ばれる。また、それらのWindowsにおけるWin32 APIの実装をWin32と呼ぶ。
(WikiPediaより引用)

平たくいえば、OSの機能を外部プログラムとやりとりするものですね。
WindowsAPIのGetWindowRect関数は、“ウィンドウ”全体の座標を取得することができます。
この関数では、あらゆる“ウィンドウ”の座標を取得できるのでまずどの“ウィンドウ”の座標を知りたいのか指定する必要があります。“ウィンドウの”指定方法は、全ての“ウィンドウ”には重複のない番号が割り当てられているのでその番号を取得してGetWindowRect関数に渡す必要があります。

今回知りたい“ウィンドウ”は、デスクトップです。
デスクトップのハンドルを知るのにGetDesktopWindow関数を使いました。


では、コードです。
まずは、デスクトップのサイズを取得する関数についてです。
モジュール名:SubModule内
Option Explicit

Declare Function GetDesktopWindow Lib "user32" () As Long
Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long

Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

Function GetWindowSize() As WindowSize
    Dim hwnd As Long
    hwnd = GetDesktopWindow()
    
    Dim Re As RECT
    Dim Result As Long
    Result = GetWindowRect(hwnd, Re)
    
    Dim WindowSize As WindowSize
    WindowSize.Width = Re.Right - Re.Left
    WindowSize.Height = Re.Bottom - Re.Top
    GetWindowSize = WindowSize
End Function
3行目:デスクトップウィンドウのハンドルを取得するためにWindowsAPIの関数を宣言
4行目:ウィンドウ全体の座標を取得する

6~11行目:WindowsAPIのGetWindowRect関数の第二引数で使う変数を定義

13行目:デスクトップの幅と高さを返す関数。返り値に幅と高さを保持したユーザー定義型を返します。
14行目:hwnd変数、デスクトップのウィンドウハンドルを取得する変数
15行目:WindowsAPIのGetDesktopWindow関数でデスクトップのウィンドウハンドルを取得

17行目:変数REをユーザー定義型RECTとして宣言。WindowsAPIのGetWindowRectの第二引数に渡す
18行目:変数Result、GetWindowRect関数の戻り値を格納。0の時関数は、取得失敗を意味する。
19行目:WindowsAPIのGetWindowRect関数で第一引数に渡したウィンドウハンドルの座標を取得

21行目:変数WindowSizeをユーザー定義型WIndowSizeとして宣言
22行目:ユーザー定義型WindowSizeを宣言した変数のWidthにデスクトップの幅を格納
23行目:ユーザー定義型WindowSizeを宣言した変数のHeightにデスクトップの高さを格納
24行目:戻り値としてユーザー定義型WindowSizeを返す


SampleCode1関数を実行することで
・ウィンドウサイズ400x300
・画面中央表示
・タイトルをこんにちは、GUI!
で表示します。

モジュール名:MainModule内
Option Explicit

Type WindowSize
    Height As Long
    Width As Long
End Type

Sub SampleCode1() '問題を含んだコード
    Application.Height = 400
    Application.Width = 300

    Dim DeskTopSize As WindowSize
    DeskTopSize = GetWindowSize()

    Application.Top = DeskTopSize.Height / 2 - Application.Height / 2
    Application.Left = DeskTopSize.Width / 2 - Application.Width / 2

    ActiveWindow.Caption = "こんにちは、GUI!"
End Sub
3~6行目:ユーザー定義型を定義します。
デスクトップの幅と高さを受け取る為に使います。

ウィンドウのサイズを変更します。
タイトルが見づらいので400x300で表示します。
9行目 :ウィンドウの高さを300pxにする
10行目:ウィンドウの幅を400pxにする

12行目:変数DeskTopSizeは、デスクトップの幅と高さを受け取る為の変数
13行目:GetWindowSize関数で、幅と高さを戻り値として受け取る。

ウィンドウの位置をデスクトップの中央に移動します。
(15、16行目に問題あり。)
15行目:デスクトップの高さ÷2 - ウィンドウの高さ÷2
16行目:デスクトップの幅 ÷2 - ウィンドウの幅 ÷2

18行目:タイトルを変更する。

あってそうな気がしますが、残念ながらこれじゃ画面中央に表示されません。
GetWindowRect関数の返す値の単位とApplication.Topや.Leftに指定する値の単位が違います。
・GetWindowRect関数 →px(ピクセル)
・Application.Top/.Left→pt(ポイント)

つまり、単位変換しないと画面中央に表示されません。
pxとptの変換は、
[pt] = [px] ÷ [解像度] × 72
[px] = [pt] × [解像度] ÷ 72

なお、解像度は、
Application.DefaultWebOptions.PixelsPerInchで調べられます。

そこらへんを考慮してSampleCode1関数を書き直すと
モジュール名:MainModule内
Option Explicit

Sub SampleCode2() '完成版
    Dim WindowSize As WindowSize
    Call GetWindowSize(WindowSize)
    Dim dpi As Double
    dpi = Application.DefaultWebOptions.PixelsPerInch '解像度求める

    Application.Width = 100
    Application.Height = 75
    Application.Top = (WindowSize.Height / dpi) * 72 / 2 - Application.Height / 2 'WindowsAPIから得た値はptへ変換する
    Application.Left = (WindowSize.Width / dpi) * 72 / 2 - Application.Width / 2 'WindowsAPIから得た値はptへ変換する
End Sub





よって、最終的なコードは下記になります。
MainModule内のSampleCode2関数を実行すると題意のとおりになります。

モジュール名:MainModule内
Option Explicit

Type WindowSize
    Height As Long
    Width As Long
End Type

Sub SampleCode2() '完成版
    Dim WindowSize As WindowSize
    Call GetWindowSize(WindowSize)
    Dim dpi As Double
    dpi = Application.DefaultWebOptions.PixelsPerInch '解像度求める

    Application.Width = 100
    Application.Height = 75
    Application.Top = (WindowSize.Height / dpi) * 72 / 2 - Application.Height / 2 'WindowsAPIから得た値はptへ変換する
    Application.Left = (WindowSize.Width / dpi) * 72 / 2 - Application.Width / 2 'WindowsAPIから得た値はptへ変換する
End Sub


モジュール名:SubModule内
Option Explicit

Declare Function GetDesktopWindow Lib "user32" () As Long
Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long

Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

Function GetWindowSize() As WindowSize
    Dim hwnd As Long
    hwnd = GetDesktopWindow()
    
    Dim Re As RECT
    Dim Result As Long
    Result = GetWindowRect(hwnd, Re)
    
    Dim WindowSize As WindowSize
    WindowSize.Width = Re.Right - Re.Left
    WindowSize.Height = Re.Bottom - Re.Top
    GetWindowSize = WindowSize
End Function

[ExcelVBA] ピラミッドを作る


■お題
ピラミッドを作る(問題出典:どう書く?org)
- 問題詳細 -
正の整数nが与えられたときに、高さnのピラミッドを出力するプログラムを作ってください。
n=4の時の出力は下のようになります。
 
   *
  ***
 *****
*******

■解答例
第一引数:繰り返す回数を指定します。
第二引数:繰り返す文字を指定します。
String関数


では、コードです。
Option Explicit

Sub MakePyramid(ByVal n As Integer)
    Dim temp() As String
    ReDim temp(n - 1)
    
    Dim i As Integer
    For i = 1 To n
        temp(i - 1) = String(n - i, " ") & String(2 * i - 1, "*")
    Next
    
    Debug.Print Join(temp, vbLf)
End Sub
3行目:引数に段数を表す値nを受取ります
4行目:変数tempには、各段で表示する*マークを格納するのに使います。最後にJoinで改行を間に挟みます。
5行目:要素の数を再定義します。
7行目:変数i For文で繰り返し回数のカウントに使います
8行目:For文開始。最初は1で最後はn段のn
9行目:temp配列に各段の文字列を格納。String関数を2回使い
最初は、空白を生成し、次に*を生成しています。
10行目:繰り返し
12行目:表示



実際にこんな感じで使う
Option Explicit

Sub Main()
    MakePyramid (10)
End Sub


実行結果
 
         *
        ***
       *****
      *******
     *********
    ***********
   *************
  ***************
 *****************
*******************

[ExcelVBA] n人中m人が当選するくじ その3


■お題
n人中m人が当選するくじ除(問題出典:どう書く?org)
- 問題詳細 -
n人の中から公平にm人を選ぶ、くじ引きプログラムを作ってください。

■解答例
・乱数とソートを使ってみる
A列に乱数、B列に参加者の番号を記入し、A列をキーにソートをかけるとB列がシャッフルされて当選者数分の番号を返せばよいですね。
↑A列に乱数、B列に参加者の番号を記入


↑A列をキーにA列、B列をソート


では、コードです。
モジュール名:SubModule
Option Explicit

Function lot(n As Integer, m As Integer) As Variant
    Randomize

    Dim r As Integer
    For r = 1 To n
        Cells(r, 1) = Rnd()
        Cells(r, 2) = r
    Next
    
    Range(Cells(1, 1), Cells(r, 2)).Sort key1:=Range("A1")
    
    Dim result As Variant
    ReDim result(m - 1)
    
    For r = 1 To m
        result(r - 1) = Cells(r, 2)
    Next
    
    lot = result
End Function
3行目:引数 n…参加者全員の数を指定、m…選ばれる人数を指定
4行目:乱数のシードを変更する。Randomize(Number) Numberは省略可。省略時、時刻からシード値を取得。
6行目:変数 r・・・行数を格納する用の変数を宣言
8行目:A列に乱数を記載する
9行目:B列に参加者の番号を記載
10行目:繰り返し
12行目:A列をキーにA列、B列をソート
14行目:変数 result・・・当選者の番号を格納する。
15行目:result配列の要素を確定する
18行目:B列の値を当選者としてresult変数に格納する
19行目:繰り返し
21行目:返り値としてresult配列を返す


実際にこんな感じで使う

モジュール名:MainModule
Option Explicit

Sub main()
    Dim result As Variant
    result = lot(10, 3)
    
    Dim i As Integer
    For i = 0 To UBound(result)
        Debug.Print result(i)
    Next
    
    Debug.Print "-------------"
End Sub


実行結果
 
 8 
 4 
 2 
-------------
 1 
 3 
 2 
-------------
 7 
 5 
 6 
-------------
 5 
 8 
 9 
-------------

[ExcelVBA] n人中m人が当選するくじ その2


■お題
n人中m人が当選するくじ除(問題出典:どう書く?org)
- 問題詳細 -
n人の中から公平にm人を選ぶ、くじ引きプログラムを作ってください。

■解答例
□Collectionオブジェクトを使ってみる
シャッフルするためにあらためてコードを書くなんてめんどくさいですね。
Collectionオブジェクトなら、.Countの数を上限に乱数で要素を選択して.Removeで要素を削除しての繰り返しでよさそうですね。

では、コードです。
モジュール名:SubModule
Option Explicit

Function lot(n As Integer, m As Integer) As Variant
    Randomize
    
    Dim list As New Collection
    Dim i As Integer
  
    For i = 1 To n
        list.Add i
    Next
    
    Dim result() As variant, r As Integer
    ReDim result(m - 1)
    
    For i = 0 To m - 1
        r = Int(Rnd() * list.Count) + 1
        result(i) = list.Item(r)
        list.Remove (r)
    Next
    
    lot = result
End Function
3行目:引数 n…参加者全員の数を指定、m…選ばれる人数を指定
4行目:乱数のシードを変更する。Randomize(Number) Numberは省略可。省略時、時刻からシード値を取得。
6行目:変数 list・・・参加者の人数分値を用意しておく。
7行目:変数 i・・・For文で使用。

9行目~11行目:人数文、Collectionオブジェクトに追加する

13行目:変数 result・・・当選者の番号を格納するよう
            変数 r・・・乱数を格納するよう
14行目:result配列の要素を確定する
17行目:乱数を生成する。ただし、Collectionオブジェクトの要素は1から始まるので1以上オブジェクト.Count以下で生成。
18行目:乱数の値を元にCollectionオブジェクトの値を当選者としてresult変数に格納する
19行目:.removeメソッドで削除する
20行目:繰り返し
22行目:返り値としてresult配列を返す



実際にこんな感じで使う

モジュール名:MainModule
Option Explicit

Sub main()
    Dim result As Variant
    result = lot(10, 3)
    
    Dim i As Integer
    For i = 0 To UBound(result)
        Debug.Print result(i)
    Next
    
    Debug.Print "-------------"
End Sub


実行結果
2 
 10 
 1 
-------------
 2 
 4 
 8 
-------------
 2 
 9 
 7 
-------------
 5 
 9 
 3 
--------------

[ExcelVBA] ウィンドウを3分割表示


■お題
マクロを実行したファイルのウィンドウを3分割して下さい。3分割の仕方ですが、下図を参考に上下で3:7に分け下段は左右に1:1に分割して下さい

■解答例
よくエラーチェックのマクロの際に上画面がエラー画面で下がエラーの該当シートみたいな使い方で複数ウィンドウは使いますねぇ。
1画面だと切り替えた時になんだっけ??
ってことになってしまうので複数ウィンドウの方がUI的に優しくてよいですよねぇ。


では、ウィンドウサイズやブックのサイズの取得等するのでそこらへんのまとめをしておきます。

□ウィンドウサイズの状態変更
オブジェクト.WindowState
定数内容
xlMaximized最大化
xlNormal通常表示
xlMinimized最小化
※ウィンドウサイズを変更禁止することのできるEnableResizeプロパティでFalseになっているとエラーになります


□ウィンドウサイズの取得
オレンジ色の領域は、下記で取得できます。
Application.Width
Application.Height


赤色の領域は、下記で取得できます。
Application.UsableWidth
Application.UsableHeight



□ブックのサイズ・位置の取得
ActiveWindow.Width
ActiveWindow.Height

ActiveWindow.Top
ActiveWindow.Left


□ブックのタイトル
ActiveWorkbook.Name
ActiveWindow.Caption

どちらも、新しいウィンドウで複数になっていなければ同一の値を返します。
例)SampleCode.xls の場合
・ActiveWorkbook.Name
⇒SampleCode.xls
・ActiveWindow.Caption
⇒SampleCode.xls


新しいウィンドウで複数ウィンドウを開いていると
例)SampleCode.xls:1 / SampleCode.xls:2
・ActiveWorkbook.Name
⇒SampleCode.xls
・ActiveWindow.Caption
⇒SampleCode.xls:1
⇒SampleCode.xls:2
Option Explicit

Sub SampleCode()
    Dim FileName As String
    FileName = ActiveWorkbook.Name
    Application.WindowState = xlMaximized
    
    'マクロ実行中のウィンドウがいくつあるかカウントする
    Dim Count As Integer, i As Integer
    For i = 1 To Windows.Count
        If FileName = Windows.Item(i).Caption _
                Or 0 < InStr(Windows.Item(i).Caption, FileName & ":") Then
            Count = Count + 1
        End If
    Next

    For i = 1 To 3 - Count
        ActiveWindow.NewWindow
    Next


    Dim AppWidth As Double, AppHeight As Double
    AppWidth = Application.UsableWidth
    AppHeight = Application.UsableHeight
    
    With Windows(FileName & ":1")
        .WindowState = xlNormal
        .Width = AppWidth
        .Height = AppHeight * 0.3
        .Left = 0
        .Top = 0
        .Activate
    End With
    Sheets("AAA").Select
    
    With Windows(FileName & ":2")
        .WindowState = xlNormal
        .Width = AppWidth / 2
        .Height = AppHeight * 0.7
        .Left = 0
        .Top = AppHeight * 0.3
        .Activate
    End With
    Sheets("BBB").Select
    
    With Windows(FileName & ":3")
        .WindowState = xlNormal
        .Width = AppWidth / 2
        .Height = AppHeight * 0.7
        .Left = AppWidth / 2
        .Top = AppHeight * 0.3
        .Activate
    End With
    Sheets("CCC").Select
End Sub
4行目:FileName変数 アクティブブックの名前格納用
5行目:アクティブブック名取得
6行目:ウィンドウを最大化する

9行目:Count変数 マクロ実行中のウィンドウの数を格納する用
            i変数 For文で使用
10行目:For文でブック名を走査する
11行目:IF文の条件について…
            FileName = Windows.Item(i).Caption
            ⇒ファイルが一つの時用の条件
            0 < InStr(Windows.Item(i).Caption, FileName & ":"
            ⇒新しいウィンドウを開いて複数になった時用の条件
13行目:マクロ実行中のファイルと同じブックがあった時にカウントする
15行目:繰返し

17行目:マクロ実行中のブックが3つになるようFor文で新しいウィンドウを開く

22行目:AppWidth変数 ウィンドウの内側の幅を格納する用、
            AppHeight変数 ウィンドウの内側の高さを格納する用
23行目:ウィンドウの内側の幅を取得
24行目:ウィンドウの内側の高さを取得
26~34行目:SampleCode.xls:1のブックを走査
            ブックの状態を普通 ブックの幅をウィンドウの内側の幅と同じにする
            ブックの高さをウィンドウの内側の高さの30%にする
            ブックの位置(左側)を0にする ブックの位置(上側)を0にする
            ブックをアクティブにする(シートを選択する為)
            シート名(AAA)を選択

36~44行目:SampleCode.xls:2のブックを走査
            ブックの状態を普通
            ブックの幅をウィンドウの内側の幅と同じにする
            ブックの高さをウィンドウの内側の高さの30%にする
            ブックの位置(左側)を0にする
            ブックの位置(上側)を内側の高さの30%にする
            ブックをアクティブにする(シートを選択する為)
            シート名(BBB)を選択

46~54行目:SampleCode.xls:3のブックを走査
            ブックの状態を普通
            ブックの幅をウィンドウの内側の幅と同じにする
            ブックの高さをウィンドウの内側の高さの30%にする
            ブックの位置(左側)を内側の幅の50%にする
            ブックの位置(上側)を内側の高さの30%にする
            ブックをアクティブにする(シートを選択する為)
            シート名(CCC)を選択

[ExcelVBA] 重複データの削除2


■お題
複数列内(シートをまたぐ場合込み)にあるデータで重複するものを削除せよ

■解答例
複数列にまたぐ場合は、前回おこなったようなソートしてから上下を見比べて削除という方法には無理が限界が生じますね。なぜならば、複数列が可能になった瞬間から1列に収まらないセルの数の可能性がでてきますからねぇ。。
でも、かいちゃいますか。

重複データを削除する方法で一番単純なやり方は、連想配列に放りこむ方法です。連想配列とは、キーと値を1セットで管理する変数のことで重複チェックをしたい物をキーとし、値は適当な数値でもセットしておけば、同一の値に出くわしてもキー自体が増えることがないので、自動的に重複をはじけるわけです。
ただし、ひたすらメモリーに記憶しつづけていくので大量なデータを相手にする場合効率的ではないですね。
他の言語はさておき、少なくともExcelVBAに関しては効率が悪くなるのはあきらかです。


簡単な方法だとこれくらいしか思いつかないので、連想配列を使う方法でいきます。

以下に最終的なコードを示しておきます
モジュール名:SubModule内
Option Explicit

Sub DelDuplicateRow(ColInfo As Variant, Optional StartRow As Long = 1, Optional EndRow As Long, Optional multiple As Boolean = False, Optional SN As String)
    '### 同一列の重複データのセルを削除する関数
    '### 引数
    '###   省略不可:ColInfo…列情報を数字(1~128)か文(A~IV)で指定
    '###   省略可能:StartRow…最初の行を指定。省略した場合は1行目とする
    '###   省略可能:EndRow…最後の行を指定。省略した場合は、指定列の最終行
    '###   省略可能:Multiple…複数列を重複削除対象列とする場合Trueを指定
    '###                       省略した場合は、False
    '###   省略可能:SN…シート名を指定。省略時アクティブシート名
    '###   
    '###   ColFromStrToNum関数を必要
    '######################################################################
    If SN = "" Then SN = ActiveSheet.Name
    
    Dim Col As Integer
    If IsNumeric(ColInfo) Then
        Col = ColInfo
    Else
        Col = ColFromStrToNum(CStr(ColInfo))
    End If

    If EndRow = 0 Then EndRow = Sheets(SN).Cells(65536, Col).End(xlUp).Row
    
    
    If Not (0 < StartRow And StartRow < 65536) Then
        MsgBox "スタート行として、1から65535の間で指定して下さい"
        End
    End If
    
    If Not (1 < EndRow And EndRow < 65537) Then
        MsgBox "最終行として、2から65536の間で指定して下さい"
        End
    End If
    
    If EndRow < StartRow Then
        MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"
        End
    End If
    
    If Not (0 < Col And Col < 257) Then
        MsgBox "列は1~256の間で指定して下さい"
        End
    End If
    
    Static hash As Object, Status As Boolean
    If multiple Then
        If Not Status Then
            Set hash = CreateObject("Scripting.Dictionary")
            Status = True
        End If
    Else
        Set hash = CreateObject("Scripting.Dictionary")
    End If
    
    
    
    Dim r As Long
    For r = StartRow To EndRow
        
        If Not hash.Exists(CStr(Sheets(SN).Cells(r, Col))) Then
            hash.Add CStr(Sheets(SN).Cells(r, Col)), 1
        Else
            Sheets(SN).Cells(r, Col) = ""
        End If
    Next
    
    Sheets(SN).Range(Sheets(SN).Cells(StartRow, Col), Sheets(SN).Cells(EndRow, Col)).sort Key1:=Sheets(SN).Cells(StartRow, Col)
End Sub
1行目:引数は、下記の通りです。
省略不可:ColInfo…列情報、数字 or 文字列どちらでも可
省略可能:StartRow…重複対象セルの開始行(省略時:1)
省略可能:EndRow…重複対象セルの終了行(省略時:指定列の最終行)
省略可能:Multiple…複数列を対象としたい時は True指定(省略時:False) 省略可能:SN…シート名を指定(省略時:アクティブシート名)

15行目:SNが渡されなかった時は、アクティブシートの名前を指定
17行目:Col…対象列を格納する変数
18行目:引数のColInfoに入っている情報が数字かそうじゃないか判別。
19行目:数字ならばColに代入
21行目:数字でなければ、ColFromStrToNum関数に渡し何列目かに変換する
24行目:引数のEndRowに値が渡されなければ最終行を取得し代入する

27行目~45行目までエラーチェック
47行目:hash…重複判定に使う連想配列を宣言。
Staticステートメントで宣言することで、関数を抜けても値を保持しておく。
Statusオブジェクト宣言後に、Scripting.DictionaryをSetしたかどうかを判定する用の変数
Staticステートメントで宣言することで、関数を抜けても値を保持しておく 48行目:MultipleがTrue(=重複削除対象列が複数列)かどうか判別
49行目:Scripting.DictionaryをセットしてあるかどうかStatus変数で判別
50行目:Scripting.Dictionaryをセット(連想配列が使えるようになる)
51行目:Statusをtrueにし、次回呼び出しの場合
54行目:Scripting.Dictionaryをセットし、連想配列を使えるようにする
60行目:For文で指定された列の最初のセルから最後のセルまでループしていきます
62行目:連想配列に登録済みかどうかを判別します
63行目:登録されてないときは、キーに追加します
65行目:セル内容を削除します
69行目:重複対象範囲をソートします ColFromStrToNum関数は、前回と一緒のままです。

モジュール名:CommonModule
Option Explicit

Function ColFromStrToNum(ColStr As String)
    '### 列情報 文字列→数字に変換する関数
    '### アルファベット以外を指定するとエラーとなる
    '##############################################
    
    Dim List() As Integer, i As Integer, position As Integer
    ReDim List(Len(ColStr) - 1)
    
    Dim Code As Integer
    position = 1
    For i = Len(ColStr) - 1 To 0 Step -1
        Code = Asc(UCase(Mid(ColStr, position, 1)))
        If Not (64 < Code And Code < 91) Then
            MsgBox "アルファベットを引数に渡し手下さい"
            End
        Else
            List(i) = Code
        End If
        position = position + 1
    Next
    
    ColFromStrToNum = 0
    For i = 0 To UBound(List)
        ColFromStrToNum = ColFromStrToNum + (List(i) - 64) * 26 ^ i
    Next
End Function
3行目:列情報を受けとる引数を用意します。
8行目:List配列…列情報を1ケタずつ分解しキャラコードに変換した値を格納
i変数…For文でList配列のインデックスとして使用
position…列情報を1文字ずつに分解する際に使用
9行目:List配列の要素数を定義。文字列-1
11行目:Code…列情報を分解した文字をキャラコードに変換した値を格納
12行目:列情報を分解する際の文字位置を初期化する
13行目:列情報のキャラコードを1文字目を配列の最後から順に格納するため For文の最初の数字を配列の最後の要素、最後を配列の最初の要素 Stepで-1を指定。26進数から10進数に計算する際に都合がよいため
14行目:列情報のうち一文字取り出し、大文字に変換し、キャラコードに変換する
15~17行目:アルファベット以外はエラーにする
19行目:配列にキャラコードを代入
21行目:列情報を1文字に切り出すための文字位置情報を1つ増やす
22行目:繰り返し
24行目:26進数から10進数へ変換する際の値を代入するまえに初期化する
25~27行目:26進数から10進数へ変換する

以下使用例を示します。
Sub t1()
    Dim c As Integer
    For c = 1 To 5
        Call DelDuplicateRow3(c)
    Next
End Sub
まずは、複数列を対象範囲にしなかった場合
それぞれの列で重複がはじかれます。 複数列のオプションを有効にすると…
Sub t2()
    Dim c As Integer
    For c = 1 To 5
        Call DelDuplicateRow3(c, multiple:=True)
    Next
End Sub
このように全体で重複セルを削除します。

>別シートも同じ容量でオプションの引数であるSNにシート名をセットすれば同様のことができます

ところで…Staticステートメントのスコープってプログラムが終了するまで値を保存ってなっているが終了っていつなんだろう??少なくともマクロのリセットボタンを押すと値を捨ててくれるようだけどただプログラムの実行が完了しただけじゃ、値を捨てないみたい。。。 扱いが要注意だな。

[ExcelVBA] 重複データの削除


■お題
同一列内にあるデータで重複するものを削除せよ

■解答例
今回は、すごい簡単な問題を使って、ユーザフレンドリーなコードを書いてみます。
最終的なコードはこちら→最終的なコード

重複データを手動で削除する場合、ソートをかけて上から順に見て行き同じデータが並んでいる時削除するという手順を繰り返すことになると思います。
では、それをそのままコード化してみます。
モジュール名:SubModule内
Sub DelDuplicateRow(Col As Integer)
    If Not (0 < Col And Col < 257) Then
        MsgBox "列は1~256の間で指定して下さい"
        End
    End If
    
    Dim EndRow As Long
    EndRow = Cells(65536, Col).End(xlUp).Row
    
    Range(Cells(1, Col), Cells(EndRow, Col)).Sort Key1:=Cells(1, Col)
    
    Dim r As Long
    For r = 1 To EndRow - 1
        If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""
    Next
    
    Range(Cells(1, Col), Cells(EndRow, Col)).Sort Key1:=Cells(1, Col)
End Sub
1行目:列情報を数字で引数に受け取ります
2~5行目:列は1~256列までなのでそれ以外はエラーとしてメッセージを出して終了します
7行目:引数に指定された列の最終行を格納するための変数EndRowを宣言します。
セルは65536あるのでLong型ですね
8行目:最終行を取得します。
10行目:ソートしています。
12行目:For文で指定列のセルを最初から最終行に向かってループを
する時に行情報を格納する為の変数rを宣言します
13行目:For文開始です。スタートは、1行目なので1。現在のセルと一つしたのセルを比較するので     終わりは、最終行目なのでEndRow - 1を指定してます
14行目:現在のセルと一つ下のセルを比較して同じデータの時現在のセルを削除します
15行目:繰り返し
17行目:重複行は空になっているので間をつめるためにソートをかけます

モジュール名:SubModule内
Sub Main()
    Call DelDuplicateRow(1)
End Sub
A列目を指定するのであれば上記のように指定すればよいですね。



もっとも簡単なコードにして親切さのかけらもないコードが完成しました!


汎用性がなく使いづらいですね。
ここからユーザーフレンドリーなコードにかえていきましょう

Q. 重複削除したいデータは、ある列の途中にあって最初が1行目最後が最終行だと困るのですが?
A.引数にスタート行と終了行を用意しましょう。
モジュール名:SubModule内
Sub DelDuplicateRow(Col As Integer, StartRow As Long, EndRow As Long)
    If Not (0 < StartRow And StartRow < 65536) Then
        MsgBox "スタート行として、1から65535の間で指定して下さい"
        End
    End If
    
    If Not (1 < EndRow And EndRow < 65537) Then
        MsgBox "最終行として、2から65536の間で指定して下さい"
        End
    End If
    
    If EndRow < StartRow Then
        MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"
        End
    End If
    
    If Not (0 < Col And Col < 257) Then
        MsgBox "列は1~256の間で指定して下さい"
        End
    End If
    
    
    'EndRow = Cells(65536, Col).End(xlUp).Row
    Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)
    
    Dim r As Long
    For r = StartRow To EndRow - 1
        If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""
    Next
    
    Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)
End Sub
変更点は、
・引数にStartRow、EndRowを追加
・最終行を取得するのを廃止
・ソートの始まりのセル、For文のはじめの値をそれぞれStartRowに変更



これで決め細やかな設定ができるようになりました!
と、思って満足したらだめです。

Q. 最初が1行目最後が最終行の決め打ちは困ると言ったけども毎回指定するなんてありえないです><
A.変数のオプション化をします。

こういう時に役立つのが変数のOptionalキーワードです。
引数に値がなければ、プログラムの方でデフォルトの値を決めるという方法です。
書き方は
Optional (受け渡し方法[ByVal]) 変数名 (as 変数型[variant]) (=デフォルト値) (括弧)表示は、省略可能を現しており[四角括弧]は、省略時のデフォルト値です 一番最後のデフォルト値は定数でなければいけません。

モジュール名:SubModule内
Sub DelDuplicateRow(Col As Integer, Optional StartRow As Long = 1, Optional EndRow As Long)
    If EndRow = 0 Then EndRow = Cells(65536, Col).End(xlUp).Row
    
    If Not (0 < StartRow And StartRow < 65536) Then
        MsgBox "スタート行として、1から65535の間で指定して下さい"
        End
    End If
    
    If Not (1 < EndRow And EndRow < 65537) Then
        MsgBox "最終行として、2から65536の間で指定して下さい"
        End
    End If
    
    If EndRow < StartRow Then
        MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"
        End
    End If
    
    If Not (0 < Col And Col < 257) Then
        MsgBox "列は1~256の間で指定して下さい"
        End
    End If
    
    
    
    Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)
    
    Dim r As Long
    For r = StartRow To EndRow - 1
        If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""
    Next
    
    Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)
End Sub
変更点は、
・引数にのうちStarRowとEndRowをオプション化
・StartRowに値がない時は1を設定(Optionalキーワード内)
・EndRowは、設定がない時にEndメソッドで最終行を取得


利用する時は、こんな風になる。
モジュール名:SubModule内
Sub Main()
    'A列 最初~最終行を対象
    Call DelDuplicateRow(1)
    
    'B列 6行目~11行目を対象
    Call DelDuplicateRow(2, 6, 11)
    
    'C列 6行目~最終行を対象
    Call DelDuplicateRow(3, 6)
    
    'D列 最初~11行目を対象
    Call DelDuplicateRow(4, EndRow:=11)

End Sub


実行結果はこうなる
↑背景色が緑の部分が重複削除セル指定範囲


これで、必要な時に引数を追加すればよくなったので利用しやすさがぐんと上がりました。 これでおしまい!
なんて思いませんよね?

Q.列名を数字に治すのがめんどいです。CI列って数字でいくつでしたっけ?><
A.ですよねぇ・・・。列名は文字列で指定しましょう。

やることは、一つ。
列名はA→Zまでいくとケタが繰り上がりAAとなるので26進数といえますね。
26進数を10進数に変換するコードをかくだけですね。


この関数は使い回しができそうですね。
モジュール名:CommonModule内
Function ColFromStrToNum(ColStr As String)
    Dim List() As Integer, i As Integer, position As Integer
    ReDim List(Len(ColStr) - 1)
    
    Dim Code As Integer
    position = 1
    For i = Len(ColStr) - 1 To 0 Step -1
        Code = Asc(UCase(Mid(ColStr, position, 1)))
        If Not (64 < Code And Code < 91) Then
            MsgBox "アルファベットを引数に渡して下さい"
            End
        Else
            List(i) = Code
        End If
        position = position + 1
    Next
    
    ColFromStrToNum = 0
    For i = 0 To UBound(List)
        ColFromStrToNum = ColFromStrToNum + (List(i) - 64) * 26 ^ i
    Next
End Function
1行目:列情報を受けとる引数を用意します。
2行目:List配列…列情報を1ケタずつ分解しキャラコードに変換した値を格納
i変数…For文でList配列のインデックスとして使用
position…列情報を1文字ずつに分解する際に使用
3行目:List配列の要素数を定義。文字列-1
5行目:Code…列情報を分解した文字をキャラコードに変換した値を格納
6行目:列情報を分解する際の文字位置を初期化する
7行目:列情報のキャラコードを1文字目を配列の最後から順に格納するため
For文の最初の数字を配列の最後の要素、最後を配列の最初の要素
Stepで-1を指定。26進数から10進数に計算する際に都合がよいため
8行目:列情報のうち一文字取り出し、大文字に変換し、キャラコードに変換する
9~11行目:
アルファベット以外はエラーにする
13行目:配列にキャラコードを代入
15行目:列情報を1文字に切り出すための文字位置情報を1つ増やす
16行目:繰り返し
18行目:26進数から10進数へ変換する際の値を代入するまえに初期化する
19~21行目:
26進数から10進数へ変換する

モジュール名:SandModule内
Sub t1()
    Debug.Print ColFromStrToNum("a")
    Debug.Print ColFromStrToNum("z")
    Debug.Print ColFromStrToNum("IV")
    Debug.Print ColFromStrToNum("iv")
    Debug.Print "-----"
End Sub

Sub t2()
    Debug.Print ColFromStrToNum("1")
    Debug.Print "-----"
End Sub
テストしてみる。 t1関数の出力は
1 
 26 
 256 
 256 
-----
期待した値がでてますね。

t2関数の出力は「アルファベットを引数に渡して下さい」とでますね。
あとは、SubModule内のDelDuplicateRowの引数を文字列で受け取り変換するコードを追加すればおしまいですね。

以下に最終的なコードを示しておきます
モジュール名:SubModule
Option Explicit

Sub DelDuplicateRow(ColStr As String, Optional StartRow As Long = 1, Optional EndRow As Long)
    '### 同一列の重複データのセルを削除する関数
    '### 引数
    '###   省略不可:ColStr…列情報を文字列で指定 A~IV
    '###   省略可能:StartRow…最初の行を指定。省略した場合は1行目とする
    '###   省略可能:EndRow…最後の行を指定。省略した場合は、指定列の最終行
    '###
    '### ColFromStrToNum関数を必要
    '######################################################################

    Dim Col As Integer
    Col = ColFromStrToNum(ColStr)
    If EndRow = 0 Then EndRow = Cells(65536, Col).End(xlUp).Row
    
    
    If Not (0 < StartRow And StartRow < 65536) Then
        MsgBox "スタート行として、1から65535の間で指定して下さい"
        End
    End If
    
    If Not (1 < EndRow And EndRow < 65537) Then
        MsgBox "最終行として、2から65536の間で指定して下さい"
        End
    End If
    
    If EndRow < StartRow Then
        MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"
        End
    End If
    
    If Not (0 < Col And Col < 257) Then
        MsgBox "列は1~256の間で指定して下さい"
        End
    End If
    
    
    
    Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)
    
    Dim r As Long
    For r = StartRow To EndRow - 1
        If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""
    Next
    
    Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)
End Sub

モジュール名:CommonModule
Option Explicit

Function ColFromStrToNum(ColStr As String)
    '### 列情報 文字列→数字に変換する関数
    '### アルファベット以外を指定するとエラーとなる
    '##############################################
    
    Dim List() As Integer, i As Integer, position As Integer
    ReDim List(Len(ColStr) - 1)
    
    Dim Code As Integer
    position = 1
    For i = Len(ColStr) - 1 To 0 Step -1
        Code = Asc(UCase(Mid(ColStr, position, 1)))
        If Not (64 < Code And Code < 91) Then
            MsgBox "アルファベットを引数に渡し手下さい"
            End
        Else
            List(i) = Code
        End If
        position = position + 1
    Next
    
    ColFromStrToNum = 0
    For i = 0 To UBound(List)
        ColFromStrToNum = ColFromStrToNum + (List(i) - 64) * 26 ^ i
    Next
End Function
本当は、まだ手を加えたかったのですが別の機会とします。

[ExcelVBA] n人中m人が当選するくじ


■お題
n人中m人が当選するくじ除(問題出典:どう書く?org)
- 問題詳細 -
n人の中から公平にm人を選ぶ、くじ引きプログラムを作ってください。

■解答例
流れとしては、配列にn人分の要素スペースを作って
シャッフルしてm人分を取り出すだけの簡単なお仕事ですね。

シャッフルは、Fisher-Yatesを使いました。
最終的なコードはこちら→最終的なコード


■Fisher-Yatesアルゴリズムを使ってみる
Fisher-Yatesアルゴリスムとは・・・
配列の一番最後の要素とそれより前の要素群の中から乱数で一つ選び入れ入替える。
そして、配列の要素を一つ前に移動して上記を繰り返すことで入替える。

例えば5つの要素をもった配列があったとします

1サイクル目--------------------------------------------------------------
最後の要素(5)とそれより前(1,2,3,4)の要素から乱数で一つ数字を選びます


乱数で選ばれた数字(ここでは2)と入替をします。




2サイクル目--------------------------------------------------------------
一つ前の要素(4)とそれより前(1,5,3)の要素から乱数で一つ数字を選びます


乱数で選ばれた数字(ここでは1)と入替をします。




3サイクル目--------------------------------------------------------------
一つ前の要素(3)とそれより前(4,5)の要素から乱数で一つ数字を選びます



乱数で選ばれた数字(ここでは5)と入替をします。




4サイクル目--------------------------------------------------------------
1番目と2番目の要素を入れ替えます




では、コードです。
モジュール名:SubModule
Option Explicit

Function lot(n As Integer, m As Integer) As Variant
    Dim list As Variant, i As Integer
    ReDim list(n - 1)
    
    For i = 0 To n - 1
        list(i) = i + 1
    Next
    
    
    Call ArrayShuffle(list)
    
    
    Dim result As Variant
    ReDim result(m - 1)
    For i = 0 To m - 1
        result(i) = list(i)
    Next
    
    lot = result
End Function
3行目:引数は2つ。全数であるnと抽選される人数であるm
4行目:抽選に使うn人分の要素を持った配列を作るためlistを宣言する
5行目:配列の要素を確定する
7~9行目:For分で各要素に抽選番号替わりの数字を格納する
12行目:配列の要素をシャッフルする自作関数。
引数は、参照渡しで渡しています
15行目:当選者を格納する配列用の変数を宣言
16行目:要素数を定義
17~19行目:m人当選するのでlistの最初の要素からm番目までをresult変数に取り出す
21行目:取り出した値を格納しているresult変数を返す



モジュール名:CommonModule
Option Explicit

Sub ArrayShuffle(ByRef list As Variant)
    Randomize
    Dim i As Integer, temp As Integer, RandNum As Integer
    For i = UBound(list) To 1 Step -1
        RandNum = Int(Rnd() * i)
        temp = list(RandNum)
        list(RandNum) = list(i)
        list(i) = temp
    Next
End Sub
参照渡しで引数を受け取ります。
配列の要素が大量にある場合、値渡しで受け取るより参照渡しで
やりとりしたほうが効率的なので、参照渡しにしています。
3行目:配列を参照渡しでうけとります。
4行目:乱数を使うのでRandomizeで乱数の初期化します。
5行目:配列のインデックス用にi変数、
配列の2要素を入れ替えるようにtemp変数
乱数を格納するようにRandNum変数を宣言
6行目:For文で一番最後の要素から2番目の要素までループさせる
7行目:乱数でi番目より前の要素から一つ選ぶ
8行目:同時に2要素を入れ替えれないので、乱数で選んだ要素の値を一時退避する
9行目:乱数で選ばれた要素にi番目の要素を複製する
10行目:i番目の要素に一時退避していた乱数で選んだ要素の値を複製する
11行目:繰り返し


実際にこんな感じで使う

モジュール名:MainModule
Option Explicit

Sub main()
    Dim result As Variant
    result = lot(10, 3)
    
    Dim i As Integer
    For i = 0 To UBound(result)
        Debug.Print result(i)
    Next
    
    Debug.Print "-------------"
End Sub


実行結果
7 
 4 
 6 
-------------
 8 
 3 
 5 
-------------
 7 
 9 
 1 
-------------
 5 
 4 
 8 
-------------


以下に最終的なコードを示しておきます
モジュール名:SubModule
Option Explicit

Function lot(n As Integer, m As Integer) As Variant
    '### n人の中からm人を選ぶ関数
    '### 引数
    '###   省略不可:n…参加者全員の数を指定
    '###   省略不可:m…選ばれる人数を指定
    '###
    '### ArrayShuffle関数を必要
    '########################################

    Dim list As Variant, i As Integer
    ReDim list(n - 1)
    
    For i = 0 To n - 1
        list(i) = i + 1
    Next
    
    
    Call ArrayShuffle(list)
    
    
    Dim result As Variant
    ReDim result(m - 1)
    For i = 0 To m - 1
        result(i) = list(i)
    Next
    
    lot = result
End Function



モジュール名:CommonModule
Option Explicit

Sub ArrayShuffle(ByRef list As Variant)
    '### 配列の要素をシャッフルする関数
    '### 変数
    '###   省略不可:list…参照渡しで配列を渡す
    '##########################################

    Randomize
    Dim i As Integer, temp As Integer, RandNum As Integer
    For i = UBound(list) To 1 Step -1
        RandNum = Int(Rnd() * i)
        temp = list(RandNum)
        list(RandNum) = list(i)
        list(i) = temp
    Next
End Sub

[ExcelVBA] 1ヶ月のカレンダーを表示する


■お題
1ヶ月表示のカレンダーを作ってみてください。

■解答例
今回は、修正のしやすさを考慮してモジュールをいくつかに分けて(整理しながら)書いてみようと思います。
今回、
MainModule…メインのコードの流れを書く
SubModule…メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
CommonModule…他のマクロからも活用する機会がありそうな関数
SetModule…設定変数が書いてある変数
SandBoxModule…わからないことや書いたコードのテストを書く
このように分けて書く事であとで修正する際にどこを見ればいいか判断しやすい状態にしておく。

ひとまず、カレンダーの完成図をイメージしてみる。

(※列の幅は、2.5になっています。)


まずは、カレンダーの基準セル(カレンダーの左上の位置)を決めその位置から
相対的にパーツを配置することで変更が簡単にできるようにしておく。

モジュール名:SetModule…(分けてる目的)設定変数をまとめておく。
Public Const BASE_CELL_ROW = 2
Public Const BASE_CELL_COL = 2

1行目:カレンダーの左上のセルで行の情報を格納
2行目:カレンダーの左上のセルで列の情報を格納
どの位置に出力するかは、この変数の値をかえればよいと。


次に、日付を表示してみます。
基準(カレンダーの左上)と同じ行で、2列隣の場所に表示することにします。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub Say_Today()
    Dim r As Long, c As Integer
    r = BASE_CELL_ROW
    c = BASE_CELL_COL
    Cells(r, c + 2) = Date
End Sub

2行目:行を格納変数、列を格納する変数を用意。行は、65536行まであることからLong型
3行目:基準セルの行を代入
4行目:基準セルの列を代入
5行目:基準セルと同じ行、2つ列を移動したとこに日付を表示


ここで、考察。
今後、関数を書くたびに行と列を格納する変数を用意し、
それぞれ毎回初期化するたびに、お決まりの2行を書くことになるので
ちょっとでも楽するために1行で代入が済ませれるよう工夫してみる。


上記コードがこうなる。
モジュール名:SetModule…(分けてる目的)設定変数をまとめておく
Sub InitBaseCell(ByRef r As Long, ByRef c As Integer)
    r = BASE_CELL_ROW
    c = BASE_CELL_COL
End Sub

Sub Say_Today()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Cells(r, c + 2) = Date
End Sub

InitBaseCellという、基準セル位置を初期化する用の関数を用意しました。
これは、参照渡しで値を渡すため、列と行の順番で渡してあげればそちらで代入することになるので
利用する時は、Call InitBaseCellの一行で済ませれます。


想定した挙動になっているか確認
モジュール名:SandBoxModule(分けてる目的)わからないことや書いたコードのテストを書く
Sub t1()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Debug.Print "Row:" & r & " Col:" & c
End Sub

特に問題ないですね。



次に、日~土の曜日を表示する関数を追加します。
モジュール名:SetModule…(分けてる目的)設定変数をまとめておく
Sub Say_DayStr()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    r = r + 1
    
    Cells(r, c) = "日"
    Cells(r, c + 1) = "月"
    Cells(r, c + 2) = "火"
    Cells(r, c + 3) = "水"
    Cells(r, c + 4) = "木"
    Cells(r, c + 5) = "金"
    Cells(r, c + 6) = "土"
End Sub

2行目:行を格納変数、列を格納する変数を用意。行は、65536行まであることからLong型
3行目:行と列変数に基準セルの行と列の値を代入
4行目:基準セルの一段下に表示したいので1加算しておく
6行~12行目:日~土を代入。


ここまでで、ちゃんと表示されるかテストしてみます。
モジュール名:SandBoxModule(分けてる目的)わからないことや書いたコードのテストを書く

Sub t2()
    'Show_Today関数のテスト
    Call Say_Today
    Call Say_DayStr
End Sub

こうなった。

(※列の幅は、2.5になっています。)

日付部分は、セルの範囲が狭いのできちんと表示されてないですけど
セルの結合等で適切なサイズのセルを用意すれば問題ないですね。
見た目の調整については、いつやっても問題ないことに関しては極力最後かつ1箇所の関数にまとめておきます。
修正する際にあちこちにちらばっているとさがすのが大変だからです。



次に、日にちを表示する関数を追加してみます。1日からその月の最終日をFor文でぐるぐる回しながら
表示していけばよさそうですね。ここで最終日を取得する関数も作ることにします。この関数は
他のマクロでも使えそうなので、CommonModuleにまとめておきます。
では、コードはこんな感じ。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub Say_Day()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim d As Integer
    r = BASE_CELL_ROW + 2
    For d = 1 To LastDay()
        c = BASE_CELL_COL + Weekday(DateSerial(Year(Date), Month(Date), d)) - 1
        Cells(r, c) = d
        If Weekday(DateSerial(Year(Date), Month(Date), d)) = 7 Then r = r + 1
    Next
End Sub

モジュール名:SubModule内
2行目:行を格納変数、列を格納する変数を用意。行は、65536行まであることからLong型
3行目:行と列変数に基準セルの行と列の値を代入
5行目:For文で使う変数d(日にち部分を格納)を宣言
6行目:日にちは、基準セル(カレンダーの左上)より2つ下の行にかくことにします
7行目:For文 初期値は、日付なので1日。TOは、その月の最後の日付。LastDayという関数を別途作る。

先にLastDay関数について
関数の目的として、他のマクロから利用しやすくするために
ある日付をあたえるとその月の最後の日にちを返すということにします。
引数は、省略可能で省略すると今月の最後の日付を返すことにします。
最終日を求めるのに、まず次の月の1日の日付を調べ、
その前日の日付を割り出すことで最終日を求めることにします。

モジュール名:CommonModule…(分けてる目的)他のマクロでも使えそうな関数
Function LastDay(Optional UserDate As Date) As Integer
    Dim SetDate As Date
    
    If UserDate Then
        SetDate = UserDate
    Else
        SetDate = Date
    End If
    
    Dim NextMonthFirstDate As Date '次の月の1日の日付を格納
    NextMonthFirstDate = DateSerial(Year(SetDate), Month(SetDate) + 1, 1)
   
    Dim LastDate As Date 'SetDate日付の最後の日付を格納
    LastDate = DateAdd("d", -1, NextMonthFirstDate)
    
    LastDay = Day(LastDate)
End Function

モジュール名:CommonModule内
1行目:引数は省略可能ということでOptionalキーワードをつけてます。
2行目:SetDateという変数を宣言します。(この変数の日付の最後の日を返す)
4行目:If文でオプション用の引数に値があるかチェック
5行目:値がある時は、引数をSetDateに格納
6行目:引数がない時
7行目:今日の日付を格納
10行目:NextMonthFirstDateを宣言。
11行目:DateSerial関数を使って、setDateに入っている日付の次の月の1日の日付を代入する
13行目:SetDateに格納されている日付の最後の日付を格納する変数を宣言
14行目:次の月の1日の日付(NextMonthFirstDate)からDateAdd関数で1日戻ることで目的の最終日を求める
16行目:最終的に、Day関数で日にちを取り出して返す


じゃ、ちゃんと動くかテストしてみます。
モジュール名:SandBoxModule(分けてる目的)わからないことや書いたコードのテストを書く
Sub t5()
    Debug.Print LastDay()
    Debug.Print LastDay(CDate("2010/2/10"))
    Debug.Print ""
End Sub

ちゃんと動いていますね。


SetModuleのFor文に戻ります。
<再掲>
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub Say_Day()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim d As Integer
    r = BASE_CELL_ROW + 2
    For d = 1 To LastDay()
        c = BASE_CELL_COL + Weekday(DateSerial(Year(Date), Month(Date), d)) - 1
        Cells(r, c) = d
        If Weekday(DateSerial(Year(Date), Month(Date), d)) = 7 Then r = r + 1
    Next
End Sub

改めてFor文の中身を解説します。
行と列の特定方法は、列は、Weekday関数を使うことで1~7の値を返してくれるので
基準セルの列 + Weekday関数の返す値(1~7) - 1(補正値)
例えば、日曜日はWeekday関数で1を返すので基準セルと同じ列に表示するには、
Weekday関数の返す値から1ひかないと合わなくなります。
補正値と書いているのは、そういうことです。

行に関しては、Weekday関数の値を監視して土曜日だったら、日付表示後に行の変数の値を1つ増やせばいいですね。
8行目:列を求める
9行目:日にちを表示
10行目:表示した日が土曜日だったら、行の変数の値を1つ加算して次の行へ移動する
11行目:繰り返し


ここらへんで、カレンダーを表示するための関数としてmainという
名前の関数を用意して今までのコードを実行してみます。
モジュール名:MainModule…(分けてる目的)SubModuleに書いた関数を組み合わせカレンダーを作成する
Sub main()
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
End Sub

シンプルで見通しがよいですね。
2行目:今日の日付を表示
3行目:曜日(日~土)を表示
4行目:1ヶ月文の日にちを表示


(※列の幅は、2.5になっています。)

データを書き込む部分は、これでおしまいですね。
あとは、見た目部分を調整していきます。
見た目は、いつ設定しても問題ないものについては最後に一つの関数内にまとめることにします。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub View()
    Dim BaseR As Long, BaseC As Integer
    Call InitBaseCell(BaseR, BaseC)
    
    '日付部分処理
    Range(Cells(BaseR, BaseC + 2), Cells(BaseR, BaseC + 4)).Merge
    With Cells(BaseR, BaseC + 2)
        .Font.Size = 8
        .Font.Bold = True
        .HorizontalAlignment = xlCenter
    End With
    
    '曜日部分処理
    Range(Cells(BaseR + 1, BaseC), Cells(BaseR + 1, BaseC + 6)).HorizontalAlignment = xlCenter

    '土曜日を青色
    Range(Cells(BaseR + 1, BaseC + 6), Cells(BaseR + 6, BaseC + 6)).Font.ColorIndex = 5
    
    '日曜日を赤色
    Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC)).Font.ColorIndex = 3

    '全体に関する設定
    With Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Borders(xlEdgeLeft).LineStyle = xlContinuous
        .Borders.ColorIndex = 56
    End With
    
    With Range(Cells(BaseR, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Interior.ColorIndex = 19
        .Font.Bold = True
        .Borders(xlEdgeLeft).Weight = xlMedium
        .Borders(xlEdgeLeft).ColorIndex = 56
        .Borders(xlEdgeBottom).Weight = xlMedium
        .Borders(xlEdgeBottom).ColorIndex = 56
        .Borders(xlEdgeRight).Weight = xlMedium
        .Borders(xlEdgeRight).ColorIndex = 56
        .Borders(xlEdgeTop).Weight = xlMedium
        .Borders(xlEdgeTop).ColorIndex = 56
    End With
    
    '本日のセルの色を変える
    Dim r As Integer
    r = 2
    Do
        If Cells(BaseR + r, BaseC + Weekday(Date) - 1) = Day(Date) Then
            Cells(BaseR + r, BaseC + Weekday(Date) - 1).Interior.ColorIndex = 35
            Exit Do
        End If
        r = r + 1
    Loop
End Sub

表示部分なので、ざくざく軽く説明していきます。
ここらへんは、マクロの記録で調べつつ余分なとこは省いたコードをのっけていってますね。
2行目:今まで他の関数ではrとcという変数を用意して作業してたのですがrとcはそれ自身の値を
書き換えて使っていたので、書き換えずに使うという意図を含めてBaseRとBaseCという変数にしました。
3行目:行と列変数に基準セルの行と列の値を代入

6行目~11行目
・日付部分のセル範囲がせまくて##になっているのでD~F列を結合して広くします。
・文字サイズは、8に変更
・太文字フォントに変更

14行目:曜日のセルをセンタリングします
17行目:土曜日の列を青色文字へ変更
20行目:日曜日の列を赤色文字へ変更

23行目~26行目:曜日より下の行と列に罫線をひきます。細線です
28行目~39行目:
・カレンダー全体に対し背景色を薄い黄色に変更
・太文字へ変更
・カレンダーの四辺を太線罫線で表示

42行目~50行目:本日のセルの色を変える
列は、Weekday関数と基準セル列があればわかりあとはDoループで今日の
日にちと一致するセルを探し見つかり次第セルの色を変えてます


上記コード内で、カレンダーの最後の行を取得する関数(CalendarEndRow)を作って使ってます。
20、23、28行目です。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Function CalendarEndRow() As Long
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim UsedCell As Integer
    UsedCell = LastDay() + Weekday(DateSerial(Year(Date), Month(Date), 1)) - 1
    r = BASE_CELL_ROW + 1 + Int(UsedCell / 7)
    If 0 < (UsedCell / 7 - Int(UsedCell / 7)) Then r = r + 1
    CalendarEndRow = r
End Function
main関数にView関数の呼び出しを追加して実行してみます。 モジュール名:MainModule…(分けてる目的)SubModuleに書いた関数を組み合わせカレンダーを作成する
Sub main()
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
    Call View
End Sub
(※列の幅は、2.5になっています。) 最後の仕上げ。 月をまたぐと始まりの日の曜日や最終日がかわるので表示が残っているとおかしなことになります。 加えて、当日のセルは緑色に変更しているので毎日緑のセルがふえていくことになってしまうので 最初に初期化するコードを挟んでおきます。 基準セル位置から8行7列を削除してます。 モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub init()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Range(Cells(r, c), Cells(r + 7, c + 8)).Clear
End Sub
そして、main関数にこの関数を追加すると完成です。 モジュール名:MainModule…(分けてる目的)SubModuleに書いた関数を組み合わせカレンダーを作成する
Sub main()
    Call init
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
    Call View
End Sub
最後に全コードを改めて掲載しておきます。 ◆モジュール名:SetModule
Option Explicit

Public Const BASE_CELL_ROW = 2
Public Const BASE_CELL_COL = 2
◆モジュール名:MainModule
Option Explicit

Sub main()
    Call init
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
    Call View
End Sub
◆モジュール名:SubMocule
Option Explicit

Sub Say_Today()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Cells(r, c + 2) = Date
End Sub

Sub Say_DayStr()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    r = r + 1
    
    Cells(r, c) = "日"
    Cells(r, c + 1) = "月"
    Cells(r, c + 2) = "火"
    Cells(r, c + 3) = "水"
    Cells(r, c + 4) = "木"
    Cells(r, c + 5) = "金"
    Cells(r, c + 6) = "土"
End Sub

Sub Say_Day()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim d As Integer
    r = BASE_CELL_ROW + 2
    For d = 1 To LastDay()
        c = BASE_CELL_COL + Weekday(DateSerial(Year(Date), Month(Date), d)) - 1
        Cells(r, c) = d
        If Weekday(DateSerial(Year(Date), Month(Date), d)) = 7 Then r = r + 1
    Next
End Sub

Sub View()
    Dim BaseR As Long, BaseC As Integer
    Call InitBaseCell(BaseR, BaseC)
    
    '日付部分処理
    Range(Cells(BaseR, BaseC + 2), Cells(BaseR, BaseC + 4)).Merge
    With Cells(BaseR, BaseC + 2)
        .Font.Size = 8
        .Font.Bold = True
        .HorizontalAlignment = xlCenter
    End With
    
    '曜日部分処理
    Range(Cells(BaseR + 1, BaseC), Cells(BaseR + 1, BaseC + 6)).HorizontalAlignment = xlCenter

    '土曜日を青色
    Range(Cells(BaseR + 1, BaseC + 6), Cells(BaseR + 6, BaseC + 6)).Font.ColorIndex = 5
    
    '日曜日を赤色
    Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC)).Font.ColorIndex = 3

    '全体設定
    '背景色を設定
    With Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Borders(xlEdgeLeft).LineStyle = xlContinuous
        .Borders.ColorIndex = 56
    End With
    
    With Range(Cells(BaseR, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Interior.ColorIndex = 19
        .Font.Bold = True
        .Borders(xlEdgeLeft).Weight = xlMedium
        .Borders(xlEdgeLeft).ColorIndex = 56
        .Borders(xlEdgeBottom).Weight = xlMedium
        .Borders(xlEdgeBottom).ColorIndex = 56
        .Borders(xlEdgeRight).Weight = xlMedium
        .Borders(xlEdgeRight).ColorIndex = 56
        .Borders(xlEdgeTop).Weight = xlMedium
        .Borders(xlEdgeTop).ColorIndex = 56
    End With
    
    '本日のセルの色を返る
    Dim r As Integer
    r = 2
    Do
        Debug.Print Cells(BaseR + r, BaseC + Weekday(Date) - 1)
        If Cells(BaseR + r, BaseC + Weekday(Date) - 1) = Day(Date) Then
            Cells(BaseR + r, BaseC + Weekday(Date) - 1).Interior.ColorIndex = 35
            Exit Do
        End If
        r = r + 1
    Loop
End Sub

Sub init()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Range(Cells(r, c), Cells(r + 7, c + 8)).Clear
End Sub

Function CalendarEndRow() As Long
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim UsedCell As Integer
    UsedCell = LastDay() + Weekday(DateSerial(Year(Date), Month(Date), 1)) - 1
    r = BASE_CELL_ROW + 1 + Int(UsedCell / 7)
    If 0 < (UsedCell / 7 - Int(UsedCell / 7)) Then r = r + 1
    CalendarEndRow = r
End Function

Sub InitBaseCell(ByRef r As Long, ByRef c As Integer)
    r = BASE_CELL_ROW
    c = BASE_CELL_COL
End Sub
◆モジュール名:CommonModule
Option Explicit

Function LastDay(Optional UserDate As Date) As Integer
    Dim SetDate As Date
    
    If UserDate Then
        SetDate = UserDate
    Else
        SetDate = Date
    End If
    
    Dim NextMonthFirstDate As Date '来月1日の日付
    NextMonthFirstDate = DateSerial(Year(SetDate), Month(SetDate) + 1, 1)
   
    Dim LastDate As Date '今月最後の日付
    LastDate = DateAdd("d", -1, NextMonthFirstDate)
    
    LastDay = Day(LastDate)
End Function
◆モジュール名:SandBox
Option Explicit

Sub t1()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Debug.Print "Row:" & r & " Col:" & c
End Sub

Sub t2()
    'Show_Today関数のテスト
    Call Say_Today
    Call Say_DayStr
End Sub

Sub t3()
    Dim yyyymmdd As Date
    yyyymmdd = DateSerial(Year(Date), Month(Date), 1)
    Debug.Print yyyymmdd
    Debug.Print Weekday(yyyymmdd) '1=>日,7=>土
    Debug.Print ""
End Sub

Sub t4()
    Dim today As Date
    today = Date
    Debug.Print "本日の日付  :" & today
    
    Dim NextMonthFirstDate As Date
    NextMonthFirstDate = DateSerial(Year(Date), Month(Date) + 1, 1)
    Debug.Print "来月1日の日付:" & NextMonthFirstDate
    
    Dim LastDate As Date
    LastDate = DateAdd("d", -1, NextMonthFirstDate)
    Debug.Print "今月最後の日付:" & LastDate
    
    Dim LastDay As Integer
    LastDay = Day(LastDate)
    Debug.Print "今月の最後の日:" & LastDay
    Debug.Print ""
End Sub

Sub t5()
    Debug.Print LastDay()
    Debug.Print LastDay(CDate("2010/2/10"))
    Debug.Print ""
End Sub

Sub t6()
    Static num As Integer
    num = num + 1
    Debug.Print num
End Sub

[ExcelVBA] 指定されたフォルダ以下のゴミ掃除


■お題
指定されたフォルダ以下のゴミ掃除(問題出典:どう書く?org)
- 問題詳細 -
指定したフォルダ以下にある、ファイル名が"~"で終わるファイルを削除するプログラムを作ってください。 指定したフォルダの中にあるフォルダのさらに中にあるファイルも削除の対象です

■解答例
まずは、指定したフォルダ以下の全てをフォルダを調べる必要がありますね。
それに関しては、以前SampleCodeで「指定したフォルダ配下の全てのフォルダを調べる」で書いたのでそれをそっくり使いましょう。フォルダの走査については、上記リンクを参照してください。

上記で書いたものは、GetAllFolderPathという関数に対し、あるフォルダのパスを引数に渡すと返り値として配列が帰ってきて、各要素のフォルダのフルパスが格納されています。よって、フルパスがわかるとこまでは何もしなくてよいので、今度は各フォルダがわかったとこで、その中に入っているフォルダが何か?またファイル名がチルダ(~)で終わっている時削除する関数を書いてみます。

Sub DellFile(ByVal f As String)
    Dim FSO As New FileSystemObject
    Dim Folder As Folder, Files As Files, File As File
    Set Folder = FSO.GetFolder(f)
    Set Files = Folder.Files
    
    For Each File In Files
        If Len(File.Name) = InStrRev(File.Name, "~") Then
            Kill Pathname:=f & "\" & File.Name
        End If
    Next
End Sub

2行目:ファイルシステムオブジェクトを生成
3行目:Folder型、Files型、File型のオブジェクトの変数を宣言
4行目:引数で受け取るフォルダのフルパスをもとにGetFolderでフォルダの情報を取得
5行目:Folder内のファイルの情報を取得
7行目:For Each文でファイルを一つ一つ処理する。
8行目:ファイルの名前の長さとファイル名のチルダの位置(後ろから検索InStrRev)が一致するか判定
9行目:Kill関数にファイルパスを指定して削除する
11行目:繰り返し

これと、フォルダの走査と組み合わせた全コードは下記の通り。

Option Explicit

Sub main()
    Dim FolderName As String, Result() As String
    FolderName = "C:\test"
    Result = GetAllFolderPath(FolderName)

    Dim f As Variant
    For Each f In Result
        DellFile (f)
    Next
End Sub

Sub DellFile(ByVal f As String)
    Dim FSO As New FileSystemObject
    Dim Folder As Folder, Files As Files, File As File
    Set Folder = FSO.GetFolder(f)
    Set Files = Folder.Files
    
    Dim position As Integer
    For Each File In Files
        If Len(File.Name) = InStrRev(File.Name, "~") Then
            Kill Pathname:=f & "\" & File.Name
        End If
    Next
End Sub

Function GetAllFolderPath(ByVal FolderName As String)
    Dim FolderPathList() As String, LastIndex As Integer
    LastIndex = -1
    Call GetFolderPath(FolderName, FolderPathList, LastIndex)
    If LastIndex = -1 Then
        GetAllFolderPath = Array("")
        Exit Function
    End If
    
    Dim i As Integer
    i = 0

    Do
        Call GetFolderPath(FolderPathList(i), FolderPathList, LastIndex)
        i = i + 1
    Loop While i < LastIndex

    GetAllFolderPath = FolderPathList
End Function

Sub GetFolderPath(ByVal FolderName As String, ByRef FolderPathList As Variant, ByRef LastIndex As Integer)
    Dim FSO As New FileSystemObject
    Dim Folders As Folders, Folder As Folder
    Set Folders = FSO.GetFolder(FolderName).SubFolders
    
    For Each Folder In Folders
        LastIndex = LastIndex + 1
        If LastIndex = 0 Then
            ReDim FolderPathList(LastIndex)
        Else
            ReDim Preserve FolderPathList(LastIndex)
        End If
        
        FolderPathList(LastIndex) = Folder.Path
    Next
End Sub
上記関数を使う部分を簡単めも
5行目:フォルダ名を指定してますね。
6行目:GetAllFolderPath関数にフォルダパスを渡し、配列で配下のフォルダパスを受け取る。
9行目:5行目で受け取ったフォルダパスの配列をForEachで展開。
10行目:先程作ったDellFile関数にフォルダパスを渡し、その先で該当するファイルを削除する。
以上。

[ExcelVBA] 指定したフォルダ配下の全てのフォルダを調べる


■お題
指定したフォルダ以下にある全てのフォルダを表示すること

■解答例
ファイルシステムオブジェクトの「オブジェクト.SubFolders」を使えばよさそうですね。
まずは、考え方です。こういう風に考えてみました。
指定フォルダに対し、「オブジェクト.SubFolders」で指定フォルダ内のサブフォルダが取得できます。
例えば、指定フォルダには10個のフォルダがあったとします。
この情報を配列に格納しておきます。


次に、この配列に対しDoLoopで一つ一つ、サブフォルダを取得するのを繰り返せばよさそうです。
ただし、DoLoop内で新たに見つけたサブフォルダを処理の候補にいれる為共通の配列に格納する必要があります。

↑新たにみつけたものを追加。

そこで、引数の受け渡しに参照渡し(ByRef)で渡してフォルダのリストを更新し続けます。

まずは、サブフォルダの一覧を取得するテストコードを書いてみます。
Option Explicit

Sub test1()
    Dim FolderPath As String
    FolderPath = "C:\test"
    
    Dim FSO As New FileSystemObject
    Dim Folders As Folders, Folder As Folder
    Set Folders = FSO.GetFolder(FolderPath).SubFolders

    For Each Folder In Folders
        Debug.Print Folder.Path
    Next
End Sub
4行目:指定するファルダ名を格納する変数
5行目:今回は、C:\testフォルダパスを指定
7行目:ファイルシステムオブジェクトを生成
8行目:Folders型とFolder型のオブジェクトの変数を宣言
9行目:指定したフォルダ内の全てのフォルダの情報を取得
11行目:For文で8行目で取得した全てのフォルダ情報を一つずつ渡していく
12行目:フォルダ情報のうち、パスのプロパティをデバック表示する
13行目:繰り返し。


これで、指定したフォルダ内にあるフォルダパスを取得できますね。
では、参照渡しで一つの配列にフォルダパスを格納するあたりを確認するコードを書いてみます。
Option Explicit

Sub test2_main()
    Dim FolderPath As String
    FolderPath = "C:\test"
    
    Dim FolderList As Variant
    FolderList = Array("FolderPass_1", "FolderPass_2", "FolderPass_3")
    Call test2_sub(FolderPath, FolderList)
    
    Dim Folder As Variant
    For Each Folder In FolderList
        Debug.Print Folder
    Next
End Sub

Sub test2_sub(ByVal FolderPath As String, ByRef FolderList As Variant)    
    Dim FSO As New FileSystemObject
    Dim Folders As Folders, Folder As Folder
    Set Folders = FSO.GetFolder(FolderPath).SubFolders
    
  Dim i As Integer
    i = UBound(FolderList) + 1
    
    For Each Folder In Folders
        ReDim Preserve FolderList(i)
        FolderList(i) = Folder.Path
                
        i = i + 1
    Next
End Sub

- test2_main -
4行目:指定するファルダ名を格納する変数
5行目:今回は、C:\testフォルダパスを指定
7行目:フォルダパスを一括管理する用の変数です。
参照渡しでサブ関数に渡してフォルダパス情報を受け取るのに使います。
8行目:フォルダパスが追加されるか確認するために、適当に何か配列に格納しておきます。
9行目:パスと、配列を渡し、"test2_b"で第一引数のパス配下にあるフォルダ情報を格納させる。
11行目:参照渡しで格納してもらった配列の中身をFor文で確認する為For文で使う変数を宣言
12行目:For文で配列の一覧を確認
13行目:デバック表示する
14行目:繰り返し

- test2_sub -
18行目:ファイルシステムオブジェクトを生成
19行目:Folders型とFolder型のオブジェクトの変数を宣言
20行目:指定したフォルダ内の全てのフォルダの情報を取得
22行目:動的に配列を増やす際に使う変数を宣言。要素数の管理に使用。
23行目:再宣言するために現在の配列の最後の要素数を調べる。
そして、再宣言するために1多くしとく。(注:後で問題発生します)
25行目:For文で19行目で取得した全てのフォルダ情報を一つずつ渡していく
26行目:配列の要素数を再定義をする。 Preserveキーワードをつけないと今まで格納した
データが消えてしまうので、Preserveキーワードをつけておく。
27行目:最後の要素にフォルダパスを格納する
29行目:配列の再定義に使う変数を1つインクリメントする
30行目:繰り返し

メモ:Uboundは、配列の一番最後の要素数を返す。

あとは、DoLoopで繰り返す部分を作れば完成ですね。

Option Explicit

Sub main()
    Dim FolderName As String, Result() As String
    FolderName = "C:\test"
    Result = GetAllFolderPath(FolderName)

    Dim f As Variant
    For Each f In Result
        Debug.Print f
    Next
End Sub

Function GetAllFolderPath(ByVal FolderName As String)
    
    Dim FolderPathList() As String, LastIndex As Integer
    LastIndex = -1
    Call GetFolderPath(FolderName, FolderPathList, LastIndex)
    If LastIndex = -1 Then
        GetAllFolderPath = Array("")
        Exit Function
    End If
    
    Dim i As Integer
    i = 0
    
    Do
        Call GetFolderPath(FolderPathList(i), FolderPathList, LastIndex)
        i = i + 1
    Loop While i < LastIndex

    GetAllFolderPath = FolderPathList
End Function

Sub GetFolderPath(ByVal FolderName As String, ByRef FolderPathList As Variant, ByRef LastIndex As Integer)
    Dim FSO As New FileSystemObject
    Dim Folders As Folders, Folder As Folder
    Set Folders = FSO.GetFolder(FolderName).SubFolders
    
    For Each Folder In Folders
        LastIndex = LastIndex + 1
        If LastIndex = 0 Then
            ReDim FolderPathList(LastIndex)
        Else
            ReDim Preserve FolderPathList(LastIndex)
        End If
        
        FolderPathList(LastIndex) = Folder.Path
    Next
End Sub
動的配列用の変数は、要素数が確定する前にUBoundを使うとエラーとなるので 使わずに、要素数を知るために、管理用に一つ変数を増やして参照渡しして管理してます。 使い方は、GetAllFolderPathに調べたいフォルダのパスを引数として渡す。 で、要素数を指定してない配列を返り値に受け取る。 以上。 ちなみに、下記フォルダ構成だと。
イミディエイトウィンドウには、こんな出力になる。

2011年12月21日水曜日

[ExcelVBA] 数あてゲーム


■お題
コンピューターが選んだ数字(1~1000)を10回以内に当てるゲーム。答えた際に予想した数字が答えより低かったら「もっと上」逆なら「もっと下」等のヒントを出すこと。

■解答例
まずは、乱数を用いてコンピュータが1~1000の数字を選ぶところから作り始める。いきなり1~1000ではなく0~3を出すコードを考えてみる。
Option Explicit

Sub TestCode1()
    Randomize
    Debug.Print (Int(Rnd() * 4))
End Sub


Randomizeについてですが、ExcelVBAではこれを指定しないとRnd関数は、Excelを開く度にに同じ値を返してしまいます。
Rnd関数が0以上1未満の連続する値であることから、それを4倍して整数部だけにすると0以上4未満の整数になるので0~3を出すのは問題なさそう。
次に1~1000にすることを考える。
Option Explicit

Sub TestCode2()
    Randomize
    Debug.Print (Int(Rnd() * 1000)) + 1
End Sub

同様の考えをすると(Int(Rnd() * 1000)で0~999の整数値が得られるので、+1をしてあげると1~1000の整数値が得られる。
乱数部分完成。まずは、こんな感じに書いてみる。
Option Explicit

Sub MainCode()
    Randomize
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
End Sub

次に、ゲームの流れを考えると・・・
ユーザーが答えを入力する→コンピュータの値と比べる→間違える→ヒントを出す→
ユーザーが答えを入力する→コンピュータの値と比べる→間違える→ヒントを出す→
ユーザーが答えを入力する→コンピュータの値と比べる→間違える→ヒントを出す→
ユーザーが答えを入力する→コンピュータの値と比べる→正解する
という流れを見るとループで上記部分を作ればよさそう。そして、コンピュータの値と比べる行為が1サイクルのうち途中に 来ているのでループ文の最初の部分(Doのすぐ後)に判定をするわけにも最後の部分(Loopのすぐ後)で判定するわけにもいかない。 よって、途中にIf文でユーザー入力値と比較して正解していればExit Doでループを抜けるとよさそう。
Option Explicit

Sub MainCode()
    Randomize
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
    
    Dim UserInputData As Integer
    Do
        If UserInputData = Answer Then
            '当たった時の処理
            MsgBox ("正解です")
            Exit Do
        Else
            'はずれた時の処理
            MsgBox ("はずれです" & "[" & Answer & "]")
        End If
    Loop
End Sub

ひとまず、テストすることを考えて間違ってたら答えを表示するようにしました。
次に、ユーザーから予想値をInputBoxにて受け取る
Option Explicit

Sub MainCode()
    Randomize
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
    
    Dim UserInputData As Integer
    Do
        UserInputData = InputBox("1~1000の間の整数値を入力してね")
        If UserInputData = Answer Then
            '当たった時の処理
            MsgBox ("正解です")
            Exit Do
        Else
            'はずれた時の処理
            MsgBox ("はずれです" & "[" & Answer & "]")
        End If
    Loop
End Sub

ここまでで実際に動かしてチェックしてみることにします。
どうやら問題なさそうです。
あとは、10回でゲームを終了させる部分と、間違っていた時のヒントを表示する部分を作ります。
まずは、ヒント部分から。間違ってたらヒントを出すわけなので、IF文のElse内でユーザーの入力値と 答えを比較して大きい場合と小さい場合でメッセージを表示します。
Option Explicit

Sub MainCode()
    Randomize
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
    
    Dim UserInputData As Integer
    Do
        UserInputData = InputBox("1~1000の間の整数値を入力してね")
        If UserInputData = Answer Then
            '当たった時の処理
            MsgBox ("正解です")
            Exit Do
        Else
            'はずれた時の処理
            If UserInputData < Answer Then
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと大きいです!")
            Else
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと小さいです!")
            End If
        End If
    Loop
End Sub
後は、10回で終了させるようにしましょう。
Option Explicit

Sub MainCode()
    Randomize
    Dim GameCount As Integer
    GameCount = 10 '何回でゲーム終了とするか設定する
    
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
    
    Dim UserInputData As Integer
    Do
        UserInputData = InputBox("1~1000の間の整数値を入力してね")
        GameCount = GameCount - 1 '入力後にゲームカウントを減らす
        
        If UserInputData = Answer Then
            '当たった時の処理
            MsgBox ("正解です")
            Exit Do
        Else
            ''はずれた時の処理
            'ゲーム続行できるか判定する
            If GameCount = 0 Then
                MsgBox ("GameOver" & vbCrLf & _
                        "答えは「" & Anwser & "」でした")
                Exit Do
            End If
            
            If UserInputData < Answer Then
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと大きいです!")
            Else
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと小さいです!")
            End If
        End If
    Loop
End Sub
ひとまず、最低限の仕様は、満たしてますね。ただし、これだとあと何回間違えると おしまいなのかとかわからないので、もうちょっとゲームらしく必要な情報を与えることにします。
Option Explicit

Sub MainCode()
    Randomize
    Dim GameCount As Integer
    GameCount = 10 '何回でゲーム終了とするか設定する
    
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
    
    Dim UserInputData As Integer
    Do
        UserInputData = InputBox("あと" & GameCount & "回答えれます!" & vbCrLf & _
                                 "1~1000の間の整数値を入力してね")
                                 
        GameCount = GameCount - 1 '入力後にゲームカウントを減らす
        
        If UserInputData = Answer Then
            '当たった時の処理
            MsgBox (Answer & "で正解です!!" & vbCrLf & _
                    10 - GameCount & "回で正解しました")
            Exit Do
        Else
            ''はずれた時の処理
            'ゲーム続行できるか判定する
            If GameCount = 0 Then
                MsgBox ("GameOver" & vbCrLf & _
                        "答えは「" & Answer & "」でした")
                Exit Do
            End If
            
            If UserInputData < Answer Then
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと大きいです!" & vbCrLf & _
                        "あと" & GameCount & "回")
            Else
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと小さいです!" & vbCrLf & _
                        "あと" & GameCount & "回")
            End If
        End If
    Loop
End Sub
MsgBox内を調整してみました。
完成
と言いたいところですが、二つ問題があります。
共にInputBox関数の話です。
一つは、InputBoxで値が入力されずキャンセルもしくは右上の×印を押されたらどうなるか?
もう一つは、InputBoxの返す値の型です。InputBox自体文字列を入力することも考えると返ってくる値の型はStringです。
たまたま今回は数字しか入力していないことと、内部的にInteger型に変換してくれていたので問題がなかったものの もし、ユーザーが数字以外を入力したらどうなるか?ということです。
共にエラーになりますよね。
本来なら入力ミスをミスと捕らえてそのままゲームを進行してもいいのですが今回は必ず何かしらの数字を入力してもらうという形にしてみます。

(完成形)
Option Explicit

Sub MainCode()
    Randomize
    Dim GameCount As Integer
    GameCount = 10 '何回でゲーム終了とするか設定する
    
    Dim Answer As Integer
    Answer = (Int(Rnd() * 1000)) + 1 '1~1000の整数
    
    Dim UserInputData As String 'InputBoxからの値はString型
    Do
        Do '必ず何かしらの数字を入力してもらう
            UserInputData = InputBox("あと" & GameCount & "回答えれます!" & vbCrLf & _
                                     "1~1000の間の整数値を入力してね")
        Loop Until IsNumeric(UserInputData)
        
        GameCount = GameCount - 1 '入力後にゲームカウントを減らす
        
        If CInt(UserInputData) = Answer Then
            '当たった時の処理
            MsgBox (Answer & "で正解です!!" & vbCrLf & _
                    10 - GameCount & "回で正解しました")
            Exit Do
        Else
            ''はずれた時の処理
            'ゲーム続行できるか判定する
            If GameCount = 0 Then
                MsgBox ("GameOver" & vbCrLf & _
                        "答えは「" & Answer & "」でした")
                Exit Do
            End If
            
            If CInt(UserInputData) < Answer Then
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと大きいです!" & vbCrLf & _
                        "あと" & GameCount & "回")
            Else
                MsgBox ("はずれです" & vbCrLf & _
                        "答えはもっと小さいです!" & vbCrLf & _
                        "あと" & GameCount & "回")
            End If
        End If
    Loop
End Sub

[ExcelVBA] プログラミング問題一覧



[ExcelVBA] FizzBuzz問題


■お題
1~100までの数字をセルに出力する。ただし、3の倍数の時に「Fizz」、5の倍数の時に「Buzz」、3と5の倍数の時に「FizzBuzz」と表示しなさい。

■解答例
まずは、1~100まで出力するコードを書く。
Option Explicit

Sub SampleCode()
    Dim row As Integer
    For row = 1 To 100
      Cells(row, 1) = row
    Next
End Sub

次に3の倍数の時、つまり3で割って余り0の時に"Fizz"と出力。5の倍数の時、つまり5で割って余り0の時に"Buzz"と出力する処理を書いてみる。
Option Explicit

Sub SampleCode()
    Dim row As Integer
    For row = 1 To 100
        If row Mod 3 = 0 Then
            Cells(row, 1) = "Fizz"

        ElseIf row Mod 5 = 0 Then
            Cells(row, 1) = "Buzz"
        
        End If
    Next
End Sub

このままだと3と5の倍数の時に3の倍数の処理が行われて"Fizz"のみになるのでこれらの処理より先に3と5の倍数の処理をしなければいけない。
(完成形その1)
Option Explicit

Sub SampleCode()
    Dim row As Integer
    For row = 1 To 100
        If row Mod 3 = 0 And row Mod 5 = 0 Then
            Cells(row, 1) = "FizzBuzz"

        ElseIf row Mod 3 = 0 Then
            Cells(row, 1) = "Fizz"

        ElseIf row Mod 5 = 0 Then
            Cells(row, 1) = "Buzz"
            
        End If
    Next
End Sub

下記のような書き方もいいですね。
(完成形その2)
Option Explicit

Sub SampleCode()
    Dim row As Integer, Temp As String
    For row = 1 To 100
        Temp = "" '条件に合うワードを一時格納する変数
        If row Mod 3 = 0 Then Temp = "Fizz" 
        If row Mod 5 = 0 Then Temp = Temp & "Buzz"
        If Temp = "" Then Temp = row
        
        Cells(row, 1) = Temp '最後に出力
    Next
End Sub

7行目:3の倍数の時は、"Fizz"。
8行目:5の倍数の時は、3と5の倍数もありえるので Temp & "Buzz"
9行目:値がない時は、3でも5の倍数でもない。
11行目:それらの結果を出力する。