[[20240202173523]] 『シリアル値を使用したDictionaryについての質問』(半日悩んだ) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) |

| 全文検索 | 過去ログ ]

 

『シリアル値を使用したDictionaryについての質問』(半日悩んだ)

こんにちは。
仕事で使用するため時間を基準とした外部データを取り込み、
そのデータをマクロで処理しようと時間をkeyとしたDictionaryを使いました。
しかし
For t = 開始 to 終了 Step TimeSerial(0, 1, 0)
v = dic.Item(t)
としたところある日時のところでitemが取得できないという現象が起きました。
ここのデバッグでdic.Exists(t)を見てみたらTrueだったので、何故なのか悩みました。
(時間データはyyyy/m/d h:mm:00のシリアル値です)

結果として
For Each t in dic.keys
v = dic.Item(t)
で解決したのですが、あまり飲み込めていません。

この理由がわかる方はご教授願えませんでしょうか。

< 使用 Excel:Microsoft365、使用 OS:Windows10 >


 > ここのデバッグでdic.Exists(t)を見てみたらTrueだったので、何故なのか悩みました。
 tが keyになくても
 v = dic.Item(t) を実行した瞬間に
 tをkey , Emptyを itemとするデータが作成される仕様です。
 ですから、dic.Exists(t)は True を返します。

(xyz) 2024/02/02(金) 18:21:16


なるほど、そこの部分は使用ですか。
では他のシリアル値ではエラーが出なかったのにその時間だけ(その後も出るかもですが)エラーが出るのは何故でしょうか?
小数点以下の割り切れない数字のせいでしょうか?
(半日悩んだ) 2024/02/02(金) 19:08:15

 情報不足なので私にはわかりません。
 dictionaryにどういうキーを与えたのか、照会のときにどんなキーを与えたのかが不明です。
 他の回答者の回答をお待ちください。
(xyz) 2024/02/02(金) 19:47:27

 原因は不明ですが、方法として思いつくのは、
 Format関数でStringに変換したものをキーにするという方法ですね。
 これなら誤差は防止できるような気がします。
 dictionaryのキーはStringのほうが効率もよいように思います。それでは。
(xyz) 2024/02/02(金) 20:14:02

 状況としては、
  1分毎のデータをキーにDictionaryに格納したのに、
  1分毎に取り出そうとしたら、そのキーが存在しない、と言う展開になった。
 それはおかしいではないか、って話ですよね(多分)

 >For t = 開始 to 終了 Step TimeSerial(0, 1, 0)

 そのステートメントは、厳密に1分毎を指定したことにならないです。
 何故って、時刻データは小数値だからです。

 開始(小数値)に1分(小数値)を加算して行ったら、小数演算誤差問題が付いて回るので、
 キッチリ一致しないキーがボコボコ出ても不思議ではないってことです。

 上記が的外れでしたら、そちらの状況をもっと詳しく説明してください。

(半平太) 2024/02/02(金) 20:25:19


半平太さんの回答が私が知りたかったことです。

やはり誤差で一致しないシリアル値が出てくるのですね。
すっきりしました。

これからは計算で誤差が出そうな時はFor Eachかxyzさんが書かれたStringに変換しようと思います。
xyzさん、半平太さん、ありがとうございました。
(半日悩んだ) 2024/02/03(土) 11:01:19


 小数演算誤差が原因というのはありえるかもしれませんが、
 下記の検証コードでは1分おきのシリアル値(VBAではDate型)では24時間以内の範囲では
 誤差は出ないようでした。

    Dim 開始 As Date, 終了 As Date
    開始 = TimeSerial(0, 0, 0)
    終了 = TimeSerial(24, 0, 0)

    Dim t As Date
    For t = 開始 To 終了 Step TimeSerial(0, 1, 0)
        If Second(t) <> 0 Then
            Debug.Print "誤差: ", t
        End If
    Next

  元データに抜けがあったいう可能性もあるのでは。

 > ここのデバッグでdic.Exists(t)を見てみたらTrueだったので、何故なのか悩みました。

 具体的にはどのように確認しましたか?
 エラーがでたときにイミディエイトで確認したのなら、
 xyzさんが指摘したDictionaryの仕様上無意味です。

 下記のような感じでデバッグコードを埋め込んで確認したのならいいのですが。

 For t = 開始 to 終了 Step TimeSerial(0, 1, 0)
     If Not dic.Exists(t) Then MsgBox t & "は存在しません"
     v = dic.Item(t)


 xyzさんが提案したFormat関数でStringに変換したものをキーにするという方法にしておけば
 小数演算誤差に悩まずにすむとは思います。

(hatena) 2024/02/03(土) 12:15:57


Sub test()
    Dim dic As Object, i&, d As Date, k, 開始 As Date, 終了 As Date, t As Date
    Set dic = CreateObject("Scripting.Dictionary")
    'Date型 yyyy/m/d h:mm:00をキーとするDictionary登録
    For i = 0 To 59
        d = "2024/2/3 8:" & i & ":00"
        dic(d) = i
    Next
    Debug.Print "-----------ちゃんと登録されているか確認-----------"
    For Each k In dic
        Debug.Print "Key(" & TypeName(k) & "型)="; k, "Value="; dic(k)
    Next
    開始 = "2024/2/3 8:00:00"
    終了 = "2024/2/3 9:00:00"
    Debug.Print "-----------誤差補正なし Step TimeSerial(0, 1, 0)でキーから値を取り出してみる-----------"
    For t = 開始 To 終了 Step TimeSerial(0, 1, 0)
        Debug.Print "Key(" & TypeName(t) & "型)="; t, "Value="; dic(t)
    Next
    Debug.Print "-----------誤差補正あり Step TimeSerial(0, 1, 0)でキーから値を取り出してみる-----------"
    For t = 開始 To 終了 Step TimeSerial(0, 1, 0)
        d = Format(t, "yyyy/m/d h:mm:00")
        Debug.Print "Key(" & TypeName(d) & "型)="; d, "Value="; dic(d)
    Next
End Sub
(*) 2024/02/03(土) 12:42:52

 前回の回答では、
 Second(t)で丸められるのでだめでした。

 下記だと20:43以降で誤差がでました。

 Public Sub Test11()
    Dim 開始 As Date, 終了 As Date
    開始 = TimeSerial(0, 0, 0)
    終了 = TimeSerial(24, 0, 0)
    Dim t As Date, i As Long
    For t = 開始 To 終了 Step TimeSerial(0, 1, 0)
        If t <> TimeSerial(0, i, 0) Then
            Debug.Print t; TimeSerial(0, i, 0) '確認用
        End If
        i = i + 1
    Next

 End Sub

(hatena) 2024/02/03(土) 13:19:41


  > 下記だと20:43以降で誤差がでました。

  私の環境(Win11、XL2010、365)でTest11を実行したら、直ぐ相違が出ましたけども。

  0:05:00 0:05:00 
  0:10:00 0:10:00 
  0:11:00 0:11:00 
  0:14:00 0:14:00 
  0:15:00 0:15:00 
    :   :   :
(半平太) 2024/02/03(土) 16:21:39

間違えました。
TimeSerial関数を使った方がいいと思いますよ。
これならシリアル値のずれ関係ありませんから。
時、分、秒の3重ループでいいと思います。
3重ループできまる時刻が
timeserial(時,分,秒)=<timeserial(終了時刻時間、終了時刻分、終了時刻秒)
の間ループを回せばよいかと。
(通りすがり) 2024/02/03(土) 22:00:39

 >間違えました。

 何を間違えたんですか?

 あと、何故TimeSerialなんですか?
 質問では、こう言っているんですけどねぇ・・
       ↓
 >、(時間データはyyyy/m/d h:mm:00のシリアル値です)

(半平太) 2024/02/03(土) 22:46:42


ごめんなさい。
ご指摘の通り出鱈目な返答をしていました。
人間は間違える生き物で私は間違いやすいようです。
うるう秒を扱う可能性もあるので。

ループを回すのに
dateadd()
を使うのがいいかもしれませんね。
開始から終了までで1分ごとのシリアル値をtに入れて
tが1時間たつごとにdebug.printしてみました。
マイクロソフトがうるう秒まできちんと内部実装してるかは知りませんが。
dateaddが一番いいんじゃないですかね。

Sub test()
Dim 開始 As Date
Dim 終了 As Date
Dim t As Date
Dim i As Long
i = 0
開始 = Now()
終了 = Now() + 2
Debug.Print 開始
Debug.Print 終了

Do While DateAdd("n", i, 開始) <= 終了

    t = DateAdd("n", i, 開始)
    If (i Mod 60) = 0 Then
        Debug.Print t
    End If
    i = i + 1
Loop
End Sub

(通りすがり) 2024/02/04(日) 03:17:00


コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.