1 module text.time.Convert;
2 
3 version(unittest) import dshould;
4 import std.array;
5 import std.datetime;
6 import std.string;
7 import text.time.Lexer;
8 
9 /**
10  * This service class provides functions to convert between date and time values and their representations.
11  *
12  * Standards: ISO 8601
13  * "Data elements and interchange formats — Information interchange — Representation of dates and times"
14  * @Immutable
15  */
16 class Convert
17 {
18 
19     /**
20      * Converts the specified representation into its (date and) time value.
21      *
22      * Throws: DateTimeException on syntax error.
23      */
24     public static T to(T : SysTime)(string value)
25     {
26         return SysTime.fromISOExtString(value);
27     }
28 
29     unittest
30     {
31         Convert.to!SysTime("2003-02-01T11:55:00+01:00")
32             .should.equal(SysTime(DateTime(2003, 2, 1, 11, 55), new immutable SimpleTimeZone(1.hours)));
33 
34         Convert.to!SysTime("2003-02-01 11:55:00").should.throwA!DateTimeException
35             .because("missing 'T'");
36         Convert.to!SysTime(null).should.throwA!DateTimeException;
37     }
38 
39     /**
40      * Converts the specified representation into its date value.
41      *
42      * Throws: DateTimeException on syntax error.
43      */
44     public static T to(T : Date)(string value)
45     {
46         return Date.fromISOExtString(value);
47     }
48 
49     unittest
50     {
51         Convert.to!Date("2003-02-01").should.equal(Date(2003, 2, 1));
52 
53         Convert.to!Date("01.02.2003").should.throwA!DateTimeException;
54         Convert.to!Date(null).should.throwA!DateTimeException;
55     }
56 
57     /**
58      * Converts the specified representation into its time-of-day value.
59      *
60      * Throws: DateTimeException on syntax error.
61      */
62     public static T to(T : TimeOfDay)(string value)
63     {
64         return TimeOfDay.fromISOExtString(value);
65     }
66 
67     unittest
68     {
69         Convert.to!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3));
70         Convert.to!TimeOfDay("23:59:59").should.equal(TimeOfDay(23, 59, 59));
71 
72         Convert.to!TimeOfDay("1:2:3").should.throwA!DateTimeException;
73         Convert.to!TimeOfDay(null).should.throwA!DateTimeException;
74     }
75 
76     /**
77      * Converts the specified representation into its duration value.
78      * For decimal fractions, digits representing less than one millisecond are disregarded.
79      *
80      * Throws: TimeException on syntax error.
81      */
82     public static T to(T : Duration)(string value)
83     {
84         return Lexer.toDuration(value);
85     }
86 
87     /**
88      * Converts the specified (date and) time value into its representation.
89      *
90      * Throws: DateTimeException when the specified time is undefined.
91      */
92     public static string toString(SysTime time) @trusted
93     {
94         return toStringSinkProxy(time);
95     }
96 
97     /// ditto
98     public static void toString(SysTime sysTime, scope void delegate(const(char)[]) sink)
99     {
100         if (sysTime == SysTime.init && sysTime.timezone is null)
101         {
102             throw new DateTimeException("time undefined");
103         }
104 
105         sysTime.fracSecs = Duration.zero;
106         sysTime.toISOExtString(sink);
107     }
108 
109     unittest
110     {
111         DateTime dateTime = DateTime.fromISOExtString("2003-02-01T11:55:00");
112 
113         Convert.toString(SysTime(dateTime)).should.equal("2003-02-01T11:55:00");
114         Convert.toString(SysTime(dateTime, UTC())).should.equal("2003-02-01T11:55:00Z");
115         Convert.toString(SysTime(dateTime, 123.msecs)).should.equal("2003-02-01T11:55:00");
116 
117         DateTime epoch = DateTime.fromISOExtString("0001-01-01T00:00:00");
118 
119         Convert.toString(SysTime(epoch)).should.equal("0001-01-01T00:00:00");
120         Convert.toString(SysTime(epoch, UTC())).should.equal("0001-01-01T00:00:00Z");
121     }
122 
123     /**
124      * Converts the specified date into its representation.
125      */
126     public static string toString(Date date) @trusted
127     {
128         return toStringSinkProxy(date);
129     }
130 
131     /// ditto
132     public static void toString(Date date, scope void delegate(const(char)[]) sink)
133     {
134         date.toISOExtString(sink);
135     }
136 
137     @safe unittest
138     {
139         Convert.toString(Date(2003, 2, 1)).should.equal("2003-02-01");
140     }
141 
142     /**
143      * Converts the specified date and time-of-day value into its representation.
144      */
145     public static string toString(TimeOfDay timeOfDay) @trusted
146     {
147         return toStringSinkProxy(timeOfDay);
148     }
149 
150     /// ditto
151     public static void toString(TimeOfDay timeOfDay, scope void delegate(const(char)[]) sink)
152     {
153         timeOfDay.toISOExtString(sink);
154     }
155 
156     @safe unittest
157     {
158         Convert.toString(TimeOfDay(1, 2, 3)).should.equal("01:02:03");
159     }
160 
161     /**
162      * Converts the specified duration value into its canonical representation.
163      */
164     public static string toString(Duration duration) @trusted
165     {
166         return toStringSinkProxy(duration);
167     }
168 
169     /// ditto
170     public static void toString(Duration duration, scope void delegate(const(char)[]) sink)
171     {
172         import std.format : formattedWrite;
173 
174         if (duration < Duration.zero)
175         {
176             sink("-");
177             duration = -duration;
178         }
179 
180         auto result = duration.split!("days", "hours", "minutes", "seconds", "msecs");
181 
182         with (result)
183         {
184             sink("P");
185 
186             if (days != 0)
187             {
188                 sink.formattedWrite("%sD", days);
189             }
190 
191             const bool allTimesNull = hours == 0 && minutes == 0 && seconds == 0 && msecs == 0;
192             const bool allNull = allTimesNull && days == 0;
193 
194             if (!allTimesNull || allNull)
195             {
196                 sink("T");
197                 if (hours != 0)
198                 {
199                     sink.formattedWrite("%sH", hours);
200                 }
201                 if (minutes != 0)
202                 {
203                     sink.formattedWrite("%sM", minutes);
204                 }
205                 if (seconds != 0 || msecs != 0 || allNull)
206                 {
207                     sink.formattedWrite("%s", seconds);
208                     sink.writeMillis(msecs);
209                     sink("S");
210                 }
211             }
212         }
213     }
214 
215     @safe unittest
216     {
217         Convert.toString(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs).should.equal("P1DT2H3M4.5S");
218         Convert.toString(1.days).should.equal("P1D");
219         Convert.toString(Duration.zero).should.equal("PT0S");
220         Convert.toString(1.msecs).should.equal("PT0.001S");
221         Convert.toString(-(1.hours + 2.minutes + 3.seconds + 450.msecs)).should.equal("-PT1H2M3.45S");
222     }
223 }
224 
225 private string toStringSinkProxy(T)(T t)
226 {
227     string str = null;
228 
229     Convert.toString(t, (fragment) { str ~= fragment; });
230 
231     return str;
232 }
233 
234 /**
235  * Converts the specified milliseconds value into a representation with as few digits as possible.
236  */
237 private void writeMillis(scope void delegate(const(char)[]) sink, long millis)
238 in (0 <= millis && millis < 1000)
239 {
240     import std.format : formattedWrite;
241 
242     if (millis == 0)
243     {
244         sink("");
245     }
246     else if (millis % 100 == 0)
247     {
248         sink.formattedWrite(".%01d", millis / 100);
249     }
250     else if (millis % 10 == 0)
251     {
252         sink.formattedWrite(".%02d", millis / 10);
253     }
254     else
255     {
256         sink.formattedWrite(".%03d", millis);
257     }
258 }