1 /**
2 *   PostgreSQL time types binary format.
3 *
4 *   Copyright: © 2014 DSoftOut
5 *   Authors: NCrashed <ncrashed@gmail.com>
6 */
7 module dpq2.conv.time;
8 
9 @safe:
10 
11 import dpq2.result;
12 import dpq2.oids: OidType;
13 import dpq2.conv.to_d_types: checkValue;
14 
15 import core.time;
16 import std.datetime.date : Date, DateTime, TimeOfDay;
17 import std.datetime.systime: SysTime;
18 import std.datetime.timezone: LocalTime, TimeZone, UTC;
19 import std.bitmanip: bigEndianToNative, nativeToBigEndian;
20 import std.math;
21 import std.conv: to;
22 
23 /++
24     Returns value timestamp with time zone as SysTime
25 
26     Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs.
27     It means that PG value will have 10 times lower precision.
28     And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime.
29 
30     Because of these differences, it can happen that database value will not fit to the SysTime range of values.
31 +/
32 SysTime binaryValueAs(T)(in Value v) @trusted
33 if( is( T == SysTime ) )
34 {
35     v.checkValue(OidType.TimeStampWithZone, long.sizeof, "timestamp with time zone");
36 
37     auto t = rawTimeStamp2nativeTime!TimeStampUTC(bigEndianToNative!long(v.data.ptr[0..long.sizeof]));
38     return SysTime(t.dateTime, t.fracSec, UTC());
39 }
40 
41 pure:
42 
43 /// Returns value data as native Date
44 Date binaryValueAs(T)(in Value v) @trusted
45 if( is( T == Date ) )
46 {
47     v.checkValue(OidType.Date, uint.sizeof, "date type");
48 
49     int jd = bigEndianToNative!uint(v.data.ptr[0..uint.sizeof]);
50     int year, month, day;
51     j2date(jd, year, month, day);
52 
53     // TODO: support PG Date like TTimeStamp manner and remove this check
54     if(year > short.max)
55         throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW,
56             "Year "~year.to!string~" is bigger than supported by std.datetime.Date", __FILE__, __LINE__);
57 
58     return Date(year, month, day);
59 }
60 
61 /// Returns value time without time zone as native TimeOfDay
62 TimeOfDay binaryValueAs(T)(in Value v) @trusted
63 if( is( T == TimeOfDay ) )
64 {
65     v.checkValue(OidType.Time, TimeADT.sizeof, "time without time zone");
66 
67     return time2tm(bigEndianToNative!TimeADT(v.data.ptr[0..TimeADT.sizeof]));
68 }
69 
70 /// Returns value timestamp without time zone as TimeStamp
71 TimeStamp binaryValueAs(T)(in Value v) @trusted
72 if( is( T == TimeStamp ) )
73 {
74     v.checkValue(OidType.TimeStamp, long.sizeof, "timestamp without time zone");
75 
76     return rawTimeStamp2nativeTime!TimeStamp(
77         bigEndianToNative!long(v.data.ptr[0..long.sizeof])
78     );
79 }
80 
81 /// Returns value timestamp with time zone as TimeStampUTC
82 TimeStampUTC binaryValueAs(T)(in Value v) @trusted
83 if( is( T == TimeStampUTC ) )
84 {
85     v.checkValue(OidType.TimeStampWithZone, long.sizeof, "timestamp with time zone");
86 
87     return rawTimeStamp2nativeTime!TimeStampUTC(
88         bigEndianToNative!long(v.data.ptr[0..long.sizeof])
89     );
90 }
91 
92 /// Returns value timestamp without time zone as DateTime (it drops the fracSecs from the database value)
93 DateTime binaryValueAs(T)(in Value v) @trusted
94 if( is( T == DateTime ) )
95 {
96     return v.binaryValueAs!TimeStamp.dateTime;
97 }
98 
99 ///
100 enum InfinityState : byte
101 {
102     NONE = 0, ///
103     INFINITY_MIN = -1, ///
104     INFINITY_MAX = 1, ///
105 }
106 
107 ///
108 struct PgDate
109 {
110     int year; ///
111     ubyte month; ///
112     ubyte day; ///
113 
114     /// '-infinity', earlier than all other dates
115     static PgDate earlier() pure { return PgDate(int.min, 0, 0); }
116 
117     /// 'infinity', later than all other dates
118     static PgDate later() pure { return PgDate(int.max, 0, 0); }
119 
120     bool isEarlier() const pure { return year == earlier.year; } /// '-infinity'
121     bool isLater() const pure { return year == later.year; } /// 'infinity'
122 }
123 
124 ///
125 static toPgDate(Date d) pure
126 {
127     return PgDate(d.year, d.month, d.day);
128 }
129 
130 /++
131     Structure to represent PostgreSQL Timestamp with/without time zone
132 +/
133 struct TTimeStamp(bool isWithTZ)
134 {
135     /**
136      * Date and time of TimeStamp
137      *
138      * If value is '-infinity' or '+infinity' it will be equal PgDate.min or PgDate.max
139      */
140     PgDate date;
141     TimeOfDay time; ///
142     Duration fracSec; /// fractional seconds, 1 microsecond resolution
143 
144     ///
145     this(DateTime dt, Duration fractionalSeconds = Duration.zero) pure
146     {
147         this(dt.date.toPgDate, dt.timeOfDay, fractionalSeconds);
148     }
149 
150     ///
151     this(PgDate d, TimeOfDay t = TimeOfDay(), Duration fractionalSeconds = Duration.zero) pure
152     {
153         date = d;
154         time = t;
155         fracSec = fractionalSeconds;
156     }
157 
158     ///
159     void throwIfNotFitsToDate() const
160     {
161         if(date.year > short.max)
162             throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW,
163                 "Year "~date.year.to!string~" is bigger than supported by std.datetime", __FILE__, __LINE__);
164     }
165 
166     ///
167     DateTime dateTime() const pure
168     {
169         if(infinity != InfinityState.NONE)
170             throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW,
171                 "TTimeStamp value is "~infinity.to!string, __FILE__, __LINE__);
172 
173         throwIfNotFitsToDate();
174 
175         return DateTime(Date(date.year, date.month, date.day), time);
176     }
177 
178     invariant()
179     {
180         assert(fracSec < 1.seconds, "fracSec can't be more than 1 second but contains "~fracSec.to!string);
181         assert(fracSec >= Duration.zero, "fracSec is negative: "~fracSec.to!string);
182         assert(!fracSec.isNegative, "fracSec is negative");
183     }
184 
185     bool isEarlier() const pure { return date.isEarlier; } /// '-infinity'
186     bool isLater() const pure { return date.isLater; } /// 'infinity'
187 
188     /// Returns infinity state
189     InfinityState infinity() const pure
190     {
191         with(InfinityState)
192         {
193             if(isEarlier) return INFINITY_MIN;
194             if(isLater) return INFINITY_MAX;
195 
196             return NONE;
197         }
198     }
199 
200     unittest
201     {
202         assert(TTimeStamp.min == TTimeStamp.min);
203         assert(TTimeStamp.max == TTimeStamp.max);
204         assert(TTimeStamp.min != TTimeStamp.max);
205 
206         assert(TTimeStamp.earlier != TTimeStamp.later);
207         assert(TTimeStamp.min != TTimeStamp.earlier);
208         assert(TTimeStamp.max != TTimeStamp.later);
209 
210         assert(TTimeStamp.min.infinity == InfinityState.NONE);
211         assert(TTimeStamp.max.infinity == InfinityState.NONE);
212         assert(TTimeStamp.earlier.infinity == InfinityState.INFINITY_MIN);
213         assert(TTimeStamp.later.infinity == InfinityState.INFINITY_MAX);
214     }
215 
216     /// Returns the TimeStamp farthest in the past which is representable by TimeStamp.
217     static immutable(TTimeStamp) min()
218     {
219         /*
220         Postgres low value is 4713 BC but here is used -4712 because
221         "Date uses the Proleptic Gregorian Calendar, so it assumes the
222         Gregorian leap year calculations for its entire length. As per
223         ISO 8601, it treats 1 B.C. as year 0, i.e. 1 B.C. is 0, 2 B.C.
224         is -1, etc." (Phobos docs). But Postgres isn't uses ISO 8601
225         for date calculation.
226         */
227         return TTimeStamp(PgDate(-4712, 1, 1), TimeOfDay.min, Duration.zero);
228     }
229 
230     /// Returns the TimeStamp farthest in the future which is representable by TimeStamp.
231     static immutable(TTimeStamp) max()
232     {
233         enum maxFract = 1.seconds - 1.usecs;
234 
235         return TTimeStamp(PgDate(294276, 12, 31), TimeOfDay(23, 59, 59), maxFract);
236     }
237 
238     /// '-infinity', earlier than all other time stamps
239     static immutable(TTimeStamp) earlier() pure { return TTimeStamp(PgDate.earlier); }
240 
241     /// 'infinity', later than all other time stamps
242     static immutable(TTimeStamp) later() pure { return TTimeStamp(PgDate.later); }
243 
244     ///
245     string toString() const
246     {
247         import std.format;
248 
249         return format("%04d-%02d-%02d %s %s", date.year, date.month, date.day, time, fracSec.toString);
250     }
251 }
252 
253 alias TimeStamp = TTimeStamp!false; /// Unknown TZ timestamp
254 alias TimeStampUTC = TTimeStamp!true; /// Assumed that this is UTC timestamp
255 
256 unittest
257 {
258     auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
259     assert(t.dateTime.hour == 14);
260 }
261 
262 unittest
263 {
264     auto dt = DateTime(2017, 11, 13, 14, 29, 17);
265     auto t = TimeStamp(dt, 75_678.usecs);
266 
267     assert(t.dateTime == dt); // test the implicit conversion to DateTime
268 }
269 
270 unittest
271 {
272     auto t = TimeStampUTC(
273             DateTime(2017, 11, 13, 14, 29, 17),
274             75_678.usecs
275         );
276 
277     assert(t.dateTime.hour == 14);
278     assert(t.fracSec == 75_678.usecs);
279 }
280 
281 unittest
282 {
283     import std.exception : assertThrown;
284 
285     auto e = TimeStampUTC.earlier;
286     auto l = TimeStampUTC.later;
287 
288     assertThrown!ValueConvException(e.dateTime.hour == 14);
289     assertThrown!ValueConvException(l.dateTime.hour == 14);
290 }
291 
292 /// Oid tests
293 unittest
294 {
295     assert(detectOidTypeFromNative!TimeStamp == OidType.TimeStamp);
296     assert(detectOidTypeFromNative!TimeStampUTC == OidType.TimeStampWithZone);
297     assert(detectOidTypeFromNative!SysTime == OidType.TimeStampWithZone);
298     assert(detectOidTypeFromNative!Date == OidType.Date);
299     assert(detectOidTypeFromNative!TimeOfDay == OidType.Time);
300 }
301 
302 ///
303 struct TimeOfDayWithTZ
304 {
305     TimeOfDay time; ///
306     TimeTZ tzSec; /// Time zone offset from UTC in seconds with east of UTC being negative
307 }
308 
309 /// Returns value time with time zone as TimeOfDayWithTZ
310 TimeOfDayWithTZ binaryValueAs(T)(in Value v) @trusted
311 if( is( T == TimeOfDayWithTZ ) )
312 {
313     enum recSize = TimeADT.sizeof + TimeTZ.sizeof;
314     static assert(recSize == 12);
315 
316     v.checkValue(OidType.TimeWithZone, recSize, "time with time zone");
317 
318     return TimeOfDayWithTZ(
319         time2tm(bigEndianToNative!TimeADT(v.data.ptr[0 .. TimeADT.sizeof])),
320         bigEndianToNative!TimeTZ(v.data.ptr[TimeADT.sizeof .. recSize])
321     );
322 }
323 
324 ///
325 struct Interval
326 {
327     long usecs; /// All time units less than days
328     int days; /// Days, after time for alignment. Sign is ignored by PG server if usecs == 0
329     int months; /// Ditto, after time for alignment. Sign is ignored by PG server if usecs == 0 and days == 0
330 }
331 
332 /// Returns value time with time zone as Interval
333 Interval binaryValueAs(T)(in Value v) @trusted
334 if( is( T == Interval ) )
335 {
336     v.checkValue(OidType.TimeInterval, long.sizeof * 2, "interval");
337 
338     return Interval(
339         bigEndianToNative!long(v.data.ptr[0 .. 8]),
340         bigEndianToNative!int(v.data.ptr[8 .. 12]),
341         bigEndianToNative!int(v.data.ptr[12 .. 16])
342     );
343 }
344 
345 package enum POSTGRES_EPOCH_DATE = Date(2000, 1, 1);
346 package enum POSTGRES_EPOCH_JDATE = POSTGRES_EPOCH_DATE.julianDay;
347 static assert(POSTGRES_EPOCH_JDATE == 2_451_545); // value from Postgres code
348 
349 private:
350 
351 T rawTimeStamp2nativeTime(T)(long raw)
352 if(is(T == TimeStamp) || is(T == TimeStampUTC))
353 {
354     import core.stdc.time: time_t;
355 
356     if(raw == long.max) return T.later; // infinity
357     if(raw == long.min) return T.earlier; // -infinity
358 
359     pg_tm tm;
360     fsec_t ts;
361 
362     if(timestamp2tm(raw, tm, ts) < 0)
363         throw new ValueConvException(
364             ConvExceptionType.OUT_OF_RANGE, "Timestamp is out of range",
365         );
366 
367     TimeStamp ret = raw_pg_tm2nativeTime(tm, ts);
368 
369     static if(is(T == TimeStamp))
370         return ret;
371     else
372         return TimeStampUTC(ret.dateTime, ret.fracSec);
373 }
374 
375 TimeStamp raw_pg_tm2nativeTime(pg_tm tm, fsec_t ts)
376 {
377     return TimeStamp(
378         PgDate(
379             tm.tm_year,
380             cast(ubyte) tm.tm_mon,
381             cast(ubyte) tm.tm_mday
382         ),
383         TimeOfDay(
384             tm.tm_hour,
385             tm.tm_min,
386             tm.tm_sec
387         ),
388         ts.dur!"usecs"
389     );
390 }
391 
392 // Here is used names from the original Postgresql source
393 
394 void j2date(int jd, out int year, out int month, out int day)
395 {
396     enum MONTHS_PER_YEAR = 12;
397 
398     jd += POSTGRES_EPOCH_JDATE;
399 
400     uint julian = jd + 32044;
401     uint quad = julian / 146097;
402     uint extra = (julian - quad * 146097) * 4 + 3;
403     julian += 60 + quad * 3 + extra / 146097;
404     quad = julian / 1461;
405     julian -= quad * 1461;
406     int y = julian * 4 / 1461;
407     julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366))
408         + 123;
409     year = (y+ quad * 4) - 4800;
410     quad = julian * 2141 / 65536;
411     day = julian - 7834 * quad / 256;
412     month = (quad + 10) % MONTHS_PER_YEAR + 1;
413 }
414 
415 private alias long Timestamp;
416 private alias long TimestampTz;
417 private alias long TimeADT;
418 private alias int  TimeTZ;
419 private alias long TimeOffset;
420 private alias int  fsec_t;      /* fractional seconds (in microseconds) */
421 
422 void TMODULO(ref long t, ref long q, double u)
423 {
424     q = cast(long)(t / u);
425     if (q != 0) t -= q * cast(long)u;
426 }
427 
428 TimeOfDay time2tm(TimeADT time)
429 {
430     immutable long USECS_PER_HOUR  = 3600000000;
431     immutable long USECS_PER_MINUTE = 60000000;
432     immutable long USECS_PER_SEC = 1000000;
433 
434     int tm_hour = cast(int)(time / USECS_PER_HOUR);
435     time -= tm_hour * USECS_PER_HOUR;
436     int tm_min = cast(int)(time / USECS_PER_MINUTE);
437     time -= tm_min * USECS_PER_MINUTE;
438     int tm_sec = cast(int)(time / USECS_PER_SEC);
439     time -= tm_sec * USECS_PER_SEC;
440 
441     return TimeOfDay(tm_hour, tm_min, tm_sec);
442 }
443 
444 struct pg_tm
445 {
446     int         tm_sec;
447     int         tm_min;
448     int         tm_hour;
449     int         tm_mday;
450     int         tm_mon;         /* origin 0, not 1 */
451     int         tm_year;        /* relative to 1900 */
452     int         tm_wday;
453     int         tm_yday;
454     int         tm_isdst;
455     long        tm_gmtoff;
456     string      tm_zone;
457 }
458 
459 alias pg_time_t = long;
460 
461 enum USECS_PER_DAY       = 86_400_000_000UL;
462 enum USECS_PER_HOUR      = 3_600_000_000UL;
463 enum USECS_PER_MINUTE    = 60_000_000UL;
464 enum USECS_PER_SEC       = 1_000_000UL;
465 
466 /**
467 * timestamp2tm() - Convert timestamp data type to POSIX time structure.
468 *
469 * Note that year is _not_ 1900-based, but is an explicit full value.
470 * Also, month is one-based, _not_ zero-based.
471 * Returns:
472 *   0 on success
473 *  -1 on out of range
474 *
475 * If attimezone is null, the global timezone (including possibly brute forced
476 * timezone) will be used.
477 */
478 int timestamp2tm(Timestamp dt, out pg_tm tm, out fsec_t fsec)
479 {
480     Timestamp   date;
481     Timestamp   time;
482     pg_time_t   utime;
483 
484     time = dt;
485     TMODULO(time, date, USECS_PER_DAY);
486 
487     if (time < 0)
488     {
489         time += USECS_PER_DAY;
490         date -= 1;
491     }
492 
493     j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday);
494     dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec);
495 
496     return 0;
497 }
498 
499 void dt2time(Timestamp jd, out int hour, out int min, out int sec, out fsec_t fsec)
500 {
501     TimeOffset  time;
502 
503     time = jd;
504     hour = cast(int)(time / USECS_PER_HOUR);
505     time -= hour * USECS_PER_HOUR;
506     min = cast(int)(time / USECS_PER_MINUTE);
507     time -= min * USECS_PER_MINUTE;
508     sec = cast(int)(time / USECS_PER_SEC);
509     fsec = cast(int)(time - sec*USECS_PER_SEC);
510 }