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 <ae@cy.md>
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 /// Common time format strings.
120 struct TimeFormats
121 {
122 static:
123 	const ATOM    = `Y-m-d\TH:i:sP`   ; ///
124 	const COOKIE  = `l, d-M-y H:i:s T`; ///
125 	const ISO8601 = `Y-m-d\TH:i:sO`   ; ///
126 	const RFC822  = `D, d M y H:i:s O`; ///
127 	const RFC850  = `l, d-M-y H:i:s T`; ///
128 	const RFC1036 = `D, d M y H:i:s O`; ///
129 	const RFC1123 = `D, d M Y H:i:s O`; ///
130 	const RFC2822 = `D, d M Y H:i:s O`; ///
131 	const RFC3339 = `Y-m-d\TH:i:sP`   ; ///
132 	const RSS     = `D, d M Y H:i:s O`; ///
133 	const W3C     = `Y-m-d\TH:i:sP`   ; ///
134 
135 	const CTIME = `D M d H:i:s Y`; /// ctime/localtime format
136 
137 	const HTML5DATE = `Y-m-d`; /// As used in HTML type="date" inputs.
138 
139 	/// Format produced by std.date.toString, e.g. "Tue Jun 07 13:23:19 GMT+0100 2011"
140 	const STD_DATE = `D M d H:i:s \G\M\TO Y`;
141 }
142 
143 /// We assume that no timezone will have a name longer than this.
144 /// If one does, it is truncated to this length.
145 enum MaxTimezoneNameLength = 256;
146 
147 /// Calculate the maximum amount of characters needed to store a time in this format.
148 /// Can be evaluated at compile-time.
149 size_t timeFormatSize(string fmt)
150 {
151 	import std.algorithm.iteration : map, reduce;
152 	import std.algorithm.comparison : max;
153 
154 	static size_t maxLength(in string[] names) { return reduce!max(map!`a.length`(WeekdayShortNames)); }
155 
156 	size_t size = 0;
157 	bool escaping = false;
158 	foreach (char c; fmt)
159 		if (escaping)
160 			size++, escaping = false;
161 		else
162 			switch (c)
163 			{
164 				case TimeFormatElement.dayOfWeekIndexISO8601:
165 				case TimeFormatElement.dayOfWeekIndex:
166 				case TimeFormatElement.yearIsLeapYear:
167 				case TimeFormatElement.isDST:
168 					size++;
169 					break;
170 				case TimeFormatElement.dayOfMonthZeroPadded:
171 				case TimeFormatElement.dayOfMonth:
172 				case TimeFormatElement.dayOfMonthOrdinalSuffix:
173 				case TimeFormatElement.weekOfYear:
174 				case TimeFormatElement.monthZeroPadded:
175 				case TimeFormatElement.month:
176 				case TimeFormatElement.daysInMonth:
177 				case TimeFormatElement.yearOfCentury:
178 				case TimeFormatElement.ampmLower:
179 				case TimeFormatElement.ampmUpper:
180 				case TimeFormatElement.hour12:
181 				case TimeFormatElement.hour:
182 				case TimeFormatElement.hour12ZeroPadded:
183 				case TimeFormatElement.hourZeroPadded:
184 				case TimeFormatElement.minute:
185 				case TimeFormatElement.second:
186 					size += 2;
187 					break;
188 				case TimeFormatElement.dayOfYear:
189 				case TimeFormatElement.milliseconds:
190 				case TimeFormatElement.millisecondsAlt: // not standard
191 					size += 3;
192 					break;
193 				case TimeFormatElement.year:
194 					size += 4;
195 					break;
196 				case TimeFormatElement.timezoneOffsetSeconds: // Timezone offset in seconds
197 				case TimeFormatElement.timezoneOffsetWithoutColon:
198 					size += 5;
199 					break;
200 				case TimeFormatElement.microseconds:
201 				case TimeFormatElement.timezoneOffsetWithColon:
202 					size += 6;
203 					break;
204 				case TimeFormatElement.nanoseconds:
205 					size += 9;
206 					break;
207 				case TimeFormatElement.timezoneAbbreviation:
208 					size += 32;
209 					break;
210 
211 				case TimeFormatElement.dayOfWeekNameShort:
212 					size += maxLength(WeekdayShortNames);
213 					break;
214 				case TimeFormatElement.dayOfWeekName:
215 					size += maxLength(WeekdayLongNames);
216 					break;
217 				case TimeFormatElement.monthName:
218 					size += maxLength(MonthLongNames);
219 					break;
220 				case TimeFormatElement.monthNameShort:
221 					size += maxLength(MonthShortNames);
222 					break;
223 
224 				case TimeFormatElement.timezoneName: // Timezone name
225 					return MaxTimezoneNameLength;
226 
227 				// Full date/time
228 				case TimeFormatElement.dateTimeISO8601:
229 					enum ISOExtLength = "-0004-01-05T00:00:02.052092+10:00".length;
230 					size += ISOExtLength;
231 					break;
232 				case TimeFormatElement.dateTimeRFC2822:
233 					size += timeFormatSize(TimeFormats.RFC2822);
234 					break;
235 				case TimeFormatElement.dateTimeUNIX:
236 					size += DecimalSize!time_t;
237 					break;
238 
239 				// Escape next character
240 				case TimeFormatElement.escapeNextCharacter:
241 					escaping = true;
242 					break;
243 
244 				// Other characters (whitespace, delimiters)
245 				default:
246 					size++;
247 			}
248 
249 	return size;
250 }
251 
252 static assert(timeFormatSize(TimeFormats.STD_DATE) == "Tue Jun 07 13:23:19 GMT+0100 2011".length);
253 
254 // ***************************************************************************
255 
256 /// English short and long weekday and month names, used when parsing and stringifying dates.
257 const WeekdayShortNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
258 const WeekdayLongNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; /// ditto
259 const MonthShortNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; /// ditto
260 const MonthLongNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; /// ditto
261