空腹おやじのログと備忘録

VBA(主にExcel)でいろいろな実験的な事とか、Linuxのコマンドとか設定とかについて忘れないように、あれこれと・・・

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その2

次回予告までしておきながら、他にVBA関係の記事を書いていたこともあり、既に2ケ月以上が過ぎ、
「いつ頃までに、まとめられるかは、不明・・・」の記載通りになってしまった。

前回も書きましたけど、64bit版のVBAではどうなるかわかりませんので!!!

今回の内容は次の通り。

  1. 受け渡しするデータの型について
  2. 処理する値を渡せるようにすること
  3. 値渡しと参照渡しについて
  4. 処理した結果や値を返してもらえるようにすること
  5. 数値型の場合の受け渡しの例


受け渡しするデータの型について

とりあえず必要なのは、VBAC++のデータ型の対応。
VBA独自の型は、C++に渡せないし、逆にC++にしかない型は、VBAが受け取れない。
(条件付きであれば、例外もあるけど・・・)
VBAの主な型を中心に対応を見てみると次のような感じ。

VBAの型 C++の型 備考
Byte unsigned char
BYTE
Integer short
SHORT
Long int
long
INT
LONG
Single float
Double double
Boolean BOOL
C++の型 VBAの型 備考
HANDLE Long
char
CHAR
Byte 最上位ビットがOFFならばそのまま使用可
unsigned short
WORD
Integer 最上位ビットがOFFならばそのまま使用可
unsigned int
unsigned long
UINT
ULONG
DWORD
Long 最上位ビットがOFFならばそのまま使用可
VBAにあってC++にない型(抜粋)
Currency
Date
Decimal
String
Object
Variant
C++にあってVBAにない型(抜粋)
WCHAR
LONGLONG
ULONGLONG

VBAのString型に対応するC++の型はありません。
ちょっと特殊です。データの受け渡しにはchar型のポインタまたは、BSTR型を使用します。
これについては、次回(?)にでも・・・

処理する値を渡せるようにすること

DLLに何らかのデータ処理をしてもらうためには、必要なデータを渡してあげないと出来ません。
データはVBAの関数と同様にパラメータに引数を渡します。
ただ、C++VBAでは、構文が違うので予め覚えておく必要があります。

値渡しと参照渡しについて

値渡し
引数のアドレスをプロシージャに渡すのではなく引数の値を渡す方法。
VBAでは、ByValを指定する事により値渡しとなる。
参照渡し
引数の値をプロシージャに渡すのではなく引数のアドレスを渡す方法。
VBAでは、デフォルトで参照渡しである。明示的に指定する場合には、ByRefを指定する。

C++の場合

値渡し
データ型の後ろに変数を指定する。
void doAnything(int hoge); intデータ型で、hoge変数
値渡し
データ型の後ろに"*"を付けその後ろに変数を指定する。(ポインタですな。)
void doSomething(long* fuga); longデータ型で、fuga変数

処理した結果や値を返してもらえるようにすること

値を返してもらう方法は、大きく分けて2つ。
一つは、関数の復帰値による方法。
これは、VBAでいえば、ファンクションプロシージャによる復帰値で結果を得る方法と同じ。
C++の場合には、関数名の前に復帰値のデータ型を指定する。
復帰値の型 関数名(パラメータリスト)
もう一つは、渡したパラメータにデータを入れてもらい、返してもらう方法。
こちらは、VBAでいえば、パラメータにByRefを指定し、返してもらう方法と同じ。
どちらを使うかは、状況に応じて使い分ければよろしいかと。
場合によっては、両方というのもありです。

数値型の場合の受け渡しの例

VBAからLongの値を渡して、2倍した結果を返してもらう「GetNumberI」という関数と、
300倍した値を返してもらう「GetNumberI2」を作ってみます。
GetNumberIは、パラメータを値渡しして、復帰値で結果をもらいます。
GetNumberI2は、パラメータを参照渡しし、そのパラメータ値を変更して返してもらいます。

前回のファイルに、書き加えていきます。

まず、ヘッダファイル(AccessibleFromVBA.h)

#pragma once

extern "C"
{
#define ACCESSIBLEFROMVBA_API __declspec(dllexport) 

	ACCESSIBLEFROMVBA_API void WINAPI DoNothing();

	ACCESSIBLEFROMVBA_API int WINAPI GetNumberI(int i);

	ACCESSIBLEFROMVBA_API void WINAPI GetNumberI2(int* pi);
}

f:id:Z1000S:20180627100359j:plain

次に、ソースファイル(AccessibleFromVBA.cpp)

#include "stdafx.h"
#include "AccessibleFromVBA.h"


ACCESSIBLEFROMVBA_API void WINAPI DoNothing()
{
	return;
}

//int型の値を受け取って、int型の復帰値を返す(VBAのファンクションプロシージャ相当)
ACCESSIBLEFROMVBA_API int WINAPI GetNumberI(int i)
{
	return i * 2;
}

//int型の値を受け取って、内部で値を変更して返す(VBAでパラメータをByRef で受け渡しするイメージ)
ACCESSIBLEFROMVBA_API void WINAPI GetNumberI2(int* pi)
{
	*pi *= 300;

	return;
}

f:id:Z1000S:20180627100406j:plain

最後に、モジュール定義ファイル(AccessibleFromVBA.def)

LIBRARY AccessibleFromVba

EXPORTS
	DoNothing
	GetNumberI
	GetNumberI2

f:id:Z1000S:20180627100410j:plain
全て追加したら、プロジェクトをビルドします。

========== ビルド: 1 正常終了、0 失敗、0 更新不要、0 スキップ ==========

と表示されればOK。
f:id:Z1000S:20180627100414j:plain

呼び出すExcel側は、

Private Declare Function GetNumberI Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByVal l As Long) As Long

Private Declare Sub GetNumberI2 Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef l As Long)

呼び出してみる

Public Sub DllCallTest2()

    Dim lValue  As Long
    Dim lResult As Long

    lValue = 1000

    lResult = GetNumberI(lValue)

    Debug.Print "GetNumberI:", lValue, lResult

    Debug.Print ""

    Debug.Print "GetNumberI2(Before):", lValue

    Call GetNumberI2(lValue)

    Debug.Print "GetNumberI2(After ):", lValue

End Sub

実行結果は、

call DllCallTest2
GetNumberI: 1000 2000


GetNumberI2(Before): 1000
GetNumberI2(After ): 300000

f:id:Z1000S:20180713081606j:plain

当てにならない次回予告

とりあえず、数値の受け渡しは出来たので、次は、

  • 文字列
  • 配列
  • 構造体

の受け渡しあたりをまとめられたらいいなぁ~


z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com

VBAによる祝日判定および祝日取得(改正東京五輪・パラリンピック特別措置法 対応)

2018年6月13日、参院本会議で可決、成立しました。

これに伴って、2020年の祝日が移動するものが出てきたようなので、先日公開した祝日判定処理を更新しました。
また、2020年からは「体育の日」が「スポーツの日」になるそうなので、合わせて対応済みです。

まぁ、祝日の定義の部分を変えただけなんですけどね。
処理内容は全く変えないで、元になる祝日データだけいじればいいので、祝日の追加、廃止、変更等のメンテナンスは楽ですよ。

ソース

実行例

2019年から2021年の7月から10月の祝日を出力した結果が以下の通り。

----- 2019 -----
2019/07/15 海の日
2019/08/11 山の日
2019/08/12 振替休日
2019/09/16 敬老の日
2019/09/23 秋分の日
2019/10/14 体育の日

----- 2020 -----
2020/07/23 海の日
2020/07/24 スポーツの日
2020/08/10 山の日
2020/09/21 敬老の日
2020/09/22 秋分の日

----- 2021 -----
2021/07/19 海の日
2021/08/11 山の日
2021/09/20 敬老の日
2021/09/23 秋分の日
2021/10/11 スポーツの日

【VBA / Excel】祝日休日の判定、取得用データを生成し、実際に判定、取得してみる

この記事では、予め用意された祝日テーブルを使用するのではなく、
クラスモジュール内で、祝日テーブルを自前で生成し、Dictionary に格納して判定処理を行っています。
祝日の定義は、クラスモジュール内に、汎用性を考慮した状態でハードコーディングしてあります。
このため、法の変更等により、祝日が変わらなければ、このまま使用できます。
別途、祝日テーブルを用意する必要はありません。

2018/9/10 追記
この記事は、祝日(振替休日等を含む)を判定の対象にしていますが、日曜日や年末年始の休み等は判定対象外です。
これらの休日も判定対象に含めたい場合には、以下の記事をあわせて参照してみて下さい。
z1000s.hatenablog.com


2018/6/14 追記
この記事には、2018/6/13の改正東京五輪パラリンピック特別措置法が参院本会議での可決に伴って、更新された記事があります。
z1000s.hatenablog.com

2019/5/23 追記
Googleで検索して来る方が多いようですが、中にはこのページが本来の目的ではない方もいるかもしれないので、とりあえず、「こんなページもあるよ」ということで載せておきます。
z1000s.hatenablog.com
z1000s.hatenablog.com


目次みたいなもの


Excelで祝日判定をしたい」というと、「祝日データを用意して、それを・・・」とお決まりの文言が出てくるのが一般的です。
では、その祝日データはどうすれば用意できるのでしょうか?

ここのように、期間を指定して生成してくれるようなところもあるようです。

が、必要なデータが変わるたびにサイトにアクセスしてというのも面倒です。
となれば、「自分で作ってしまえ作れるようにしてしまえ」となりました。

以上



???普通ならそうはならない???
まぁ、いいじゃないですか。私がそう思ったんだから。
それに、ここで終わったら、ただの落書きで終わってしましますよ。


そろそろ本題に。

まず、処理するに当たって祝日を分類してみる。カッコ内は例。

  1. 月、日が固定のもの(元日:1月1日)
  2. 月、第N ○曜日が固定のもの(成人の日:1月第2月曜日)
  3. 月は固定だが、日が可変のもの(春分の日:3月19〜22日)
  4. 月、日が定まっておらず、他の祝日などに依存するもの(振替休日、国民の休日

1、2項は簡単に求められる。
3項は、計算式があるのでこれも問題なし。
4項は、他の祝日に依存するため、単独では求められない。予め1〜3項の祝日がわかっている必要がある。(或いは、その都度依存する日の状態を確認するか。)

では具体的にどうすれば求められるか、簡単(?)かもしれない解説。
前提条件として、データはDate型で保持するものとします。

1.月、日が固定のもの

VBA では、DateSerial 関数を使用すると、年、月、日を指定してDate 型の値を取得できます。

DateSerial( year, month, day )

DateSerial 関数については、こちら

2.月、第N ○曜日が固定のもの

指定された月の1日の曜日がわかれば、1日以降の最初の指定された曜日(第1○曜日)へのオフセット値(何日補正すればよいか)がわかります。
それがわかれば、7の倍数を加算することで求められます。

まず、指定月の1日を求めるのは、1項で登場したDateSerial 関数

DateSerial( year, month, 1 )

と日に「1」を指定すればOK。

次にその曜日
曜日は、Weekday 関数を使用します。

Weekday( date , [ firstdayofweek ] )

dateに、上で求めた1日の日付データを渡します。
firstdayofweekは省略可能です。今回は使用しません。
そうすると、指定した日付によって1〜7の値が返ってきます。

定数 説明
vbSunday 1 日曜日
vbMonday 2 月曜日
vbTuesday 3 火曜日
vbWednesday 4 水曜日
vbThursday 5 木曜日
vbFriday 6 金曜日
vbSaturday 7 土曜日

これを書いている2018年5月の場合を考えてみます。
第2週までのカレンダーは、以下の通りです。

1 2 3 4 5
6 7 8 9 10 11 12

最初の曜日までの日数は、以下のようになります。

曜日 指定曜日の日付 1日から指定曜日までの日数
日曜日 6 5
月曜日 7 6
火曜日 1 0
水曜日 2 1
木曜日 3 2
金曜日 4 3
土曜日 5 4

で、曜日を使って「指定曜日までの日数」をどうやって求めるかというと
前述の曜日の定数を使って「指定曜日」 から 「1日の曜日」を引きます。
ただし、「指定曜日」が「1日の曜日」より前の場合、結果が負の値となるのでその場合には7を加えます。

5月1日は火曜日なので、vbTuesday ===> 3
例えば、第1金曜日なら、vbFriday ===> 6
なので、6 - 3 = 3 となります。

第1日曜日なら、vbSunday ===> 1
なので、1 - 3 = -2 となりますが、結果が負なので、+7 して
-2 + 7 = 5 となります。

この値が求められれば、次は1日の日付に加算を行います。
日付の加減算は、DateAdd 関数を使用します。

DateAdd(interval, number, date)

intervalには、日数の計算を行うので、"d" を指定します。
numberには、上記の計算で求めた値を指定します。
dateには、1日の日付データを指定します。

最初の曜日であれば、これで良いのですが2回め以降の曜日の場合、更に7の倍数を加算する必要があります。
第2曜日であれば、+7
第3曜日であれば、+14
第4曜日であれば、+21
第5曜日であれば、+28
となるので、第N曜日の場合
(N ー 1)*7
を加算すれば良いことになります。

最終的には、以下のようなプロシージャにより求められます。

Public Function getNthWeeksDayOfWeek(ByVal lYear As Long, _
                                      ByVal lMonth As Long, _
                                      ByVal lNth As Long, _
                                      ByVal lDayOfWeek As VbDayOfWeek) As Date

    Dim dt1stDate       As Date
    Dim lDayOfWeek1st   As Long
    Dim lOffset         As Long

    '指定年月の1日を取得
    dt1stDate = DateSerial(lYear, lMonth, 1)

    '1日の曜日を取得
    lDayOfWeek1st = Weekday(dt1stDate)

    '指定日へのオフセットを取得
    lOffset = lDayOfWeek - lDayOfWeek1st

    If lDayOfWeek1st > lDayOfWeek Then
        lOffset = lOffset + 7
    End If

    lOffset = lOffset + 7 * (lNth - 1)

    getNthWeeksDayOfWeek = DateAdd("d", lOffset, dt1stDate)

End Function

ただし、N >= 5を指定した場合、日付が翌月になる場合がありますので、注意が必要です。

Weekday 関数については、こちら
DateAdd 関数については、こちら

3.月は固定だが、日が可変のもの

これは、春分の日秋分の日が該当。
Wikipediaによると
春分の日は、3月19~22日のいずれか。
秋分の日は、22~24日のいずれか。
となっている。
一般に、

'春分の日
Int(20.8431 + 0.242194 * (- 1980)) - Int((- 1980) / 4)
'秋分の日
Int(23.2488 + 0.242194 * (- 1980)) - Int((- 1980) / 4)

という計算で求められるようです。
1980年以降に限定すれば、以下の内容でもOK。
というか、こうしたかったんだけれど、1979年以前の場合、
演算子の結果が、Fix 関数と同等になり、Int 関数とは結果が変わってしまうため
If で振り分ける等の必要が生じてしまうので、上記の式をそのまま使用することにしました。

'春分の日
Int(20.8431 + 0.242194 * (- 1980)) - (- 1980) \ 4
'秋分の日
Int(23.2488 + 0.242194 * (- 1980)) - (- 1980) \ 4

ちなみに、¥演算子を使用した場合は、内部で整数演算が行われるが、
/演算子の場合、整数同士の演算であっても、内部では浮動小数点数で演算が行われます。

4.月日が定まっておらず、他の祝日などに依存するもの

4.1 振替休日

1973年の国民の祝日に関する法律が改正されたことにより制定された。これにより祝日(国民の祝日)が日曜日の場合、その翌日となる月曜日が振替休日となった(ハッピーマンデー制度の法律と同じ)。1973年の天皇誕生日(4月29日)が日曜日で、同年4月30日が最初の適用日となった。

当初は祝日が2日以上連続することがなかったため、「国民の祝日の日曜日の翌日の月曜日」としていた。しかし2005年の国民の祝日に関する法律の改正(2007年施行)で、4月29日が「昭和の日」となり、「みどりの日」が4月29日から5月4日へ変更。5月3日から5月5日まで祝日が3日連続することになり、その直後の国民の祝日の日曜日の翌日の月曜日以降の国民の祝日でない祝日の翌日」を休日とすることと改められ振替先が月曜日固定ではなくなった。

振替休日 - Wikipedia

ということなので、
祝日が日曜日であった場合、適用された年から判断し、その翌日または、それ以降で最初の祝日でない日を求めればOK。
2007年以降では、当然ながらこの日が月曜日とは限りません。
2015年5月3日の憲法記念日の振替休日は、6日の水曜日でした。
 5月3日(日):憲法記念日
 5月4日(月):みどりの日
 5月5日(火):こどもの日
 5月6日(水):5月3日の振替休日

4.2 国民の休日

前後が祝日である平日は、国民の休日となり、休日となる。

国民の休日 - Wikipedia

こちらは、翌々日まで確認が必要なので、要件は振替休日より複雑。

  1. 最初の祝日が月曜日から金曜日の間にあること。
    1. 最初の祝日が日曜日の場合、翌日は振替休日となるので、2項を満足しない。
    2. 最初の祝日が土曜日の場合、翌日は日曜日となるので、平日ではない。
  2. 最初の祝日の翌日が、平日であること。(祝日でなく、休日でもないこと。)
  3. 最初の祝日の翌々日が、祝日であること。

これらの条件を満たした時、最初の祝日の翌日が「国民の休日」となる。

2項、3項の祝日は、年によって日付が変動する祝日であり、「移動祝日」または「移動祝祭日」と呼ぶそうです。

最終的なコード

VBAのクラスとして作成しました。
▶クリックでソースを展開/縮小
2021/4/11
都合により、
VBAによる「祝日判定処理」を「休日判定処理」に拡張してみた - 空腹おやじのログと備忘録
と、コードを統合した上で、
こちら に移動しました。

CCompanyHoliday.clsのみ
https://github.com/Z1000R/determining-and-retrieving-holidays/blob/main/Source/CCompanyHoliday.cls

その他諸々を含めて一式
github.com

パブリックメソッド一覧

isNationalHoliday(ByVal dtDate As Date) As Boolean

指定日が国民の祝日(休日)か?

引 数 IN/OUT 内容
dtDate As Date IN 判定したい日付
復帰値 内容
True 国民の祝日(休日)である。
False 国民の祝日(休日)ではない。
isNationalHoliday2(ByVal dtDate As Date, ByRef sHolidayName As String) As Boolean

指定日が国民の祝日(休日)か?そうであれば、その祝日名を合わせて返す

引 数 IN/OUT 内容
dtDate As Date IN 判定したい日付
sHolidayName As String OUT 祝日名
復帰値 内容
True 国民の祝日(休日)である。
False 国民の祝日(休日)ではない。
getNationalHolidays(ByVal lYear As Long, ByRef dtHolidays() As Date) As Long

指定年の国民の祝日を配列に格納して返す

引 数 IN/OUT 内容
lYear As Long IN 取得したい年
dtHolidays() As Date OUT 指定された年の国民の祝日
復帰値
国民の祝日の件数
getNationalHolidayName(ByVal dtHoliday As Date) As String

指定日の国民の祝日名を返す

引 数 IN/OUT 内容
dtHoliday As Date IN 日付
復帰値
指定された日付の国民の祝日

指定された日付が国民の祝日でない場合、復帰値は長さ0の文字列となる。

reInitialize(ByVal lLastYear As Long)

指定年までの祝日データが生成されていなければ、追加生成する。

引 数 IN/OUT 内容
lLastYear As Long IN 生成したい最終年

現在の何年までのデータが生成されているかは、後述のInitializedLastYearプロパティで取得できる。

パブリックプロパティ

InitializedLastYear() As Long

何年までの国民の祝日データが生成されているか
(読み取り専用)

使い方と実行例

クラス名は、CNationalHoliday としています。
使用する前に、Newを付けて、インスタンスを生成して下さい。
(そのタイミングで、デフォルトで現在の年から5年後の年末までの祝日を内部で生成します。)
その後に、必要なメソッドを呼び出します。

日付を指定して、祝日判定をして、祝日名を取得する
Public Sub Test1()

    Dim cnh     As New CNationalHoliday
    Dim dt      As Date
    Dim sHolidayName As String

    dt = #8/11/2015#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #8/11/2016#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #8/12/2018#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #8/12/2019#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    dt = #2/23/2020#
    Debug.Print Format$(dt, "yyyy/mm/dd"), cnh.getNationalHolidayName(dt), cnh.isNationalHoliday2(dt, sHolidayName)

    Set cnh = Nothing

End Sub

実行結果

call Test1
2015/08/11                  False
2016/08/11    山の日        True
2018/08/12                  False
2019/08/12    振替休日      True
2020/02/23    天皇誕生日    True
年を指定して、祝日および祝日名を取得する
Public Sub Test2(ByVal lYear As Long)

    Dim cnh     As New CNationalHoliday
    Dim dt()    As Date
    Dim i As Long

    Call cnh.getNationalHolidays(lYear, dt)

    For i = 0 To UBound(dt)
        Debug.Print dt(i), cnh.getNationalHolidayName(dt(i))
    Next i

    Set cnh = Nothing

End Sub

実行結果

call Test2(2018)
2018/01/01    元日
2018/01/08    成人の日
2018/02/11    建国記念の日
2018/02/12    振替休日
2018/03/21    春分の日
2018/04/29    昭和の日
2018/04/30    振替休日
2018/05/03    憲法記念日
2018/05/04    みどりの日
2018/05/05    こどもの日
2018/07/16    海の日
2018/08/11    山の日
2018/09/17    敬老の日
2018/09/23    秋分の日
2018/09/24    振替休日
2018/10/08    体育の日
2018/11/03    文化の日
2018/11/23    勤労感謝の日
2018/12/23    天皇誕生日
2018/12/24    振替休日
祝日データを、デフォルトの5年後より後の分も生成する
Public Sub Test3()

    Dim cnh As CNationalHoliday
    Dim dt  As Date
    Dim i As Long

    Set cnh = New CNationalHoliday

    Debug.Print "Before reInitialize", cnh.InitializedLastYear

    cnh.reInitialize 2030

    Debug.Print "After reInitialize", cnh.InitializedLastYear

    Set cnh = Nothing

End Sub

実行結果

call Test3
Before reInitialize          2023 
After reInitialize           2030 

お約束

掲載したコードの使用については、特に制限は設けません MITライセンスとします。
ご自由にお使い下さい。
使用にあたって、私への連絡等は不要です。

ただし、使用した結果、何らかのトラブル、損害、その他諸々の事象が発生しても、私は一切関与しません。
使用する方ソースコードを組み込んだ方が責任を取れる範囲内で使って下さい。(2018/9/9 一部修正)

バグ、要望、気が付いた事などあれば、コメントしていただければと思います。

参考にしたサイト

秀丸で、選択行の行頭に文字を挿入するマクロ

先日、人力検索はてなで、
q.hatena.ne.jp
に回答して、ベストアンサーを頂いたのですが
ベースになった物があって、C++のソースを編集する際に、

  • 行コメントの追加
  • 既存の行を改修してコメント化

するために以前作ったもの(下記)でした。
(今回の件で、ちょっとしたバグを発見して修正したけど・・・)

あちらにアップしたものは、コメント削除したものなので、わかりにくかったかも。

$comment_prefix = "//";

//範囲選択されているか
if ( selecting == 1 )
{
	//範囲選択されていたら解除
	escape;

	//選択開始行
	#TargetLine = seltopy + 1;

	//選択終了行
	if ( selendx == 0 )
	{
		//選択終了行のカーソル位置が行先頭なら、その行はコメント化しない
		#EndLine    = selendy;
	}
	else
	{
		#EndLine    = selendy + 1;
	}

	//選択開始行に移動
	movetolineno 1, seltopy + 1;
}
else
{
	//選択開始行
	#TargetLine = lineno;

	//選択終了行
	#EndLine    = lineno;
}

//最終行に移動
gofileend;

//最終行取得
#FileEndLine = lineno;

//元の行に戻る
movetolineno 1, #TargetLine;

while ( #TargetLine != #EndLine + 1 )
{
	//行先頭に移動
	golinetop;

	//コメント化
	insert $comment_prefix;

	//処理行をインクリメント
	#TargetLine = #TargetLine + 1;

	if ( #TargetLine > #FileEndLine )
	{
		break;
	}

	//1行下へ移動
	movetolineno 1, #TargetLine;
}

LINEのバックアップがGoogleドライブに見当たらない?

スマホの機種変更をしようとしていた知人から、
「LINEのバックアップをGoogleドライブに保存したけど、ファイルが見当たらない。このまま移行作業を進めてもいいのか?」
と相談されました。
LINEの設定画面には、バックアップ日時が表示されているし、
再度バックアップすると、日時が更新されるので、
バックアップデータは、アップロードされているような雰囲気。

自分のスマホでバックアップして、Googleドライブを覗いてみても確かに見当たらない。
隠しファイルを表示させても見えない。
試しにPCのブラウザから、ドライブを確認してみても見当たらない。

で、探したら見つけました。
PCのブラウザで、Googleドライブを表示させ、「設定」-「アプリの管理」を表示させると、「非表示のアプリデータ」として保存されていました。
f:id:Z1000S:20180508233154j:plain
見えないと、本当に保存されているのか不安ですよね。
とりあえず解決ということで・・・

MS AccessのインストールされていないPCで、VBA(Excel)を使って accdbを最適化する

最近VBAネタが続く・・・

今使っているOffice 2013を買う時、Accessはあれば便利かもしれないけど、
「どうせ使う機会は無いだろう」という事で、Office Personalを買ったので
Accessは持っていない。

しかし、最近になって〇〇なデータ管理をしようかと思ったが、どうにもExcelだけでは厳しい。
ローカルで手軽に使えるデータベースが欲しいけれど、このためだけにAccessを買うというのももったいない。
LibreOffice Calc Baseという選択肢はない。)
バックエンド用として使えれば、フロントエンドはC#とか最悪Excelでもなんとか出来る。

あれ、mdbってVBAで作れなかった?
やっぱり作れるじゃないか、accdbも!

で、このゴールデンウィークに、accdbファイル自体と、
それに、テーブルを作るためのツールを作った。
肝心のデータ管理用のフロントエンドは全く出来ていないし
データもインサートしてないのに、気が早くて「最適化ツールは絶対必要だ!!!」
となり、作り始めたはいいが、コンパイルエラー
"DBEngine"を、「変数が定義されていません」とおっしゃる。
f:id:Z1000S:20180506161438j:plain

DAOの参照設定してないじゃん。
Microsoft DAO 3.6 Object Library 参照設定して、コンパイルもOK。
いざ、実行!

実行時エラー 3343
データベースの形式 'C:\Datas\sample.accdb' を認識できません。

調べていくと

引数 options に次のいずれかの定数を使用すると、最適化されたデータベースのデータの形式のバージョンを指定できます。
・・・
dbVersion120
最適化に Microsoft Access データベース エンジン バージョン 12.0 のファイル形式を使用するデータベースを作成します。

DBEngine.CompactDatabase method (DAO) | Microsoft Docs

オプションに、dbVersion120を追記して、コンパイルしてみる。
"dbVersion120"に対して、"変数が定義されていません。"
f:id:Z1000S:20180506163124j:plain
まだ、だめですか・・・orz

さらに調べていくと、

DAO 12.0 (ACEDAO.DLL) の DBEngine.CompactDatabase メソッドを利用する。

VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板)

という記載を発見。

でも、参照設定に、DAO 12.0なんて見当たらない。
そもそも、ACEDAO.DLLなんて入ってるの?
f:id:Z1000S:20180506163729j:plain
DLLはあった。
じゃあ、どれを参照すればいいの???

オブジェクトブラウザーで「DAO」を確認すると「Microsoft office 15.0 Access database engine Object」に含まれているんですね。

ふひろ通信: Microsoft Access 2013の参照設定で「Microsoft DAO 3.6 Object Library」を有効にしようとすると警告が出る

これ(Microsoft Office 15.0 Access database engine Object Library)か?
f:id:Z1000S:20180506165710j:plain
参照設定チェックしてみると
f:id:Z1000S:20180506170103j:plain
オブジェクトブラウザに、dbVersion120の文字が表示された!
コンパイルもOK。
いざ、実行。
エラーの発生もなく、無事終了。

ダミーのデータ10万件をinsert後、全てdeleteしてから
最適化処理でファイルサイズが小さくなっているのを確認。
(拡張子".tmp"となっているのが最適化実行直後(リネーム前))
f:id:Z1000S:20180508074139j:plain

最終的なコードは、これ

'参照設定
'Microsoft Office 15.0 Access database engine Object Library

Public Sub compactDB()

    Dim sSrcPath    As String
    Dim sDestPath   As String

    '最適化するファイルのパス
    sSrcPath = "C:\Datas\sample.accdb"

    '最適化後のファイル名
    sDestPath = sSrcPath & ".tmp"

    On Error GoTo ERR_COMPACT_DB

    '最適化の実行
    DBEngine.CompactDatabase sSrcPath, sDestPath, DAO.LanguageConstants.dbLangJapanese, DAO.DatabaseTypeEnum.dbVersion120
    '冗長に書いているが、下記でもOK
    'DBEngine.CompactDatabase sSrcPath, sDestPath, dbLangJapanese, dbVersion120

    '最適化前のファイルを削除
    Kill sSrcPath

    '最適化後のファイル名を最適化前のファイル名にリネーム
    Name sDestPath As sSrcPath

ERR_COMPACT_DB:
    If Err.Number <> 0 Then
        Debug.Print "[" & CStr(Err.Number) & "]" & Err.Description
    Else
        Debug.Print "Done."
    End If

End Sub

自分一人で使うやつなので、エラー処理は最低限。
あと、参照設定の15.0の部分は、Officeのバージョンで変わるらしい。

mdbの場合は、dbVersion120の代わりに、dbVersion40を使えばいけるのかな?(未確認)

参考サイト
DBEngine.CompactDatabase method (DAO) | Microsoft Docs
DatabaseTypeEnum enumeration (DAO) | Microsoft Docs

VBAのDictionaryのItemに動的配列を格納する

C++のmultimapのように、特定のキーに対し、複数のデータをDictionaryに格納できないか?
キー毎にデータ数が異なる場合、最大データ数を求めてから固定長配列を格納するのは無駄だし、
実際にデータがいくつ入っているか先頭から当たっていかないとわからなそう。
それは、やりたくない。

で、動的配列をItemとして格納してみたら、案外あっさりと出来ました。

 

ワークシートから読み込んだ地名の先頭1文字をキーとして、その地名を配列にしてItemに格納する例です。

Public Sub dicTest()

    Dim dicAddress  As New Dictionary
    Dim sAddress()  As String
    Dim sKey        As String
    Dim lItems      As Long
    Dim sValue      As String
    Dim lRow        As Long
    Dim i           As Long

    lRow = 1

    lItems = 0
    ReDim sAddress(lItems)

    With ThisWorkbook.Worksheets("Sheet1")
        sValue = .Cells(lRow, 1).Value

        Do Until (Len(sValue) = 0)

            sKey = Left$(sValue, 1)

            If dicAddress.Exists(sKey) = True Then
                'キーありなら、格納されている配列を取得
                sAddress = dicAddress.Item(sKey)

                lItems = UBound(sAddress) + 1

                '配列の要素数をひとつ増やす
                ReDim Preserve sAddress(lItems)
            Else
                'キーなしなら、1個分のデータを格納できるよう、配列を初期化
                lItems = 0

                ReDim sAddress(lItems)
            End If

            sAddress(lItems) = sValue

            '配列を格納
            dicAddress(sKey) = sAddress

            lRow = lRow + 1

            sValue = .Cells(lRow, 1).Value

            'Dictionaryには、コピーされた配列が格納される(ようです)。
            '消さなくても試した限りでは問題はなさそうだけど、残して何度も使い回すのは気持ち悪いので消す。
            Erase sAddress
        Loop
    End With

    '格納したデータを書き出す
    Debug.Print "----- い -----"
    For i = 0 To UBound(dicAddress.Item("い"))
        Debug.Print dicAddress.Item("い")(i)
    Next i

    Debug.Print "----- お -----"
    For i = 0 To UBound(dicAddress.Item("お"))
        Debug.Print dicAddress.Item("お")(i)
    Next i

End Sub

テストデータは
---------------------------------
あきる野市
あわら市
あま市
あさぎり町
おいらせ町
いわき市
つくばみらい市
さいたま市
いすみ市
おおい町
いなべ市
いの町
いちき串木野市

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

実行結果は、
----- い -----
いわき市
いすみ市
いなべ市
いの町
いちき串木野市

----- お -----
おいらせ町
おおい町

こんな感じ。


ポイントは、

1.キーがあった時の、データ(動的配列)の取り出し方。
2.データを追するのためのReDim PreserveReDimの使い方。
3.データ読み出し時の指定方法dicAddress.Item("い")(i)
あたりでしょうか。

 

まあ、ちょっとしたネタなので、「もっといい方法がある」とか突っ込まないで下さい。

 

こんなのもあります。

z1000s.hatenablog.com