1 module text.time.Lexer;
2 
3 import core.time;
4 version(unittest) import dshould;
5 import std.conv;
6 import std.string;
7 
8 /**
9  * This service class allows to convert duration representations into corresponding values.
10  *
11  * Standards: ISO 8601
12  * "Data elements and interchange formats — Information interchange — Representation of dates and times"
13  */
14 package struct Lexer
15 {
16     private static const char END = 0;
17 
18     private static const char DIGITS = '0';
19 
20     private string representation;
21 
22     /**
23      * The slice of the representation yet to be analyzed.
24      */
25     private string rest;
26 
27     private string value;
28 
29     private this(string representation) @nogc nothrow pure @safe
30     {
31         this.representation = representation;
32         this.rest = representation;
33     }
34 
35     /**
36      * Converts the specified representation into its duration value. While the duration data type of the XML Schema
37      * language allows to also specify year and month fields, such representations cannot be converted to durations.
38      * Consequently, only the derived data type 'dayTimeDuration' of the XML Schema language is supported here,
39      * where all representations with year or month fields are excluded.
40      * For decimal fractions, digits representing less than one millisecond are disregarded.
41      *
42      * Throws: TimeException on syntax error.
43      */
44     package static Duration toDuration(string representation) pure @safe
45     {
46         auto lexer = Lexer(representation);
47 
48         return lexer.toDuration;
49     }
50 
51     unittest
52     {
53         toDuration("P1DT2H3M4.5S").should.equal(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs);
54         toDuration("-P1D").should.equal(-1.days);
55         toDuration("+PT1S").should.equal(1.seconds);
56 
57         toDuration("PT1S2M3H").should.throwA!TimeException
58             .because("disarranged representation");
59         toDuration("01H05M10S").should.throwA!TimeException
60             .because("missing 'P'");
61         toDuration("PT1.S").should.throwA!TimeException
62             .because("clipped decimal fraction");
63         toDuration("PT1").should.throwA!TimeException
64             .because("missing 'S'");
65         toDuration("PT").should.throwA!TimeException;
66     }
67 
68     /**
69      * Converts the representation according to the following (simplified) regular expression into its duration value:
70      *
71      *   ('+'|'-')? 'P' ([0-9]+ 'D')? ('T' ([0-9]+ 'H')? ([0-9]+ 'M')? ([0-9]+ ('.' [0-9]+)? 'S')?)?
72      */
73     private Duration toDuration() pure @safe
74     {
75         Duration duration;
76         bool positive = true;
77         bool complete = false;
78         enum ERROR_MESSAGE = "'%s' is not a valid duration value";
79         char prev;
80         char next;
81 
82         next = readSymbol;
83         if (next == '+' || next == '-')
84         {
85             positive = (next == '+');
86             next = readSymbol;
87         }
88         if (next != 'P')
89         {
90             throw new TimeException(format!ERROR_MESSAGE(this.representation));
91         }
92         prev = readSymbol;
93         next = readSymbol;
94         if (prev == DIGITS && next == 'D')
95         {
96             complete = true;
97             duration += this.value.to!long.days;
98             prev = readSymbol;
99             next = readSymbol;
100         }
101         if (prev == 'T')
102         {
103             complete = false;
104             prev = next;
105             next = readSymbol;
106             if (prev == DIGITS && next == 'H')
107             {
108                 complete = true;
109                 duration += this.value.to!long.hours;
110                 prev = readSymbol;
111                 next = readSymbol;
112             }
113             if (prev == DIGITS && next == 'M')
114             {
115                 complete = true;
116                 duration += this.value.to!long.minutes;
117                 prev = readSymbol;
118                 next = readSymbol;
119             }
120             if (prev == DIGITS)
121             {
122                 complete = true;
123                 duration += this.value.to!long.seconds;
124                 if (next == '.')
125                 {
126                     next = readSymbol;
127                     if (next != DIGITS)
128                     {
129                         throw new TimeException(format!ERROR_MESSAGE(this.representation));
130                     }
131                     this.value = (this.value ~ "000")[0 .. 3];
132                     duration += this.value.to!long.msecs;
133                     next = readSymbol;
134                 }
135                 if (next != 'S')
136                 {
137                     throw new TimeException(format!ERROR_MESSAGE(this.representation));
138                 }
139                 prev = readSymbol;
140             }
141         }
142         if (!complete || prev != END)
143         {
144             throw new TimeException(format!ERROR_MESSAGE(this.representation));
145         }
146         return (positive) ? duration : -duration;
147     }
148 
149     /**
150      * Returns: The next "symbol" in the representation ('END', 'DIGITS', or the next character itself).
151      */
152     private char readSymbol() @nogc nothrow pure @safe
153     {
154         if (this.rest.length == 0)
155         {
156             return END;
157         }
158 
159         char c = this.rest[0];
160 
161         if ('0' <= c && c <= '9')
162         {
163             uint index = 0;
164 
165             do
166             {
167                 ++index;
168                 if (index >= this.rest.length)
169                 {
170                     break;
171                 }
172                 c = this.rest[index];
173             }
174             while ('0' <= c && c <= '9');
175             this.value = this.rest[0 .. index];
176             this.rest = this.rest[index .. $];
177             return DIGITS;
178         }
179         this.rest = this.rest[1 .. $];
180         return c;
181     }
182 
183 }