1 /**
2  * Time formatting functions.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.time.format;
15 
16 import std.algorithm.comparison;
17 import std.conv : text;
18 import std.datetime;
19 import std.format;
20 import std.math : abs;
21 
22 import ae.utils.meta;
23 import ae.utils.text;
24 import ae.utils.textout;
25 import ae.utils.time.common;
26 
27 private struct FormatContext(Char)
28 {
29 	SysTime t;
30 	DateTime dt;
31 	bool escaping;
32 }
33 
34 private void putToken(alias c, alias context, alias sink)()
35 {
36 	with (context)
37 	{
38 		void putOneDigit(uint i)
39 		{
40 			debug assert(i < 10);
41 			sink.put(cast(char)('0' + i));
42 		}
43 
44 		void putOneOrTwoDigits(uint i)
45 		{
46 			debug assert(i < 100);
47 			if (i >= 10)
48 			{
49 				sink.put(cast(char)('0' + (i / 10)));
50 				sink.put(cast(char)('0' + (i % 10)));
51 			}
52 			else
53 				sink.put(cast(char)('0' +  i      ));
54 		}
55 
56 		void putTimezoneName(string tzStr)
57 		{
58 			if (tzStr.length)
59 				sink.put(tzStr[0..min($, MaxTimezoneNameLength)]);
60 			else
61 		//	if (t.timezone.utcToTZ(t.stdTime) == t.stdTime)
62 		//		sink.put("UTC");
63 		//	else
64 			{
65 				enum fmt = 'P';
66 				putToken!(fmt, context, sink)();
67 			}
68 		}
69 
70 		if (escaping)
71 			sink.put(c), escaping = false;
72 		else
73 			switch (c)
74 			{
75 				// Day
76 				case 'd':
77 					sink.put(toDecFixed!2(dt.day));
78 					break;
79 				case 'D':
80 					sink.put(WeekdayShortNames[dt.dayOfWeek]);
81 					break;
82 				case 'j':
83 					putOneOrTwoDigits(dt.day);
84 					break;
85 				case 'l':
86 					sink.put(WeekdayLongNames[dt.dayOfWeek]);
87 					break;
88 				case 'N':
89 					putOneDigit((dt.dayOfWeek+6)%7 + 1);
90 					break;
91 				case 'S':
92 					switch (dt.day)
93 					{
94 						case 1:
95 						case 21:
96 						case 31:
97 							sink.put("st");
98 							break;
99 						case 2:
100 						case 22:
101 							sink.put("nd");
102 							break;
103 						case 3:
104 						case 23:
105 							sink.put("rd");
106 							break;
107 						default:
108 							sink.put("th");
109 					}
110 					break;
111 				case 'w':
112 					putOneDigit(cast(int)dt.dayOfWeek);
113 					break;
114 				case 'z':
115 					sink.put(text(dt.dayOfYear-1));
116 					break;
117 
118 				// Week
119 				case 'W':
120 					sink.put(toDecFixed!2(dt.isoWeek));
121 					break;
122 
123 				// Month
124 				case 'F':
125 					sink.put(MonthLongNames[dt.month-1]);
126 					break;
127 				case 'm':
128 					sink.put(toDecFixed!2(dt.month));
129 					break;
130 				case 'M':
131 					sink.put(MonthShortNames[dt.month-1]);
132 					break;
133 				case 'n':
134 					putOneOrTwoDigits(dt.month);
135 					break;
136 				case 't':
137 					putOneOrTwoDigits(dt.daysInMonth);
138 					break;
139 
140 				// Year
141 				case 'L':
142 					sink.put(dt.isLeapYear ? '1' : '0');
143 					break;
144 				// case 'o': TODO (ISO 8601 year number)
145 				case 'Y':
146 					sink.put(toDecFixed!4(cast(uint)dt.year)); // Hack? Assumes years are in 1000-9999 AD range
147 					break;
148 				case 'y':
149 					sink.put(toDecFixed!2(cast(uint)dt.year % 100));
150 					break;
151 
152 				// Time
153 				case 'a':
154 					sink.put(dt.hour < 12 ? "am" : "pm");
155 					break;
156 				case 'A':
157 					sink.put(dt.hour < 12 ? "AM" : "PM");
158 					break;
159 				// case 'B': TODO (Swatch Internet time)
160 				case 'g':
161 					putOneOrTwoDigits((dt.hour+11)%12 + 1);
162 					break;
163 				case 'G':
164 					putOneOrTwoDigits(dt.hour);
165 					break;
166 				case 'h':
167 					sink.put(toDecFixed!2(cast(uint)(dt.hour+11)%12 + 1));
168 					break;
169 				case 'H':
170 					sink.put(toDecFixed!2(dt.hour));
171 					break;
172 				case 'i':
173 					sink.put(toDecFixed!2(dt.minute));
174 					break;
175 				case 's':
176 					sink.put(toDecFixed!2(dt.second));
177 					break;
178 				case 'u':
179 					sink.put(toDecFixed!6(cast(uint)t.fracSecs.split!"usecs".usecs));
180 					break;
181 				case 'E': // not standard
182 					sink.put(toDecFixed!3(cast(uint)t.fracSecs.split!"msecs".msecs));
183 					break;
184 
185 				// Timezone
186 				case 'e':
187 					putTimezoneName(t.timezone.name);
188 					break;
189 				case 'I':
190 					sink.put(t.dstInEffect ? '1': '0');
191 					break;
192 				case 'O':
193 				{
194 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
195 					sink.reference.formattedWrite("%+03d%02d", minutes/60, abs(minutes%60));
196 					break;
197 				}
198 				case 'P':
199 				{
200 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
201 					sink.reference.formattedWrite("%+03d:%02d", minutes/60, abs(minutes%60));
202 					break;
203 				}
204 				case 'T':
205 					putTimezoneName(t.timezone.stdName);
206 					break;
207 				case 'Z':
208 					sink.putDecimal((t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000);
209 					break;
210 
211 				// Full date/time
212 				case 'c':
213 					sink.put(dt.toISOExtString());
214 					break;
215 				case 'r':
216 					putTime(sink, t, TimeFormats.RFC2822);
217 					break;
218 				case 'U':
219 					sink.putDecimal(t.toUnixTime());
220 					break;
221 
222 				// Escape next character
223 				case '\\':
224 					escaping = true;
225 					break;
226 
227 				// Other characters (whitespace, delimiters)
228 				default:
229 					put(sink, c);
230 			}
231 	}
232 }
233 
234 /// Format a SysTime using the format spec fmt.
235 /// This version generates specialized code for the given fmt.
236 string formatTime(string fmt)(SysTime t)
237 {
238 	enum maxSize = timeFormatSize(fmt);
239 	auto result = StringBuilder(maxSize);
240 	putTime!fmt(result, t);
241 	return result.get();
242 }
243 
244 /// ditto
245 void putTime(string fmt, S)(ref S sink, SysTime t)
246 	if (IsStringSink!S)
247 {
248 	putTimeImpl!fmt(sink, t);
249 }
250 
251 /// Format a SysTime using the format spec fmt.
252 /// This version parses fmt at runtime.
253 string formatTime(SysTime t, string fmt)
254 {
255 	auto result = StringBuilder(timeFormatSize(fmt));
256 	putTime(result, t, fmt);
257 	return result.get();
258 }
259 
260 /// ditto
261 deprecated string formatTime(string fmt, SysTime t = Clock.currTime())
262 {
263 	auto result = StringBuilder(48);
264 	putTime(result, fmt, t);
265 	return result.get();
266 }
267 
268 /// ditto
269 void putTime(S)(ref S sink, SysTime t, string fmt)
270 	if (IsStringSink!S)
271 {
272 	putTimeImpl!fmt(sink, t);
273 }
274 
275 /// ditto
276 deprecated void putTime(S)(ref S sink, string fmt, SysTime t = Clock.currTime())
277 	if (IsStringSink!S)
278 {
279 	putTimeImpl!fmt(sink, t);
280 }
281 
282 void putTimeImpl(alias fmt, S)(ref S sink, SysTime t)
283 {
284 	FormatContext!(char) context;
285 	context.t = t;
286 	context.dt = cast(DateTime)t;
287 	foreach (c; CTIterate!fmt)
288 		putToken!(c, context, sink)();
289 }
290 
291 unittest
292 {
293 	assert(SysTime.fromUnixTime(0, UTC()).formatTime!(TimeFormats.STD_DATE) == "Thu Jan 01 00:00:00 GMT+0000 1970");
294 	assert(SysTime(0, new immutable(SimpleTimeZone)(Duration.zero)).formatTime!"T" == "+00:00");
295 }