【.NET】ParseExact関数に渡す文字列から元号を省略したときの挙動
"300110"
この数列が何を表しているか分かりますか?
そう、日付です。平成30年01月10日です。分かりませんね。
今回諸般の事情でこのような形のデータを処理しなければならなくなり、また改元にも対応しなくてはということでJapaneseCalendarクラスについて色々と調べてみたのですが、標題の疑問が解決しなかったので自分でテストしてみました。
テスト1
さくっと書きます(System.Globalization名前空間を使います)。
Dim ConvertJapaneseDate = Function(targetDate As String, targetFormat As String) As Date Dim cultureJapan As CultureInfo = New CultureInfo("ja-JP", True) cultureJapan.DateTimeFormat.Calendar = New JapaneseCalendar() Return DateTime.ParseExact(targetDate, targetFormat, cultureJapan) End Function Debug.Print(ConvertJapaneseDate("30/01/10", "yy/MM/dd"))
結果:
ちゃんと変換されてる。ちゃんとというか指定してないのに平成扱いされました。
テスト2
さて、JapaneseCalendarクラスが何を元に元号を処理しているかと言えば、レジストリです。
regeditで下記のフォルダを覗いてみましょう。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese\Eras
ありました。
試しにここに新しい値を追加して…
先ほどのコードを走らせると…
結果が変わりました。
最終テスト
ここで冒頭の疑問に立ち返りますが、では一体この中からどんな基準で元号を補完しているのでしょうか。
考えられるのはシステム日付でしょう。つまり「レジストリとシステム日付から現在の元号を取得し補完している」という仮説です。
レジストリに未来日付を登録し…
Debug.Print(ToDay)を加えて実行します。
今日は2018年の1月25日です。
結果:
はい。というわけでシステム日付は関係ありませんでした。常に最新の元号が補完されるようです。
補足
これでデフォルトの挙動は掴めましたが、やはり自分で元号を明示的に補完した方が圧倒的に安心です。
よほど楽がしたいでもなければ
Dim regKey As Microsoft.Win32.RegistryKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese\Eras", False) Dim eraDic As New Dictionary(Of Date, String) For Each eName In regKey.GetValueNames eraDic.Add(CDate(eName.Replace(" ", "/")), regKey.GetValue(eName)) Next
等で元号の一覧を拾って自分で日付に加えてあげるといいんじゃないかと思います。
VBAでWeb API(REST API)を使うときの作法
巷でVBAが古い、使えない、ゴミ、カメムシと言われる理由をここで改めて書くことはしませんが、少なくともVBAで凝ったことをしようとすれば最終的には他の言語の知識が必須になります。
というかそもそも名前からしてfor Applicationsなので…というところもあり、
- VBAから金の匂いがする
- 宗教上の理由でGoogle Appsが使えない
- 足りない機能を補うためWScriptからPowerShellを立ち上げて.NET Frameworkの関数を呼ぶような実装に何らかの興奮を覚える
といった特殊な環境に置かれた諸兄以外はあまりVBAを突き詰める必要はないと思います。
今回お話しするWebAPIもそんなVBAの苦手分野の1つで、諸々の理由と相まってネット上にはヤバいコードがごろごろ転がっています。
流石にあれを業務で使うのもアレなので、本稿ではVBAでもそこそこまともにWebAPIを叩く方法を考えていきます。
JSONデータはVBA-JSONに委ねる
VBAからJSONを扱うのであれば現状これが一番楽だと思われます。
パースがとにかく楽。
32ビット環境であればScriptControlという手もあるのですが、JSONオブジェクトが使いにくいのでどの道あまりオススメはしません。
配布する場合はライセンス表記と参照設定について注意してください。
パラメータは連想配列にまとめる
Const attack = 155 Const defence = 120 Const speed = 105
のようにパラメータを1つずつ定数や変数に入れるとコードが冗長になります。
VBAでは文字列から変数や定数の値を取得することができないため、
このやり方ではAPIのパラメータ名と値が紐付かないのです。
というわけで連想配列の出番です。
Dim param As Object Set param = CreateObject("Scripting.Dictionary") With param .Add "attack", 155 .Add "defence", 120 .Add "speed", 105 End With
これであればパラメータ名と値が直接紐付きますし、可読性も良くなります。
APIをキックする部分は関数化する
さて、ここまできちんと準備をすれば関数自体もシンプルに書けます。
Public Function KickAPI( _ ByVal request As String, _ ByVal URL As String, _ ByVal paramType As Long, _ Optional ByVal param As Object) As Object With CreateObject("MSXML2.XMLHTTP") .Open request, URL, False Select Case paramType Case 1 .SetRequestHeader "Content-Type", "application/x-www-form-urlencoded" .send (ConvertToQueryString(param)) Case 2 .SetRequestHeader "Content-Type", "application/json; charset=UTF-8" .send (ConvertToJson(param)) End Select If .ResponseText <> "" Then Set KickAPI = ParseJson(.ResponseText) End If End With End Function Private Function ConvertToQueryString(ByVal dic As Object) As String If dic Is Nothing Then Exit Function Dim key As Variant For Each key In dic.keys ConvertToQueryString = ConvertToQueryString & "&" & key & "=" & dic.Item(key) Next End Function 'ConvertToJson及びParseJsonは上記VBA-JSONに含まれる関数です。 '連想配列をクエリ文字列に変換する関数の名前もこれに寄せてみました。
実際にはここにアクセストークンの有無なんかも入ってきてもう少しごちゃごちゃするのですが、基本はこんなもん。
あとはこれを使うAPIにあわせてカスタマイズしていけば、メンテナンス性の高いコードが書けるはずです。
【VBA】64ビット版対応って結局何をすればいいの?
そもそも
当ブログ一発目にこの話題を選んだ理由がこれです。
iOS 11 のアップデートについて - Apple サポート
iOS 11は64ビットApp用にパフォーマンスが最適化されています。32ビットAppをこのバージョンのiOSで動作させるにはデベロッパ によるアップデートが必要になります。
要は「iOS11以降32ビットのアプリは動きませんよ」ということです。ついに来たかという感じですね。
ちなみにmacOSも現在のHigh Sierraが32ビットアプリをサポートする最後のバージョンになるそうです。OS Xが64ビット版のみになってから既に5年半も経っていることを思えば当然の流れだと言えるでしょう。
さて、この記事を書いている2017/10/29現在でもMicrosoftはOfficeについて32ビット版のインストールを推奨しています。Windowsは最新バージョンであるWindows 10でも32ビット版を用意していますし、Apple製品のように近々で32ビットアプリが使えなくなるというようなことはないと考えられます。
しかし、上記のような状況を踏まえれば、今から新しく起こすVBAのコードや、5年前に辞めたあの人が遺したアレの64ビット版対応というのは決して優先順位の低いものではないと
そうクダを巻いて多めに工数をもらいましょう。
補足
Excel: Declaring API functions in 64 bit Office
を見て各関数の宣言をコピペするというのも一つの正解なのですが、宣言が長くなりますし、メンテナンスできる人も一層限られますし、なにより今更2007に対応する必要もないと思いますので
極力条件付きコンパイルは避けてDeclareしたいと思います。
①DeclareステートメントにPtrSafe属性を追加する
これは特に補足することもないですね。
Declare Sub/Function Hoge ~
を
Declare PtrSafe Sub/Function Hoge ~
に変えるだけです。この文言が入っていないと64ビット環境ではコンパイルエラーになります。
②ポインタ及びハンドルを代入する変/定数をLong型からLongPtr型に変更する
一番難しいところだと思います。
詳しい説明は他のサイト様に委ねるとして、要はポインタだハンドルだと呼ばれるものは32ビット環境と64ビット環境でサイズ(大きさ)が違いますよ、だからそれぞれで違うサイズの型を使いましょうねということです。
LongPtr データ型
こちらに説明がありますがLongPtr型というのは32ビット環境と64ビット環境とでサイズの異なる整数型に解決されます。
…じゃあ全部LongPtrにしちゃえば良くない?といつかの私は思いました。ダメでした。
例えば下記のコードを64ビット環境で実行するとエラーが出ます。
Sub test() Dim c As LongPtr Dim hoge() As Variant c = Selection.Count ReDim hoge(c) End Sub
恐らくですが配列の長さの最大数がLongLong型のそれより小さいのでしょう。仕方がないです。
あとはやはりよく分からないコードをよく分からないまま使うのは純粋に良くないかなと思います。
良くないかなというか、いいんですが、後で必ず自分が痛い目を見ます。
最適な変数に最適な型を指定しましょう。
③条件付きコンパイルを設定する
これらの上位互換である
・GetWindowLongPtr
・SetWindowLongPtr
関数。MSDNには32ビットと64ビットで互換性があると書かれていますが、VBAにおいては嘘です。
32ビット環境でPtr付きの関数を実行しようとすると普通にエラーが出ますので、泣く泣く条件付きコンパイルを設定しましょう。
Set~の方だけですが
#If Win64 Then Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongPtrA" ( _ ByVal hWnd As LongPtr, _ ByVal nIndex As Long, _ ByVal dwNewLong As LongPtr) As LongPtr #Else Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongA" ( _ ByVal hWnd As LongPtr, _ ByVal nIndex As Long, _ ByVal dwNewLong As LongPtr) As LongPtr #End If
こんな感じですね。エイリアスを設定することによって実体がどちらであれ「SetWindowLong」の名前で使用できるようにします。
実践
Excel VBA を学ぶなら moug モーグ | 即効テクニック | クリップボードへデータを送信する方法
VBAでクリップボードを操作する方法はいくつかありますが、ループ系の処理ではやっぱりAPIを使うのが一番安定するんですよね。
というわけで実際にこれを64ビット環境で動かしてみます。
Option Explicit '指定したサイズ分のメモリを割り当て Private Declare PtrSafe Function GlobalAlloc Lib "kernel32" ( _ ByVal wFlags As Long, _ ByVal dwBytes As LongPtr) As LongPtr 'メモリブロックをロックして最初の1バイトへのポインタを返す Private Declare PtrSafe Function GlobalLock Lib "kernel32" ( _ ByVal hMem As LongPtr) As LongPtr 'バッファに文字列をコピー Private Declare PtrSafe Function lstrcpy Lib "kernel32" ( _ ByVal lpString1 As LongPtr, _ ByVal lpString2 As String) As LongPtr 'メモリのロックを解除 Private Declare PtrSafe Function GlobalUnlock Lib "kernel32" ( _ ByVal hMem As LongPtr) As Long 'クリップボードを排他的に開く Private Declare PtrSafe Function OpenClipboard Lib "User32" ( _ ByVal hWnd As LongPtr) As Long 'クリップボードを初期化 Private Declare PtrSafe Function EmptyClipboard Lib "User32" ( _ ) As Long 'クリップボードにデータを渡す Private Declare PtrSafe Function SetClipboardData Lib "User32" ( _ ByVal wFormat As Long, _ ByVal hMem As LongPtr) As LongPtr 'クリップボードを閉じる Private Declare PtrSafe Function CloseClipboard Lib "User32" ( _ ) As Long 'GlobalAlloc Private Const GHND = &H42 'SetClipboardData Private Const CF_TEXT = &H1 Public Function SetClipBoard(setStr As String) As LongPtr Dim hGlobalMemory As LongPtr Dim lpGlobalMemory As LongPtr 'ヒープからメモリを確保 hGlobalMemory = GlobalAlloc(GHND, LenB(setStr) + 1) lpGlobalMemory = GlobalLock(hGlobalMemory) If lpGlobalMemory = 0 Then MsgBox "メモリを確保できません", vbExclamation Exit Function End If '確保したメモリに文字列を保存し、ロックを解除 Call lstrcpy(lpGlobalMemory, setStr) Call GlobalUnlock(hGlobalMemory) 'メモリからクリップボードにデータを渡す If OpenClipboard(0) = 0 Then MsgBox "クリップボードが開けません", vbExclamation Exit Function End If Call EmptyClipboard SetClipBoard = SetClipboardData(CF_TEXT, hGlobalMemory) Call CloseClipboard 'クリップボードは開いたら必ず閉じる End Function
不要な部分はバッサリ削っていますが、基本的には上記①②だけです。
元のコード及びMSDNとにらめっこしながら確認してみてください。
終わりに
色々書きましたが一番手っ取り早いのは
「とりあえず64ビット版を入れてみる」
ことだと思います。動かなくても一つ一つ戻り値を確認していけば
思ったほど大変な作業にはならないでしょう。
はじめに(利用規約)
このブログには私が仕事や趣味で覚えたVBAに関するあれこれを書いていきます。
実際のコードも載せますので、良識の範囲内で自由にお使いいただければと思います。
なお下記利用規約はいつも大変お世話になっておりますt-hom様のブログのものを参考にさせていただきました。
ありがとうございます。
利用規約
- 当ブログに掲載しているコードの一部または全部を質問サイトやブログ、Webサイト、その他のメディアに転載する場合、該当記事のURLを併記してください。
- 掲載されているコードをそのまま業務に使用する場合、コメントに該当記事のURL及びこのページのURLを記載してください。
- 上記2点につき事前の申告は不要です。