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