1 // SDLang-D 2 // Written in the D programming language. 3 4 module sdlang.token; 5 6 import std.array; 7 import std.base64; 8 import std.conv; 9 import std.datetime; 10 import std.meta; 11 import std.range; 12 import std.string; 13 import std.traits; 14 import std.typetuple; 15 import std.variant; 16 17 import sdlang.exception; 18 import sdlang.symbol; 19 import sdlang.util; 20 21 /// DateTime doesn't support milliseconds, but SDLang's "Date Time" type does. 22 /// So this is needed for any SDL "Date Time" that doesn't include a time zone. 23 struct DateTimeFrac 24 { 25 DateTime dateTime; 26 Duration fracSecs; 27 deprecated("Use fracSecs instead.") { 28 @property FracSec fracSec() const { return FracSec.from!"hnsecs"(fracSecs.total!"hnsecs"); } 29 @property void fracSec(FracSec v) { fracSecs = v.hnsecs.hnsecs; } 30 } 31 } 32 33 /++ 34 If a "Date Time" literal in the SDL file has a time zone that's not found in 35 your system, you get one of these instead of a SysTime. (Because it's 36 impossible to indicate "unknown time zone" with `std.datetime.TimeZone`.) 37 38 The difference between this and `DateTimeFrac` is that `DateTimeFrac` 39 indicates that no time zone was specified in the SDL at all, whereas 40 `DateTimeFracUnknownZone` indicates that a time zone was specified but 41 data for it could not be found on your system. 42 +/ 43 struct DateTimeFracUnknownZone 44 { 45 DateTime dateTime; 46 Duration fracSecs; 47 deprecated("Use fracSecs instead.") { 48 @property FracSec fracSec() const { return FracSec.from!"hnsecs"(fracSecs.total!"hnsecs"); } 49 @property void fracSec(FracSec v) { fracSecs = v.hnsecs.hnsecs; } 50 } 51 string timeZone; 52 53 bool opEquals(const DateTimeFracUnknownZone b) const 54 { 55 return opEquals(b); 56 } 57 bool opEquals(ref const DateTimeFracUnknownZone b) const 58 { 59 return 60 this.dateTime == b.dateTime && 61 this.fracSecs == b.fracSecs && 62 this.timeZone == b.timeZone; 63 } 64 } 65 66 /++ 67 SDLang's datatypes map to D's datatypes as described below. 68 Most are straightforward, but take special note of the date/time-related types. 69 70 --------------------------------------------------------------- 71 Boolean: bool 72 Null: typeof(null) 73 Unicode Character: dchar 74 Double-Quote Unicode String: string 75 Raw Backtick Unicode String: string 76 Integer (32 bits signed): int 77 Long Integer (64 bits signed): long 78 Float (32 bits signed): float 79 Double Float (64 bits signed): double 80 Decimal (128+ bits signed): real 81 Binary (standard Base64): ubyte[] 82 Time Span: Duration 83 84 Date (with no time at all): Date 85 Date Time (no timezone): DateTimeFrac 86 Date Time (with a known timezone): SysTime 87 Date Time (with an unknown timezone): DateTimeFracUnknownZone 88 --------------------------------------------------------------- 89 +/ 90 alias ValueTypes = TypeTuple!( 91 bool, 92 string, dchar, 93 int, long, 94 float, double, real, 95 Date, DateTimeFrac, SysTime, DateTimeFracUnknownZone, Duration, 96 ubyte[], 97 typeof(null), 98 ); 99 100 alias Value = Algebraic!( ValueTypes ); ///ditto 101 enum isValueType(T) = staticIndexOf!(T, ValueTypes) != -1; 102 103 enum isSink(T) = 104 isOutputRange!T && 105 is(ElementType!(T)[] == string); 106 107 string toSDLString(T)(T value) if(is(T==Value) || isValueType!T) 108 { 109 Appender!string sink; 110 toSDLString(value, sink); 111 return sink.data; 112 } 113 114 /// Throws SDLangException if value is infinity, -infinity or NaN, because 115 /// those are not currently supported by the SDLang spec. 116 void toSDLString(Sink)(Value value, ref Sink sink) if(isOutputRange!(Sink,char)) 117 { 118 foreach(T; ValueTypes) 119 { 120 if(value.type == typeid(T)) 121 { 122 toSDLString( value.get!T(), sink ); 123 return; 124 } 125 } 126 127 throw new Exception("Internal SDLang-D error: Unhandled type of Value. Contains: "~value.toString()); 128 } 129 130 @("toSDLString on infinity and NaN") 131 unittest 132 { 133 import std.exception; 134 135 auto floatInf = float.infinity; 136 auto floatNegInf = -float.infinity; 137 auto floatNaN = float.nan; 138 139 auto doubleInf = double.infinity; 140 auto doubleNegInf = -double.infinity; 141 auto doubleNaN = double.nan; 142 143 auto realInf = real.infinity; 144 auto realNegInf = -real.infinity; 145 auto realNaN = real.nan; 146 147 assertNotThrown( toSDLString(0.0F) ); 148 assertNotThrown( toSDLString(0.0) ); 149 assertNotThrown( toSDLString(0.0L) ); 150 151 assertThrown!ValidationException( toSDLString(floatInf) ); 152 assertThrown!ValidationException( toSDLString(floatNegInf) ); 153 assertThrown!ValidationException( toSDLString(floatNaN) ); 154 155 assertThrown!ValidationException( toSDLString(doubleInf) ); 156 assertThrown!ValidationException( toSDLString(doubleNegInf) ); 157 assertThrown!ValidationException( toSDLString(doubleNaN) ); 158 159 assertThrown!ValidationException( toSDLString(realInf) ); 160 assertThrown!ValidationException( toSDLString(realNegInf) ); 161 assertThrown!ValidationException( toSDLString(realNaN) ); 162 163 assertThrown!ValidationException( toSDLString(Value(floatInf)) ); 164 assertThrown!ValidationException( toSDLString(Value(floatNegInf)) ); 165 assertThrown!ValidationException( toSDLString(Value(floatNaN)) ); 166 167 assertThrown!ValidationException( toSDLString(Value(doubleInf)) ); 168 assertThrown!ValidationException( toSDLString(Value(doubleNegInf)) ); 169 assertThrown!ValidationException( toSDLString(Value(doubleNaN)) ); 170 171 assertThrown!ValidationException( toSDLString(Value(realInf)) ); 172 assertThrown!ValidationException( toSDLString(Value(realNegInf)) ); 173 assertThrown!ValidationException( toSDLString(Value(realNaN)) ); 174 } 175 176 void toSDLString(Sink)(typeof(null) value, ref Sink sink) if(isOutputRange!(Sink,char)) 177 { 178 sink.put("null"); 179 } 180 181 void toSDLString(Sink)(bool value, ref Sink sink) if(isOutputRange!(Sink,char)) 182 { 183 sink.put(value? "true" : "false"); 184 } 185 186 //TODO: Figure out how to properly handle strings/chars containing lineSep or paraSep 187 void toSDLString(Sink)(string value, ref Sink sink) if(isOutputRange!(Sink,char)) 188 { 189 sink.put('"'); 190 191 // This loop is UTF-safe 192 foreach(char ch; value) 193 { 194 if (ch == '\n') sink.put(`\n`); 195 else if(ch == '\r') sink.put(`\r`); 196 else if(ch == '\t') sink.put(`\t`); 197 else if(ch == '\"') sink.put(`\"`); 198 else if(ch == '\\') sink.put(`\\`); 199 else 200 sink.put(ch); 201 } 202 203 sink.put('"'); 204 } 205 206 void toSDLString(Sink)(dchar value, ref Sink sink) if(isOutputRange!(Sink,char)) 207 { 208 sink.put('\''); 209 210 if (value == '\n') sink.put(`\n`); 211 else if(value == '\r') sink.put(`\r`); 212 else if(value == '\t') sink.put(`\t`); 213 else if(value == '\'') sink.put(`\'`); 214 else if(value == '\\') sink.put(`\\`); 215 else 216 sink.put(value); 217 218 sink.put('\''); 219 } 220 221 void toSDLString(Sink)(int value, ref Sink sink) if(isOutputRange!(Sink,char)) 222 { 223 sink.put( "%s".format(value) ); 224 } 225 226 void toSDLString(Sink)(long value, ref Sink sink) if(isOutputRange!(Sink,char)) 227 { 228 sink.put( "%sL".format(value) ); 229 } 230 231 private void checkUnsupportedFloatingPoint(T)(T value) if(isFloatingPoint!T) 232 { 233 import std.exception; 234 import std.math; 235 236 enforce!ValidationException( 237 !isInfinity(value), 238 "SDLang does not currently support infinity for floating-point types" 239 ); 240 241 enforce!ValidationException( 242 !isNaN(value), 243 "SDLang does not currently support NaN for floating-point types" 244 ); 245 } 246 247 private string trimmedDecimal(string str) 248 { 249 Appender!string sink; 250 trimmedDecimal(str, sink); 251 return sink.data; 252 } 253 254 private void trimmedDecimal(Sink)(string str, ref Sink sink) if(isOutputRange!(Sink,char)) 255 { 256 // Special case 257 if(str == ".") 258 { 259 sink.put("0"); 260 return; 261 } 262 263 for(auto i=str.length-1; i>0; i--) 264 { 265 if(str[i] == '.') 266 { 267 // Trim up to here, PLUS trim trailing '.' 268 sink.put(str[0..i]); 269 return; 270 } 271 else if(str[i] != '0') 272 { 273 // Trim up to here 274 sink.put(str[0..i+1]); 275 return; 276 } 277 } 278 279 // Nothing to trim 280 sink.put(str); 281 } 282 283 @("trimmedDecimal") 284 unittest 285 { 286 assert(trimmedDecimal("123.456000") == "123.456"); 287 assert(trimmedDecimal("123.456") == "123.456"); 288 assert(trimmedDecimal("123.000") == "123"); 289 assert(trimmedDecimal("123.0") == "123"); 290 assert(trimmedDecimal("123.") == "123"); 291 assert(trimmedDecimal("123") == "123"); 292 assert(trimmedDecimal("1.") == "1"); 293 assert(trimmedDecimal("1") == "1"); 294 assert(trimmedDecimal("0") == "0"); 295 assert(trimmedDecimal(".") == "0"); 296 } 297 298 void toSDLString(Sink)(float value, ref Sink sink) if(isOutputRange!(Sink,char)) 299 { 300 checkUnsupportedFloatingPoint(value); 301 "%.10f".format(value).trimmedDecimal(sink); 302 sink.put("F"); 303 } 304 305 void toSDLString(Sink)(double value, ref Sink sink) if(isOutputRange!(Sink,char)) 306 { 307 checkUnsupportedFloatingPoint(value); 308 "%.30f".format(value).trimmedDecimal(sink); 309 sink.put("D"); 310 } 311 312 void toSDLString(Sink)(real value, ref Sink sink) if(isOutputRange!(Sink,char)) 313 { 314 checkUnsupportedFloatingPoint(value); 315 "%.90f".format(value).trimmedDecimal(sink); 316 sink.put("BD"); 317 } 318 319 // Regression test: Issue #50 320 @("toSDLString: No scientific notation") 321 unittest 322 { 323 import std.algorithm, sdlang.parser; 324 auto tag = parseSource(` 325 foo \ 326 420000000000000000000f \ 327 42000000000000000000000000000000000000d \ 328 420000000000000000000000000000000000000000000000000000000000000bd \ 329 `).getTag("foo"); 330 import std.stdio; 331 writeln(tag.values[0].toSDLString); 332 writeln(tag.values[1].toSDLString); 333 writeln(tag.values[2].toSDLString); 334 335 assert(!tag.values[0].toSDLString.canFind("+")); 336 assert(!tag.values[0].toSDLString.canFind("-")); 337 338 assert(!tag.values[1].toSDLString.canFind("+")); 339 assert(!tag.values[1].toSDLString.canFind("-")); 340 341 assert(!tag.values[2].toSDLString.canFind("+")); 342 assert(!tag.values[2].toSDLString.canFind("-")); 343 } 344 345 void toSDLString(Sink)(Date value, ref Sink sink) if(isOutputRange!(Sink,char)) 346 { 347 sink.put(to!string(value.year)); 348 sink.put('/'); 349 sink.put(to!string(cast(int)value.month)); 350 sink.put('/'); 351 sink.put(to!string(value.day)); 352 } 353 354 void toSDLString(Sink)(DateTimeFrac value, ref Sink sink) if(isOutputRange!(Sink,char)) 355 { 356 toSDLString(value.dateTime.date, sink); 357 sink.put(' '); 358 sink.put("%.2s".format(value.dateTime.hour)); 359 sink.put(':'); 360 sink.put("%.2s".format(value.dateTime.minute)); 361 362 if(value.dateTime.second != 0) 363 { 364 sink.put(':'); 365 sink.put("%.2s".format(value.dateTime.second)); 366 } 367 368 if(value.fracSecs != 0.msecs) 369 { 370 sink.put('.'); 371 sink.put("%.3s".format(value.fracSecs.total!"msecs")); 372 } 373 } 374 375 void toSDLString(Sink)(SysTime value, ref Sink sink) if(isOutputRange!(Sink,char)) 376 { 377 auto dateTimeFrac = DateTimeFrac(cast(DateTime)value, value.fracSecs); 378 toSDLString(dateTimeFrac, sink); 379 380 sink.put("-"); 381 382 auto tzString = value.timezone.name; 383 384 // If name didn't exist, try abbreviation. 385 // Note that according to std.datetime docs, on Windows the 386 // stdName/dstName may not be properly abbreviated. 387 version(Windows) {} else 388 if(tzString == "") 389 { 390 auto tz = value.timezone; 391 auto stdTime = value.stdTime; 392 393 if(tz.hasDST()) 394 tzString = tz.dstInEffect(stdTime)? tz.dstName : tz.stdName; 395 else 396 tzString = tz.stdName; 397 } 398 399 if(tzString == "") 400 { 401 auto offset = value.timezone.utcOffsetAt(value.stdTime); 402 sink.put("GMT"); 403 404 if(offset < seconds(0)) 405 { 406 sink.put("-"); 407 offset = -offset; 408 } 409 else 410 sink.put("+"); 411 412 sink.put("%.2s".format(offset.split.hours)); 413 sink.put(":"); 414 sink.put("%.2s".format(offset.split.minutes)); 415 } 416 else 417 sink.put(tzString); 418 } 419 420 void toSDLString(Sink)(DateTimeFracUnknownZone value, ref Sink sink) if(isOutputRange!(Sink,char)) 421 { 422 auto dateTimeFrac = DateTimeFrac(value.dateTime, value.fracSecs); 423 toSDLString(dateTimeFrac, sink); 424 425 sink.put("-"); 426 sink.put(value.timeZone); 427 } 428 429 void toSDLString(Sink)(Duration value, ref Sink sink) if(isOutputRange!(Sink,char)) 430 { 431 if(value < seconds(0)) 432 { 433 sink.put("-"); 434 value = -value; 435 } 436 437 auto days = value.total!"days"(); 438 if(days != 0) 439 { 440 sink.put("%s".format(days)); 441 sink.put("d:"); 442 } 443 444 sink.put("%.2s".format(value.split.hours)); 445 sink.put(':'); 446 sink.put("%.2s".format(value.split.minutes)); 447 sink.put(':'); 448 sink.put("%.2s".format(value.split.seconds)); 449 450 if(value.split.msecs != 0) 451 { 452 sink.put('.'); 453 sink.put("%.3s".format(value.split.msecs)); 454 } 455 } 456 457 void toSDLString(Sink)(ubyte[] value, ref Sink sink) if(isOutputRange!(Sink,char)) 458 { 459 sink.put('['); 460 sink.put( Base64.encode(value) ); 461 sink.put(']'); 462 } 463 464 /// This only represents terminals. Nonterminals aren't 465 /// constructed since the AST is directly built during parsing. 466 struct Token 467 { 468 Symbol symbol = sdlang.symbol.symbol!"Error"; /// The "type" of this token 469 Location location; 470 Value value; /// Only valid when `symbol` is `symbol!"Value"`, otherwise null 471 string data; /// Original text from source 472 473 @disable this(); 474 this(Symbol symbol, Location location, Value value=Value(null), string data=null) 475 { 476 this.symbol = symbol; 477 this.location = location; 478 this.value = value; 479 this.data = data; 480 } 481 482 /// Tokens with differing symbols are always unequal. 483 /// Tokens with differing values are always unequal. 484 /// Tokens with differing Value types are always unequal. 485 /// Member `location` is always ignored for comparison. 486 /// Member `data` is ignored for comparison *EXCEPT* when the symbol is Ident. 487 bool opEquals(Token b) 488 { 489 return opEquals(b); 490 } 491 bool opEquals(ref Token b) ///ditto 492 { 493 if( 494 this.symbol != b.symbol || 495 this.value.type != b.value.type || 496 this.value != b.value 497 ) 498 return false; 499 500 if(this.symbol == .symbol!"Ident") 501 return this.data == b.data; 502 503 return true; 504 } 505 506 bool matches(string symbolName)() 507 { 508 return this.symbol == .symbol!symbolName; 509 } 510 } 511 512 @("sdlang token") 513 unittest 514 { 515 auto loc = Location("", 0, 0, 0); 516 auto loc2 = Location("a", 1, 1, 1); 517 518 assert(Token(symbol!"EOL",loc) == Token(symbol!"EOL",loc )); 519 assert(Token(symbol!"EOL",loc) == Token(symbol!"EOL",loc2)); 520 assert(Token(symbol!":", loc) == Token(symbol!":", loc )); 521 assert(Token(symbol!"EOL",loc) != Token(symbol!":", loc )); 522 assert(Token(symbol!"EOL",loc,Value(null),"\n") == Token(symbol!"EOL",loc,Value(null),"\n")); 523 524 assert(Token(symbol!"EOL",loc,Value(null),"\n") == Token(symbol!"EOL",loc,Value(null),";" )); 525 assert(Token(symbol!"EOL",loc,Value(null),"A" ) == Token(symbol!"EOL",loc,Value(null),"B" )); 526 assert(Token(symbol!":", loc,Value(null),"A" ) == Token(symbol!":", loc,Value(null),"BB")); 527 assert(Token(symbol!"EOL",loc,Value(null),"A" ) != Token(symbol!":", loc,Value(null),"A" )); 528 529 assert(Token(symbol!"Ident",loc,Value(null),"foo") == Token(symbol!"Ident",loc,Value(null),"foo")); 530 assert(Token(symbol!"Ident",loc,Value(null),"foo") != Token(symbol!"Ident",loc,Value(null),"BAR")); 531 532 assert(Token(symbol!"Value",loc,Value(null),"foo") == Token(symbol!"Value",loc, Value(null),"foo")); 533 assert(Token(symbol!"Value",loc,Value(null),"foo") == Token(symbol!"Value",loc2,Value(null),"foo")); 534 assert(Token(symbol!"Value",loc,Value(null),"foo") == Token(symbol!"Value",loc, Value(null),"BAR")); 535 assert(Token(symbol!"Value",loc,Value( 7),"foo") == Token(symbol!"Value",loc, Value( 7),"BAR")); 536 assert(Token(symbol!"Value",loc,Value( 7),"foo") != Token(symbol!"Value",loc, Value( "A"),"foo")); 537 assert(Token(symbol!"Value",loc,Value( 7),"foo") != Token(symbol!"Value",loc, Value( 2),"foo")); 538 assert(Token(symbol!"Value",loc,Value(cast(int)7)) != Token(symbol!"Value",loc, Value(cast(long)7))); 539 assert(Token(symbol!"Value",loc,Value(cast(float)1.2)) != Token(symbol!"Value",loc, Value(cast(double)1.2))); 540 } 541 542 @("sdlang Value.toSDLString()") 543 unittest 544 { 545 // Bool and null 546 assert(Value(null ).toSDLString() == "null"); 547 assert(Value(true ).toSDLString() == "true"); 548 assert(Value(false).toSDLString() == "false"); 549 550 // Base64 Binary 551 assert(Value(cast(ubyte[])"hello world".dup).toSDLString() == "[aGVsbG8gd29ybGQ=]"); 552 553 // Integer 554 assert(Value(cast( int) 7).toSDLString() == "7"); 555 assert(Value(cast( int)-7).toSDLString() == "-7"); 556 assert(Value(cast( int) 0).toSDLString() == "0"); 557 558 assert(Value(cast(long) 7).toSDLString() == "7L"); 559 assert(Value(cast(long)-7).toSDLString() == "-7L"); 560 assert(Value(cast(long) 0).toSDLString() == "0L"); 561 562 // Floating point 563 import std.stdio; 564 writeln(1.5f); 565 writeln(Value(cast(float) 1.5).toSDLString()); 566 assert(Value(cast(float) 1.5).toSDLString() == "1.5F"); 567 assert(Value(cast(float)-1.5).toSDLString() == "-1.5F"); 568 assert(Value(cast(float) 0).toSDLString() == "0F"); 569 assert(Value(cast(float)0.25).toSDLString() == "0.25F"); 570 571 assert(Value(cast(double) 1.5).toSDLString() == "1.5D"); 572 assert(Value(cast(double)-1.5).toSDLString() == "-1.5D"); 573 assert(Value(cast(double) 0).toSDLString() == "0D"); 574 assert(Value(cast(double)0.25).toSDLString() == "0.25D"); 575 576 assert(Value(cast(real) 1.5).toSDLString() == "1.5BD"); 577 assert(Value(cast(real)-1.5).toSDLString() == "-1.5BD"); 578 assert(Value(cast(real) 0).toSDLString() == "0BD"); 579 assert(Value(cast(real)0.25).toSDLString() == "0.25BD"); 580 581 // String 582 assert(Value("hello" ).toSDLString() == `"hello"`); 583 assert(Value(" hello ").toSDLString() == `" hello "`); 584 assert(Value("" ).toSDLString() == `""`); 585 assert(Value("hello \r\n\t\"\\ world").toSDLString() == `"hello \r\n\t\"\\ world"`); 586 assert(Value("日本語").toSDLString() == `"日本語"`); 587 588 // Chars 589 assert(Value(cast(dchar) 'A').toSDLString() == `'A'`); 590 assert(Value(cast(dchar)'\r').toSDLString() == `'\r'`); 591 assert(Value(cast(dchar)'\n').toSDLString() == `'\n'`); 592 assert(Value(cast(dchar)'\t').toSDLString() == `'\t'`); 593 assert(Value(cast(dchar)'\'').toSDLString() == `'\''`); 594 assert(Value(cast(dchar)'\\').toSDLString() == `'\\'`); 595 assert(Value(cast(dchar) '月').toSDLString() == `'月'`); 596 597 // Date 598 assert(Value(Date( 2004,10,31)).toSDLString() == "2004/10/31"); 599 assert(Value(Date(-2004,10,31)).toSDLString() == "-2004/10/31"); 600 601 // DateTimeFrac w/o Frac 602 assert(Value(DateTimeFrac(DateTime(2004,10,31, 14,30,15))).toSDLString() == "2004/10/31 14:30:15"); 603 assert(Value(DateTimeFrac(DateTime(2004,10,31, 1, 2, 3))).toSDLString() == "2004/10/31 01:02:03"); 604 assert(Value(DateTimeFrac(DateTime(-2004,10,31, 14,30,15))).toSDLString() == "-2004/10/31 14:30:15"); 605 606 // DateTimeFrac w/ Frac 607 assert(Value(DateTimeFrac(DateTime(2004,10,31, 14,30,15), 123.msecs)).toSDLString() == "2004/10/31 14:30:15.123"); 608 assert(Value(DateTimeFrac(DateTime(2004,10,31, 14,30,15), 120.msecs)).toSDLString() == "2004/10/31 14:30:15.120"); 609 assert(Value(DateTimeFrac(DateTime(2004,10,31, 14,30,15), 100.msecs)).toSDLString() == "2004/10/31 14:30:15.100"); 610 assert(Value(DateTimeFrac(DateTime(2004,10,31, 14,30,15), 12.msecs)).toSDLString() == "2004/10/31 14:30:15.012"); 611 assert(Value(DateTimeFrac(DateTime(2004,10,31, 14,30,15), 1.msecs)).toSDLString() == "2004/10/31 14:30:15.001"); 612 assert(Value(DateTimeFrac(DateTime(-2004,10,31, 14,30,15), 123.msecs)).toSDLString() == "-2004/10/31 14:30:15.123"); 613 614 // DateTimeFracUnknownZone 615 assert(Value(DateTimeFracUnknownZone(DateTime(2004,10,31, 14,30,15), 123.msecs, "Foo/Bar")).toSDLString() == "2004/10/31 14:30:15.123-Foo/Bar"); 616 617 // SysTime 618 assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone( hours(0) ))).toSDLString() == "2004/10/31 14:30:15-GMT+00:00"); 619 assert(Value(SysTime(DateTime(2004,10,31, 1, 2, 3), new immutable SimpleTimeZone( hours(0) ))).toSDLString() == "2004/10/31 01:02:03-GMT+00:00"); 620 assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone( hours(2)+minutes(10) ))).toSDLString() == "2004/10/31 14:30:15-GMT+02:10"); 621 assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone(-hours(5)-minutes(30) ))).toSDLString() == "2004/10/31 14:30:15-GMT-05:30"); 622 assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), new immutable SimpleTimeZone( hours(2)+minutes( 3) ))).toSDLString() == "2004/10/31 14:30:15-GMT+02:03"); 623 assert(Value(SysTime(DateTime(2004,10,31, 14,30,15), 123.msecs, new immutable SimpleTimeZone( hours(0) ))).toSDLString() == "2004/10/31 14:30:15.123-GMT+00:00"); 624 625 // Duration 626 assert( "12:14:42" == Value( days( 0)+hours(12)+minutes(14)+seconds(42)+msecs( 0)).toSDLString()); 627 assert("-12:14:42" == Value(-days( 0)-hours(12)-minutes(14)-seconds(42)-msecs( 0)).toSDLString()); 628 assert( "00:09:12" == Value( days( 0)+hours( 0)+minutes( 9)+seconds(12)+msecs( 0)).toSDLString()); 629 assert( "00:00:01.023" == Value( days( 0)+hours( 0)+minutes( 0)+seconds( 1)+msecs( 23)).toSDLString()); 630 assert( "23d:05:21:23.532" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(532)).toSDLString()); 631 assert( "23d:05:21:23.530" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(530)).toSDLString()); 632 assert( "23d:05:21:23.500" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs(500)).toSDLString()); 633 assert("-23d:05:21:23.532" == Value(-days(23)-hours( 5)-minutes(21)-seconds(23)-msecs(532)).toSDLString()); 634 assert("-23d:05:21:23.500" == Value(-days(23)-hours( 5)-minutes(21)-seconds(23)-msecs(500)).toSDLString()); 635 assert( "23d:05:21:23" == Value( days(23)+hours( 5)+minutes(21)+seconds(23)+msecs( 0)).toSDLString()); 636 }