1 module text.xml.Convert;
2 
3 version(unittest) import dshould;
4 import std.array;
5 static import std.conv;
6 import std.datetime;
7 import std.exception;
8 import std.string;
9 import std.traits;
10 static import text.time.Convert;
11 import text.xml.XmlException;
12 
13 /**
14  * This service class provides static member functions to convert between values and their representations
15  * according to the data type definitions of the XML Schema language. When a conversion (from representation to value)
16  * cannot be performed, an exception indicating the XML validity violation is thrown.
17  * @Immutable
18  */
19 class Convert
20 {
21 
22     /**
23      * Converts the specified representation into its boolean value.
24      *
25      * Throws: XmlException on validity violation.
26      */
27     public static T to(T : bool)(string value)
28     {
29         switch (value.strip)
30         {
31             case "false":
32             case "0":
33                 return false;
34             case "true":
35             case "1":
36                 return true;
37             default:
38                 throw new XmlException(format!`"%s" is not a valid value of type boolean`(value));
39         }
40     }
41 
42     unittest
43     {
44         to!bool(" true ").should.equal(true);
45         to!bool(" false ").should.equal(false);
46 
47         to!bool("True").should.throwAn!XmlException;
48     }
49 
50     /**
51      * Converts the specified representation into its integer or floating-point value.
52      *
53      * Throws: XmlException on validity violation.
54      */
55     public static T to(T)(string value)
56     if ((isIntegral!T && !is(T == enum)) || isFloatingPoint!T)
57     {
58         try
59         {
60             return std.conv.to!T(value.strip);
61         }
62         catch (std.conv.ConvException)
63         {
64             throw new XmlException(format!`"%s" is not a valid value of type %s`(value, T.stringof));
65         }
66     }
67 
68     unittest
69     {
70         to!int(" -1 ").should.equal(-1);
71         to!uint(" 0 ").should.equal(0);
72         to!long("-9223372036854775808").should.equal(long.min);
73         to!ulong(" 18446744073709551615 ").should.equal(ulong.max);
74         to!double(" 1.2 ").should.be.approximately(1.2, error = 1e-6);
75 
76         to!int("1.2").should.throwAn!XmlException;
77         to!uint("0xab").should.throwAn!XmlException;
78     }
79 
80     /**
81      * Converts the specified representation into its positive integer value.
82      *
83      * Throws: XmlException on validity violation.
84      */
85     public static T toPositive(T)(string value)
86     if (isIntegral!T)
87     out (result; result > 0)
88     {
89         try
90         {
91             T result = std.conv.to!T(value.strip);
92 
93             if (result <= 0)
94             {
95                 throw new XmlException(format!`"%s" is not a valid value of type positive integer`(value));
96             }
97             return result;
98         }
99         catch (std.conv.ConvException)
100         {
101             throw new XmlException(format!`"%s" is not a valid value of type %s`(value, T.stringof));
102         }
103     }
104 
105     unittest
106     {
107         toPositive!int(" +1 ").should.equal(1);
108         toPositive!long("9223372036854775807").should.equal(long.max);
109 
110         toPositive!uint("0").should.throwAn!XmlException;
111     }
112 
113     /**
114      * Converts the specified representation into its enumeration value.
115      *
116      * Throws: XmlException on validity violation.
117      */
118     public static T to(T)(string value)
119     if (is(T == enum))
120     {
121         try
122         {
123             return std.conv.to!T(value.strip);
124         }
125         catch (std.conv.ConvException)
126         {
127             throw new XmlException(format!`"%s" is not a valid value of enumeration %s`(value, T.stringof));
128         }
129     }
130 
131     unittest
132     {
133         enum Enum {VALUE}
134 
135         to!Enum(" VALUE ").should.equal(Enum.VALUE);
136 
137         to!Enum(" 0 ").should.throwAn!XmlException;
138     }
139 
140     /**
141      * Converts the specified representation into its date value.
142      *
143      * Throws: XmlException on validity violation.
144      */
145     public static T to(T : Date)(string value)
146     {
147         import std.ascii : isDigit;
148 
149         try
150         {
151             return text.time.Convert.Convert.to!Date(value.strip);
152         }
153         catch (DateTimeException)
154         {
155             value = value.strip;
156 
157             size_t index = value.length;
158 
159             enum NoResult = ptrdiff_t.max;
160 
161             ptrdiff_t endsWithTimezone()
162             {
163                 import std.algorithm : max;
164 
165                 if (value.endsWith("Z"))
166                 {
167                     return "Z".length;
168                 }
169 
170                 auto offsetIndex = max(value.lastIndexOf('+'), value.lastIndexOf('-'));
171 
172                 if (offsetIndex == -1)
173                 {
174                     return NoResult;
175                 }
176 
177                 auto tzRange = value[offsetIndex + 1 .. $];
178 
179                 if (tzRange.length != "00:00".length)
180                 {
181                     return NoResult;
182                 }
183 
184                 if (isDigit(tzRange[0]) && isDigit(tzRange[1]) && tzRange[2] == ':'
185                     && isDigit(tzRange[3]) && isDigit(tzRange[4]))
186                 {
187                     return "+00:00".length;
188                 }
189                 else
190                 {
191                     return NoResult;
192                 }
193             }
194 
195             auto timezoneLength = endsWithTimezone();
196 
197             if (timezoneLength != NoResult)
198             {
199                 index -= timezoneLength;
200             }
201             try
202             {
203                 return cast(Date) to!SysTime(value[0 .. index] ~ "T00:00:00" ~ value[index .. $]);
204             }
205             catch (XmlException)
206             {
207                 throw new XmlException(format!`"%s" is not a valid value of type date`(value));
208             }
209         }
210     }
211 
212     unittest
213     {
214         to!Date("2003-02-01").should.equal(Date(2003, 2, 1));
215         to!Date("2003-02-01Z").should.equal(Date(2003, 2, 1));
216         to!Date("2003-02-01-01:00").should.equal(Date(2003, 2, 1));
217 
218         to!Date("01.02.2003").should.throwAn!XmlException;
219         to!Date("today").should.throwAn!XmlException;
220     }
221 
222     /**
223      * Converts the specified representation into its date and time value.
224      *
225      * Throws: XmlException on validity violation.
226      */
227     public static T to(T : SysTime)(string value)
228     {
229         try
230         {
231             return text.time.Convert.Convert.to!SysTime(value.strip);
232         }
233         catch (DateTimeException)
234         {
235             return fixDateTimeInTooDistantFuture(value);
236         }
237     }
238 
239     unittest
240     {
241         to!SysTime(" 2003-02-01T11:55:00+01:00 ")
242             .should.equal(SysTime(DateTime(2003, 2, 1, 11, 55), new immutable SimpleTimeZone(1.hours)));
243         to!SysTime("292278994-08-17T08:12:55+01:00").should.equal(SysTime.max);
244 
245         to!SysTime("2003-02-01 11:55:00").should.throwAn!XmlException
246             .because("missing 'T'");
247         to!SysTime("2003-02-01T24:00:00").should.throwAn!XmlException
248             .because("XML Schema 1.1 not yet supported");
249     }
250 
251     /**
252      * Throws: XmlException when the given value does not match the lexical representation of a date and time,
253      * or when the date is not in the too distant future.
254      */
255     private static SysTime fixDateTimeInTooDistantFuture(string value) @safe
256     {
257         // std.regex unduly explodes our compiletime
258 
259         // import std.regex;
260         // auto lexicalRepresentation = regex(`^(?P<year>-?\d{4,})-(?P<month>\d{2})-(?P<day>\d{2})T`
261         //     ~ `(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?P<timezone>(Z|[+-]\d{2}:\d{2})?)$`);
262 
263         string yearStr;
264         bool matchLexicalRepresentationSaveYear() @nogc @safe
265         {
266             import std.ascii : isDigit;
267             import text.RecursiveDescentParser : RecursiveDescentParser;
268 
269             with (RecursiveDescentParser(value))
270             {
271                 alias acceptDigits = i => matchTimes(i, () => acceptAsciiChar(ch => ch.isDigit));
272 
273                 // ^(?P<year>-?\d{4,})
274                 if (!captureGroupInto(yearStr, () =>
275                     matchOptional(() => accept("-"))
276                     && acceptDigits(4) && matchZeroOrMore(() => acceptDigits(1))))
277                 {
278                     return false;
279                 }
280 
281                 // -\d{2}-\d{2}T\d{2}:\d{2}:\d{2}
282                 if (!(accept("-")
283                     && acceptDigits(2) && accept("-")
284                     && acceptDigits(2) && accept("T")
285                     && acceptDigits(2) && accept(":")
286                     && acceptDigits(2) && accept(":")
287                     && acceptDigits(2)))
288                 {
289                     return false;
290                 }
291 
292                 // (Z|[+-]\d{2}:\d{2})?
293                 accept("Z") || matchGroup(() =>
294                     acceptAsciiChar((ch) => ch == '+' || ch == '-')
295                         && acceptDigits(2) && accept(":") && acceptDigits(2));
296 
297                 // $
298                 if (!eof)
299                 {
300                     return false;
301                 }
302 
303                 return true;
304             }
305         }
306 
307         // if (auto captures = value.strip.matchFirst(lexicalRepresentation))
308         if (matchLexicalRepresentationSaveYear)
309         {
310             try
311             {
312                 const year = std.conv.to!long(yearStr);
313 
314                 if (year > SysTime.max.year)
315                 {
316                     return SysTime.max;
317                 }
318             }
319             catch (std.conv.ConvException)
320             {
321                 // fall through
322             }
323         }
324         throw new XmlException(format!`"%s" is not a valid value of type date-time`(value));
325     }
326 
327     /**
328      * Converts the specified representation into its duration (strictly speaking, 'dayTimeDuration') value.
329      * For decimal fractions, digits representing less than one millisecond are disregarded.
330      *
331      * Throws: XmlException on validity violation.
332      */
333     public static T to(T : Duration)(string value)
334     {
335         try
336         {
337             return text.time.Convert.Convert.to!Duration(value.strip);
338         }
339         catch (TimeException)
340         {
341             throw new XmlException(format!`"%s" is not a valid value of type duration`(value));
342         }
343     }
344 
345     unittest
346     {
347         to!Duration("P1DT2H3M4.5S").should.equal(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs);
348 
349         to!Duration("PT1S2M3H").should.throwAn!XmlException.
350             because("disarranged representation");
351     }
352 
353     /**
354      * Converts the specified representation into its time value.
355      *
356      * Throws: XmlException on validity violation.
357      */
358     public static T to(T : TimeOfDay)(string value)
359     {
360         try
361         {
362             return text.time.Convert.Convert.to!TimeOfDay(value.strip);
363         }
364         catch (DateTimeException)
365         {
366             throw new XmlException(format!`"%s" is not a valid value of type time`(value));
367         }
368     }
369 
370     unittest
371     {
372         Convert.to!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3));
373 
374         to!TimeOfDay("1:2:3").should.throwAn!XmlException;
375         to!TimeOfDay("24:00:00").should.throwAn!XmlException
376             .because("XML Schema 1.1 not yet supported");
377     }
378 
379     /**
380      * Returns the specified string value.
381      * This specialization allows to use the template with any relevant type.
382      */
383     public static T to(T : string)(string value)
384     {
385         return value;
386     }
387 
388     /**
389      * Converts the specified "time" representation (time of day with optional fractional seconds)
390      * into the corresponding time of day (without fractional seconds).
391      *
392      * Throws: XmlException on validity violation.
393      */
394     public static T toTime(T : TimeOfDay)(string value)
395     {
396         return TimeOfDay.min + toTime!Duration(value);
397     }
398 
399     @safe unittest
400     {
401         toTime!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3));
402         toTime!TimeOfDay("01:02:03.456").should.equal(TimeOfDay(1, 2, 3));
403     }
404 
405     /**
406      * Converts the specified "time" representation (time of day with optional fractional seconds)
407      * into the corresponding duration since midnight.
408      *
409      * Throws: XmlException on validity violation.
410      */
411     public static T toTime(T : Duration = Duration)(string value)
412     {
413         import std.algorithm : findSplitBefore;
414 
415         auto result = value.strip.findSplitBefore(".");
416 
417         try
418         {
419             const timeOfDay = TimeOfDay.fromISOExtString(result[0]);
420             const fracSecs = fracSecsFromISOString(result[1]);
421 
422             return timeOfDay - TimeOfDay.min + fracSecs;
423         }
424         catch (DateTimeException)
425         {
426             throw new XmlException(format!`"%s" is not a valid value of type time`(value));
427         }
428     }
429 
430     @safe unittest
431     {
432         toTime("01:02:03").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min);
433         toTime("01:02:03.456").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min + 456.msecs);
434     }
435 
436     /**
437      * See_Also: private helper function of std.datetime with same name
438      * Throws: DateTimeException on syntax error.
439      */
440     private static Duration fracSecsFromISOString(string value) @safe
441     {
442         import std.conv : ConvException, to;
443         import std.range : empty;
444 
445         if (value.empty)
446         {
447             return Duration.zero;
448         }
449 
450         enforce!DateTimeException(value.front == '.' && value.length > 1);
451 
452         char[7] digits;  // hnsecs
453 
454         foreach (i, ref digit; digits)
455         {
456             digit = (i + 1 < value.length) ? value[i + 1] : '0';
457         }
458         try
459         {
460             return digits.to!int.hnsecs;
461         }
462         catch (ConvException exception)
463         {
464             throw new DateTimeException(exception.msg);
465         }
466 
467     }
468 
469     unittest
470     {
471         fracSecsFromISOString("").should.equal(Duration.zero);
472         fracSecsFromISOString(".1").should.equal(1_000_000.hnsecs);
473         fracSecsFromISOString(".01").should.equal(100_000.hnsecs);
474         fracSecsFromISOString(".001").should.equal(10_000.hnsecs);
475         fracSecsFromISOString(".0001").should.equal(1_000.hnsecs);
476         fracSecsFromISOString(".00001").should.equal(100.hnsecs);
477         fracSecsFromISOString(".000001").should.equal(10.hnsecs);
478         fracSecsFromISOString(".0000001").should.equal(1.hnsecs);
479 
480         fracSecsFromISOString("?").should.throwA!DateTimeException;
481         fracSecsFromISOString(".").should.throwA!DateTimeException;
482         fracSecsFromISOString("...").should.throwA!DateTimeException;
483     }
484 
485     /**
486      * Converts the specified boolean value into its canonical representation.
487      */
488     public static string toString(bool value) @nogc @safe
489     {
490         return value ? "true" : "false";
491     }
492 
493     @safe unittest
494     {
495         toString(true).should.equal("true");
496         toString(false).should.equal("false");
497     }
498 
499     /**
500      * Converts the specified integer or floating-point value into its canonical representation.
501      */
502     public static string toString(T)(T value)
503     if (isIntegral!T || isFloatingPoint!T)
504     {
505         return std.conv.to!string(value);
506     }
507 
508     @safe unittest
509     {
510         toString(42).should.equal("42");
511         toString(-42).should.equal("-42");
512         toString(1.2).should.equal("1.2");
513     }
514 
515     /**
516      * Converts the specified date into its canonical representation.
517      */
518     public static string toString(Date date) @safe
519     {
520         return text.time.Convert.Convert.toString(date);
521     }
522 
523     @safe unittest
524     {
525         toString(Date(2003, 2, 1)).should.equal("2003-02-01");
526     }
527 
528     /**
529      * Converts the specified date and time value into its canonical representation.
530      */
531     public static string toString(SysTime dateTime) @safe
532     {
533         return text.time.Convert.Convert.toString(dateTime);
534     }
535 
536     @safe unittest
537     {
538         DateTime dateTime = DateTime.fromISOExtString("2003-02-01T11:55:00");
539 
540         toString(SysTime(dateTime)).should.equal("2003-02-01T11:55:00");
541         toString(SysTime(dateTime, UTC())).should.equal("2003-02-01T11:55:00Z");
542     }
543 
544     /**
545      * Converts the specified duration value into its canonical representation.
546      */
547     public static string toString(Duration duration) @safe
548     {
549         return text.time.Convert.Convert.toString(duration);
550     }
551 
552     @safe unittest
553     {
554         toString(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs).should.equal("P1DT2H3M4.5S");
555     }
556 
557     /**
558      * Converts the specified time of day value into its canonical representation.
559      */
560     public static string toString(TimeOfDay timeOfDay) @safe
561     {
562         return text.time.Convert.Convert.toString(timeOfDay);
563     }
564 
565     @safe unittest
566     {
567         toString(TimeOfDay(1, 2, 3)).should.equal("01:02:03");
568     }
569 
570 }