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;
13 import dpq2.conv.to_d_types: throwTypeComplaint;
14 import dpq2.exception;
15 
16 import std.datetime;
17 import std.bitmanip: bigEndianToNative;
18 import std.math;
19 import core.stdc.time: time_t;
20 
21 pure:
22 
23 /// Returns value data as native Date
24 Date binaryValueAs(T)(in Value v) @trusted
25 if( is( T == Date ) )
26 {
27     if(!(v.oidType == OidType.Date))
28         throwTypeComplaint(v.oidType, "Date", __FILE__, __LINE__);
29 
30     if(!(v.data.length == uint.sizeof))
31         throw new AnswerConvException(ConvExceptionType.SIZE_MISMATCH,
32             "Value length isn't equal to Postgres date type", __FILE__, __LINE__);
33 
34     int jd = bigEndianToNative!uint(v.data.ptr[0..uint.sizeof]);
35     int year, month, day;
36     j2date(jd, year, month, day);
37 
38     return Date(year, month, day);
39 }
40 
41 /// Returns value time without time zone as native TimeOfDay
42 TimeOfDay binaryValueAs(T)(in Value v) @trusted
43 if( is( T == TimeOfDay ) )
44 {
45     if(!(v.oidType == OidType.Time))
46         throwTypeComplaint(v.oidType, "time without time zone", __FILE__, __LINE__);
47 
48     if(!(v.data.length == TimeADT.sizeof))
49         throw new AnswerConvException(ConvExceptionType.SIZE_MISMATCH,
50             "Value length isn't equal to Postgres time without time zone type", __FILE__, __LINE__);
51 
52     return time2tm(bigEndianToNative!TimeADT(v.data.ptr[0..TimeADT.sizeof]));
53 }
54 
55 /// Returns value timestamp without time zone as TimeStampWithoutTZ
56 TimeStampWithoutTZ binaryValueAs(T)(in Value v) @trusted
57 if( is( T == TimeStampWithoutTZ ) )
58 {
59     if(!(v.oidType == OidType.TimeStamp))
60         throwTypeComplaint(v.oidType, "timestamp without time zone", __FILE__, __LINE__);
61 
62     if(!(v.data.length == long.sizeof))
63         throw new AnswerConvException(ConvExceptionType.SIZE_MISMATCH,
64             "Value length isn't equal to Postgres timestamp without time zone type", __FILE__, __LINE__);
65 
66     return rawTimeStamp2nativeTime(
67         bigEndianToNative!long(v.data.ptr[0..long.sizeof])
68     );
69 }
70 
71 struct TimeStampWithoutTZ
72 {
73     DateTime dateTime;
74     Duration fracSec; /// fractional seconds
75 
76     invariant()
77     {
78         assert(fracSec >= Duration.zero);
79         assert(fracSec < 1.seconds);
80     }
81 
82     static max()
83     {
84         return TimeStampWithoutTZ(DateTime.max, long.max.hnsecs);
85     }
86 
87     static min()
88     {
89         return TimeStampWithoutTZ(DateTime.min, Duration.zero);
90     }
91 }
92 
93 private:
94 
95 TimeStampWithoutTZ rawTimeStamp2nativeTime(long raw)
96 {
97     version(Have_Int64_TimeStamp)
98     {
99         if(raw >= time_t.max) return TimeStampWithoutTZ.max;
100         if(raw <= time_t.min) return TimeStampWithoutTZ.min;
101     }
102 
103     pg_tm tm;
104     fsec_t ts;
105 
106     if(timestamp2tm(raw, tm, ts) < 0)
107         throw new AnswerException(
108             ExceptionType.OUT_OF_RANGE, "Timestamp is out of range",
109             __FILE__, __LINE__
110         );
111 
112     return raw_pg_tm2nativeTime(tm, ts);
113 }
114 
115 TimeStampWithoutTZ raw_pg_tm2nativeTime(pg_tm tm, fsec_t ts)
116 {
117     TimeStampWithoutTZ res;
118 
119     res.dateTime = DateTime(
120             tm.tm_year,
121             tm.tm_mon,
122             tm.tm_mday,
123             tm.tm_hour,
124             tm.tm_min,
125             tm.tm_sec
126         );
127 
128     version(Have_Int64_TimeStamp)
129     {
130         res.fracSec = dur!"usecs"(ts);
131     }
132     else
133     {
134         res.fracSec = dur!"usecs"((cast(long)(ts * 10e6)));
135     }
136 
137     return res;
138 }
139 
140 // Here is used names from the original Postgresql source
141 
142 void j2date(int jd, out int year, out int month, out int day)
143 {
144     enum POSTGRES_EPOCH_JDATE = 2451545;
145     enum MONTHS_PER_YEAR = 12;
146 
147     jd += POSTGRES_EPOCH_JDATE;
148 
149     uint julian = jd + 32044;
150     uint quad = julian / 146097;
151     uint extra = (julian - quad * 146097) * 4 + 3;
152     julian += 60 + quad * 3 + extra / 146097;
153     quad = julian / 1461;
154     julian -= quad * 1461;
155     int y = julian * 4 / 1461;
156     julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366))
157         + 123;
158     year = (y+ quad * 4) - 4800;
159     quad = julian * 2141 / 65536;
160     day = julian - 7834 * quad / 256;
161     month = (quad + 10) % MONTHS_PER_YEAR + 1;
162 }
163 
164 version(Have_Int64_TimeStamp)
165 {
166     private alias long Timestamp;
167     private alias long TimestampTz;
168     private alias long TimeADT;
169     private alias long TimeOffset;
170     private alias int  fsec_t;      /* fractional seconds (in microseconds) */
171     
172     void TMODULO(ref long t, ref long q, double u)
173     {
174         q = cast(long)(t / u);
175         if (q != 0) t -= q * cast(long)u;
176     }
177 }
178 else
179 {
180     private alias Timestamp = double;
181     private alias TimestampTz = double;
182     private alias TimeADT = double;
183     private alias TimeOffset = double;
184     private alias fsec_t = double;    /* fractional seconds (in seconds) */
185     
186     void TMODULO(T)(ref double t, ref T q, double u)
187         if(is(T == double) || is(T == int))
188     {
189         q = cast(T)((t < 0) ? ceil(t / u) : floor(t / u));
190         if (q != 0) t -= rint(q * u);
191     }
192     
193     double TIMEROUND(double j) 
194     {
195         enum TIME_PREC_INV = 10000000000.0;
196         return rint((cast(double) j) * TIME_PREC_INV) / TIME_PREC_INV;
197     }
198 }
199 
200 TimeOfDay time2tm(TimeADT time)
201 {
202     version(Have_Int64_TimeStamp)
203     {
204         immutable long USECS_PER_HOUR  = 3600000000;
205         immutable long USECS_PER_MINUTE = 60000000;
206         immutable long USECS_PER_SEC = 1000000;
207 
208         int tm_hour = cast(int)(time / USECS_PER_HOUR);
209         time -= tm_hour * USECS_PER_HOUR;
210         int tm_min = cast(int)(time / USECS_PER_MINUTE);
211         time -= tm_min * USECS_PER_MINUTE;
212         int tm_sec = cast(int)(time / USECS_PER_SEC);
213         time -= tm_sec * USECS_PER_SEC;
214 
215         return TimeOfDay(tm_hour, tm_min, tm_sec);
216     }
217     else
218     {
219         enum SECS_PER_HOUR = 3600;
220         enum SECS_PER_MINUTE = 60;
221 
222         double      trem;
223         int tm_hour, tm_min, tm_sec;
224     recalc:
225         trem = time;
226         TMODULO(trem, tm_hour, cast(double) SECS_PER_HOUR);
227         TMODULO(trem, tm_min, cast(double) SECS_PER_MINUTE);
228         TMODULO(trem, tm_sec, 1.0);
229         trem = TIMEROUND(trem);
230         /* roundoff may need to propagate to higher-order fields */
231         if (trem >= 1.0)
232         {
233             time = ceil(time);
234             goto recalc;
235         }
236 
237         return TimeOfDay(tm_hour, tm_min, tm_sec);
238     }
239 }
240 
241 struct pg_tm
242 {
243     int         tm_sec;
244     int         tm_min;
245     int         tm_hour;
246     int         tm_mday;
247     int         tm_mon;         /* origin 0, not 1 */
248     int         tm_year;        /* relative to 1900 */
249     int         tm_wday;
250     int         tm_yday;
251     int         tm_isdst;
252     long        tm_gmtoff;
253     string      tm_zone;
254 }
255 
256 alias pg_time_t = long;
257 
258 immutable ulong SECS_PER_DAY = 86400;
259 immutable ulong POSTGRES_EPOCH_JDATE = 2451545;
260 immutable ulong UNIX_EPOCH_JDATE     = 2440588;
261 
262 immutable ulong USECS_PER_DAY    = 86_400_000_000;
263 immutable ulong USECS_PER_HOUR   = 3_600_000_000;
264 immutable ulong USECS_PER_MINUTE = 60_000_000;
265 immutable ulong USECS_PER_SEC    = 1_000_000;
266 
267 immutable ulong SECS_PER_HOUR   = 3600;
268 immutable ulong SECS_PER_MINUTE = 60;
269 
270 /**
271 * timestamp2tm() - Convert timestamp data type to POSIX time structure.
272 *
273 * Note that year is _not_ 1900-based, but is an explicit full value.
274 * Also, month is one-based, _not_ zero-based.
275 * Returns:
276 *   0 on success
277 *  -1 on out of range
278 *
279 * If attimezone is null, the global timezone (including possibly brute forced
280 * timezone) will be used.
281 */
282 int timestamp2tm(Timestamp dt, out pg_tm tm, out fsec_t fsec)
283 {
284     Timestamp   date;
285     Timestamp   time;
286     pg_time_t   utime;
287 
288     version(Have_Int64_TimeStamp)
289     {
290         time = dt;
291         TMODULO(time, date, USECS_PER_DAY);
292 
293         if (time < 0)
294         {
295             time += USECS_PER_DAY;
296             date -= 1;
297         }
298 
299         j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday);
300         dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec);
301     } else
302     {
303         time = dt;
304         TMODULO(time, date, cast(double) SECS_PER_DAY);
305 
306         if (time < 0)
307         {
308             time += SECS_PER_DAY;
309             date -= 1;
310         }
311 
312     recalc_d:
313         j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday);
314     recalc_t:
315         dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec);
316 
317         fsec = TSROUND(fsec);
318         /* roundoff may need to propagate to higher-order fields */
319         if (fsec >= 1.0)
320         {
321             time = cast(Timestamp)ceil(time);
322             if (time >= cast(double) SECS_PER_DAY)
323             {
324                 time = 0;
325                 date += 1;
326                 goto recalc_d;
327             }
328             goto recalc_t;
329         }
330     }
331 
332     return 0;
333 }
334 
335 void dt2time(Timestamp jd, out int hour, out int min, out int sec, out fsec_t fsec)
336 {
337     TimeOffset  time;
338 
339     time = jd;
340     version(Have_Int64_TimeStamp)
341     {
342         hour = cast(int)(time / USECS_PER_HOUR);
343         time -= hour * USECS_PER_HOUR;
344         min = cast(int)(time / USECS_PER_MINUTE);
345         time -= min * USECS_PER_MINUTE;
346         sec = cast(int)(time / USECS_PER_SEC);
347         fsec = cast(int)(time - sec*USECS_PER_SEC);
348     } else
349     {
350         hour = cast(int)(time / SECS_PER_HOUR);
351         time -= hour * SECS_PER_HOUR;
352         min = cast(int)(time / SECS_PER_MINUTE);
353         time -= min * SECS_PER_MINUTE;
354         sec = cast(int)time;
355         fsec = cast(int)(time - sec);
356     }
357 }