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.value: throwTypeComplaint; 14 15 import core.time; 16 import std.datetime.date : Date, DateTime, TimeOfDay; 17 import std.datetime.systime: SysTime; 18 import std.datetime.timezone: LocalTime, TimeZone, UTC; 19 import std.bitmanip: bigEndianToNative, nativeToBigEndian; 20 import std.math; 21 import std.conv: to; 22 23 /++ 24 Returns value timestamp with time zone as SysTime 25 26 Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs. 27 It means that PG value will have 10 times lower precision. 28 And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime. 29 30 Because of these differences, it can happen that database value will not fit to the SysTime range of values. 31 +/ 32 SysTime binaryValueAs(T)(in Value v) @trusted 33 if( is( T == SysTime ) ) 34 { 35 if(!(v.oidType == OidType.TimeStampWithZone)) 36 throwTypeComplaint(v.oidType, "timestamp with time zone", __FILE__, __LINE__); 37 38 if(!(v.data.length == long.sizeof)) 39 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 40 "Value length isn't equal to Postgres timestamp with time zone type", __FILE__, __LINE__); 41 42 auto t = rawTimeStamp2nativeTime!TimeStampUTC(bigEndianToNative!long(v.data.ptr[0..long.sizeof])); 43 return SysTime(t.dateTime, t.fracSec, UTC()); 44 } 45 46 pure: 47 48 /// Returns value data as native Date 49 Date binaryValueAs(T)(in Value v) @trusted 50 if( is( T == Date ) ) 51 { 52 if(!(v.oidType == OidType.Date)) 53 throwTypeComplaint(v.oidType, "Date", __FILE__, __LINE__); 54 55 if(!(v.data.length == uint.sizeof)) 56 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 57 "Value length isn't equal to Postgres date type", __FILE__, __LINE__); 58 59 int jd = bigEndianToNative!uint(v.data.ptr[0..uint.sizeof]); 60 int year, month, day; 61 j2date(jd, year, month, day); 62 63 // TODO: support PG Date like TTimeStamp manner and remove this check 64 if(year > short.max) 65 throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW, 66 "Year "~year.to!string~" is bigger than supported by std.datetime.Date", __FILE__, __LINE__); 67 68 return Date(year, month, day); 69 } 70 71 /// Returns value time without time zone as native TimeOfDay 72 TimeOfDay binaryValueAs(T)(in Value v) @trusted 73 if( is( T == TimeOfDay ) ) 74 { 75 if(!(v.oidType == OidType.Time)) 76 throwTypeComplaint(v.oidType, "time without time zone", __FILE__, __LINE__); 77 78 if(!(v.data.length == TimeADT.sizeof)) 79 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 80 "Value length isn't equal to Postgres time without time zone type", __FILE__, __LINE__); 81 82 return time2tm(bigEndianToNative!TimeADT(v.data.ptr[0..TimeADT.sizeof])); 83 } 84 85 /// Returns value timestamp without time zone as TimeStamp 86 TimeStamp binaryValueAs(T)(in Value v) @trusted 87 if( is( T == TimeStamp ) ) 88 { 89 if(!(v.oidType == OidType.TimeStamp)) 90 throwTypeComplaint(v.oidType, "timestamp without time zone", __FILE__, __LINE__); 91 92 if(!(v.data.length == long.sizeof)) 93 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 94 "Value length isn't equal to Postgres timestamp without time zone type", __FILE__, __LINE__); 95 96 return rawTimeStamp2nativeTime!TimeStamp( 97 bigEndianToNative!long(v.data.ptr[0..long.sizeof]) 98 ); 99 } 100 101 /// Returns value timestamp with time zone as TimeStampUTC 102 TimeStampUTC binaryValueAs(T)(in Value v) @trusted 103 if( is( T == TimeStampUTC ) ) 104 { 105 if(!(v.oidType == OidType.TimeStampWithZone)) 106 throwTypeComplaint(v.oidType, "timestamp with time zone", __FILE__, __LINE__); 107 108 if(!(v.data.length == long.sizeof)) 109 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 110 "Value length isn't equal to Postgres timestamp with time zone type", __FILE__, __LINE__); 111 112 return rawTimeStamp2nativeTime!TimeStampUTC( 113 bigEndianToNative!long(v.data.ptr[0..long.sizeof]) 114 ); 115 } 116 117 /// Returns value timestamp without time zone as DateTime (it drops the fracSecs from the database value) 118 DateTime binaryValueAs(T)(in Value v) @trusted 119 if( is( T == DateTime ) ) 120 { 121 return v.binaryValueAs!TimeStamp.dateTime; 122 } 123 124 /// 125 enum InfinityState : byte 126 { 127 NONE = 0, /// 128 INFINITY_MIN = -1, /// 129 INFINITY_MAX = 1, /// 130 } 131 132 /// 133 struct PgDate 134 { 135 int year; /// 136 ubyte month; /// 137 ubyte day; /// 138 139 /// '-infinity', earlier than all other dates 140 static PgDate earlier() pure { return PgDate(int.min, 0, 0); } 141 142 /// 'infinity', later than all other dates 143 static PgDate later() pure { return PgDate(int.max, 0, 0); } 144 145 bool isEarlier() const pure { return year == earlier.year; } /// '-infinity' 146 bool isLater() const pure { return year == later.year; } /// 'infinity' 147 } 148 149 /// 150 static toPgDate(Date d) pure 151 { 152 return PgDate(d.year, d.month, d.day); 153 } 154 155 /++ 156 Structure to represent PostgreSQL Timestamp with/without time zone 157 +/ 158 struct TTimeStamp(bool isWithTZ) 159 { 160 /** 161 * Date and time of TimeStamp 162 * 163 * If value is '-infinity' or '+infinity' it will be equal PgDate.min or PgDate.max 164 */ 165 PgDate date; 166 TimeOfDay time; /// 167 Duration fracSec; /// fractional seconds, 1 microsecond resolution 168 169 /// 170 this(DateTime dt, Duration fractionalSeconds = Duration.zero) pure 171 { 172 this(dt.date.toPgDate, dt.timeOfDay, fractionalSeconds); 173 } 174 175 /// 176 this(PgDate d, TimeOfDay t = TimeOfDay(), Duration fractionalSeconds = Duration.zero) pure 177 { 178 date = d; 179 time = t; 180 fracSec = fractionalSeconds; 181 } 182 183 /// 184 void throwIfNotFitsToDate() const 185 { 186 if(date.year > short.max) 187 throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW, 188 "Year "~date.year.to!string~" is bigger than supported by std.datetime", __FILE__, __LINE__); 189 } 190 191 /// 192 DateTime dateTime() const pure 193 { 194 if(infinity != InfinityState.NONE) 195 throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW, 196 "TTimeStamp value is "~infinity.to!string, __FILE__, __LINE__); 197 198 throwIfNotFitsToDate(); 199 200 return DateTime(Date(date.year, date.month, date.day), time); 201 } 202 203 invariant() 204 { 205 assert(fracSec < 1.seconds, "fracSec can't be more than 1 second but contains "~fracSec.to!string); 206 assert(fracSec >= Duration.zero, "fracSec is negative: "~fracSec.to!string); 207 assert(fracSec % 1.usecs == 0.hnsecs, "fracSec have 1 microsecond resolution but contains "~fracSec.to!string); 208 } 209 210 bool isEarlier() const pure { return date.isEarlier; } /// '-infinity' 211 bool isLater() const pure { return date.isLater; } /// 'infinity' 212 213 /// Returns infinity state 214 InfinityState infinity() const pure 215 { 216 with(InfinityState) 217 { 218 if(isEarlier) return INFINITY_MIN; 219 if(isLater) return INFINITY_MAX; 220 221 return NONE; 222 } 223 } 224 225 unittest 226 { 227 assert(TTimeStamp.min == TTimeStamp.min); 228 assert(TTimeStamp.max == TTimeStamp.max); 229 assert(TTimeStamp.min != TTimeStamp.max); 230 231 assert(TTimeStamp.earlier != TTimeStamp.later); 232 assert(TTimeStamp.min != TTimeStamp.earlier); 233 assert(TTimeStamp.max != TTimeStamp.later); 234 235 assert(TTimeStamp.min.infinity == InfinityState.NONE); 236 assert(TTimeStamp.max.infinity == InfinityState.NONE); 237 assert(TTimeStamp.earlier.infinity == InfinityState.INFINITY_MIN); 238 assert(TTimeStamp.later.infinity == InfinityState.INFINITY_MAX); 239 } 240 241 /// Returns the TimeStamp farthest in the past which is representable by TimeStamp. 242 static immutable(TTimeStamp) min() 243 { 244 /* 245 Postgres low value is 4713 BC but here is used -4712 because 246 "Date uses the Proleptic Gregorian Calendar, so it assumes the 247 Gregorian leap year calculations for its entire length. As per 248 ISO 8601, it treats 1 B.C. as year 0, i.e. 1 B.C. is 0, 2 B.C. 249 is -1, etc." (Phobos docs). But Postgres isn't uses ISO 8601 250 for date calculation. 251 */ 252 return TTimeStamp(PgDate(-4712, 1, 1), TimeOfDay.min, Duration.zero); 253 } 254 255 /// Returns the TimeStamp farthest in the future which is representable by TimeStamp. 256 static immutable(TTimeStamp) max() 257 { 258 enum maxFract = 1.seconds - 1.usecs; 259 260 return TTimeStamp(PgDate(294276, 12, 31), TimeOfDay(23, 59, 59), maxFract); 261 } 262 263 /// '-infinity', earlier than all other time stamps 264 static immutable(TTimeStamp) earlier() pure { return TTimeStamp(PgDate.earlier); } 265 266 /// 'infinity', later than all other time stamps 267 static immutable(TTimeStamp) later() pure { return TTimeStamp(PgDate.later); } 268 269 /// 270 string toString() const 271 { 272 import std.format; 273 274 return format("%04d-%02d-%02d %s %s", date.year, date.month, date.day, time, fracSec.toString); 275 } 276 } 277 278 alias TimeStamp = TTimeStamp!false; /// Unknown TZ timestamp 279 alias TimeStampUTC = TTimeStamp!true; /// Assumed that this is UTC timestamp 280 281 unittest 282 { 283 auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs); 284 assert(t.dateTime.hour == 14); 285 } 286 287 unittest 288 { 289 auto dt = DateTime(2017, 11, 13, 14, 29, 17); 290 auto t = TimeStamp(dt, 75_678.usecs); 291 292 assert(t.dateTime == dt); // test the implicit conversion to DateTime 293 } 294 295 unittest 296 { 297 auto t = TimeStampUTC( 298 DateTime(2017, 11, 13, 14, 29, 17), 299 75_678.usecs 300 ); 301 302 assert(t.dateTime.hour == 14); 303 assert(t.fracSec == 75_678.usecs); 304 } 305 306 unittest 307 { 308 import std.exception : assertThrown; 309 310 auto e = TimeStampUTC.earlier; 311 auto l = TimeStampUTC.later; 312 313 assertThrown!ValueConvException(e.dateTime.hour == 14); 314 assertThrown!ValueConvException(l.dateTime.hour == 14); 315 } 316 317 /// Oid tests 318 unittest 319 { 320 assert(detectOidTypeFromNative!TimeStamp == OidType.TimeStamp); 321 assert(detectOidTypeFromNative!TimeStampUTC == OidType.TimeStampWithZone); 322 assert(detectOidTypeFromNative!SysTime == OidType.TimeStampWithZone); 323 assert(detectOidTypeFromNative!Date == OidType.Date); 324 assert(detectOidTypeFromNative!TimeOfDay == OidType.Time); 325 } 326 327 package enum POSTGRES_EPOCH_DATE = Date(2000, 1, 1); 328 package enum POSTGRES_EPOCH_JDATE = POSTGRES_EPOCH_DATE.julianDay; 329 static assert(POSTGRES_EPOCH_JDATE == 2_451_545); // value from Postgres code 330 331 private: 332 333 T rawTimeStamp2nativeTime(T)(long raw) 334 if(is(T == TimeStamp) || is(T == TimeStampUTC)) 335 { 336 import core.stdc.time: time_t; 337 338 if(raw == long.max) return T.later; // infinity 339 if(raw == long.min) return T.earlier; // -infinity 340 341 pg_tm tm; 342 fsec_t ts; 343 344 if(timestamp2tm(raw, tm, ts) < 0) 345 throw new AnswerException( 346 ExceptionType.OUT_OF_RANGE, "Timestamp is out of range", 347 __FILE__, __LINE__ 348 ); 349 350 TimeStamp ret = raw_pg_tm2nativeTime(tm, ts); 351 352 static if(is(T == TimeStamp)) 353 return ret; 354 else 355 return TimeStampUTC(ret.dateTime, ret.fracSec); 356 } 357 358 TimeStamp raw_pg_tm2nativeTime(pg_tm tm, fsec_t ts) 359 { 360 return TimeStamp( 361 PgDate( 362 tm.tm_year, 363 cast(ubyte) tm.tm_mon, 364 cast(ubyte) tm.tm_mday 365 ), 366 TimeOfDay( 367 tm.tm_hour, 368 tm.tm_min, 369 tm.tm_sec 370 ), 371 ts.dur!"usecs" 372 ); 373 } 374 375 // Here is used names from the original Postgresql source 376 377 void j2date(int jd, out int year, out int month, out int day) 378 { 379 enum MONTHS_PER_YEAR = 12; 380 381 jd += POSTGRES_EPOCH_JDATE; 382 383 uint julian = jd + 32044; 384 uint quad = julian / 146097; 385 uint extra = (julian - quad * 146097) * 4 + 3; 386 julian += 60 + quad * 3 + extra / 146097; 387 quad = julian / 1461; 388 julian -= quad * 1461; 389 int y = julian * 4 / 1461; 390 julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366)) 391 + 123; 392 year = (y+ quad * 4) - 4800; 393 quad = julian * 2141 / 65536; 394 day = julian - 7834 * quad / 256; 395 month = (quad + 10) % MONTHS_PER_YEAR + 1; 396 } 397 398 private alias long Timestamp; 399 private alias long TimestampTz; 400 private alias long TimeADT; 401 private alias long TimeOffset; 402 private alias int fsec_t; /* fractional seconds (in microseconds) */ 403 404 void TMODULO(ref long t, ref long q, double u) 405 { 406 q = cast(long)(t / u); 407 if (q != 0) t -= q * cast(long)u; 408 } 409 410 TimeOfDay time2tm(TimeADT time) 411 { 412 immutable long USECS_PER_HOUR = 3600000000; 413 immutable long USECS_PER_MINUTE = 60000000; 414 immutable long USECS_PER_SEC = 1000000; 415 416 int tm_hour = cast(int)(time / USECS_PER_HOUR); 417 time -= tm_hour * USECS_PER_HOUR; 418 int tm_min = cast(int)(time / USECS_PER_MINUTE); 419 time -= tm_min * USECS_PER_MINUTE; 420 int tm_sec = cast(int)(time / USECS_PER_SEC); 421 time -= tm_sec * USECS_PER_SEC; 422 423 return TimeOfDay(tm_hour, tm_min, tm_sec); 424 } 425 426 struct pg_tm 427 { 428 int tm_sec; 429 int tm_min; 430 int tm_hour; 431 int tm_mday; 432 int tm_mon; /* origin 0, not 1 */ 433 int tm_year; /* relative to 1900 */ 434 int tm_wday; 435 int tm_yday; 436 int tm_isdst; 437 long tm_gmtoff; 438 string tm_zone; 439 } 440 441 alias pg_time_t = long; 442 443 enum USECS_PER_DAY = 86_400_000_000UL; 444 enum USECS_PER_HOUR = 3_600_000_000UL; 445 enum USECS_PER_MINUTE = 60_000_000UL; 446 enum USECS_PER_SEC = 1_000_000UL; 447 448 /** 449 * timestamp2tm() - Convert timestamp data type to POSIX time structure. 450 * 451 * Note that year is _not_ 1900-based, but is an explicit full value. 452 * Also, month is one-based, _not_ zero-based. 453 * Returns: 454 * 0 on success 455 * -1 on out of range 456 * 457 * If attimezone is null, the global timezone (including possibly brute forced 458 * timezone) will be used. 459 */ 460 int timestamp2tm(Timestamp dt, out pg_tm tm, out fsec_t fsec) 461 { 462 Timestamp date; 463 Timestamp time; 464 pg_time_t utime; 465 466 time = dt; 467 TMODULO(time, date, USECS_PER_DAY); 468 469 if (time < 0) 470 { 471 time += USECS_PER_DAY; 472 date -= 1; 473 } 474 475 j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday); 476 dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec); 477 478 return 0; 479 } 480 481 void dt2time(Timestamp jd, out int hour, out int min, out int sec, out fsec_t fsec) 482 { 483 TimeOffset time; 484 485 time = jd; 486 hour = cast(int)(time / USECS_PER_HOUR); 487 time -= hour * USECS_PER_HOUR; 488 min = cast(int)(time / USECS_PER_MINUTE); 489 time -= min * USECS_PER_MINUTE; 490 sec = cast(int)(time / USECS_PER_SEC); 491 fsec = cast(int)(time - sec*USECS_PER_SEC); 492 }