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 static assert(raw.sizeof == time_t.min.sizeof); 339 static assert(raw.sizeof == time_t.max.sizeof); 340 341 if(raw == time_t.max) return T.later; // infinity 342 if(raw == time_t.min) return T.earlier; // -infinity 343 344 pg_tm tm; 345 fsec_t ts; 346 347 if(timestamp2tm(raw, tm, ts) < 0) 348 throw new AnswerException( 349 ExceptionType.OUT_OF_RANGE, "Timestamp is out of range", 350 __FILE__, __LINE__ 351 ); 352 353 TimeStamp ret = raw_pg_tm2nativeTime(tm, ts); 354 355 static if(is(T == TimeStamp)) 356 return ret; 357 else 358 return TimeStampUTC(ret.dateTime, ret.fracSec); 359 } 360 361 TimeStamp raw_pg_tm2nativeTime(pg_tm tm, fsec_t ts) 362 { 363 return TimeStamp( 364 PgDate( 365 tm.tm_year, 366 cast(ubyte) tm.tm_mon, 367 cast(ubyte) tm.tm_mday 368 ), 369 TimeOfDay( 370 tm.tm_hour, 371 tm.tm_min, 372 tm.tm_sec 373 ), 374 ts.dur!"usecs" 375 ); 376 } 377 378 // Here is used names from the original Postgresql source 379 380 void j2date(int jd, out int year, out int month, out int day) 381 { 382 enum MONTHS_PER_YEAR = 12; 383 384 jd += POSTGRES_EPOCH_JDATE; 385 386 uint julian = jd + 32044; 387 uint quad = julian / 146097; 388 uint extra = (julian - quad * 146097) * 4 + 3; 389 julian += 60 + quad * 3 + extra / 146097; 390 quad = julian / 1461; 391 julian -= quad * 1461; 392 int y = julian * 4 / 1461; 393 julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366)) 394 + 123; 395 year = (y+ quad * 4) - 4800; 396 quad = julian * 2141 / 65536; 397 day = julian - 7834 * quad / 256; 398 month = (quad + 10) % MONTHS_PER_YEAR + 1; 399 } 400 401 private alias long Timestamp; 402 private alias long TimestampTz; 403 private alias long TimeADT; 404 private alias long TimeOffset; 405 private alias int fsec_t; /* fractional seconds (in microseconds) */ 406 407 void TMODULO(ref long t, ref long q, double u) 408 { 409 q = cast(long)(t / u); 410 if (q != 0) t -= q * cast(long)u; 411 } 412 413 TimeOfDay time2tm(TimeADT time) 414 { 415 immutable long USECS_PER_HOUR = 3600000000; 416 immutable long USECS_PER_MINUTE = 60000000; 417 immutable long USECS_PER_SEC = 1000000; 418 419 int tm_hour = cast(int)(time / USECS_PER_HOUR); 420 time -= tm_hour * USECS_PER_HOUR; 421 int tm_min = cast(int)(time / USECS_PER_MINUTE); 422 time -= tm_min * USECS_PER_MINUTE; 423 int tm_sec = cast(int)(time / USECS_PER_SEC); 424 time -= tm_sec * USECS_PER_SEC; 425 426 return TimeOfDay(tm_hour, tm_min, tm_sec); 427 } 428 429 struct pg_tm 430 { 431 int tm_sec; 432 int tm_min; 433 int tm_hour; 434 int tm_mday; 435 int tm_mon; /* origin 0, not 1 */ 436 int tm_year; /* relative to 1900 */ 437 int tm_wday; 438 int tm_yday; 439 int tm_isdst; 440 long tm_gmtoff; 441 string tm_zone; 442 } 443 444 alias pg_time_t = long; 445 446 enum USECS_PER_DAY = 86_400_000_000UL; 447 enum USECS_PER_HOUR = 3_600_000_000UL; 448 enum USECS_PER_MINUTE = 60_000_000UL; 449 enum USECS_PER_SEC = 1_000_000UL; 450 451 /** 452 * timestamp2tm() - Convert timestamp data type to POSIX time structure. 453 * 454 * Note that year is _not_ 1900-based, but is an explicit full value. 455 * Also, month is one-based, _not_ zero-based. 456 * Returns: 457 * 0 on success 458 * -1 on out of range 459 * 460 * If attimezone is null, the global timezone (including possibly brute forced 461 * timezone) will be used. 462 */ 463 int timestamp2tm(Timestamp dt, out pg_tm tm, out fsec_t fsec) 464 { 465 Timestamp date; 466 Timestamp time; 467 pg_time_t utime; 468 469 time = dt; 470 TMODULO(time, date, USECS_PER_DAY); 471 472 if (time < 0) 473 { 474 time += USECS_PER_DAY; 475 date -= 1; 476 } 477 478 j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday); 479 dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec); 480 481 return 0; 482 } 483 484 void dt2time(Timestamp jd, out int hour, out int min, out int sec, out fsec_t fsec) 485 { 486 TimeOffset time; 487 488 time = jd; 489 hour = cast(int)(time / USECS_PER_HOUR); 490 time -= hour * USECS_PER_HOUR; 491 min = cast(int)(time / USECS_PER_MINUTE); 492 time -= min * USECS_PER_MINUTE; 493 sec = cast(int)(time / USECS_PER_SEC); 494 fsec = cast(int)(time - sec*USECS_PER_SEC); 495 }