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 = TimeFormatElement.timezoneOffsetWithColon;
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 TimeFormatElement.dayOfMonthZeroPadded:
77 					sink.put(toDecFixed!2(dt.day));
78 					break;
79 				case TimeFormatElement.dayOfWeekNameShort:
80 					sink.put(WeekdayShortNames[dt.dayOfWeek]);
81 					break;
82 				case TimeFormatElement.dayOfMonth:
83 					putOneOrTwoDigits(dt.day);
84 					break;
85 				case TimeFormatElement.dayOfWeekName:
86 					sink.put(WeekdayLongNames[dt.dayOfWeek]);
87 					break;
88 				case TimeFormatElement.dayOfWeekIndexISO8601:
89 					putOneDigit((dt.dayOfWeek+6)%7 + 1);
90 					break;
91 				case TimeFormatElement.dayOfMonthOrdinalSuffix:
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 TimeFormatElement.dayOfWeekIndex:
112 					putOneDigit(cast(int)dt.dayOfWeek);
113 					break;
114 				case TimeFormatElement.dayOfYear:
115 					sink.put(text(dt.dayOfYear-1));
116 					break;
117 
118 				// Week
119 				case TimeFormatElement.weekOfYear:
120 					sink.put(toDecFixed!2(dt.isoWeek));
121 					break;
122 
123 				// Month
124 				case TimeFormatElement.monthName:
125 					sink.put(MonthLongNames[dt.month-1]);
126 					break;
127 				case TimeFormatElement.monthZeroPadded:
128 					sink.put(toDecFixed!2(dt.month));
129 					break;
130 				case TimeFormatElement.monthNameShort:
131 					sink.put(MonthShortNames[dt.month-1]);
132 					break;
133 				case TimeFormatElement.month:
134 					putOneOrTwoDigits(dt.month);
135 					break;
136 				case TimeFormatElement.daysInMonth:
137 					putOneOrTwoDigits(dt.daysInMonth);
138 					break;
139 
140 				// Year
141 				case TimeFormatElement.yearIsLeapYear:
142 					sink.put(dt.isLeapYear ? '1' : '0');
143 					break;
144 				// case TimeFormatElement.yearForWeekNumbering: TODO (ISO 8601 year number)
145 				case TimeFormatElement.year:
146 					sink.put(toDecFixed!4(cast(uint)dt.year)); // Hack? Assumes years are in 1000-9999 AD range
147 					break;
148 				case TimeFormatElement.yearOfCentury:
149 					sink.put(toDecFixed!2(cast(uint)dt.year % 100));
150 					break;
151 
152 				// Time
153 				case TimeFormatElement.ampmLower:
154 					sink.put(dt.hour < 12 ? "am" : "pm");
155 					break;
156 				case TimeFormatElement.ampmUpper:
157 					sink.put(dt.hour < 12 ? "AM" : "PM");
158 					break;
159 				// case TimeFormatElement.swatchInternetTime: TODO (Swatch Internet time)
160 				case TimeFormatElement.hour12:
161 					putOneOrTwoDigits((dt.hour+11)%12 + 1);
162 					break;
163 				case TimeFormatElement.hour:
164 					putOneOrTwoDigits(dt.hour);
165 					break;
166 				case TimeFormatElement.hour12ZeroPadded:
167 					sink.put(toDecFixed!2(cast(uint)(dt.hour+11)%12 + 1));
168 					break;
169 				case TimeFormatElement.hourZeroPadded:
170 					sink.put(toDecFixed!2(dt.hour));
171 					break;
172 				case TimeFormatElement.minute:
173 					sink.put(toDecFixed!2(dt.minute));
174 					break;
175 				case TimeFormatElement.second:
176 					sink.put(toDecFixed!2(dt.second));
177 					break;
178 				case TimeFormatElement.microseconds:
179 					sink.put(toDecFixed!6(cast(uint)t.fracSecs.split!"usecs".usecs));
180 					break;
181 				case TimeFormatElement.milliseconds:
182 				case TimeFormatElement.millisecondsAlt: // not standard
183 					sink.put(toDecFixed!3(cast(uint)t.fracSecs.split!"msecs".msecs));
184 					break;
185 
186 				// Timezone
187 				case TimeFormatElement.timezoneName:
188 					putTimezoneName(t.timezone.name);
189 					break;
190 				case TimeFormatElement.isDST:
191 					sink.put(t.dstInEffect ? '1': '0');
192 					break;
193 				case TimeFormatElement.timezoneOffsetWithoutColon:
194 				{
195 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
196 					sink.reference.formattedWrite("%+03d%02d", minutes/60, abs(minutes%60));
197 					break;
198 				}
199 				case TimeFormatElement.timezoneOffsetWithColon:
200 				{
201 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
202 					sink.reference.formattedWrite("%+03d:%02d", minutes/60, abs(minutes%60));
203 					break;
204 				}
205 				case TimeFormatElement.timezoneAbbreviation:
206 					putTimezoneName(t.timezone.stdName);
207 					break;
208 				case TimeFormatElement.timezoneOffsetSeconds:
209 					sink.putDecimal((t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000);
210 					break;
211 
212 				// Full date/time
213 				case TimeFormatElement.dateTimeISO8601:
214 					sink.put(dt.toISOExtString());
215 					break;
216 				case TimeFormatElement.dateTimeRFC2822:
217 					putTime(sink, t, TimeFormats.RFC2822);
218 					break;
219 				case TimeFormatElement.dateTimeUNIX:
220 					sink.putDecimal(t.toUnixTime());
221 					break;
222 
223 				// Escape next character
224 				case TimeFormatElement.escapeNextCharacter:
225 					escaping = true;
226 					break;
227 
228 				// Other characters (whitespace, delimiters)
229 				default:
230 					put(sink, c);
231 			}
232 	}
233 }
234 
235 /// Format a SysTime using the format spec fmt.
236 /// This version generates specialized code for the given fmt.
237 string formatTime(string fmt)(SysTime t)
238 {
239 	enum maxSize = timeFormatSize(fmt);
240 	auto result = StringBuilder(maxSize);
241 	putTime!fmt(result, t);
242 	return result.get();
243 }
244 
245 /// ditto
246 void putTime(string fmt, S)(ref S sink, SysTime t)
247 	if (isStringSink!S)
248 {
249 	putTimeImpl!fmt(sink, t);
250 }
251 
252 /// Format a SysTime using the format spec fmt.
253 /// This version parses fmt at runtime.
254 string formatTime(SysTime t, string fmt)
255 {
256 	auto result = StringBuilder(timeFormatSize(fmt));
257 	putTime(result, t, fmt);
258 	return result.get();
259 }
260 
261 /// ditto
262 deprecated string formatTime(string fmt, SysTime t = Clock.currTime())
263 {
264 	auto result = StringBuilder(48);
265 	putTime(result, fmt, t);
266 	return result.get();
267 }
268 
269 /// ditto
270 void putTime(S)(ref S sink, SysTime t, string fmt)
271 	if (isStringSink!S)
272 {
273 	putTimeImpl!fmt(sink, t);
274 }
275 
276 /// ditto
277 deprecated void putTime(S)(ref S sink, string fmt, SysTime t = Clock.currTime())
278 	if (isStringSink!S)
279 {
280 	putTimeImpl!fmt(sink, t);
281 }
282 
283 void putTimeImpl(alias fmt, S)(ref S sink, SysTime t)
284 {
285 	FormatContext!(char) context;
286 	context.t = t;
287 	context.dt = cast(DateTime)t;
288 	foreach (c; CTIterate!fmt)
289 		putToken!(c, context, sink)();
290 }
291 
292 unittest
293 {
294 	assert(SysTime.fromUnixTime(0, UTC()).formatTime!(TimeFormats.STD_DATE) == "Thu Jan 01 00:00:00 GMT+0000 1970");
295 	assert(SysTime(0, new immutable(SimpleTimeZone)(Duration.zero)).formatTime!"T" == "+00:00");
296 }