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