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