[Notes/Domino] 設計解析講座: TIMEDATE 構造体
TIMEDATE 構造体については、この連載では解説しないつもりだったのですが、やっぱり解説しておくことにします。これを理解しておくと、いろいろと便利だと思いますので。
構造体の定義
TIMEDATE 構造体は、以下のように定義されています。
typedef struct tagTIMEDATE { DWORD Innards[2]; } TIMEDATE;
DWORD は4バイトの変数ですから、単純に4バイトの変数が2つくっついているだけです。
これを LotusScript で宣言すると、以下のようになります。
Type TIMEDATE Innards(1) As long End Type
場合によっては、構造体を定義せずに、8バイトの変数(Double、Currency)を使うこともできます。(4バイトずつにバラすのが若干面倒ですが。)
ここまでは簡単ですね。
TIMEDATE の値とレプリカID
では次に、データの中身を見てみましょう。
「2010/11/23 21:55:35」という日時値が入っているフィールドをバイナリダンプしてみると、以下のようになります。(※バイナリダンプするツール(管理人作)については、年末~来年早々に公開予定です。)
D5 01 47 00 E4 77 25 49
バイトオーダーはリトルエンディアンです。したがって、実際の16進値は以下のようになります。
492577E4004701D5
この値を見て気付くことはありませんか? そうレプリカIDですね。レプリカIDとは、実はそのDBが作成されたときの日時値(TIMEDATE)なのです。
これは UNID でも同じで、UNID は TIMEDATE が2つ重なったものです。1つ目の TIMEDATE はランダムな値、2つ目の TIMEDATE は作成日時を表します。
0F86C6A1:78995762-492577D4:004C6D3F └ ランダム値 ┘ └ 作成日時 ┘
ちなみに、TIMEDATE を上述した LotusScript の構造体にセットすると、以下のようになります。
Innards(0) = 0x004701D5 Innards(1) = 0x492577E4
4バイトの変数が2つなので、上記のようになることに注意してください。
この場合、Innards(0) が時刻値で、Innards(1) が日付値です。
日付のみ、時刻のみの場合
TIMEDATE で注意すべきなのは、日付のみ、時刻のみの値があるということです。日付のみの場合は、Innards(0) が 0xFFFFFFFF、時刻のみの場合は、Innards(1) が 0xFFFFFFFF になります。(ワイルドカードの場合は両方とも 0xFFFFFFFF になります。)
日付のみ、または時刻のみの場合は、現地時刻がそのままセットされますが、日付・時刻の両方がセットされる場合は、タイムゾーン情報+グリニッジ標準時がセットされます。この点、特に注意してください。
時刻値
時刻値は100分の1秒単位で値を持っています。ノーツクライアントからは秒までしか持っていないように見えますが、実は100分の1秒単位でデータを持っているのです。
たとえば例の「0x004701D5」=「21:55:35」(=グリニッジ標準時で「12:55:35」)の場合、以下のように構成されています。
12時 * 60 * 60 * 100 = 4,320,000 + 55分 * 60 * 100 = 330,000 + 35秒 * 100 = 3,500 + 25(100分の1秒) = 25 ---------------------------------- 4,653,525 = 0x004701D5
時刻値は比較的簡単ですね。
日付値
日付値はちょっとややこしいです。例の「0x492577E4」をビット値に直して解説します。
0 1 00 1001 0010 0101 0111 0111 1110 0100 A B C D E
- A = サマータイム(DST)実施地域の場合 1、それ以外は 0
- B = グリニッジ標準時より東の場合は 1、西の場合は 0
- C = グリニッジとの時差(分)(0 = 0分、1 = 15分、2 = 30分、3 = 45分)
- D = グリニッジとの時差(時)(これは数値がそのまま時間となる)
- E = 紀元前4713年1月1日からの経過日数
最初の1バイト(= 8 ビット)にタイムゾーン情報が入っていて、残りが経過日数というわけです。経過日数の基準日の、紀元前4713年1月1日という値がどこから出てきたのかよくわかりませんが、とにかく、LotusScriptの基準日1900年1月1日とは異なる点に注意してください。
上記を見ると、日本で作成されたレプリカIDが「4925~」で始まる理由がよくわかります。「0x49」はタイムゾーンの値ですし、「0x25」は紀元前4713年1月1日からの経過日数があまりにも大きいので、数十年レベルでそこの値が変わることはないのです。
変換関数
TIMEDATE の値を文字列に変換するのは簡単です。ConvertTIMEDATEToText を使って以下のような関数を用意するとよいでしょう。
Type TIMEDATE Innards(1) As long End Type Public Const NOTESDLL = "nnotes.dll" Declare Function ConvertTIMEDATEToText Lib NOTESDLL (ByVal IntlFormat As Integer, _ ByVal TimeFormat As Integer, InputTime As TIMEDATE, ByVal retTextBuffer As String, _ ByVal TextBufferLength As Integer, retTextLength As Integer) As Integer Function APITIMEDATE2String(td As TIMEDATE) As String Dim s As String * 81 Dim i As Integer Call ConvertTIMEDATEToText(0, 0, td, s, 80, i) APITIMEDATE2String = StrLeft(s & Chr(0), Chr(0)) End Function
これだけだと芸がないので、TIMEDATE と LotusScript の日時値を相互変換する関数を書いてみました(おもいっきり日本仕様です)。まぁ、TIMEDATE → Variant の場合は、ConvertTIMEDATEToText で返った戻り値を CDat すればいいだけの話ですが、こういうふうにも書けるというのを見てみてください。カスタマイズすれば、いろいろな処理が書けると思います。
Type TIMEDATE Innards(1) As long End Type Public Sub APITIMEDATE2Variant(td As TIMEDATE, retDt As variant) Dim dblDate As Double Dim dblTime As Double Dim lngTime As Long Dim lngHour As Long Dim lngMin As Long Dim lngSec As Long lngTime = td.Innards(0) \ 100 lngHour = lngTime \ 3600 lngMin = (lngTime - (lngHour * 3600)) \ 60 lngSec = lngTime - (lngHour * 3600) - (lngMin * 60) dblDate = CDbl((td.Innards(1) And &H00FFFFFF&) - 2415019) dblTime = CDbl(TimeNumber(lngHour, lngMin, lngSec)) If td.Innards(0) = &HFFFFFFFF& Then retDt = CDat(dblDate) ElseIf td.Innards(1) = &HFFFFFFFF& Then retDt = CDat(dblTime) Else retDt = CDat(dblDate + dblTime) retDt = retDt + TimeNumber(9,0,0) '日本仕様(時差+9h) End If End Sub Public Sub APIVariant2TIMEDATE(ByVal dt As Variant, retTD As TIMEDATE) Dim lngDate As Long If retTD.Innards(0) <> &HFFFFFFFF& And retTD.Innards(1) <> &HFFFFFFFF& Then dt = dt - TimeNumber(9,0,0) '日本仕様(グリニッジ標準時にする) End If If retTD.Innards(0) <> &HFFFFFFFF& Then retTD.Innards(0) = (Hour(dt) * 3600 + Minute(dt) * 60 + Second(dt)) * 100 End If If retTD.Innards(1) <> &HFFFFFFFF& Then lngDate = CLng(DateNumber(Year(dt), Month(dt), Day(dt))) If retTD.Innards(0) <> &HFFFFFFFF& Then lngDate = lngDate Or &H49000000& '日本仕様(タイムゾーン情報) End If retTD.Innards(1) = (lngDate + 2415019) End If End Sub
「2415019」という数字は、紀元前4713年1月1日~紀元後1900年1月1日の日数です。この値を使うと、日付値の相互変換ができます。
上記サンプルは、現地時刻ではなく日本時刻に無理やり合わせています。現地時刻を取得したい場合は、タイムゾーン情報から時差などを取得する必要があるので、ちょっと面倒です。おとなしく API 関数を使っておいたほうが楽ですね。
コメント
コメントはありません
※コメントは承認制となっております。管理者が承認するまで表示されません。申し訳ありませんが、投稿が表示されるまでしばらくお待ちください。