1 /**
2  * Time formats for string formatting and parsing.
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.common;
15 
16 import core.stdc.time : time_t;
17 
18 import ae.utils.text;
19 
20 /// Based on php.net/date
21 enum TimeFormatElement : char
22 {
23 	/// Year, all digits
24 	year                        = 'Y',
25 	/// Year, last 2 digits
26 	yearOfCentury               = 'y',
27 	// /// ISO-8601 week-numbering year
28 	// yearForWeekNumbering        = 'o',
29 	/// '1' if the year is a leap year, '0' otherwise
30 	yearIsLeapYear              = 'L',
31 
32 	/// Month index, 1 or 2 digits (1 = January)
33 	month                       = 'n',
34 	/// Month index, 2 digits with leading zeroes (01 = January)
35 	monthZeroPadded             = 'm',
36 	/// Month name, full ("January", "February" ...)
37 	monthName                   = 'F',
38 	/// Month name, three letters ("Jan", "Feb" ...)
39 	monthNameShort              = 'M',
40 	/// Number of days within the month, 2 digits
41 	daysInMonth                 = 't',
42 
43 	/// ISO-8601 week index
44 	weekOfYear                  = 'W',
45 
46 	/// Day of year (January 1st = 0)
47 	dayOfYear                   = 'z',
48 
49 	/// Day of month, 1 or 2 digits
50 	dayOfMonth                  = 'j',
51 	/// Day of month, 2 digits with leading zeroes
52 	dayOfMonthZeroPadded        = 'd',
53 	/// English ordinal suffix for the day of month, 2 characters
54 	dayOfMonthOrdinalSuffix     = 'S',
55 
56 	/// Weekday index (0 = Sunday, 1 = Monday, ... 6 = Saturday)
57 	dayOfWeekIndex              = 'w',
58 	/// Weekday index, ISO-8601 numerical representation (1 = Monday, 2 = Tuesday, ... 7 = Sunday)
59 	dayOfWeekIndexISO8601       = 'N',
60 	/// Weekday name, three letters ("Mon", "Tue", ...)
61 	dayOfWeekNameShort          = 'D',
62 	/// Weekday name, full ("Monday", "Tuesday", ...)
63 	dayOfWeekName               = 'l',
64 
65 	// /// Swatch Internet time
66 	// swatchInternetTime          = 'B',
67 
68 	/// "am" / "pm"
69 	ampmLower                   = 'a',
70 	/// "AM" / "PM"
71 	ampmUpper                   = 'A',
72 
73 	/// Hour (24-hour format), 1 or 2 digits
74 	hour                        = 'G',
75 	/// Hour (24-hour format), 2 digits with leading zeroes
76 	hourZeroPadded              = 'H',
77 	/// Hour (12-hour format), 1 or 2 digits (12 = midnight/noon)
78 	hour12                      = 'g',
79 	/// Hour (12-hour format), 2 digits with leading zeroes (12 = midnight/noon)
80 	hour12ZeroPadded            = 'h',
81 
82 	/// Minute, 2 digits with leading zeroes
83 	minute                      = 'i',
84 	/// Second, 2 digits with leading zeroes
85 	second                      = 's',
86 	/// Milliseconds within second, 3 digits
87 	milliseconds                = 'v',
88 	/// Milliseconds within second, 3 digits (ae extension)
89 	millisecondsAlt             = 'E',
90 	/// Microseconds within second, 6 digits
91 	microseconds                = 'u',
92 	/// Nanoseconds within second, 9 digits (ae extension)
93 	nanoseconds                 = '9',
94 
95 	/// Timezone identifier
96 	timezoneName                = 'e',
97 	/// Timezone abbreviation (e.g. "EST")
98 	timezoneAbbreviation        = 'T',
99 	/// Difference from GMT, with colon (e.g. "+02:00")
100 	timezoneOffsetWithColon     = 'P',
101 	/// Difference from GMT, without colon (e.g. "+0200")
102 	timezoneOffsetWithoutColon  = 'O',
103 	/// Difference from GMT in seconds
104 	timezoneOffsetSeconds       = 'Z',
105 	/// '1' if DST is in effect, '0' otherwise
106 	isDST                       = 'I',
107 
108 	/// Full ISO 8601 date/time (e.g. "2004-02-12T15:19:21+00:00")
109 	dateTimeISO8601             = 'c',
110 	/// Full RFC 2822 date/time (e.g. "Thu, 21 Dec 2000 16:01:07 +0200")
111 	dateTimeRFC2822             = 'r',
112 	/// UNIX time (seconds since January 1 1970 00:00:00 UTC)
113 	dateTimeUNIX                = 'U',
114 
115 	/// Treat the next character verbatim (copy when formatting, expect when parsing)
116 	escapeNextCharacter         = '\\',
117 }
118 
119 struct TimeFormats
120 {
121 static:
122 	const ATOM = `Y-m-d\TH:i:sP`;
123 	const COOKIE = `l, d-M-y H:i:s T`;
124 	const ISO8601 = `Y-m-d\TH:i:sO`;
125 	const RFC822 = `D, d M y H:i:s O`;
126 	const RFC850 = `l, d-M-y H:i:s T`;
127 	const RFC1036 = `D, d M y H:i:s O`;
128 	const RFC1123 = `D, d M Y H:i:s O`;
129 	const RFC2822 = `D, d M Y H:i:s O`;
130 	const RFC3339 = `Y-m-d\TH:i:sP`;
131 	const RSS = `D, d M Y H:i:s O`;
132 	const W3C = `Y-m-d\TH:i:sP`;
133 
134 	const CTIME = `D M d H:i:s Y`; /// ctime/localtime format
135 
136 	const HTML5DATE = `Y-m-d`;
137 
138 	/// Format produced by std.date.toString, e.g. "Tue Jun 07 13:23:19 GMT+0100 2011"
139 	const STD_DATE = `D M d H:i:s \G\M\TO Y`;
140 }
141 
142 /// We assume that no timezone will have a name longer than this.
143 /// If one does, it is truncated to this length.
144 enum MaxTimezoneNameLength = 256;
145 
146 /// Calculate the maximum amount of characters needed to store a time in this format.
147 /// Can be evaluated at compile-time.
148 size_t timeFormatSize(string fmt)
149 {
150 	import std.algorithm.iteration : map, reduce;
151 	import std.algorithm.comparison : max;
152 
153 	static size_t maxLength(in string[] names) { return reduce!max(map!`a.length`(WeekdayShortNames)); }
154 
155 	size_t size = 0;
156 	bool escaping = false;
157 	foreach (char c; fmt)
158 		if (escaping)
159 			size++, escaping = false;
160 		else
161 			switch (c)
162 			{
163 				case TimeFormatElement.dayOfWeekIndexISO8601:
164 				case TimeFormatElement.dayOfWeekIndex:
165 				case TimeFormatElement.yearIsLeapYear:
166 				case TimeFormatElement.isDST:
167 					size++;
168 					break;
169 				case TimeFormatElement.dayOfMonthZeroPadded:
170 				case TimeFormatElement.dayOfMonth:
171 				case TimeFormatElement.dayOfMonthOrdinalSuffix:
172 				case TimeFormatElement.weekOfYear:
173 				case TimeFormatElement.monthZeroPadded:
174 				case TimeFormatElement.month:
175 				case TimeFormatElement.daysInMonth:
176 				case TimeFormatElement.yearOfCentury:
177 				case TimeFormatElement.ampmLower:
178 				case TimeFormatElement.ampmUpper:
179 				case TimeFormatElement.hour12:
180 				case TimeFormatElement.hour:
181 				case TimeFormatElement.hour12ZeroPadded:
182 				case TimeFormatElement.hourZeroPadded:
183 				case TimeFormatElement.minute:
184 				case TimeFormatElement.second:
185 					size += 2;
186 					break;
187 				case TimeFormatElement.dayOfYear:
188 				case TimeFormatElement.milliseconds:
189 				case TimeFormatElement.millisecondsAlt: // not standard
190 					size += 3;
191 					break;
192 				case TimeFormatElement.year:
193 					size += 4;
194 					break;
195 				case TimeFormatElement.timezoneOffsetSeconds: // Timezone offset in seconds
196 				case TimeFormatElement.timezoneOffsetWithoutColon:
197 					size += 5;
198 					break;
199 				case TimeFormatElement.microseconds:
200 				case TimeFormatElement.timezoneOffsetWithColon:
201 					size += 6;
202 					break;
203 				case TimeFormatElement.nanoseconds:
204 					size += 9;
205 					break;
206 				case TimeFormatElement.timezoneAbbreviation:
207 					size += 32;
208 					break;
209 
210 				case TimeFormatElement.dayOfWeekNameShort:
211 					size += maxLength(WeekdayShortNames);
212 					break;
213 				case TimeFormatElement.dayOfWeekName:
214 					size += maxLength(WeekdayLongNames);
215 					break;
216 				case TimeFormatElement.monthName:
217 					size += maxLength(MonthLongNames);
218 					break;
219 				case TimeFormatElement.monthNameShort:
220 					size += maxLength(MonthShortNames);
221 					break;
222 
223 				case TimeFormatElement.timezoneName: // Timezone name
224 					return MaxTimezoneNameLength;
225 
226 				// Full date/time
227 				case TimeFormatElement.dateTimeISO8601:
228 					enum ISOExtLength = "-0004-01-05T00:00:02.052092+10:00".length;
229 					size += ISOExtLength;
230 					break;
231 				case TimeFormatElement.dateTimeRFC2822:
232 					size += timeFormatSize(TimeFormats.RFC2822);
233 					break;
234 				case TimeFormatElement.dateTimeUNIX:
235 					size += DecimalSize!time_t;
236 					break;
237 
238 				// Escape next character
239 				case TimeFormatElement.escapeNextCharacter:
240 					escaping = true;
241 					break;
242 
243 				// Other characters (whitespace, delimiters)
244 				default:
245 					size++;
246 			}
247 
248 	return size;
249 }
250 
251 static assert(timeFormatSize(TimeFormats.STD_DATE) == "Tue Jun 07 13:23:19 GMT+0100 2011".length);
252 
253 // ***************************************************************************
254 
255 const WeekdayShortNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
256 const WeekdayLongNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
257 const MonthShortNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
258 const MonthLongNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
259