1 /**
2  * Proleptic Gregorian calendar: dates
3  *
4  * Copyright: Maxim Freck, 2016–2017.
5  * Authors:   Maxim Freck <maxim@freck.pp.ru>
6  * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7  */
8 module pgc.date;
9 
10 public import pgc.exception;
11 
12 alias Date = uint;
13 
14 private enum int YEAR_MASK = 0x7FFFFF; //8_388_607
15 /// 4_194_303 – maximum year value is 4_194_303 BCE or 4_194_303 CE
16 enum int MAX_YEAR  = 0x3FFFFF; 
17 /// Common era start 01.01.01
18 enum int CE_START  = 0x80000021;
19 
20 ///Date era enum
21 enum Era:byte {
22 	BCE = -1,
23 	CE  = 1,
24 }
25 
26 /***********************************
27  * Returns: Date uint for a given date (day, month and year)
28  *
29  * Params:
30  *  day   = the number of the day
31  *  month = the number of the month
32  *  year  = the number of the year
33  */
34 pure Date mkDate(int day, int month, int year) @safe
35 {
36 	assertDate(day, month, year);
37 	return ( (day & 0x1F) | ((month & 0x0F) << 5) | (((year+MAX_YEAR)&YEAR_MASK) << 9) );
38 }
39 
40 /***********************************
41  * Returns: Date uint for a given date using ISO year (1 BC = 0, 2 BC = -1 and so on)
42  *
43  * Params:
44  *  day   = the number of the day
45  *  month = the number of the month
46  *  year  = the number of the year accroding to ISO 8601
47  */
48 pure Date mkDateISO(int day, int month, int year) @safe
49 {
50 	return mkDate(day, month, (year < 1) ? --year : year);
51 }
52 
53 /***********************************
54  * Checks the validity of a date
55  *
56  * Params:
57  *  day   = the number of the day
58  *  month = the number of the month
59  *  year  = the number of the year
60  */
61 pure private void assertDate(int day, int month, int year) @safe
62 {
63 	import std.conv: to;
64 	if (year < -MAX_YEAR || year == 0 || year > MAX_YEAR) {
65 		throw new PgcException("The absolute year value "~to!string(year)~" is out of bounds [1..4 194 303]");
66 	}
67 	if (month < 1 || month > 12) {
68 		throw new PgcException("The month value "~to!string(month)~" is out of bounds [1..12]");
69 	}
70 	auto maxDay = daysInMonth(month, year);
71 	if (day < 1 || day > maxDay) {
72 		throw new PgcException(
73 			"The day value "~to!string(day)~" is out of bounds [1.."~to!string(maxDay)~
74 			"] (month "~to!string(month)~", year "~to!string(year)~")"
75 		);
76 	}
77 }
78 
79 /***********************************
80  * Returns: the quantity of days in a given month
81  *
82  * Params:
83  *  month = the number of the month
84  *  year  = the number of the year
85  */
86 @safe pure nothrow ubyte daysInMonth(int month, int year)
87 {
88 	switch (month) {
89 		case 4:
90 		case 6:
91 		case 9:
92 		case 11:
93 			return 30;
94 		case 2:
95 			return year.isLeap ? 29 : 28;
96 		default:
97 			return 31;
98 	}
99 }
100 
101 /***********************************
102  * Returns: true for a Leap year
103  *
104  * Params:
105  *  year  = the number of the year
106  */
107 pure nothrow bool isLeap(int year) @safe @nogc
108 {
109 	if (year <= 0) year++;
110 	if (year%4 != 0) return false;
111 	if (year%100 != 0) return true;
112 	if (year%400 != 0) return false;
113 	return true;
114 }
115 
116 /***********************************
117  * Returns: the number of days between two given dates
118  *
119  * Params:
120  *  d1 = first date
121  *  d2 = second date
122  */
123 pure nothrow uint daysBetween(Date d1, Date d2) @safe @nogc
124 {
125 	import std.algorithm: max, min;
126 	auto dateMin = min(d1, d2);
127 	auto dateMax = max(d1, d2);
128 
129 	return g(dateMax.day, dateMax.month, dateMax.isoYear) - g(dateMin.day, dateMin.month, dateMin.isoYear);
130 }
131 
132 private pure nothrow uint g(ubyte d, ubyte m, int y) @safe @nogc
133 {
134 	m = (m + 9) % 12;
135 	y = y - m/10;
136 	return 365*y + y/4 - y/100 + y/400 + (m*306 + 5)/10 + ( d - 1 );
137 }
138 
139 /***********************************
140  * Returns: the day of a given date
141  *
142  * Params:
143  *  d = date
144  */
145 pure nothrow ubyte day(Date d) @safe @nogc
146 {
147 	return cast(ubyte)(d & 0x1F);
148 }
149 
150 /***********************************
151  * Returns: the month of a given date
152  *
153  * Params:
154  *  d = date
155  */
156 pure nothrow ubyte month(Date d) @safe @nogc
157 {
158 	return cast(ubyte)((d >> 5) & 0x0F);
159 }
160 
161 /***********************************
162  * Returns: the year of a given date
163  *
164  * Params:
165  *  d = date
166  */
167 pure nothrow int year(Date d) @safe @nogc
168 {
169 	return cast(int)( ((d >> 9) & YEAR_MASK) - MAX_YEAR);
170 }
171 
172 /***********************************
173  * Returns: the absolute year value of a given date
174  *
175  * Params:
176  *  d = date
177  */
178 pure nothrow int absYear(Date d) @safe @nogc
179 {
180 	auto year = d.year;
181 	return (year < 0) ? -year : year;
182 }
183 
184 /***********************************
185  * Returns: the year of a given date according to ISO 8601
186  *
187  * Params:
188  *  d = date
189  */
190 pure nothrow int isoYear(Date d) @safe @nogc
191 {
192 	auto year = d.year;
193 	return (year < 0) ? year+1 : year;
194 }
195 
196 /***********************************
197  * Returns: the holocene year of a given date
198  *
199  * Params:
200  *  d = date
201  */
202 pure nothrow int holoceneYear(Date d) @safe @nogc
203 {
204 	return 10_000 + d.isoYear;
205 }
206 
207 /***********************************
208  * Returns: the era of a given date (Common Era, Before Common Era)
209  *
210  * Params:
211  *  d = date
212  */
213 pure nothrow Era era(Date d) @safe @nogc
214 {
215 	return (d < CE_START) ? Era.BCE : Era.CE;
216 }
217 
218 /***********************************
219  * Returns: the next day of a given date
220  *
221  * Params:
222  *  d = date
223  */
224 pure Date nextDay(Date d) @safe
225 {
226 	int day = d.day;
227 	int month = d.month;
228 	int year = d.year;
229 	if (++day > daysInMonth(month, year)) {
230 		day = 1;
231 		if (++month > 12) {
232 			month = 1;
233 			year++;
234 			if (year == 0) year++;
235 		}
236 	}
237 	return mkDate(day, month, year);
238 }
239 
240 /***********************************
241  * Returns: the previous day of a given date
242  *
243  * Params:
244  *  d = date
245  */
246 pure Date prevDay(Date d) @safe
247 {
248 	int day = d.day;
249 	int month = d.month;
250 	int year = d.year;
251 
252 	if (--day < 1) {
253 		if (--month < 1) {
254 			month = 12;
255 			year--;
256 			if (year == 0) year--;
257 		}
258 		day = daysInMonth(month, year);
259 	}
260 
261 	return mkDate(day, month, year);
262 }
263 
264 
265 version(Posix) {
266 	import core.stdc.time: gmtime, localtime, time;
267 
268 	/***********************************
269 	 * Returns: the current date in UTC
270 	 */
271 	Date todayUTC()
272 	{
273 		auto timer = time(null);
274 		auto tm = gmtime(&timer);
275 		return mkDate(tm.tm_mday, tm.tm_mon+1, tm.tm_year+1900);
276 	}
277 
278 	/***********************************
279 	 * Returns: the current local date
280 	 */
281 	Date todayLocal()
282 	{
283 		auto timer = time(null);
284 		auto tm = localtime(&timer);
285 		return mkDate(tm.tm_mday, tm.tm_mon+1, tm.tm_year+1900);
286 	}
287 }
288 
289 version(Windows) {
290 	import core.sys.windows.windows: GetSystemTime, SYSTEMTIME;
291 
292 	/***********************************
293 	 * Returns: the current date in UTC
294 	 */
295 	Date todayUTC()
296 	{
297 		SYSTEMTIME time;
298 		GetSystemTime(&time);
299 		return mkDate(time.wDay, time.wMonth, time.wYear);
300 	}
301 
302 	/***********************************
303 	 * Returns: the current local date
304 	 */
305 	Date todayLocal()
306 	{
307 		SYSTEMTIME time;
308 		GetLocalTime(&time);
309 		return mkDate(time.wDay, time.wMonth, time.wYear);
310 	}
311 }
312 
313 
314 @safe unittest
315 {
316 	void assertLeap()
317 	{
318 		assert(800.isLeap == true);
319 		assert((-1).isLeap == true);
320 		assert((-97).isLeap == true);
321 	
322 		assert((-1500).isLeap == false);
323 		assert((-101).isLeap == false);
324 	}
325 
326 	void assertCreation()
327 	{
328 		immutable platonBirth = mkDate(10,11,-427);
329 
330 		assert(platonBirth.day == 10);
331 		assert(platonBirth.month == 11);
332 
333 		assert(platonBirth.year == -427);
334 		assert(platonBirth.absYear == 427);
335 		assert(platonBirth.isoYear == -426);
336 		assert(platonBirth.holoceneYear == 9574);
337 
338 		assert(platonBirth.era == Era.BCE);
339 	}
340 
341 	void assertIntervals()
342 	{
343 		auto sputnikOne = mkDate(4,10,1957);
344 		auto gagarin = mkDate(12,4,1961);
345 
346 		assert(daysBetween(sputnikOne, gagarin) == 1286);
347 	}
348 
349 	void assertIteration()
350 	{
351 		immutable date = mkDate(31,12,-1);
352 		immutable next = date.nextDay;
353 
354 		assert(next.day == 1);
355 		assert(next.month == 1);
356 		assert(next.year == 1);
357 
358 		immutable prev = next.prevDay;
359 		assert(prev == date);
360 	}
361 
362 
363 	assertLeap();
364 	assertCreation();
365 	assertIntervals();
366 	assertIteration();
367 }