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