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      * Converts the specified representation to an arbitrary type,
253      * so long the type has a fromString method.
254      */
255     public static T to(T)(string value)
256     if (__traits(compiles, T.fromString(value)))
257     {
258         return T.fromString(value);
259     }
260 
261     @("Convert.to with fromString")
262     unittest
263     {
264         struct S
265         {
266             int i;
267 
268             static S fromString(string s)
269             {
270                 import std.conv : to;
271 
272                 return S(s.to!int);
273             }
274         }
275 
276         Convert.to!S("5").should.equal(S(5));
277     }
278 
279     /**
280      * Throws: XmlException when the given value does not match the lexical representation of a date and time,
281      * or when the date is not in the too distant future.
282      */
283     private static SysTime fixDateTimeInTooDistantFuture(string value) @safe
284     {
285         // std.regex unduly explodes our compiletime
286 
287         // import std.regex;
288         // auto lexicalRepresentation = regex(`^(?P<year>-?\d{4,})-(?P<month>\d{2})-(?P<day>\d{2})T`
289         //     ~ `(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?P<timezone>(Z|[+-]\d{2}:\d{2})?)$`);
290 
291         string yearStr;
292         bool matchLexicalRepresentationSaveYear() @nogc @safe
293         {
294             import std.ascii : isDigit;
295             import text.RecursiveDescentParser : RecursiveDescentParser;
296 
297             with (RecursiveDescentParser(value))
298             {
299                 alias acceptDigits = i => matchTimes(i, () => acceptAsciiChar(ch => ch.isDigit));
300 
301                 // ^(?P<year>-?\d{4,})
302                 if (!captureGroupInto(yearStr, () =>
303                     matchOptional(() => accept("-"))
304                     && acceptDigits(4) && matchZeroOrMore(() => acceptDigits(1))))
305                 {
306                     return false;
307                 }
308 
309                 // -\d{2}-\d{2}T\d{2}:\d{2}:\d{2}
310                 if (!(accept("-")
311                     && acceptDigits(2) && accept("-")
312                     && acceptDigits(2) && accept("T")
313                     && acceptDigits(2) && accept(":")
314                     && acceptDigits(2) && accept(":")
315                     && acceptDigits(2)))
316                 {
317                     return false;
318                 }
319 
320                 // (Z|[+-]\d{2}:\d{2})?
321                 accept("Z") || matchGroup(() =>
322                     acceptAsciiChar((ch) => ch == '+' || ch == '-')
323                         && acceptDigits(2) && accept(":") && acceptDigits(2));
324 
325                 // $
326                 if (!eof)
327                 {
328                     return false;
329                 }
330 
331                 return true;
332             }
333         }
334 
335         // if (auto captures = value.strip.matchFirst(lexicalRepresentation))
336         if (matchLexicalRepresentationSaveYear)
337         {
338             try
339             {
340                 const year = std.conv.to!long(yearStr);
341 
342                 if (year > SysTime.max.year)
343                 {
344                     return SysTime.max;
345                 }
346             }
347             catch (std.conv.ConvException)
348             {
349                 // fall through
350             }
351         }
352         throw new XmlException(format!`"%s" is not a valid value of type date-time`(value));
353     }
354 
355     /**
356      * Converts the specified representation into its duration (strictly speaking, 'dayTimeDuration') value.
357      * For decimal fractions, digits representing less than one millisecond are disregarded.
358      *
359      * Throws: XmlException on validity violation.
360      */
361     public static T to(T : Duration)(string value)
362     {
363         try
364         {
365             return text.time.Convert.Convert.to!Duration(value.strip);
366         }
367         catch (TimeException)
368         {
369             throw new XmlException(format!`"%s" is not a valid value of type duration`(value));
370         }
371     }
372 
373     unittest
374     {
375         to!Duration("P1DT2H3M4.5S").should.equal(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs);
376 
377         to!Duration("PT1S2M3H").should.throwAn!XmlException.
378             because("disarranged representation");
379     }
380 
381     /**
382      * Converts the specified representation into its time value.
383      *
384      * Throws: XmlException on validity violation.
385      */
386     public static T to(T : TimeOfDay)(string value)
387     {
388         try
389         {
390             return text.time.Convert.Convert.to!TimeOfDay(value.strip);
391         }
392         catch (DateTimeException)
393         {
394             throw new XmlException(format!`"%s" is not a valid value of type time`(value));
395         }
396     }
397 
398     unittest
399     {
400         Convert.to!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3));
401 
402         to!TimeOfDay("1:2:3").should.throwAn!XmlException;
403         to!TimeOfDay("24:00:00").should.throwAn!XmlException
404             .because("XML Schema 1.1 not yet supported");
405     }
406 
407     /**
408      * Returns the specified string value.
409      * This specialization allows to use the template with any relevant type.
410      */
411     public static T to(T : string)(string value)
412     {
413         return value;
414     }
415 
416     /**
417      * Converts the specified "time" representation (time of day with optional fractional seconds)
418      * into the corresponding time of day (without fractional seconds).
419      *
420      * Throws: XmlException on validity violation.
421      */
422     public static T toTime(T : TimeOfDay)(string value)
423     {
424         return TimeOfDay.min + toTime!Duration(value);
425     }
426 
427     @safe unittest
428     {
429         toTime!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3));
430         toTime!TimeOfDay("01:02:03.456").should.equal(TimeOfDay(1, 2, 3));
431     }
432 
433     /**
434      * Converts the specified "time" representation (time of day with optional fractional seconds)
435      * into the corresponding duration since midnight.
436      *
437      * Throws: XmlException on validity violation.
438      */
439     public static T toTime(T : Duration = Duration)(string value)
440     {
441         import std.algorithm : findSplitBefore;
442 
443         auto result = value.strip.findSplitBefore(".");
444 
445         try
446         {
447             const timeOfDay = TimeOfDay.fromISOExtString(result[0]);
448             const fracSecs = fracSecsFromISOString(result[1]);
449 
450             return timeOfDay - TimeOfDay.min + fracSecs;
451         }
452         catch (DateTimeException)
453         {
454             throw new XmlException(format!`"%s" is not a valid value of type time`(value));
455         }
456     }
457 
458     @safe unittest
459     {
460         toTime("01:02:03").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min);
461         toTime("01:02:03.456").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min + 456.msecs);
462     }
463 
464     /**
465      * See_Also: private helper function of std.datetime with same name
466      * Throws: DateTimeException on syntax error.
467      */
468     private static Duration fracSecsFromISOString(string value) @safe
469     {
470         import std.conv : ConvException, to;
471         import std.range : empty;
472 
473         if (value.empty)
474         {
475             return Duration.zero;
476         }
477 
478         enforce!DateTimeException(value.front == '.' && value.length > 1);
479 
480         char[7] digits;  // hnsecs
481 
482         foreach (i, ref digit; digits)
483         {
484             digit = (i + 1 < value.length) ? value[i + 1] : '0';
485         }
486         try
487         {
488             return digits.to!int.hnsecs;
489         }
490         catch (ConvException exception)
491         {
492             throw new DateTimeException(exception.msg);
493         }
494 
495     }
496 
497     unittest
498     {
499         fracSecsFromISOString("").should.equal(Duration.zero);
500         fracSecsFromISOString(".1").should.equal(1_000_000.hnsecs);
501         fracSecsFromISOString(".01").should.equal(100_000.hnsecs);
502         fracSecsFromISOString(".001").should.equal(10_000.hnsecs);
503         fracSecsFromISOString(".0001").should.equal(1_000.hnsecs);
504         fracSecsFromISOString(".00001").should.equal(100.hnsecs);
505         fracSecsFromISOString(".000001").should.equal(10.hnsecs);
506         fracSecsFromISOString(".0000001").should.equal(1.hnsecs);
507 
508         fracSecsFromISOString("?").should.throwA!DateTimeException;
509         fracSecsFromISOString(".").should.throwA!DateTimeException;
510         fracSecsFromISOString("...").should.throwA!DateTimeException;
511     }
512 
513     /**
514      * Converts the specified boolean value into its canonical representation.
515      */
516     public static string toString(bool value) @nogc @safe
517     {
518         return value ? "true" : "false";
519     }
520 
521     @safe unittest
522     {
523         toString(true).should.equal("true");
524         toString(false).should.equal("false");
525     }
526 
527     /**
528      * Converts the specified integer or floating-point value into its canonical representation.
529      */
530     public static string toString(T)(T value)
531     if (isIntegral!T || isFloatingPoint!T)
532     {
533         return std.conv.to!string(value);
534     }
535 
536     @safe unittest
537     {
538         toString(42).should.equal("42");
539         toString(-42).should.equal("-42");
540         toString(1.2).should.equal("1.2");
541     }
542 
543     /**
544      * Converts the specified date into its canonical representation.
545      */
546     public static string toString(Date date) @safe
547     {
548         return text.time.Convert.Convert.toString(date);
549     }
550 
551     @safe unittest
552     {
553         toString(Date(2003, 2, 1)).should.equal("2003-02-01");
554     }
555 
556     /**
557      * Converts the specified date and time value into its canonical representation.
558      */
559     public static string toString(SysTime dateTime) @safe
560     {
561         return text.time.Convert.Convert.toString(dateTime);
562     }
563 
564     @safe unittest
565     {
566         DateTime dateTime = DateTime.fromISOExtString("2003-02-01T11:55:00");
567 
568         toString(SysTime(dateTime)).should.equal("2003-02-01T11:55:00");
569         toString(SysTime(dateTime, UTC())).should.equal("2003-02-01T11:55:00Z");
570     }
571 
572     /**
573      * Converts the specified duration value into its canonical representation.
574      */
575     public static string toString(Duration duration) @safe
576     {
577         return text.time.Convert.Convert.toString(duration);
578     }
579 
580     @safe unittest
581     {
582         toString(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs).should.equal("P1DT2H3M4.5S");
583     }
584 
585     /**
586      * Converts the specified time of day value into its canonical representation.
587      */
588     public static string toString(TimeOfDay timeOfDay) @safe
589     {
590         return text.time.Convert.Convert.toString(timeOfDay);
591     }
592 
593     @safe unittest
594     {
595         toString(TimeOfDay(1, 2, 3)).should.equal("01:02:03");
596     }
597 
598 }