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: throwTypeComplaint;
14 
15 import core.time;
16 import std.datetime.date : Date, DateTime, TimeOfDay;
17 import std.datetime.systime : LocalTime, SysTime, TimeZone, UTC;
18 import std.bitmanip: bigEndianToNative, nativeToBigEndian;
19 import std.math;
20 import core.stdc.time: time_t;
21 
22 /++
23     Returns value timestamp with time zone as SysTime
24     Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs.
25     It means that PG value will have 10 times lower precision.
26     And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime.
27 
28     Because of these differences, it can happen that database value will not fit to the SysTime range of values.
29 +/
30 SysTime binaryValueAs(T)(in Value v) @trusted
31 if( is( T == SysTime ) )
32 {
33     if(!(v.oidType == OidType.TimeStampWithZone))
34         throwTypeComplaint(v.oidType, "timestamp with time zone", __FILE__, __LINE__);
35 
36     if(!(v.data.length == long.sizeof))
37         throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
38             "Value length isn't equal to Postgres timestamp with time zone type", __FILE__, __LINE__);
39 
40     auto t = rawTimeStamp2nativeTime!TimeStampUTC(bigEndianToNative!long(v.data.ptr[0..long.sizeof]));
41     return SysTime(t.dateTime, t.fracSec, UTC());
42 }
43 
44 pure:
45 
46 /// Returns value data as native Date
47 Date binaryValueAs(T)(in Value v) @trusted
48 if( is( T == Date ) )
49 {
50     if(!(v.oidType == OidType.Date))
51         throwTypeComplaint(v.oidType, "Date", __FILE__, __LINE__);
52 
53     if(!(v.data.length == uint.sizeof))
54         throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
55             "Value length isn't equal to Postgres date type", __FILE__, __LINE__);
56 
57     int jd = bigEndianToNative!uint(v.data.ptr[0..uint.sizeof]);
58     int year, month, day;
59     j2date(jd, year, month, day);
60 
61     return Date(year, month, day);
62 }
63 
64 /// Returns value time without time zone as native TimeOfDay
65 TimeOfDay binaryValueAs(T)(in Value v) @trusted
66 if( is( T == TimeOfDay ) )
67 {
68     if(!(v.oidType == OidType.Time))
69         throwTypeComplaint(v.oidType, "time without time zone", __FILE__, __LINE__);
70 
71     if(!(v.data.length == TimeADT.sizeof))
72         throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
73             "Value length isn't equal to Postgres time without time zone type", __FILE__, __LINE__);
74 
75     return time2tm(bigEndianToNative!TimeADT(v.data.ptr[0..TimeADT.sizeof]));
76 }
77 
78 /// Returns value timestamp without time zone as TimeStamp
79 TimeStamp binaryValueAs(T)(in Value v) @trusted
80 if( is( T == TimeStamp ) )
81 {
82     if(!(v.oidType == OidType.TimeStamp))
83         throwTypeComplaint(v.oidType, "timestamp without time zone", __FILE__, __LINE__);
84 
85     if(!(v.data.length == long.sizeof))
86         throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
87             "Value length isn't equal to Postgres timestamp without time zone type", __FILE__, __LINE__);
88 
89     return rawTimeStamp2nativeTime!TimeStamp(
90         bigEndianToNative!long(v.data.ptr[0..long.sizeof])
91     );
92 }
93 
94 /// Returns value timestamp with time zone as TimeStampUTC
95 TimeStampUTC binaryValueAs(T)(in Value v) @trusted
96 if( is( T == TimeStampUTC ) )
97 {
98     if(!(v.oidType == OidType.TimeStampWithZone))
99         throwTypeComplaint(v.oidType, "timestamp with time zone", __FILE__, __LINE__);
100 
101     if(!(v.data.length == long.sizeof))
102         throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
103             "Value length isn't equal to Postgres timestamp with time zone type", __FILE__, __LINE__);
104 
105     return rawTimeStamp2nativeTime!TimeStampUTC(
106         bigEndianToNative!long(v.data.ptr[0..long.sizeof])
107     );
108 }
109 
110 /// Returns value timestamp without time zone as DateTime (it drops the fracSecs from the database value)
111 DateTime binaryValueAs(T)(in Value v) @trusted
112 if( is( T == DateTime ) )
113 {
114     return v.binaryValueAs!TimeStamp.dateTime;
115 }
116 
117 /++
118     Structure to represent PostgreSQL Timestamp with/without time zone
119 +/
120 private struct TTimeStamp(bool isWithTZ)
121 {
122     DateTime dateTime; /// date and time of TimeStamp
123     Duration fracSec; /// fractional seconds
124 
125     alias dateTime this;
126 
127     invariant()
128     {
129         import std.conv : to;
130 
131         assert(fracSec >= Duration.zero, fracSec.to!string);
132         assert(fracSec < 1.seconds, fracSec.to!string);
133     }
134 
135     /// Returns the TimeStamp farthest in the future which is representable by TimeStamp.
136     static max()
137     {
138         return TTimeStamp(DateTime.max, long.max.hnsecs);
139     }
140 
141     /// Returns the TimeStamp farthest in the past which is representable by TimeStamp.
142     static min()
143     {
144         return TTimeStamp(DateTime.min, Duration.zero);
145     }
146 
147     string toString() const
148     {
149         return dateTime.toString~" "~fracSec.toString;
150     }
151 }
152 
153 alias TimeStamp = TTimeStamp!false; /// Unknown TZ timestamp
154 alias TimeStampUTC = TTimeStamp!true; /// Assumed that this is UTC timestamp
155 
156 unittest
157 {
158     {
159         auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
160         assert(t.dateTime.hour == 14);
161     }
162     {
163         auto dt = DateTime(2017, 11, 13, 14, 29, 17);
164         auto t = TimeStamp(dt, 75_678.usecs);
165 
166         assert(t == dt); // test the implicit conversion to DateTime
167     }
168     {
169         auto t = TimeStampUTC(
170                 DateTime(2017, 11, 13, 14, 29, 17),
171                 75_678.usecs
172             );
173 
174         assert(t.dateTime.hour == 14);
175         assert(t.fracSec == 75_678.usecs);
176     }
177 }
178 
179 package enum POSTGRES_EPOCH_DATE = Date(2000, 1, 1);
180 package enum POSTGRES_EPOCH_JDATE = POSTGRES_EPOCH_DATE.julianDay;
181 static assert(POSTGRES_EPOCH_JDATE == 2_451_545); // value from Postgres code
182 
183 private:
184 
185 T rawTimeStamp2nativeTime(T)(long raw)
186 if(is(T == TimeStamp) || is(T == TimeStampUTC))
187 {
188     if(raw >= time_t.max) return T.max;
189     if(raw <= time_t.min) return T.min;
190 
191     pg_tm tm;
192     fsec_t ts;
193 
194     if(timestamp2tm(raw, tm, ts) < 0)
195         throw new AnswerException(
196             ExceptionType.OUT_OF_RANGE, "Timestamp is out of range",
197             __FILE__, __LINE__
198         );
199 
200     TimeStamp ret = raw_pg_tm2nativeTime(tm, ts);
201 
202     static if(is(T == TimeStamp))
203         return ret;
204     else
205         return TimeStampUTC(ret.dateTime, ret.fracSec);
206 }
207 
208 TimeStamp raw_pg_tm2nativeTime(pg_tm tm, fsec_t ts)
209 {
210     auto dateTime = DateTime(
211             tm.tm_year,
212             tm.tm_mon,
213             tm.tm_mday,
214             tm.tm_hour,
215             tm.tm_min,
216             tm.tm_sec
217         );
218 
219     auto fracSec = dur!"usecs"(ts);
220 
221     return TimeStamp(dateTime, fracSec);
222 }
223 
224 // Here is used names from the original Postgresql source
225 
226 void j2date(int jd, out int year, out int month, out int day)
227 {
228     enum MONTHS_PER_YEAR = 12;
229 
230     jd += POSTGRES_EPOCH_JDATE;
231 
232     uint julian = jd + 32044;
233     uint quad = julian / 146097;
234     uint extra = (julian - quad * 146097) * 4 + 3;
235     julian += 60 + quad * 3 + extra / 146097;
236     quad = julian / 1461;
237     julian -= quad * 1461;
238     int y = julian * 4 / 1461;
239     julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366))
240         + 123;
241     year = (y+ quad * 4) - 4800;
242     quad = julian * 2141 / 65536;
243     day = julian - 7834 * quad / 256;
244     month = (quad + 10) % MONTHS_PER_YEAR + 1;
245 }
246 
247 private alias long Timestamp;
248 private alias long TimestampTz;
249 private alias long TimeADT;
250 private alias long TimeOffset;
251 private alias int  fsec_t;      /* fractional seconds (in microseconds) */
252 
253 void TMODULO(ref long t, ref long q, double u)
254 {
255     q = cast(long)(t / u);
256     if (q != 0) t -= q * cast(long)u;
257 }
258 
259 TimeOfDay time2tm(TimeADT time)
260 {
261     immutable long USECS_PER_HOUR  = 3600000000;
262     immutable long USECS_PER_MINUTE = 60000000;
263     immutable long USECS_PER_SEC = 1000000;
264 
265     int tm_hour = cast(int)(time / USECS_PER_HOUR);
266     time -= tm_hour * USECS_PER_HOUR;
267     int tm_min = cast(int)(time / USECS_PER_MINUTE);
268     time -= tm_min * USECS_PER_MINUTE;
269     int tm_sec = cast(int)(time / USECS_PER_SEC);
270     time -= tm_sec * USECS_PER_SEC;
271 
272     return TimeOfDay(tm_hour, tm_min, tm_sec);
273 }
274 
275 struct pg_tm
276 {
277     int         tm_sec;
278     int         tm_min;
279     int         tm_hour;
280     int         tm_mday;
281     int         tm_mon;         /* origin 0, not 1 */
282     int         tm_year;        /* relative to 1900 */
283     int         tm_wday;
284     int         tm_yday;
285     int         tm_isdst;
286     long        tm_gmtoff;
287     string      tm_zone;
288 }
289 
290 alias pg_time_t = long;
291 
292 enum USECS_PER_DAY       = 86_400_000_000UL;
293 enum USECS_PER_HOUR      = 3_600_000_000UL;
294 enum USECS_PER_MINUTE    = 60_000_000UL;
295 enum USECS_PER_SEC       = 1_000_000UL;
296 
297 /**
298 * timestamp2tm() - Convert timestamp data type to POSIX time structure.
299 *
300 * Note that year is _not_ 1900-based, but is an explicit full value.
301 * Also, month is one-based, _not_ zero-based.
302 * Returns:
303 *   0 on success
304 *  -1 on out of range
305 *
306 * If attimezone is null, the global timezone (including possibly brute forced
307 * timezone) will be used.
308 */
309 int timestamp2tm(Timestamp dt, out pg_tm tm, out fsec_t fsec)
310 {
311     Timestamp   date;
312     Timestamp   time;
313     pg_time_t   utime;
314 
315     time = dt;
316     TMODULO(time, date, USECS_PER_DAY);
317 
318     if (time < 0)
319     {
320         time += USECS_PER_DAY;
321         date -= 1;
322     }
323 
324     j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday);
325     dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec);
326 
327     return 0;
328 }
329 
330 void dt2time(Timestamp jd, out int hour, out int min, out int sec, out fsec_t fsec)
331 {
332     TimeOffset  time;
333 
334     time = jd;
335     hour = cast(int)(time / USECS_PER_HOUR);
336     time -= hour * USECS_PER_HOUR;
337     min = cast(int)(time / USECS_PER_MINUTE);
338     time -= min * USECS_PER_MINUTE;
339     sec = cast(int)(time / USECS_PER_SEC);
340     fsec = cast(int)(time - sec*USECS_PER_SEC);
341 }