github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/date.go (about) 1 // Copyright © 2020. All rights reserved. 2 // Author: Ilya Stroy. 3 // Contacts: iyuryevich@pm.me, https://github.com/qioalice 4 // License: https://opensource.org/licenses/MIT 5 6 package ekatime 7 8 import ( 9 "math" 10 ) 11 12 type ( 13 // Day is a special type that has enough space to store Day's number. 14 // Valid values: [1..31]. 15 Day int8 16 17 // Days is like time.Duration from std Golang lib, 18 // but indicates time's range in passed days. 19 Days int16 20 21 // Month is a special type that has enough space to store Month's number. 22 // Valid values: [1..12]. Use predefined constants to make it clear. 23 Month int8 24 25 // Year is a special type that has enough space to store Year's number. 26 // Valid values: [1900..4095]. 27 Year int16 28 29 // WeekNumber is a special type that has enough space to store week's number. 30 // Valid ranges: [0..53]. Depends on WeekNumber's getter. 31 WeekNumber int8 32 33 // Date is a special object that has enough space to store a some Date 34 // (including Day, Month, Year) but does it most RAM efficient way 35 // taking only 4 bytes. 36 // 37 // WARNING! 38 // DO NOT COMPARE Date OBJECTS JUST BY EQUAL OPERATOR! PREPARE THEM BEFORE. 39 // 40 // Because of the internal parts, the Date objects that represents the same 41 // date (e.g: 01 Jan 1970) may not be equal by just eq comparison, like: 42 // 43 // d1 := NewTimestamp(NewDate(1970, 1, 1), 0).Date() 44 // d2 := NewDate(1970, 1, 1) 45 // d1 == d2 // false <--- 46 // 47 // For being able to use eq operator, call ToCmp() before, 48 // or use Equal() method: 49 // 50 // d1 := NewTimestamp(NewDate(1970, 1, 1), 0).Date() 51 // d2 := NewDate(1970, 1, 1) 52 // d1.ToCmp() == d2.ToCmp() // true <--- 53 // d1.Equal(d2) // true <--- 54 // 55 Date uint32 56 ) 57 58 //noinspection GoSnakeCaseUsage,GoUnusedConst 59 const ( 60 MONTH_JANUARY Month = 1 + iota 61 MONTH_FEBRUARY 62 MONTH_MARCH 63 MONTH_APRIL 64 MONTH_MAY 65 MONTH_JUNE 66 MONTH_JULY 67 MONTH_AUGUST 68 MONTH_SEPTEMBER 69 MONTH_OCTOBER 70 MONTH_NOVEMBER 71 MONTH_DECEMBER 72 ) 73 74 // BelongsToMonth reports whether passed Day belongs to provided Month. 75 // 76 // It returns true for 29 Feb, cause there's no leap year check. 77 // If you want it, use Day.BelongsToMonthAndYear() method. 78 func (d Day) BelongsToMonth(m Month) bool { 79 80 // 2004 is a leap year, so leap check will pass. 81 // Thus, call Day(29).BelongsToMonth() will always return true. 82 83 return d.BelongsToMonthAndYear(2004, m) 84 } 85 86 // BelongsToMonthAndYear reports whether passed Day belongs to provided Month, 87 // with checking leap year. 88 func (d Day) BelongsToMonthAndYear(y Year, m Month) bool { 89 return (m.IsValid() && d <= _Table0[m-1]) || (d == 29 && m == MONTH_FEBRUARY && y.IsLeap()) 90 } 91 92 // BelongsToYear reports whether current Days object belongs to provided Year 93 // (has value [1..365/366]). 94 // The upper bound depends of whether provided year is leap or not. 95 func (d Days) BelongsToYear(y Year) bool { 96 return (1 <= d && d <= 365) || (d == 366 && y.IsLeap()) 97 } 98 99 // IsValid reports whether m is valid month (any of Jan,Feb,...,Dec). 100 func (m Month) IsValid() bool { 101 return MONTH_JANUARY <= m && m <= MONTH_DECEMBER 102 } 103 104 // DaysInForYear is an alias for DaysInMonth(y, m), where m is the current Month. 105 func (m Month) DaysInForYear(y Year) Day { 106 return DaysInMonth(y, m) 107 } 108 109 // DaysInIgnoreYear is an alias for DaysInMonthIgnoreYear(m), 110 // where m is the current Month. 111 func (m Month) DaysInIgnoreYear() Day { 112 return DaysInMonthIgnoreYear(m) 113 } 114 115 // String returns a full capitalized month name in English. 116 func (m Month) String() string { 117 switch m { 118 case MONTH_JANUARY: 119 return "January" 120 case MONTH_FEBRUARY: 121 return "February" 122 case MONTH_MARCH: 123 return "March" 124 case MONTH_APRIL: 125 return "April" 126 case MONTH_MAY: 127 return "May" 128 case MONTH_JUNE: 129 return "June" 130 case MONTH_JULY: 131 return "July" 132 case MONTH_AUGUST: 133 return "August" 134 case MONTH_SEPTEMBER: 135 return "September" 136 case MONTH_OCTOBER: 137 return "October" 138 case MONTH_NOVEMBER: 139 return "November" 140 case MONTH_DECEMBER: 141 return "December" 142 default: 143 return "Unknown" 144 } 145 } 146 147 // IsValid reports whether current Year is valid and between [1900..4095] years. 148 func (y Year) IsValid() bool { 149 return _YEAR_MIN <= y && y <= _YEAR_MAX 150 } 151 152 // IsLeap returns true if the current year is leap (e.g. 1992, 1996, 2000, 2004, etc). 153 func (y Year) IsLeap() bool { 154 return IsLeap(y) 155 } 156 157 // IsValidDate reports whether 'y', 'm' and 'd' in their valid ranges. 158 // Also checks leap year, february 28,29, correct day number for month. 159 func IsValidDate(y Year, m Month, d Day) bool { 160 return !(y < 0 || y > 4095 || m < 1 || m > 12 || d < 1 || 161 (IsLeap(y) && m == 2 && d > 29) || d > _Table0[m-1]) 162 } 163 164 // IsValid is an alias for IsValidDate(). 165 func (dd Date) IsValid() bool { 166 return IsValidDate(dd.Split()) 167 } 168 169 // ToCmp returns the current Date object ready for being compared using eq operator ==. 170 // Yes you MUST NOT compare Date objects directly w/o call this method. 171 // See Date's doc for more details. 172 func (dd Date) ToCmp() Date { 173 return dd &^ (Date(_DATE_MASK_WEEKDAY) << _DATE_OFFSET_WEEKDAY) 174 } 175 176 // Equal returns true if the current Date is the same as 'other'. 177 func (dd Date) Equal(other Date) bool { 178 return dd.ToCmp() == other.ToCmp() 179 } 180 181 // Year returns the year number the current Date includes which. 182 // 183 // It guarantees that Year() returns the valid year number Date is of, 184 // only if Date has not been created manually but using constructors 185 // like NewDate(), Event.Date(), Timestamp.Date(), Timestamp.Split(), etc. 186 func (dd Date) Year() Year { 187 return Year(dd>>_DATE_OFFSET_YEAR) & _DATE_MASK_YEAR 188 } 189 190 // Month returns the month number the current Date includes which. 191 // January is 1, not 0. 192 // 193 // It guarantees that Month() returns the valid month number Date is of, 194 // only if Date has not been created manually but using constructors 195 // like NewDate(), Event.Date(), Timestamp.Date(), Timestamp.Split(), etc. 196 func (dd Date) Month() Month { 197 return Month(dd>>_DATE_OFFSET_MONTH) & _DATE_MASK_MONTH 198 } 199 200 // Day returns the day number the current Date includes which. 201 // 202 // It guarantees that Day() returns the valid day number for the month Date is of, 203 // only if Date has not been created manually but using constructors 204 // like NewDate(), Event.Date(), Timestamp.Date(), Timestamp.Split(), etc. 205 func (dd Date) Day() Day { 206 return Day(dd>>_DATE_OFFSET_DAY) & _DATE_MASK_DAY 207 } 208 209 // DaysInMonth returns how much days the month contains the current Date includes which. 210 func (dd Date) DaysInMonth() Day { 211 y, m, d := normalizeDate(dd.Split()) 212 d = _Table0[m-1] 213 if m == MONTH_FEBRUARY && y.IsLeap() { 214 d++ 215 } 216 return d 217 } 218 219 // DaysInMonth returns how much days requested Month contains in passed Year. 220 // Returns -1, if Year and Month not in their allowed ranges. 221 func DaysInMonth(y Year, m Month) Day { 222 if m.IsValid() { 223 d := _Table0[m-1] 224 if m == MONTH_FEBRUARY && y.IsLeap() { 225 d++ 226 } 227 return d 228 } 229 return -1 230 } 231 232 // DaysInMonthIgnoreYear does the same thing as DaysInMonth, 233 // but does not require the Year, meaning that it will be always 28 for MONTH_FEBRUARY, 234 // even for leap years like 2000, 2004, etc. 235 func DaysInMonthIgnoreYear(m Month) Day { 236 if m.IsValid() { 237 return _Table0[m-1] 238 } else { 239 return -1 240 } 241 } 242 243 // Weekday returns the current Date's day of week. 244 func (dd Date) Weekday() Weekday { 245 if w := Weekday(dd>>_DATE_OFFSET_WEEKDAY) & _DATE_MASK_WEEKDAY; w > 0 { 246 return w - 1 247 } else { 248 return dd.WithTime(0, 0, 0).Weekday() 249 } 250 } 251 252 // ISOWeek returns an ISO week of the current Date. 253 // Returned value will be in the range [1..53]. 254 // Returns -1 if Date is not valid. 255 // 256 // Read more: 257 // https://en.wikipedia.org/wiki/ISO_week_date 258 func (dd Date) ISOWeek() WeekNumber { 259 y, m, d := dd.Split() 260 dow := dd.Weekday().To06() 261 if dow == 0 { 262 dow = 7 263 } 264 thu := d + Day(4-dow) // nearest thursday 265 if m == MONTH_DECEMBER && thu > 31 { 266 return 1 267 } 268 if m == MONTH_JANUARY && thu < 1 { 269 y-- 270 m = MONTH_DECEMBER 271 thu += 31 272 } 273 doy := math.Floor(275*float64(m)/9) + float64(thu-31) 274 if m > MONTH_FEBRUARY { 275 if IsLeap(y) { 276 doy++ 277 } 278 doy -= 2 279 } 280 return WeekNumber(1 + math.Floor(doy/7)) 281 } 282 283 // Split returns the year number, month number and day number the current Date 284 // includes which. 285 // It's just like a separate Year(), Month(), Day() calls. 286 func (dd Date) Split() (y Year, m Month, d Day) { 287 return dd.Year(), dd.Month(), dd.Day() 288 } 289 290 // NewDate creates a new Date object using provided year number, month number, 291 // day number, normalizing these values and shifting date if it's required. 292 // 293 // Totally, it just stores the provided data if values are in their valid ranges, 294 // like: [1900..4095] for year, [1..12] for month and [1..X] for day, 295 // where X depends by month and year. 296 // 297 // If they are not, the date may be (will be) shifted. E.g: 298 // 0 May 2016 (y == 2016, m == 5, d == 0) -> 30 April 2016 (y == 2016, m == 4, d == 30). 299 func NewDate(y Year, m Month, d Day) Date { 300 301 if y > 4095 { 302 y = 4095 303 } 304 305 y, m, d = normalizeDate(y, m, d) 306 307 // Do not forgot 'bitwise AND' between y, m, d and their bitmasks 308 // if you will change the logic of getting valid y, m, d from invalid ones. 309 // Now it's unnecessary (redundant). 310 //y, m, d = y & _DATE_MASK_YEAR, m & _DATE_MASK_MONTH, d & _DATE_MASK_DAY 311 312 return (Date(y) << _DATE_OFFSET_YEAR) | 313 (Date(m) << _DATE_OFFSET_MONTH) | 314 (Date(d) << _DATE_OFFSET_DAY) 315 } 316 317 // NewDateFromDayOfYear creates a new Date object using provided year number 318 // and the day of year on row. 319 // 320 // It's allowed to pass < 0 days 321 // to construct a date less than 1 Jan of your year. 322 // 323 // Examples: 324 // 325 // NewDateFromDayOfYear(2021, 1) // 1 Jan 2021 326 // NewDateFromDayOfYear(2021, 2) // 2 Jan 2021 327 // NewDateFromDayOfYear(2021, 33) // 2 Feb 2021 328 // NewDateFromDayOfYear(2021, 254) // 11 Sep 2021 329 // 330 func NewDateFromDayOfYear(y Year, days Days) Date { 331 return NewDate(y, MONTH_JANUARY, 1).AddDays(days - 1) 332 } 333 334 // Replace returns a new Date based on the current. 335 // It returns the current Date with changed Year, Month, Day to those passed values, 336 // which are in their allowed ranges. Does not doing date addition. Only replacing. 337 // For date addition, use Add() method. 338 // 339 // Examples: 340 // NewDate(2021, MONTH_FEBRUARY, 10) // -> 10 Feb 2021 341 // .Replace(2013, MONTH_JANUARY, 2) // -> 02 Jan 2013 342 // .Replace(2020, -2, 4) // -> 04 Jan 2020 343 // .Replace(0, 0, -50) // -> 04 Jan 2020 344 // .Replace(1899, 61, 31) // -> 31 Jan 2020 (1900 is min year) 345 // .Replace(2013, MONTH_FEBRUARY, -2) // -> 31 Jan 2013 (31 Feb is not allowed). 346 // .Replace(2014, MONTH_FEBRUARY, 30) // -> 30 Jan 2014 347 // .Replace(2000, MONTH_FEBRUARY, 29) // -> 29 Feb 2000 348 // .Replace(2001, -1, -1) // -> 29 Feb 2000 (29 Feb in 2001 is not allowed) 349 func (dd Date) Replace(y Year, m Month, d Day) Date { 350 y_, m_, d_ := dd.Split() 351 352 // There are some edge cases, that must be handled: 353 // 1. dd is 29 Feb Leap's year, changing year is requested, check it. 354 // 2. dd is 31 of some month, changing to 30-days month is requested, check it. 355 356 var ( 357 ndOK = 1 <= d && d <= 31 && d != d_ 358 nmOK = MONTH_JANUARY <= m && m <= MONTH_DECEMBER && m != m_ 359 nyOK = _YEAR_MIN <= y && y <= _YEAR_MAX && y != y_ 360 ) 361 362 //goland:noinspection GoSnakeCaseUsage 363 var ( 364 Zym = DaysInMonth(y, m) 365 Zy_m = DaysInMonth(y_, m) 366 Zy_m_ = DaysInMonth(y_, m_) 367 Zym_ = DaysInMonth(y, m_) 368 ) 369 370 //switch { 371 //case nmOK && nyOK && ndOK && d <= Zym: 372 // y_, m_, d_ = y, m, d 373 // 374 //case nmOK && nyOK && ndOK && d <= Zy_m: 375 // m_, d_ = m, d 376 // 377 //case nmOK && nyOK && ndOK && d <= Zym_: 378 // y_, d_ = y, d 379 // 380 //case nmOK && nyOK && ndOK && d <= Zy_m_: 381 // d_ = d 382 // 383 //case nmOK && nyOK && !ndOK && d_ <= Zym: 384 // y_, m_ = y, m 385 // 386 //case nmOK && nyOK && !ndOK && d_ <= Zy_m: 387 // m_ = m 388 // 389 //case nmOK && nyOK && !ndOK && d_ <= Zym_: 390 // y_ = y 391 // 392 //case !nmOK && nyOK && ndOK && d <= Zym_: 393 // y_, d_ = y, d 394 // 395 //case !nmOK && nyOK && ndOK && d_ <= Zym_: 396 // y_ = y 397 // 398 //case nmOK && !nyOK && ndOK && d <= Zy_m: 399 // m_, d_ = m, d 400 // 401 //case nmOK && !nyOK && ndOK && d_ <= Zy_m: 402 // m_ = m 403 // 404 //case nmOK && !nyOK && !ndOK && d_ <= Zy_m: 405 // m_ = m 406 // 407 //case !nmOK && nyOK && !ndOK && d_ <= Zym_: 408 // y_ = y 409 // 410 //case !nmOK && !nyOK && ndOK && d <= Zy_m_: 411 // d_ = d 412 //} 413 414 // Switch above is simplified, using CNDF: 415 // https://en.wikipedia.org/wiki/Canonical_normal_form 416 417 var ( 418 A = nmOK 419 B = nyOK 420 C = ndOK 421 D = d <= Zym 422 E = d <= Zy_m 423 F = d <= Zy_m_ 424 G = d <= Zym_ 425 H = d_ <= Zym 426 I = d_ <= Zy_m 427 K = d_ <= Zym_ 428 ) 429 430 if A && B && (C && (D || G) || H || K) || B && (C && (G || K) || K) { 431 y_ = y 432 } 433 if A && B && (C && (D || E) || H || I) || A && (C && (E || I) || I) { 434 m_ = m 435 } 436 if A && B && C && (D || E || G || F) || C && (B && G || A && E || F) { 437 d_ = d 438 } 439 440 return NewDate(y_, m_, d_) 441 } 442 443 // Add returns a new Date based on the current. 444 // It returns the current Date with changed Year, Month, Day using passed values 445 // as their addition's deltas. 446 // 447 // Examples: 448 // NewDate(2021, MONTH_FEBRUARY, 10) // 10 Feb 2021 449 // .Add(1, 2, 3) // 13 Apr 2022 450 // .Add(0, -1, -13) // 28 Feb 2022 (0 Mar 2022 -> 28 Feb 2022) 451 // .Add(0, 1, 3) // 31 Mar 2022 452 // .Add(0, 1, 0) // 01 May 2022 (31 Apr 2022 (not exist) -> 01 May 2022) 453 // .Add(0, 127, 0) // 01 Dec 2032 (works OK with potential integer overflow) 454 func (dd Date) Add(y Year, m Month, d Day) Date { 455 y_, m_, d_ := dd.Split() 456 457 if y > _YEAR_MAX { 458 y = _YEAR_MAX 459 } 460 if y < -(y_ - _YEAR_MIN) { 461 y = -(y_ - _YEAR_MIN) 462 } 463 464 y_ += y 465 y_ += Year(m / 12) 466 467 m_ += m % 12 468 469 if y_ > _YEAR_MAX { 470 y_ = _YEAR_MAX 471 } 472 if y_ < _YEAR_MIN { 473 y_ = _YEAR_MIN 474 } 475 476 return NewDate(y_, m_, d_).AddDays(Days(d)) 477 } 478 479 // Sub is the same as Add(-y, -m, -d), but exists to make your life easier, 480 // when you have a Date, not its parts. 481 // 482 // Compare: 483 // dd1 := NewDate(...) 484 // dd2 := NewDate(...) 485 // y2, m2, d2 := dd2.Split() 486 // dd1.Add(-y2, -m2, d2) // dd1 - dd2 487 // or 488 // dd1.Sub(dd2.Split()) 489 func (dd Date) Sub(y Year, m Month, d Day) Date { 490 return dd.Add(-y, -m, -d) 491 } 492 493 // DayOfYear returns an accumulated number of days that has been passed since 1 Jan. 494 // Returns 0 if current Date is not valid. 495 func (dd Date) DayOfYear() Days { 496 497 // Zeller's congruence. 498 // https://en.wikipedia.org/wiki/Ordinal_date 499 500 y, m, d := dd.Split() 501 if m < MONTH_MARCH { 502 m += 12 503 } 504 505 doy := (153*(Days(m)-3)+2)/5 + Days(d) 506 if m <= MONTH_DECEMBER { 507 doy += 59 508 if y.IsLeap() { 509 doy += 1 510 } 511 } else { 512 doy -= 306 513 } 514 515 return doy 516 } 517 518 // AddDays returns a new Date based on the current Date. 519 // It additions exactly passed days to the current Date and returns a result. 520 func (dd Date) AddDays(days Days) Date { 521 return (dd.WithTime(0, 0, 0) + Timestamp(days)*SECONDS_IN_DAY).Date() 522 } 523 524 // WithTime returns the current Date with the presented Time's hour, minute, second 525 // as a new Timestamp object. 526 func (dd Date) WithTime(hh Hour, mm Minute, ss Second) Timestamp { 527 y, m, d := dd.Split() 528 return NewTimestamp(y, m, d, hh, mm, ss) 529 } 530 531 // IsLeap returns true if 'y' Year is leap (e.g. 1992, 1996, 2000, 2004, etc). 532 func IsLeap(y Year) bool { 533 return y%400 == 0 || (y%4 == 0 && y%100 != 0) 534 } 535 536 // InMonth returns how much seconds month 'm' in year 'y' contains. 537 // 538 // Returns it as Timestamp because of easy arithmetic operations, 539 // but it's NOT A TIMESTAMP! 540 func InMonth(y Year, m Month) Timestamp { 541 y, m, _ = normalizeDate(y, m, 1) 542 543 sec := _Table1[m-1] 544 if m == MONTH_FEBRUARY && IsLeap(y) { 545 sec += SECONDS_IN_DAY 546 } 547 548 return sec 549 } 550 551 // InYear returns how much seconds year 'y' contains. 552 // 553 // Returns it as Timestamp because of easy arithmetic operations, 554 // but it's NOT A TIMESTAMP! 555 func InYear(y Year) Timestamp { 556 if IsLeap(y) { 557 return SECONDS_IN_366_YEAR 558 } else { 559 return SECONDS_IN_365_YEAR 560 } 561 } 562 563 // BeginningOfYear returns the Timestamp of the 'y' year beginning: 564 // 1 January, 00:00:00 (12:00:00 AM). 565 // 566 // It's up to 10 times faster for [1970..N+10] years, where N is current year. 567 func BeginningOfYear(y Year) Timestamp { 568 return getBeginningOfYear(y) // there is no need of normalizing 569 } 570 571 // EndOfYear returns the Timestamp of the 'y' year end: 572 // 31 December, 23:59:59 (11:59:59 PM). 573 // 574 // It's up to 4.8 times faster for [1970..N+10] years, where N is current year. 575 func EndOfYear(y Year) Timestamp { 576 return getBeginningOfYear(y) + InYear(y) - 1 // there is no need of normalizing 577 } 578 579 // BeginningAndEndOfYear is like BeginningOfYear() and EndOfYear() calls. 580 func BeginningAndEndOfYear(y Year) TimestampPair { 581 by := getBeginningOfYear(y) // there is no need of normalizing 582 return TimestampPair{by, by + InYear(y) - 1} // there is no need of normalizing 583 } 584 585 // BeginningOfMonth returns the Timestamp of the 'm' month beginning in 'y' year: 586 // 1st day, 00:00:00 (12:00:00 AM). 587 // 588 // It's up to 7 times faster for [1970..N+10] years, where N is current year. 589 func BeginningOfMonth(y Year, m Month) Timestamp { 590 return getBeginningOfMonth(y, m) // there is no need of normalizing 591 } 592 593 // EndOfMonth returns the Timestamp of the 'm' month end in 'y' year: 594 // 29/30/31 (depends by month) day, 23:59:59 (11:59:59 PM). 595 // 596 // It's up to 2.1 times faster for [1970..N+10] years, where N is current year. 597 func EndOfMonth(y Year, m Month) Timestamp { 598 return getBeginningOfMonth(y, m) + InMonth(y, m) - 1 // there is no need of normalizing 599 } 600 601 // BeginningAndEndOfMonth is like BeginningOfMonth() and EndOfMonth() calls. 602 func BeginningAndEndOfMonth(y Year, m Month) TimestampPair { 603 bm := getBeginningOfMonth(y, m) // there is no need of normalizing 604 return TimestampPair{bm, bm + InMonth(y, m) - 1} // there is no need of normalizing 605 }