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 }