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