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 }