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 import ae.utils.time.types : AbsTime;
27 
28 private struct FormatContext
29 {
30 	SysTime t;
31 	DateTime dt;
32 	bool escaping;
33 }
34 
35 private FormatContext makeContext(SysTime t) { return FormatContext(t, cast(DateTime)t); }
36 private FormatContext makeContext(DateTime t) { return FormatContext(SysTime(t), t); }
37 private FormatContext makeContext(Date t) { return FormatContext(SysTime(t), DateTime(t)); }
38 private FormatContext makeContext(AbsTime t) { auto s = t.sysTime(UTC()); return FormatContext(s, cast(DateTime)s); }
39 // TODO: TimeOfDay support
40 
41 private void putToken(alias c, alias context, alias sink)()
42 {
43 	with (context)
44 	{
45 		void putOneDigit(uint i)
46 		{
47 			debug assert(i < 10);
48 			sink.put(cast(char)('0' + i));
49 		}
50 
51 		void putOneOrTwoDigits(uint i)
52 		{
53 			debug assert(i < 100);
54 			if (i >= 10)
55 			{
56 				sink.put(cast(char)('0' + (i / 10)));
57 				sink.put(cast(char)('0' + (i % 10)));
58 			}
59 			else
60 				sink.put(cast(char)('0' +  i      ));
61 		}
62 
63 		void putTimezoneName(string tzStr)
64 		{
65 			if (tzStr.length)
66 				sink.put(tzStr[0..min($, MaxTimezoneNameLength)]);
67 			else
68 		//	if (t.timezone.utcToTZ(t.stdTime) == t.stdTime)
69 		//		sink.put("UTC");
70 		//	else
71 			{
72 				enum fmt = TimeFormatElement.timezoneOffsetWithColon;
73 				putToken!(fmt, context, sink)();
74 			}
75 		}
76 
77 		if (escaping)
78 			sink.put(c), escaping = false;
79 		else
80 			switch (c)
81 			{
82 				// Day
83 				case TimeFormatElement.dayOfMonthZeroPadded:
84 					sink.put(toDecFixed!2(dt.day));
85 					break;
86 				case TimeFormatElement.dayOfWeekNameShort:
87 					sink.put(WeekdayShortNames[dt.dayOfWeek]);
88 					break;
89 				case TimeFormatElement.dayOfMonth:
90 					putOneOrTwoDigits(dt.day);
91 					break;
92 				case TimeFormatElement.dayOfWeekName:
93 					sink.put(WeekdayLongNames[dt.dayOfWeek]);
94 					break;
95 				case TimeFormatElement.dayOfWeekIndexISO8601:
96 					putOneDigit((dt.dayOfWeek+6)%7 + 1);
97 					break;
98 				case TimeFormatElement.dayOfMonthOrdinalSuffix:
99 					switch (dt.day)
100 					{
101 						case 1:
102 						case 21:
103 						case 31:
104 							sink.put("st");
105 							break;
106 						case 2:
107 						case 22:
108 							sink.put("nd");
109 							break;
110 						case 3:
111 						case 23:
112 							sink.put("rd");
113 							break;
114 						default:
115 							sink.put("th");
116 					}
117 					break;
118 				case TimeFormatElement.dayOfWeekIndex:
119 					putOneDigit(cast(int)dt.dayOfWeek);
120 					break;
121 				case TimeFormatElement.dayOfYear:
122 					sink.put(text(dt.dayOfYear-1));
123 					break;
124 
125 				// Week
126 				case TimeFormatElement.weekOfYear:
127 					sink.put(toDecFixed!2(dt.isoWeek));
128 					break;
129 
130 				// Month
131 				case TimeFormatElement.monthName:
132 					sink.put(MonthLongNames[dt.month-1]);
133 					break;
134 				case TimeFormatElement.monthZeroPadded:
135 					sink.put(toDecFixed!2(dt.month));
136 					break;
137 				case TimeFormatElement.monthNameShort:
138 					sink.put(MonthShortNames[dt.month-1]);
139 					break;
140 				case TimeFormatElement.month:
141 					putOneOrTwoDigits(dt.month);
142 					break;
143 				case TimeFormatElement.daysInMonth:
144 					putOneOrTwoDigits(dt.daysInMonth);
145 					break;
146 
147 				// Year
148 				case TimeFormatElement.yearIsLeapYear:
149 					sink.put(dt.isLeapYear ? '1' : '0');
150 					break;
151 				// case TimeFormatElement.yearForWeekNumbering: TODO (ISO 8601 year number)
152 				case TimeFormatElement.year:
153 					sink.put(toDecFixed!4(cast(uint)dt.year)); // Hack? Assumes years are in 1000-9999 AD range
154 					break;
155 				case TimeFormatElement.yearOfCentury:
156 					sink.put(toDecFixed!2(cast(uint)dt.year % 100));
157 					break;
158 
159 				// Time
160 				case TimeFormatElement.ampmLower:
161 					sink.put(dt.hour < 12 ? "am" : "pm");
162 					break;
163 				case TimeFormatElement.ampmUpper:
164 					sink.put(dt.hour < 12 ? "AM" : "PM");
165 					break;
166 				// case TimeFormatElement.swatchInternetTime: TODO (Swatch Internet time)
167 				case TimeFormatElement.hour12:
168 					putOneOrTwoDigits((dt.hour+11)%12 + 1);
169 					break;
170 				case TimeFormatElement.hour:
171 					putOneOrTwoDigits(dt.hour);
172 					break;
173 				case TimeFormatElement.hour12ZeroPadded:
174 					sink.put(toDecFixed!2(cast(uint)(dt.hour+11)%12 + 1));
175 					break;
176 				case TimeFormatElement.hourZeroPadded:
177 					sink.put(toDecFixed!2(dt.hour));
178 					break;
179 				case TimeFormatElement.minute:
180 					sink.put(toDecFixed!2(dt.minute));
181 					break;
182 				case TimeFormatElement.second:
183 					sink.put(toDecFixed!2(dt.second));
184 					break;
185 				case TimeFormatElement.microseconds:
186 					sink.put(toDecFixed!6(cast(uint)t.fracSecs.split!"usecs".usecs));
187 					break;
188 				case TimeFormatElement.milliseconds:
189 				case TimeFormatElement.millisecondsAlt: // not standard
190 					sink.put(toDecFixed!3(cast(uint)t.fracSecs.split!"msecs".msecs));
191 					break;
192 				case TimeFormatElement.nanoseconds: // not standard
193 					sink.put(toDecFixed!9(cast(uint)t.fracSecs.split!"nsecs".nsecs));
194 					break;
195 
196 				// Timezone
197 				case TimeFormatElement.timezoneName:
198 					putTimezoneName(t.timezone.name);
199 					break;
200 				case TimeFormatElement.isDST:
201 					sink.put(t.dstInEffect ? '1': '0');
202 					break;
203 				case TimeFormatElement.timezoneOffsetWithoutColon:
204 				{
205 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
206 					sink.reference.formattedWrite("%+03d%02d", minutes/60, abs(minutes%60));
207 					break;
208 				}
209 				case TimeFormatElement.timezoneOffsetWithColon:
210 				{
211 					auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60;
212 					sink.reference.formattedWrite("%+03d:%02d", minutes/60, abs(minutes%60));
213 					break;
214 				}
215 				case TimeFormatElement.timezoneAbbreviation:
216 					putTimezoneName(t.timezone.stdName);
217 					break;
218 				case TimeFormatElement.timezoneOffsetSeconds:
219 					sink.putDecimal((t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000);
220 					break;
221 
222 				// Full date/time
223 				case TimeFormatElement.dateTimeISO8601:
224 					sink.put(dt.toISOExtString());
225 					break;
226 				case TimeFormatElement.dateTimeRFC2822:
227 					putTime(sink, t, TimeFormats.RFC2822);
228 					break;
229 				case TimeFormatElement.dateTimeUNIX:
230 					sink.putDecimal(t.toUnixTime());
231 					break;
232 
233 				// Escape next character
234 				case TimeFormatElement.escapeNextCharacter:
235 					escaping = true;
236 					break;
237 
238 				// Other characters (whitespace, delimiters)
239 				default:
240 					put(sink, c);
241 			}
242 	}
243 }
244 
245 enum isFormattableTime(T) = is(typeof({ T t = void; return makeContext(t); }));
246 
247 /// Format a time value using the format spec fmt.
248 /// This version generates specialized code for the given fmt.
249 string formatTime(string fmt, Time)(Time t)
250 if (isFormattableTime!Time)
251 {
252 	enum maxSize = timeFormatSize(fmt);
253 	auto result = StringBuilder(maxSize);
254 	putTime!fmt(result, t);
255 	return result.get();
256 }
257 
258 /// ditto
259 void putTime(string fmt, S, Time)(ref S sink, Time t)
260 if (isStringSink!S && isFormattableTime!Time)
261 {
262 	putTimeImpl!fmt(sink, t);
263 }
264 
265 /// Format a time value using the format spec fmt.
266 /// This version parses fmt at runtime.
267 string formatTime(Time)(Time t, string fmt)
268 if (isFormattableTime!Time)
269 {
270 	auto result = StringBuilder(timeFormatSize(fmt));
271 	putTime(result, t, fmt);
272 	return result.get();
273 }
274 
275 /// ditto
276 deprecated string formatTime(string fmt, SysTime t = Clock.currTime())
277 {
278 	auto result = StringBuilder(48);
279 	putTime(result, fmt, t);
280 	return result.get();
281 }
282 
283 /// ditto
284 void putTime(S, Time)(ref S sink, Time t, string fmt)
285 if (isStringSink!S && isFormattableTime!Time)
286 {
287 	putTimeImpl!fmt(sink, t);
288 }
289 
290 /// ditto
291 deprecated void putTime(S)(ref S sink, string fmt, SysTime t = Clock.currTime())
292 if (isStringSink!S)
293 {
294 	putTimeImpl!fmt(sink, t);
295 }
296 
297 private void putTimeImpl(alias fmt, S, Time)(ref S sink, Time t)
298 if (isFormattableTime!Time)
299 {
300 	auto context = makeContext(t);
301 	foreach (c; CTIterate!fmt)
302 		putToken!(c, context, sink)();
303 }
304 
305 unittest
306 {
307 	assert(SysTime.fromUnixTime(0, UTC()).formatTime!(TimeFormats.STD_DATE) == "Thu Jan 01 00:00:00 GMT+0000 1970");
308 	assert(SysTime(0, new immutable(SimpleTimeZone)(Duration.zero)).formatTime!"T" == "+00:00");
309 
310 	assert((cast(DateTime)SysTime.fromUnixTime(0, UTC())).formatTime!(TimeFormats.HTML5DATE) == "1970-01-01");
311 
312 	assert(AbsTime(1).formatTime!(TimeFormats.HTML5DATE) == "0001-01-01");
313 }