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 <ae@cy.md>
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 				case TimeFormatElement.nanoseconds: // not standard
186 					sink.put(toDecFixed!9(cast(uint)t.fracSecs.split!"nsecs".nsecs));
187 					break;
188 
189 				// Timezone
190 				case TimeFormatElement.timezoneName:
191 					putTimezoneName(t.timezone.name);
192 					break;
193 				case TimeFormatElement.isDST:
194 					sink.put(t.dstInEffect ? '1': '0');
195 					break;
196 				case TimeFormatElement.timezoneOffsetWithoutColon:
197 				{
198 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
199 					sink.reference.formattedWrite("%+03d%02d", minutes/60, abs(minutes%60));
200 					break;
201 				}
202 				case TimeFormatElement.timezoneOffsetWithColon:
203 				{
204 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
205 					sink.reference.formattedWrite("%+03d:%02d", minutes/60, abs(minutes%60));
206 					break;
207 				}
208 				case TimeFormatElement.timezoneAbbreviation:
209 					putTimezoneName(t.timezone.stdName);
210 					break;
211 				case TimeFormatElement.timezoneOffsetSeconds:
212 					sink.putDecimal((t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000);
213 					break;
214 
215 				// Full date/time
216 				case TimeFormatElement.dateTimeISO8601:
217 					sink.put(dt.toISOExtString());
218 					break;
219 				case TimeFormatElement.dateTimeRFC2822:
220 					putTime(sink, t, TimeFormats.RFC2822);
221 					break;
222 				case TimeFormatElement.dateTimeUNIX:
223 					sink.putDecimal(t.toUnixTime());
224 					break;
225 
226 				// Escape next character
227 				case TimeFormatElement.escapeNextCharacter:
228 					escaping = true;
229 					break;
230 
231 				// Other characters (whitespace, delimiters)
232 				default:
233 					put(sink, c);
234 			}
235 	}
236 }
237 
238 /// Format a SysTime using the format spec fmt.
239 /// This version generates specialized code for the given fmt.
240 string formatTime(string fmt)(SysTime t)
241 {
242 	enum maxSize = timeFormatSize(fmt);
243 	auto result = StringBuilder(maxSize);
244 	putTime!fmt(result, t);
245 	return result.get();
246 }
247 
248 /// ditto
249 void putTime(string fmt, S)(ref S sink, SysTime t)
250 	if (isStringSink!S)
251 {
252 	putTimeImpl!fmt(sink, t);
253 }
254 
255 /// Format a SysTime using the format spec fmt.
256 /// This version parses fmt at runtime.
257 string formatTime(SysTime t, string fmt)
258 {
259 	auto result = StringBuilder(timeFormatSize(fmt));
260 	putTime(result, t, fmt);
261 	return result.get();
262 }
263 
264 /// ditto
265 deprecated string formatTime(string fmt, SysTime t = Clock.currTime())
266 {
267 	auto result = StringBuilder(48);
268 	putTime(result, fmt, t);
269 	return result.get();
270 }
271 
272 /// ditto
273 void putTime(S)(ref S sink, SysTime t, string fmt)
274 	if (isStringSink!S)
275 {
276 	putTimeImpl!fmt(sink, t);
277 }
278 
279 /// ditto
280 deprecated void putTime(S)(ref S sink, string fmt, SysTime t = Clock.currTime())
281 	if (isStringSink!S)
282 {
283 	putTimeImpl!fmt(sink, t);
284 }
285 
286 private void putTimeImpl(alias fmt, S)(ref S sink, SysTime t)
287 {
288 	FormatContext!(char) context;
289 	context.t = t;
290 	context.dt = cast(DateTime)t;
291 	foreach (c; CTIterate!fmt)
292 		putToken!(c, context, sink)();
293 }
294 
295 unittest
296 {
297 	assert(SysTime.fromUnixTime(0, UTC()).formatTime!(TimeFormats.STD_DATE) == "Thu Jan 01 00:00:00 GMT+0000 1970");
298 	assert(SysTime(0, new immutable(SimpleTimeZone)(Duration.zero)).formatTime!"T" == "+00:00");
299 }