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