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