経理屋とVBAの日記

経理業務で役に立つかもしれないExcel/Access VBAのネタを書きます

【.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"))

結果:
f:id:nicco_mirai:20180125233504p:plain


ちゃんと変換されてる。ちゃんとというか指定してないのに平成扱いされました。

テスト2

さて、JapaneseCalendarクラスが何を元に元号を処理しているかと言えば、レジストリです。
regeditで下記のフォルダを覗いてみましょう。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\Calendars\Japanese\Eras
f:id:nicco_mirai:20180125233506p:plain


ありました。
試しにここに新しい値を追加して…
f:id:nicco_mirai:20180125233509p:plain


先ほどのコードを走らせると…
f:id:nicco_mirai:20180125233511p:plain


結果が変わりました。

最終テスト

ここで冒頭の疑問に立ち返りますが、では一体この中からどんな基準で元号を補完しているのでしょうか。
考えられるのはシステム日付でしょう。つまり「レジストリとシステム日付から現在の元号を取得し補完している」という仮説です。


レジストリに未来日付を登録し…
f:id:nicco_mirai:20180125233632p:plain


Debug.Print(ToDay)を加えて実行します。
今日は2018年の1月25日です。


結果:
f:id:nicco_mirai:20180125233513p:plain


はい。というわけでシステム日付は関係ありませんでした。常に最新の元号が補完されるようです。

補足

これでデフォルトの挙動は掴めましたが、やはり自分で元号を明示的に補完した方が圧倒的に安心です。
よほど楽がしたいでもなければ

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を叩く方法を考えていきます。

定義

使用するAPIですが、今回は

  • REST API
  • パラメータの形式はクエリ文字列かJSON
  • レスポンスはJSON(か空)

とさせてください。XMLについては記事の最後で少しだけ触れます。

ポイント

前置きが長くなりましたが、ポイントは3つです。

  • APIをキックする部分は関数化する
  • パラメータは連想配列にまとめる
  • JSONデータはVBA-JSONに委ねる

あえて逆順で説明していきます。

JSONデータはVBA-JSONに委ねる

github.com

  • JSONをパースして連想配列にする
  • 連想配列やコレクションから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-JSONが非常に便利なので、これを中心に組むことが省エネへの一番の近道になると思います。
     ちなみに同じ作者のVBA-XMLというのもあるので、気になる方は是非触ってみて使用感を教えてください。

    【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様のブログのものを参考にさせていただきました。

    ありがとうございます。

    thom.hateblo.jp

    利用規約

    1. 当ブログに掲載しているコードの一部または全部を質問サイトやブログ、Webサイト、その他のメディアに転載する場合、該当記事のURLを併記してください。
    2. 掲載されているコードをそのまま業務に使用する場合、コメントに該当記事のURL及びこのページのURLを記載してください。
    3. 上記2点につき事前の申告は不要です。