はもちくわ

コードについて自分なりの解釈を書いてます。

【Excel】結局さ、フォームコントロールのリストボックスの複数選択は飾りってことなの?!(ユーザーフォームを使わないリストボックスウインドウ)

ちょっと私のスキルではどうにもならんかったので、記事に書いてます(笑)
私、今はWinのExcel使っていますが、もともとMacがメインだったので、業務効率化するためにVBA使う場合、「ActiveXコントロール」とか「ユーザーフォーム」とか「ファイルシステムオブジェクト」とか、なるべく使わない処理を心がけているんです。だってMacじゃ使えないですから(笑)。

で、いま作ってる業務効率化ツールで、視覚的にわかりやすいものにしたい箇所がありまして、フォームコントロールのリストボックスを使うことにしました。
でも、、、、これ、、、、単一選択なら問題なく動きますが、複数選択は使えないことないですか??選択方法が複数と拡張の2種類あるのに(笑)
で、いろいろ他のサイトでも調べてみました。
「ユーザーフォーム」や「ActiveXコントロール」についてはたくさんでてきますが、「フォームコントロール」は「使わないほうがいい」の一点張り(笑)すくないながら、紹介があっても、「マクロで扱うやつだから複数は省略」っておいおい(笑)。Microsoftサポートに至っては「ActiveXを使うことをお勧めします」って(笑)

いやー、たしかにMacユーザー少ないだろうけど無視はいかん(笑)。
それに、リストボックスの設定ウインドウでは、複数選択を選択できますし、リストを複数選択すればちゃんと選択されています。
ということは、選択されたことをリストボックスは認識できているわけなので、おそらく、値を抽出する方法はあるだろうって思ったわけです。

よし!それじゃ、なんとかしてやろうじゃないかっ!て、ひねくれ者の私は頑張ってみたのです。。。。これが1日前のワタシ(笑)



で、今のワタシは、、、、
いやー、だめでした〜〜〜〜!(笑)
で、やったことなんですが、結局どこのサイトにも説明がないので、リストボックスをオブジェクト型の変数にぶち込んで、ローカルウインドウで見てみることにしました。

Dim li_ob As Object
Set li_ob = ActiveSheet.Shapes("list_test").ControlFormat
このあとにブレークポイント入れてあとでローカル確認

こんなかんじ
すると、、、
あれ〜、単一でも複数でもListCount、ListIndexは「0」
ListIndexは配列にでもなってのかなって思ったんですが、、、、、
でも、どちらも「0」っておかしくないですか??

よしっ!それじゃあ、それぞれをMsgBoxで表示させてみますか、、、

上のコードにつづけて、
MsgBox "ListIndex =" & li_ob.ListIndex
MsgBox "ListCount =" & li_ob.ListCount
ここはブレークポイント取っておいてもいいかな。。

これで、単一と複数で動かすと、、、、
当然、単一は問題ないっす。だって正常に動いてるからあたりまえっす。
で、、、、問題の複数さんはどうなったかといいますと、、、、、
ListIndexは、「ListBoxクラスのListIndexプロパティを取得できません。」とエラー。
ListCountは正常。。。。まあ、リストの数は複数だろうと単一だろうとかわらないから当然か。。。
クラスに実装してないのかな?そういう使い方は想定してないってことか?配列でだしてくれりゃいいのに(笑)
さて、クラスの中身は私にはわからんのでMicrosoft Learnの日本語訳を見ながら確認していくことにしてみました。
ExcelVBAのオブジェクトのリファレンス見ながらやったんですが、、、、とてもわかりやすい!!ちょっと日本語おかしな点がありますが、十分理解できる。
で、、、、結果なんですが、 多分、私では無理(笑)
わかったことは、

  1. LinkedCellは複数では使えない
  2. Listindexは複数では使えない
  3. Multiselectで単一と複数を選べるけど、単一以外の使い方は「使えない」ことしか書いてない
  4. なぜか、Multiselectの例文が複数になってる(笑)使えんのに(笑)
  5. Outlookのほうだけど、クラスをNewしてコンストラクタしないでねって変な日本語でかいてある

以上っす。
また、気が向いてなんとかしてやろうと思ったら挑戦します。
こんなことに無駄な時間を使ってしまった(笑)

とりあえず、今作っているものは、よくシステムにあるような、リストボックスを左右にならべて、左が選択前の一覧、右を選択したもの一覧ってことにして、選択してボタンを押すと左から右へ持っていくやつにしますわ。それならまあ、単一しか使えない状況でも複数選ぶような感じになるかなぁ。。。。いちいち手操作するよりゃ少しはマシでしょう!!あと、全選択とか削除ボタンもオマケでつけておきますかっ!!

さて、、、、やるか(笑)



追記:複数選択できなかったのでこうしました例です(笑)

このように、ユーザーフォームでなくて、オートシェイプで先にウインドウっぽく作ります。ボタンもリストボックスもフォームコントロールを使ってます。フォームのボタンはActiveXに比べてデザインがあまり変えれないので、いっそボタンの自作っていう手もあります(笑)

動作を考えながらウインドウを先にきれいに作ったら、グループ化して名前つけて、「検索と選択」でグループごと見えなくしちゃいます。このウインドウはあえてシート側の操作を邪魔する位置におくのが都合がいいですよ〜。シートの処理されてもこまりますからね(笑)本当にシートをガードしたいときは、ウインドウのすぐ下に、画面いっぱいのオートシェイプ作っておいて、ウインドウと一緒に表示させればいいですよ。で、Protect DrawingObjectsでガードしちゃいえばOK!!あ〜、でもこの方法取るときは、リストボックスはロック外しておかないと、選択できなくなっちゃいますのでご注意を。。。。。

で、シート側にウインドウを開くキッカケ(ボタンでもDoubleClickでも)を作っておいて、
Shapes("名前").Visble=Trueで表示させるわけです。
そして、この表示のキッカケとともに、リスト化するデータを次のように配列で作成します。このときの配列はモジュール内で使い回せるように、プロシージャ内で配列を宣言せずに、モジュールの方に宣言しておきます。Publicでもいいっす。別に配列じゃなくてシートで管理でもいいです。シートの方がうっかり消えちゃったってことがないからいいかもしれませんが、、、、遅いっす(笑)

ここでは、説明しやすいように項目行を設けてますが、データ扱うときには邪魔になるのでなくていいです(笑)わたしは入れてません。
はい、、、ここのポイントですが、リストボックスはあくまで表示させるツールであって、データ自体の管理は、配列でするということです。なので、この2カラム目のファイル名のように、実際に使いたいデータが、リスト表示させたものとは違うとき便利になります。
リスト表示の状況判断は、3カラム目のリスト位置でやります。0が選択前、1が選択後ということです。

このように、ウインドウを表示させる時点ではすべて選択前なので配列の3カラム目は全部「0」でよいわけです。
そして、「0」のリストボックスから「1」へリストを移動させるときは、、、

このようにします。
ここでは、移動させるキッカケは矢印にマクロを登録してクリックしたら動くようにしました。
選択されてなかったらっていうガード忘れずに(笑)
こうみますと、現在のリストボックスの表示、順番はあまり関係ないのがわかりますか?
要は、いまの配列がどうなっているか分けて表示しているだけです。リストのデータを覚えておいて動かしているわけじゃないんですね〜。
なので、「1」から「0」に移動させるときは、この逆をすればいいだけです。

ほら簡単。
スクショでは、「全選択」、「全解除」というボタンを作ってあるのがわかると思いますが、これも、リストの表示は関係なくて、配列のリスト位置を全部「0」か「1」にして、そのとおりに一方は全リストを作り、もう一方は、全リストを消せば解決です。最後に選択されたリストを抽出したいときは、配列の3カラム目の「1」だけ検索すればOKです!
そして、「キャンセル」ボタンを押したら、RemoveAllItemでリストボックスすべてを消して、グループ化したウインドウのVisibleをFalseにすれば、ウインドウが消えたみたいになるって仕掛けです!

うーん、複数選択さえできれば、リストボックスは1つで解決したのに、、、、矢印作ってマクロ登録して、、、ボタンも増やして、、、、、、たかだかファイル作成するデータを選ぶだけの作業に、、、、Excelって、めんどくさいなぁ(笑)

【ExcelVBA】「セルの内容が変わったら処理をさせる」ときのクセモノ!(Changeイベント)

今回の記事は、セルの内容が変わったら、Excelになにか処理をさせる、WorksheetのChangeイベントを使った処理の話です。
わぁ〜、夏のイベントとか楽しそう〜!!。。。いやいや、このイベントは処理をさせるためのキッカケの話ですよ(笑)

 

さて、このChangeイベント、みなさんは使っていますか???
いやいや、、、なるべく使わないようにしているよ。。。なんて方も結構いらっしゃるのではないでしょうか??
私は、もちろん、、、、、使わなくていいなら使いません(笑)

なぜかと言いますと、Changeイベントはコードをスタートさせるキッカケが多くなるので、コードを制御する場面が多くなるんですね。
もし、制御してないキッカケが残っていると、エラーが出て止まってしまうかもしれませんし、最悪の場合、無限ループに陥って強制停止、、、、なんてこともありえます。。。。実は、こんなことがよく発生するイベントなんです(笑)
だから、デバックが不完全なままでリリースしてしまうと、、、自動化で便利になるどころか、もう、ただの迷惑行為です(笑)気をつけたいですね!
これに対して、BeforeDoubleClickイベントは扱いやすいです!過去記事(BeforeDoubleClickイベントの記事)で書いてますので、良かったら読んでやってください(笑)
、、、、というわけで、今回はChangeイベントのクセモノをなんとかするという話です!

今回つくるサンプルは、番号を入力すると、その番号にあったデータをマスタから探し、入力した番号の右隣のセルに表示させ、次の項目の入力待機のため、さらに右隣のセルをVBAで選択させるものを作ります。セルの自動選択以外は、VLOOKUP関数と同じ動きです。
このマスタデータは別のExcelファイルとします。そして、番号と連動して表示させたあとは右隣に2項目入力があるものとします。
サンプルの割に、実用するかのようなサンプルですが、、、、実は、つい最近バイト先でつくったのです(笑)なので、その簡易版をサンプルにしようと思ったわけです。

 

では、まずマスタデータを準備します。
今回は東京の地区名と地域コード一覧をネット上から拾ってマスタとしましょう。

こんな感じにしました。


次は入力用のシートです。

先程と拡張子が変わり、マクロ対応ブックです。


それでは、この入力ファイルのVBAを開いて、コードを入力していきます。
最初は、マスタファイルを読み込み専用で開いてデータを配列に記憶させてからファイルを閉じるコードにしましょう。ちなみに、マスタファイルは入力用ファイルと同じフォルダにあるものとします。

いつもはコードをコピーできるようにHTMLで入力しているのですが、、、、今回は、コード紹介ではないので、上のように図で説明しておくだけにします。ご質問あればコメントで、、、

さて、、、これが実行されれば配列にマスタデータが記憶され、配列をPublicで宣言していますので、処理のたびに実行しなくても、エラーで処理が止まらない限り、記憶したデータは使い回せます。配列を添字フリー(数を決めない)で宣言って書いてますが、正確には「動的」っすね。まあ、どっちでもいいか〜(笑)

じゃあ、このコードを実行するタイミングですが、、、、、まあ、このファイルが開かれたときでいいんじゃないでしょうかね。

はい!これでファイルが開かれるたびに読み込みがされます。
このようにWorkbookのOpenイベントを使えば、ファイルが開かれるたびに実行されます。初期設定とかシートの整備とかさせるのに便利です。

次は、いよいよChangeイベント使っていきます!

はい、まずは上のようにしました。
まだこれ不完全ですので、このまま真似しなしでくださいね(笑)
何が不完全なのかExcelの挙動を確認しながら説明していきます!
まず、狙ったとおりに動くかやってみます。

うん、想定通りです。
では項目1の動きをみてみます。

よいですね。
では、項目2の動きは、、、

よし、問題ありません。
このままちょっと入力を続けてみると、、、

問題ありません。
当然ですね。この動きになるようにコードを組んでますので問題ありません。
「これで完成!」って言いたい気になるんですが、ちゃんと検証してみましょう。
まずは、入力したものを全部選択して消してみます。

ほら、このままじゃ使えませんね(笑)
型がちがうって言ってますね。こういうときは、デバックを選択して、ローカルウインドウで確認すると何が起きているかわかります。
今回は、ローカルウインドウでの説明はしませんので、もし機会があったら確認してみてください。

さて、、、一体何がおきているのか?ですが、簡単に言いますと、Changeイベントの対象セルが「複数」あるので、値も「複数」あるよ!ということなんです。一度に「複数」のセルを選択して削除、つまり「値を変更した」ので当然ですね。
このサンプルコードでは、buf_valっていう変数にTarget.valueで値をいれてますが、このとき、しれっと変数buf_valが2次元配列に化けてます(笑)
一気にたくさんのセルが変わったのですから、値が複数になるのは当然ですよね。
でも、、、、サンプルコードをよくみてください。行、列の位置によってIFやSelect Caseで条件分岐していますよね?なんで値は複数あるのに、行、列は1つのセルを想定した条件のまま、一致したのでしょうか???
このように、Changeイベントが発生したときの対象セルが、どういった戻り値になるのか知らないと使いこなせません。だから面倒くさいし、使いたくない(笑)このあたりの扱いづらさ、、、クセモノですね(笑)

では、セルが単一のときと、複数のときの各戻り値がどうなっているのかまとめてみます。

このようになっています。
実は、複数選択時の行、列は、なんと!左上隅だけのことなんです。なので、今回の不完全サンプルコードでは、どちらも同じ条件で一致してしまったわけです。
そして、値の型を見てください。
先程のエラーの「型がちがう」理由がわかるかと思います。
じゃあ、エラーを回避するために、これを加味してコードを書いていけばいいわけですが、、、。。実はこの不完全コード、、、値を表示する、移動させるだけしか対応していません(笑)
実際に動かすのであれば、もっと細かく、色々な場面を想定して書かねばなりません。このサンプルならば、、、

  • 地区名が表示されている状態で、地区一覧にヒットしない番号を上書き入力されたときはどうする?
  • 値が消された場合、選択されたの状態が単一、列指定、行列指定のどれなのか?
  • コピペも上と同じく選択された状態によってそれぞれ考えなければいけない。

面倒くさいですね〜〜〜。さらに普通に運用するなら列もしくは行が削除されることも想定しなければなりません。現在行列を行番号、列番号で直接指定してますが、削除も考えると、シートに目印をつけて、なにか別の仕組みを入れて、相対的に位置を指定できる方法をとる必要があります。
このサンプルではそこまでやりません。ここでは、どちらがされても何も起きません。

では、、、、これらを加味してコードを書き加えるとどうなるかといいますと、、、、

こんなになります(笑)
もっと同じような挙動を一緒にして、圧縮すればもう少しマシになるかもしれませんが、、、結局、考えて、試行錯誤して圧縮する、、、面倒くさい事には変わらない(笑)
あと、今回はセルの内容が変わる無限連鎖が起きない処理なので使ってませんが、例えばセルに入力した値によって、同じセルの値を変える。みたいな、関数では考えられないような処理もVBAなら可能です。しかし、これを普通にコードで表現すると、、、、
入力して値が変わる→コードで値が変わる→コードで値が変わる・・・・と無限ループが始まります。コード上で値を変えてもChangeイベントは有効なので、変えたはなから、コードがまた最初から始まります。これが繰り返されちゃうわけです。こんな時は、コード上で値を変える箇所の前にイベントを止め、終わったら再開させる処理をします。

Application.EnableEvents = False
〜処理〜
Application.EnableEvents = True

こんな感じです。
画面の更新を止めたり、再開したりするScreenUpdatingとよく似てますね。これはよく使いませんか?これのイベントバージョンだと思ってください。
この、EnableEventsを使う上で気をつけることなんですが、ちゃんとイベントを再開させることです。これしないと、Changeイベントだけじゃなく、全てのイベントが止まります。知らないとけっこうあせるかも(笑)
また、デバック時は、特に気をつけてください。イベントを止めたあと、エラーによって処理が止まった場合、イベント停止がキャンセルされ自動再開するわけではありません。イベントは止まったままです。
なので、復帰させるには、イベント再開のコードを実行しなければなりません。
こういったことがあるので、私からのオススメなのですが、、、もし、みなさんが、VBAで何かを開発する際、イベントを止める処理を入れるぞっとなった場合、デバック前に復帰用のイベント再開のプロシージャを作っておくと便利ですよ。1行だけのコードで十分です。

と、、、、まあ、このように、ほぼVLOOKUP関数の挙動と変わらないものを、あえてVBAでつくる意味があるのか?と思いませんか??
確かに、関数を使わない利点も多いです。けれど、その利点に見合う手間なのか?!を考えないと、時間と手間が勿体ないことになります。
今回、私がバイト先で、できれば使いたくないと思っているにも関わらず、この方法を選択して作成したのは、、、キーボード、マウス以外の操作デバイスをメインで使わなくてはならない状況だったからです。
ホント面倒くさいんで、やむを得ない状況に追い詰められるまではおすすめしません(笑)

さて、いかがでしたでしょうか?
面倒くささは伝わったでしょうか(笑)この記事が、みなさんのお役に立てれば光栄です。
ここまで読んでいただき、ありがとうございました。



追加記事(2023年7月29日)

バイト先で書き殴ったコードを整理してたら、スッキリできたのでご紹介(笑)
このサンプルのメイン部分を書き換えると、、、

Dim rp_ed As Integer,ck_val As String,d_cnt As Boolean
Application.EnableEvents = False
If r>1 Then
    If InStr(buf_add,":")=0 Then
        rp_ed = 1
        ck_val = buf_val
        d_cnt = True
    ElseIf InStr(buf_add,"A")=2 Then
        rp_ed = UBound(buf_val,1)
        ck_val = buf_val(1,1)
        d_cnt = False
    End If
    
    For i=1 To rp_ed
        If i>=2 Then ck_val = buf_val(i,1)
        If ck_val = "" Then
            ActiveSheet.Range(ActiveSheet.Cells(r+i-1,2),ActiveSheet.cells(r+i-1,4)).ClearContents
        Else
            fl = False
            For j=0 To UBound(e_data,2)
                If ck_val = e_data(0,j) Then
                    ActiveSheet.Cells(r+i-1,2)=e_data(1,j)
                    If d_cnt = True Then .Offset(0,2).Select
                    fl = True
                    Exit For
                End If
            Next j
            If fl=False Then
                ActiveSheet.Cells(r+i-1,2).ClearContents
                If d_cnt = True Then .Offset(0,1).Select
            End If
        End If
    Next i
End If
Application.EnableEvents = True

HTMLにベタ打ちで、コードが長くなるとしんどいので、SelectCaseの分岐は省略しました(笑)
これならスッキリな気がします。このサンプルだとあまり実感できないかもしれないですが、実際にバイト先で作ったコードは、読み込んだデータとヒットした場合、3列ぐらい表示させ、セル移動もいくつかパターンがあったのでよりスッキリが実感できるコードになったんですけどね〜。
あと、すごく大事な点がもう1つ。これがクセモノのラスボスですね。
今回のようにコピペをされたものをループさせて一気にたくさんの結果を出したいようなとき、やはりイベントは一連の作業が終わるまで止めておいたほうがよいです!
なので、今回は大外にイベント止めのコードを差し込んでます。
実際にバイト先で作った方には、念のため追記したコードのようにイベントを止めて作ってました。実際に動かしたとき変な悪さされるとやっかいですからね。
これぐらいのサンプルぐらいなら、無しでも不具合なく動くので気にしてませんでしたが、、、、試しにコピペの量を増やしてみると、、、、、、やはりコードの中で値を変える処理のあとにあっちこっちに飛んでます(笑)順番がめちゃめちゃ。なので、無限ループにならなくてもコード上で値を変えるときは、イベント止めましょう!!

いろいろ判明して、嵐のようにベタ打ちしたのでタイプミスあったらごめんなさい(笑)もし、自分で気がついたら修正しておきます。。。

やっぱりファイル名って被ることがあるぞ!(反省・・・)

バイト先で、私がExcelVBAで作ったシステムに、すごい久しぶりにバグが出たんです。どうも、ファイルからデータを自動読込みする際に、こちらの意図した挙動になってないファイルがあるのです。

それでですね、ちょっとこの原因を突き止めるのに時間がかかったんです。
なんで時間がかかったのか?といいますと、、、、常々バイト先では、私、肉体労働が本業なので、自作コードは作りっぱなしで、あまり見返してないんです。なので、久しぶりに呼ばれて自分のコードを見返したせいもありますし、そもそも深刻なエラーでもなかったので、ぶっちゃけ面倒くさいから後回しにw、、、、、あーーー、いえいえw、、、結局、私の思い込みが原因だったのですがw
ちゃんと突き止めました。

 

どんなことが起きたのかと言いますと、まず、私の作ったシステムは下のようになってました。

取引先からメールで届くExcelファイルを所定のフォルダに気づいた人がバンバンいれている状態がありまして、自作システムは起動するたびに、そのフォルダを見に行きます。そして、フォルダ内のファイルが、過去一覧と照合し、読んだことがないか?、ファイルが更新されているかどうか?を判定し、条件に一致したら、データベースにファイル情報を追加するという処理をします。システムからフォルダを見に行くときは、Dirを使い、過去一覧と比較してます。このときのファイル更新日はFileDateTimeで取得してます。ちなみに、過去一覧はCSV形式でシステム側のフォルダに保存しているのでシステム起動と同時に配列に入っている状態です。



どうでしょう?システム側の状態はご理解いただけましたでしょうか?この設計で1年以上無事に動いていたので、完成!って思ってました。
で、何が起こったのか?ですが、、、それは、起動のたびに、必ず1つのファイルが条件をすり抜けて読み込まれてしまうのです。
もちろん、ファイル名も更新日時も変わってないのに、です。。。。

おーーーー???ってVBAのステップインでデータを確認してみます。
すると、、、ファイル名はフォルダ側と過去一覧が同じ、、、、、で、更新日はフォルダ側が「2023.6.10 10:38」、過去一覧が「2022.6.10 10:38」
一緒ということは、、、、、コードの判断条件は、以上じゃなくてより大きいにしてるから、スルーされるはず、、、、、、あれ?なんで読み込み処理になるんだ?!

と、気づかないでかなり長いこと悩んでいたわけですよ!(笑)
みなさんはもう気づきましたよね?
はい!そうです!全く同じ日の同じ時間で「年」が違うんです!!そう、去年のファイル名と同じものが入り込んでいたのです!!

同じフォルダ内に同じファイル名が入るはずはないのですが、気の利く面倒見の良いかたが、フォルダ内を整理してくれたんです。過去ファイルは別フォルダを作ってそちらに移動してくれていたんですね。で、システムが作る過去一覧はファイルがなくなっても消えずに残っているので、同じファイル名だけど違うファイルを何回も読み込んでしまっていたというわけです。
そして!さらに奇跡が起きてまして、この送られてくるファイル名なのですが、取引先の複数の方が各々自由に設定されているものなので、あまり被らないのですが、「基本ファイル名(月.日)」のように年を抜いた日付を入れてくれる方が、去年と同じ日の同じ時間に、同じファイル名で送ってくれていたのです!

いやー、本当におどろいた。偶然が重なるってこういうことなんですね〜。
結局、過去一覧のファイル更新日が新しくなっていた場合は、更新する処理を追加し、さらに、読み込まれたデータ自体が被ってないか照合する処理も追加して解決しました。
このファイル更新日を更新する処理自体をしてなかったのは致命的ですwもともとはデータベース側で更新日を管理していたつもりだったのですが、同じファイルが違うファイル名として入っていたことを想定していたので、ファイル名が違うときは更新日を照合する処理をしていたのに対して、同じファイル名で更新日が新しいのは、絶対にデータが更新されているわけだから、なにも照合処理はせずに、データ日付を上書きするだけにしてたんです。意味ない更新日管理になってましたw
バカだなぁ....って反省しました。

バグ見つけるために、とにかくたくさん使ってもらって、、、と思ってましたが、やはり、もっとトリッキーな使い方をする方がいると、もっと早くバグを潰せるのになぁって思いました。

「一緒になるわけない」より「一緒になるかもしれない」って思えるほうが安全な橋を渡れますね。今後は気をつけよう!と思いました。みなさんもお気をつけください。。。

プログラムはちょっと離れるとわすれちゃうぞ!

仕事で、人の目を使ってコード番号をチェックしたり、入力ミスをしないように2度も3度も見直したり、、、、それでも間違えてるとかクレーム受けたり(笑)、、、、
「いや、人間だからミスはあるから!そもそも、PCあるのになんで人頼みなの?!」って、心の中で開き直りながら、謝罪している私は、、、、この国ではダメ人間なんだろう(笑)
そんなダメ人間の私は、面倒でミスが多いことは、なるべくPCにやってもらう事で、辛うじてダメ人間を回避しています。

バイト先でも、業務時間を少しずつ使って、面倒でミスが多い業務をExcelVBAで自動化、効率化を進めまして、、、今では、平均2時間かかっていた業務が15分に短縮し、ミスもほぼ無くなりました。おかけで、空いた時間に別の仕事をするはめに、、、、、と墓穴を掘る(笑)ことにもなりましたが、その業務自体のミスは、誰がやっても無くなり、精神的に楽になりました。言われるのは誰だって嫌ですからね〜。
そうしたら、使ってくれる人が増えて、追加機能需要もガンガンでてくるから、負けずにガンガン追加して、他の業務もミスも少なくなって、、、、こりゃ、面白いなって思ってたんですが、、、そろそろやることも一段落ついたので、自作ページを整理しようとちょっと前からHTML、CSSPHPJavascriptで書いた自分のコードを見返しています。



私、数字選択くじの結果とこよみの関連性を計算して検証するのが好きで、コレログラムとか使って自己相関で計算していたんです。ほとんどくじは買わないのですが、絶対検証できないものをあーだこーだするのが楽しくて(笑)
でも、バイト先の効率化の方が面白くなってしまい、もう1年以上データを放置しちゃってて。
そんなわけで、まず、この空白期間を埋めるため、MySQLに眠っているデータに、続きのデータをCSVで追加する作業から入ることにしました。
でも、SQLでいちいち入れるのも面倒くさい(笑)phpmyadminからCSVでインポートするにも失敗したときに確認するのも面倒くさい(笑)
じゃあ、PHPSQL操作して自動化しよう!と思っても、CSVファイルをいちいちFTPでサーバーに送るのが面倒くさい(笑)だってサーバー容量も限らているからアップしたファイルはSQLに入れたら消さねばならないし。。。。
ああ、そうだ、FileAPIでブラウザからアップロードしたFileをSQLに入れちゃおう!ってことで、いざFileAPIのコードを見返したら、、、、あ、、、ちょっとわからないぞ(笑)自分で作ったコードが分からない(笑)
コード自体も膨大になっててインクルードされたFileはどれで、関数化したのはどこで、、、、
やっぱりプログラムって、ずっと使ってないと忘れちゃいますね!やり直さねば(笑)

いままでの自作ページは、ポートフォリオにしていた場所以外は、ログイン機能で閲覧制限をつけて、自分だけが検証を楽しむページでしたが、次は誰でも見てもらえるように作ろうかな、、、と思っています。検証方法の解説もつけてやってみたいな。
また、制作過程は気が向いたら記事にします(笑)今回はただの日記でした。

EXCEL】関数で万年業務カレンダー(日付固定)を作ってみる〜第5回〜セルに2つの結果を併記する方法

全5回に渡って、下のような万年業務カレンダーをつくっています。

記事の構成は下のようになっています。内容が重なるものはサラッと書きますので、詳しく知りたい方は、過去回をご確認ください。

万年業務カレンダーの作成全5回

今回で最後になります。
第2〜4回で、カレンダーに反映させる予定の指定方法を、日付によるものと、曜日によるものの2つを作りました。
こういった予定の決め方は、正社員で勤めていたときは、結構みたな〜という印象で、割と第2月曜と10日、第3月曜と20日なんかが、がかぶりやすかったな、、、みたいな思い出があります。みなさんはいかがでしょうか?!

はい!そういうわけで、今回は、違う指定方法の予定が重なってしまっても、両方表示できるカレンダーにしていきます。
また、予定が重なったら、予定変更をする可能性もあるので、重複がわかりやすいように、アラートもつけましょう!


完成はこのようにします。

ちょっと見にくいですが、画像のように月を4月から8月に変えると、8月のほうは、第3月曜と、20日(翌営業日)と設定した予定が重なりました。
この場合は、上のように、1つのセルに予定が並んで表示されるようにします。
また、予定設定欄の欄外に重複したことがわかるように、「※マーク」を出します。

こういった2つの結果を、Excel関数で表示しようと考えた時、真っ先に思いつくのは、結果ごとにセルを分ける手法じゃないでしょうか?
Excel表計算ソフトなので、そうなるのは当たり前といえば当たり前ですよね。
でも、今回のカレンダーのようなものや、契約書、納品書のように文面として書式を気にするものって、それだと不便なときがあります。
例えば、

こういったことです。
まあ、好みといえば好みですが、いかにもExcelの列が邪魔した書式形式はどうもな、、、、という方、いらっしゃるのではないでしょうか?私はそうです(笑)

なので、今回は1つのセルに併記できるようにしていきます。
まずは、それぞれの予定を抽出する式を考えます。
日付指定の予定は、

こうすれば、日付指定した予定は抽出できますね。関数の使い方などの詳細は「関数で万年カレンダー(曜日固定)をつくってみる(後編)」で書いてますので省きますが、意味は図の通りです。
さて、もうひとつの曜日指定ですが、指定場所が日付指定の下にズレているだけです。なので、関数式の形は同じで、参照先だけ下のように変えればいいです。

はい、これで各予定は抽出できました。
あとはこれを併記するだけですね。
さあ!いよいよ、タイトルにするぐらいですから、何か必殺技がっ!!

、、、と、そんなものは無くて、、、ただ文字列を結合するだけです。
文字列の結合は「&」を使えばよいですね。数値計算の「+」と一緒です。
ただ、この2つの式をそのまま「&」で結合しただけですと、下のようになってしまいます。

このように、予定が1つの時と、無い時はうまく表示されるのですが、予定が2つあるときは、予定同士がくっついてしまい不自然です。もし、予定内容がうまくくっついて別の意味を持ってしまうと、「予定が1つにしか見えない」なんてこともあるかもしれませんね(笑)

なので、もうひと手間加えます。

方法としてはいくつかあるのですが、手っ取り早いのは、表示予定の「後ろに必ず」文字列スペース(””でくくられたスペース) を入れておく方法です。これなら続く予定が無く、予定のあとにスペースが残ったとしても見えないので、予定が1つの時でも、不自然な空白になりません。さらに、併記する予定が3つ、4つ・・・と増えても、対応できますし、スペースを調整すれば好きな間隔にできます。簡単ですね!
逆に、前につけては駄目ですよ。条件によっては、不自然な空白が前に出てしまいますからね。
ブロックとか積み木をイメージして関数書くと良いと思います。

「いや〜、違うんだよな。スペースじゃなくて、スラッシュとか中点とかハイフンで、区切りをつけたいんだよなぁ!」って思われた方。今回はその方法にします(笑)
このサンプルは予定が2つですが、3つ、4つになる場合も含めて説明していきます。

先程のスペースと違う点は、区切り文字は「見える」ということです。なので、条件によって、表示したり、消したりをする必要があるということです。
気づかれた方もいらっしゃいますかね。。。そうです。予定と同じ事をすればいいんです。では、条件を考えていきます。
まずは、予定を2つ並べる時の条件になります。

見てのとおりです。区切り文字の前後に予定があるときは表示という条件でいいですね。
では、3つ以上の予定を並べるときはどうなるのでしょうか?考えてみます。

こうなります。予定によって組み合わせが発生しているように見えますが、先程と考え方は同じで、区切り文字の前後に予定があれば表示です。
区切り文字の前の予定が複数あるので、迷いそうですが、結局、前の予定全部のうち、どれか1つでも表示されていたら表示になるだけです。

このように、関数で表示をコントロールするには、判定する予定の数だけ表示する準備が必要になります。
予定が2つのときはこう、予定が3つのときはこう。。。のように予定をみて判断するわけではありません。表示されている予定表には、予定のあるもののみが並んでいるように見えても、実際は予定がないものも、見えないけど、全部表示されているというわけです。
VBAなどのコードで書けば予定の数だけループしながら表示される内容をつくればいいですが、関数はそれができないので、すべて準備しておくということです。

それでは、ここのカレンダーではどうすればいいのか下に関数式を書いてみます。

=IF(COUNTIF($O$3:$O$7,A3)=0,””,INDEX($L$3:$L$7,MATCH(A3,$O$3:$O$7,0),1)) & 
    IF(AND(COUNTIF($O$3:$O$7,A3)<>0,COUNTIF($O$9:$O$13,A3)<>0)," ・ ","") & 
    IF(COUNTIF($O$9:$O$13,A3)=0,””,INDEX($L$9:$L$13,MATCH(A3,$O$9:$O$13,0),1))

となります。これをカレンダーの予定欄にいれ、コピーしたら完成です!

いかんいかん。。。一応重複チェックも書いておきますか。
予定を作っている人からすれば、間違えないように人の目でみるって結構大変です。今回のように、ある程度自動化してしまうと「PCだから間違いないよね。」みたいな考えが頭の片隅に知らぬ間にいて、間違えちゃいます(笑)ちょっとしたアラート出しておくだけでも結構助かるもんです。ちゃちゃっと下のようにします。

ちょっと手抜きだな。。。。まあ、これはおまけということで。。。。

さあ、これで全5回が終わりました!
どれか一つでもなにかの参考になれば嬉しいです。
ここまで読んでいただきまして、本当にありがとうございました。

最後に、他の記事では、マクロを使ってないExcelファイルは、サンプルを準備して私のサイトにアップしたりしてましたが、全く需要がないみたい(笑)なので、今回はやめました。
自分のページはフリーサーバーなんで、需要なし、意味無しでは、容量がもったいないですしね(笑)
もし、ファイルでほしい方はここのコメントかTwitterからDMください。何かお届けする方法を考えます。まあ、ファイル転送サービスかなぁ。。。

それではまた。

【EXCEL】関数で万年業務カレンダー(日付固定)を作ってみる〜第4回〜前、翌営業日の計算(任意の休みの場合)

全5回に渡って、下のような万年業務カレンダーをつくっています。

記事の構成は下のようになっています。内容が重なるものはサラッと書きますので、詳しく知りたい方は、過去回をご確認ください。

万年業務カレンダーの作成全5回

今回は、任意の休みの場合の前営業日、翌営業日の計算です。
多くの場合、会社や業務によって、休みが違うでしょうから、前回の記事を読んで「土日休みの場合だけ説明されてもねぇ」と、思われた方はたくさんいらっしゃるのではないかと思います(笑)

そんなわけで、ちゃんと業務で使えるように、任意の休みの場合で計算したいと思います!

と、言いましても、、、、私はみなさんの休みを知りませんし、計算してわかるはずありません!(笑)
なので、休み一覧を別シートに準備し、それを参照して判定をする方法をとります!
このサンプルでは、過去記事の「関数で万年カレンダー(曜日固定)をつくってみる(後編)」で使った、内閣府が配布している祝日のCSVデータを別シートにコピーして使います。このデータの形ですが、A列に日付(年月日)、B列に祝日名が入っているものです。今回は祝日名は使わないので、みなさんは休みの日付がA列に並んでいるデータを準備していただければよいです。シート名は「休日」にします。

さて、予定の入出力方法は第3回と同じにして、日付の算出方法だけ変えていきます。

第3回のときは曜日で休み判定をしたので、WEEKDAY関数を使いましたね。今回は曜日で判断ができませんので使えません。どうするのかといいますと、、、、人間と同じように1日ずつ日付と休日を確認する方法でやっていきます。まあまあアナログな手法です(笑)

申し訳ないですが、もう1枚シートを準備してください。サンプルではシート名を「計算」としておきます。カレンダーも別シートから参照しますので、「カレンダー」というシート名をつけます。
シートばかり増やすのはあまり好きでな無い方は、予定を入力する表の右横を使っても構いません。ただ、その方法ですと、セルを0幅にしても関数が消されたり、崩されたりするリスクが出てきます。シートを別にして非表示させる方がまだ安全だと思いますので、一度、ご検討ください。

では、「計算」シートを使って、どのように前、翌営業日を算出するかですが、下のようなものを作って判定していきます。

このように実際に入力された予定日を起点に、前営業日なら前に、翌営業日なら後ろに1日ずつ3週ズラして判定するというアナログな方法です。VBAなら、配列にするとか、ループ判定するとかできるのですが、、、私のスキルで関数でなんとかというと、この方法しか思いつかなかったです。

では、実際に式を作っていきます。
「カレンダー」シートから参照するセルは、下の4つです。

そして、「計算」シートは下のように使います。

予定日を出力する式は、セルA2に、このように入力します。

=IF(カレンダー!M3="","",DATE(カレンダー!$C$1,カレンダー!$E$1,カレンダー!$M3))

「カレンダー」の予定設定の日付欄(セルM3)が空欄のときは空白を表示する条件分岐をつけてますが、「カレンダー」の参照先から年月日をそれぞれ指定してDATE関数で日付を表示させているだけです。

つぎは、休日判定の式です。
先程説明したように、判定の式は、当日の休日判定と3週分の判定と分けて考えていきます。式のすぐ横のセルB2に予定日当日、さらにその横のセルC2以降に1日ずつ横にズラして3週分判定していきます。
はじめにセルB2に入れる予定当日の休日判定です。

=IF(A2="","",COUNTIF(休日!$A:$A,A2))

「休日」シートのA列に休みの日付一覧が入ってますので、その中から、セルA2で作った予定日データと同じ日があればカウントするようにします。当然ですが、祝日と同じ日があれば「1」、なければ「0」になります。
そして、セルC2に入れる、予定日から1日ズラした休日判定です。

=IF(B2="","",COUNTIF(休日!$A:$A,$A2+(COLUMN()-2)*IF(カレンダー!$N3="前",-1,1)))

この式をD列側に向かって必要数コピーしていきます。サンプルでは3週分(21日分)にしてます。さすがに会社で3週連続で休むことは無いと思いますので、ここまでは必要ないかと思いますが、学校ならもっと必要かもしれませんね。このへんはご自身の環境にあわせて作成してください。

関数の説明ですが、やっていることは当日の判定式と同じです。当日はセルA2を参照していましたが、そこから1列ズレるごとに1日ズレた日付を参照しないといけませんので、セルA2から条件によって減算、加算をする式が入ってます。
COLUMN関数は 関数で万年カレンダー(曜日固定)をつくってみる(前編) で出てきましたね。列数を戻り値として使える関数です。列が増えていくことを利用して起点日から離れていく数字を表現し、オートフィルできるようにしています。
減算、加算の判定は、ユーザーからの入力結果(セルN3)を受けて変える式になっています。
減算、加算の式をそのまま書いてもいいのですが、予定日から1日ずつ3週間までズラす数字は、前だろうと、後ろだろうと変わらず、足すのか引くのかの違いだけです。なので、引く場合は「−1」を、足す場合は「+1」をズラす数字に掛けて減算、加算を変化させてます。
あとは、下方向に判定したい分だけコピーすればOKです。
もう、このままで十分良いのですが、3週分ズラして考えた式は、実は逆方向の当日にオートフィルすると、当日の計算になっちゃいます(笑)COLUNM関数から−2して増える数字を表現してますが、逆に戻すと予定日から0日ずらすになるわけです。要するに当日のことですね。考えやすいので分けてましたが、結局一緒の式で全部いけるということでした。関数の管理の上では一緒にしておいたほうが、あとあと整理するときに楽かもしれませんね。


さて、これで前、翌営業日によって起点となる予定日から3週間の状態がわかるようになりました。
この状態から、「0」を検索して計算に組み込めばいいだけです。もう探すといえば、、、、MATCH関数 です!MATCH関数のことは、過去記事で何回も何回も書いたので周知のものとして書いていきます(笑)

休みと営業日が横に並んでいる状態で、左から初めて現れる「0」の位置がわかればいいわけですから、まさにMATCH関数はうってつけですね。
今度は、「カレンダー」のセルN3の横、セルO3に

=IF(M3="","",IF(N3="",M3,M3+((MATCH(0,計算!B2:W2,0)-1)*IF(N3="前",-1,1))))

と入れて、必要な分だけ下方向にコピーします。
予定日からズラしたい日数をMATCH関数で調べています。加算するか減算するかの仕組みは、先程の休日判定のときと同じです。
そして、MATCH関数の戻り値から「ー1」をしています。これは、先程、「計算」シートで休日判定をしましたが、当日も含んで判定していました。そのため、ズラしたい日数に当日が入り込んでしまってますので、「−1」が必要なわけです。
これ、当日を外して参照すればよくない?と考えてしまいそうですが、そうしますと、当日が営業日だった場合、困ってしまいます。当日の判定のために、条件分岐を一つ増やさなければならないからです。そういうわけで、当日も含んで休みかどうかを判定し、当日の判定を別で考えなくていいというわけです。

そして、前、翌営業日を必ず指定する予定ばかりではありません。休日でも実施する予定もありえます。そういった場合は前後の指定をせずに空白にしてもらい、空白の場合はそのまま指定日を表示するという分岐をすればいいですね。

文章だけでつたわるのでしょうか(笑)一回図を作ってみたのですが、うまくまとめられなくて、、、、またまとめられたら記事を更新しますね。


さて、、、もう一つ。応用編です。今作ってきた仕組みでは、休み判定できる種類が1つだけですね。
でも、現実世界では会社の定休日、国民の祝日、曜日定休、、、と結構休みになる要素って多いですよね。このままですと、実用にはちょっと足りない感じです。

安心してください!はいて・・・、じゃない(笑)
実は!!この回でやってきた方法は、複数の休み要素もまとめて表現できます。
ベースはこのままです。「計算」シートに条件を追加していくだけです。
では、どうやって追加するのか?
実際に、土日休みの場合を式に入れてみましょう。ちょっと長くなるので改行とインデントつけていきます。

=IF(B2="","",
  COUNTIF(休日!$A:$A,$A2+(COLUMN()-2)*IF(カレンダー!$N3="前",-1,1))
    +
  IF(WEEKDAY($A2+(COLUMN()-2)*IF(カレンダー!$N3="前",-1,1),2)<6,0,1)
 )

 

先程の休日判定をした式に続いて、WEEKDAY関数で曜日判定した結果、土日なら「1」、それ以外は「0」を加算してます。 こんな感じで条件を加算していけばいいだけです。
前、翌営業日をMATCH関数で探すとき、営業日である「0」を探してました。
なので、休みの条件が重なって、「2」とか「3」になったとしても、「0」はすべての休み要素に関わらない休みということなんですね


さあ、これで第3回と同じ用に、前営業日、翌営業日が任意の日付でも対応表示されるようになりました。
あとは、この内容をカレンダーに予定を表示させるわけですが、、、第5回でやります(笑)
第5回では、別の予定設定をしたとき、予定が被っても2つ表示させられるようにします!

今回はすこし作成に時間がかかってしまいました。。。いい年して肉体労働のフリーターなんで、疲れて寝ちゃうんですよね(笑)
なんか文章ばかりでわかりにくいし、、、、なんか申し訳ないです。

それでは今回はここまでです。読んで頂きありがとうございました。

【EXCEL】関数で万年業務カレンダー(日付固定)を作ってみる〜第3回〜前、翌営業日の計算(土日休みの場合)

全5回に渡って、下のような万年業務カレンダーをつくっていきます。

記事の構成は下のようになっています。内容が重なるものはサラッと書きますので、詳しく知りたい方は、過去回をご確認ください。

万年業務カレンダーの作成全5回

今回は、土日休みの場合の、前営業日、翌営業日の計算です。

このサンプルカレンダーには、日付を指定して予定を設定する箇所があります。会社規定なんかで日付が決まった業務って多分結構あるんじゃないですか?給料支給日なんかは典型的だと思います。
ただ、予定日が休業日に当たってしまって、営業日の前か後に予定変更になることってありますよね。これ、自分で実際にカレンダーをみて確認するよりも、自動で表示できたら便利じゃないですかっ!

そんなわけで、やっていきます!
でも、まあ、、、今回は土日のみの計算なので、正直、使いみちがないかもしれませんが、、、、お付き合いください!

さて、土日が休業日ということは、指定された日付が何曜日なのか?がわからないといけませんね。それを調べるのは言うもでも無く、WEEKDAY関数です。
サンプルカレンダーの形で該当日の曜日を関数で導き出すとすると、

このように指定すれば、該当日が月曜から何日ズレているかを調べることができます。
では、この関数から得られた結果(以後、戻り値)から判断できることと、求めたいものとの関係を図に表してみます。

こうしてみると、休みであることは「戻り値が6以上なら休み」という条件でいけることがわかります。それでは休みの時、前営業日を指定するにはどうすればいいでしょうか?
戻り値から考えてみます。
戻り値が6(土曜日)のときは−1日、7(日曜日)のときは−2日すれば前営業日になりますね。
それならば、それぞれについて、1つずつ条件分岐してと、、、、いやいや、翌営業日のことも考えなくてはなりませんし、できたら条件分岐や式は、ある程度まとめたいですね。

こんなときは、共通点や法則を探します。
6のときは1、7のときは2になればいいわけです。

いかがでしょうか?共通点見つかりましたか?

はい、そうです。どちらも戻り値から「5」を引くことで前営業日を指定できそうです。
この「5」はどこからきたのでしょうか??
ちょっと上の図を見てください。「5」は、前営業日「金曜日」の戻り値ですね。なんか、日付を移動することに関連がありそうじゃないですか!?(笑)

こうなると、翌営業日にも共通点があるかもしれません!一回考えてみましょう。
戻り値6(土曜日)のときは+2日、戻り値7(日曜日)のときは+1日すれば翌営業日を指定できます。こちらの共通点は、どちらも「8」から戻り値を引くと求めたい数になっています。
先程の図に当てはめると、翌営業日の「月曜日」は「8」です。
これは完全に関連がありますね!文面では前と翌営業日で、戻り値から引く、戻り値を引くで違って見えますが、マイナスなのかプラスなのかの違いを加味すると実は一緒です。

では、この共通性をWEEKDAY関数の戻り値を受けた式にまとめてみます。

今回の場合、休みの前か後ろかは、ユーザー判断で変わるものですから、結局、それに合わせて条件分岐をする必要があって、それぞれで数値を使います。上のように「結果曜日の数値」とまとめてますが、使いません(笑)
ですが、あとあと修正したり、見直したりすることを考えると、ある程度、式は同じ形にしておくと、見直しが少なくなりますので便利です。まあ、これしなくても動きますので、面倒なら条件の数だけ式書いちゃってもいいですけどね(笑)私の過去記事「関数だけで勤務時間表をつくってみる」では、結局、あるだけ条件並べて時間判断してますからね(笑)できる人はもっとまとめると思います。

で、この部分だけを式で表すと

これで営業日の前後によって結果が変わる式はできました。式をひとつにまとめたので、見た目もわかりやすくできましたね。
ただ、これだけでは、予定日が休みになっているかどうかの判定やその他の条件分岐がありません。なので、外側につけていきます。
長くなるので上の式を「営業日計算式」とまとめます。

=IF(M3="","",IF(OR(WEEKDAY(DATE($C$1,$E$1,M3),2)<6,N2=""),M3,「営業日計算式」))

【お詫びの追記 2023年4月27日】・WEEKDAY関数式の記述を間違えてましたので修正しました

この式については解説図をつけませんが、

  • 予定日の設定がなければ空白
  • 予定日が休みでない場合と前後の記載がない場合は予定日
  • 予定日が休みで前後の記載があるときは営業日を計算する

という3区分の条件分岐になりました。この判断式を表の右端セルO3に入れて使います。もちろん、そのまま表の終わりまで下方向にコピーしてくださいね。また、このまま使うと、予定日の前後の判定は、「前」以外、何か値をいれたらすべて翌営業日と判断されます。サンプルでは「後」「翌」どちらでいれてもいいように、そうしてますが、「翌」だけのほうがよければ少し条件分岐を工夫してください。ただ、、関数でだけで縛らなくても、「前・後」、「前・翌」どちらでもいいので入力してもらう場所自体に入力規制リストを使って縛ったほうが良さそうですけどね。

さあ、これで日付設定の表に日付と営業日の前か後を入れると、土日は除いた該当日付が表示されるようになりました。
第2回と同じく、この内容をカレンダーに予定を表示させるのは第5回でやります。第5回では、曜日設定と予定が被っても2つとも表示させられるようにします。

それでは今回はここまでです。読んで頂きありがとうございました。