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 }