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