pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/timeutil/timeutil.go (about) 1 // Package timeutil provides methods for working with time and date 2 package timeutil 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2022 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "bytes" 13 "fmt" 14 "math" 15 "strconv" 16 "strings" 17 "time" 18 19 "pkg.re/essentialkaos/ek.v12/mathutil" 20 "pkg.re/essentialkaos/ek.v12/pluralize" 21 ) 22 23 // ////////////////////////////////////////////////////////////////////////////////// // 24 25 const ( 26 _SECOND int64 = 1 27 _MINUTE int64 = 60 28 _HOUR int64 = 3600 29 _DAY int64 = 86400 30 _WEEK int64 = 604800 31 ) 32 33 // ////////////////////////////////////////////////////////////////////////////////// // 34 35 // PrettyDuration returns pretty duration (e.g. 1 hour 45 seconds) 36 func PrettyDuration(d interface{}) string { 37 dur, ok := convertDuration(d) 38 39 if !ok { 40 return "" 41 } 42 43 if dur != 0 && dur < time.Second { 44 return getPrettyShortDuration(dur) 45 } 46 47 return getPrettyLongDuration(dur) 48 } 49 50 // PrettyDurationInDays returns pretty duration in days (e.g. 15 days) 51 func PrettyDurationInDays(d interface{}) string { 52 dur, ok := convertDuration(d) 53 54 if !ok { 55 return "" 56 } 57 58 switch { 59 case dur <= 5*time.Minute: 60 return "just now" 61 case dur <= time.Hour*24: 62 return "today" 63 } 64 65 days := int(dur.Hours()) / 24 66 67 return pluralize.PS(pluralize.En, "%d %s", days, "day", "days") 68 } 69 70 // ShortDuration returns pretty short duration (e.g. 1:37) 71 func ShortDuration(d interface{}, highPrecision ...bool) string { 72 dur, ok := convertDuration(d) 73 74 if !ok { 75 return "" 76 } 77 78 if dur == 0 { 79 return "0:00" 80 } 81 82 if len(highPrecision) != 0 && highPrecision[0] == true { 83 return getShortDuration(dur, true) 84 } 85 86 return getShortDuration(dur, false) 87 } 88 89 // Format returns formatted date as a string 90 // 91 // Interpreted sequences: 92 // 93 // '%%' a literal % 94 // '%a' locale's abbreviated weekday name (e.g., Sun) 95 // '%A' locale's full weekday name (e.g., Sunday) 96 // '%b' locale's abbreviated month name (e.g., Jan) 97 // '%B' locale's full month name (e.g., January) 98 // '%c' locale's date and time (e.g., Thu Mar 3 23:05:25 2005) 99 // '%C' century; like %Y, except omit last two digits (e.g., 20) 100 // '%d' day of month (e.g, 01) 101 // '%D' date; same as %m/%d/%y 102 // '%e' day of month, space padded 103 // '%F' full date; same as %Y-%m-%d 104 // '%g' last two digits of year of ISO week number (see %G) 105 // '%G' year of ISO week number (see %V); normally useful only with %V 106 // '%h' same as %b 107 // '%H' hour (00..23) 108 // '%I' hour (01..12) 109 // '%j' day of year (001..366) 110 // '%k' hour ( 0..23) 111 // '%K' milliseconds (000..999) 112 // '%l' hour ( 1..12) 113 // '%m' month (01..12) 114 // '%M' minute (00..59) 115 // '%n' a newline 116 // '%N' nanoseconds (000000000..999999999) 117 // '%p' AM or PM 118 // '%P' like %p, but lower case 119 // '%r' locale's 12-hour clock time (e.g., 11:11:04 PM) 120 // '%R' 24-hour hour and minute; same as %H:%M 121 // '%s' seconds since 1970-01-01 00:00:00 UTC 122 // '%S' second (00..60) 123 // '%t' a tab 124 // '%T' time; same as %H:%M:%S 125 // '%u' day of week (1..7); 1 is Monday 126 // '%U' week number of year, with Sunday as first day of week (00..53) 127 // '%V' ISO week number, with Monday as first day of week (01..53) 128 // '%w' day of week (0..6); 0 is Sunday 129 // '%W' week number of year, with Monday as first day of week (00..53) 130 // '%x' locale's date representation (e.g., 12/31/99) 131 // '%X' locale's time representation (e.g., 23:13:48) 132 // '%y' last two digits of year (00..99) 133 // '%Y' year 134 // '%z' +hhmm numeric timezone (e.g., -0400) 135 // '%:z' +hh:mm numeric timezone (e.g., -04:00) 136 // '%Z' alphabetic time zone abbreviation (e.g., EDT) 137 func Format(d time.Time, f string) string { 138 input := bytes.NewBufferString(f) 139 output := bytes.NewBufferString("") 140 141 for { 142 r, _, err := input.ReadRune() 143 144 if err != nil { 145 break 146 } 147 148 switch r { 149 case '%': 150 replaceDateTag(d, input, output) 151 default: 152 output.WriteRune(r) 153 } 154 } 155 156 return output.String() 157 } 158 159 // DurationToSeconds converts time.Duration to float64 160 func DurationToSeconds(d time.Duration) float64 { 161 return float64(d) / 1000000000.0 162 } 163 164 // SecondsToDuration converts float64 to time.Duration 165 func SecondsToDuration(d float64) time.Duration { 166 return time.Duration(1000000000.0 * d) 167 } 168 169 // ParseDuration parses duration in 1w2d3h5m6s format and return as seconds 170 func ParseDuration(dur string, defMod ...rune) (int64, error) { 171 if dur == "" { 172 return 0, nil 173 } 174 175 var err error 176 var result int64 177 178 buf := &bytes.Buffer{} 179 180 for _, sym := range dur { 181 switch sym { 182 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 183 buf.WriteRune(sym) 184 case 'w', 'W': 185 result, err = appendDur(result, buf, _WEEK) 186 case 'd', 'D': 187 result, err = appendDur(result, buf, _DAY) 188 case 'h', 'H': 189 result, err = appendDur(result, buf, _HOUR) 190 case 'm', 'M': 191 result, err = appendDur(result, buf, _MINUTE) 192 case 's', 'S': 193 result, err = appendDur(result, buf, _SECOND) 194 default: 195 return 0, fmt.Errorf("Unsupported symbol \"%s\"", string(sym)) 196 } 197 198 if err != nil { 199 return 0, err 200 } 201 } 202 203 if buf.Len() != 0 { 204 if result != 0 { 205 return 0, fmt.Errorf("Misformatted duration \"%s\"", dur) 206 } 207 208 mod := 's' 209 210 if len(defMod) != 0 { 211 mod = defMod[0] 212 } 213 214 result, err = strconv.ParseInt(buf.String(), 10, 64) 215 216 if err != nil { 217 return 0, err 218 } 219 220 switch mod { 221 case 'w', 'W': 222 result *= _WEEK 223 case 'd', 'D': 224 result *= _DAY 225 case 'h', 'H': 226 result *= _HOUR 227 case 'm', 'M': 228 result *= _MINUTE 229 case 's', 'S': 230 result *= _SECOND 231 } 232 } 233 234 return result, nil 235 } 236 237 // PrevDay returns previous day date 238 func PrevDay(t time.Time) time.Time { 239 return t.AddDate(0, 0, -1) 240 } 241 242 // PrevMonth returns previous month date 243 func PrevMonth(t time.Time) time.Time { 244 return t.AddDate(0, -1, 0) 245 } 246 247 // PrevYear returns previous year date 248 func PrevYear(t time.Time) time.Time { 249 return t.AddDate(-1, 0, 0) 250 } 251 252 // NextDay returns next day date 253 func NextDay(t time.Time) time.Time { 254 return t.AddDate(0, 0, 1) 255 } 256 257 // NextMonth returns next month date 258 func NextMonth(t time.Time) time.Time { 259 return t.AddDate(0, 1, 0) 260 } 261 262 // NextYear returns next year date 263 func NextYear(t time.Time) time.Time { 264 return t.AddDate(1, 0, 0) 265 } 266 267 // NextWorkday returns previous workday 268 func PrevWorkday(t time.Time) time.Time { 269 for { 270 t = t.AddDate(0, 0, -1) 271 272 switch t.Weekday() { 273 case time.Saturday, time.Sunday: 274 continue 275 } 276 277 return t 278 } 279 } 280 281 // NextWeekend returns previous weekend 282 func PrevWeekend(t time.Time) time.Time { 283 for { 284 t = t.AddDate(0, 0, -1) 285 286 switch t.Weekday() { 287 case time.Saturday, time.Sunday: 288 return t 289 } 290 } 291 } 292 293 // NextWorkday returns next workday 294 func NextWorkday(t time.Time) time.Time { 295 for { 296 t = t.AddDate(0, 0, 1) 297 298 switch t.Weekday() { 299 case time.Monday, time.Tuesday, time.Wednesday, 300 time.Thursday, time.Friday: 301 return t 302 } 303 } 304 } 305 306 // NextWeekend returns next weekend 307 func NextWeekend(t time.Time) time.Time { 308 for { 309 t = t.AddDate(0, 0, 1) 310 311 switch t.Weekday() { 312 case time.Saturday, time.Sunday: 313 return t 314 } 315 } 316 } 317 318 // ////////////////////////////////////////////////////////////////////////////////// // 319 320 // It's ok to have so long method here 321 // codebeat:disable[LOC,ABC] 322 323 func convertDuration(d interface{}) (time.Duration, bool) { 324 switch u := d.(type) { 325 case time.Duration: 326 return u, true 327 case int: 328 return time.Duration(u) * time.Second, true 329 case int16: 330 return time.Duration(u) * time.Second, true 331 case int32: 332 return time.Duration(u) * time.Second, true 333 case uint: 334 return time.Duration(u) * time.Second, true 335 case uint16: 336 return time.Duration(u) * time.Second, true 337 case uint32: 338 return time.Duration(u) * time.Second, true 339 case uint64: 340 return time.Duration(u) * time.Second, true 341 case float32: 342 return time.Duration(u) * time.Second, true 343 case float64: 344 return time.Duration(u) * time.Second, true 345 case int64: 346 return time.Duration(u) * time.Second, true 347 } 348 349 return 0, false 350 } 351 352 func replaceDateTag(d time.Time, input, output *bytes.Buffer) { 353 r, _, err := input.ReadRune() 354 355 if err != nil { 356 return 357 } 358 359 switch r { 360 case '%': 361 fmt.Fprintf(output, "%%") 362 case 'a': 363 output.WriteString(getShortWeekday(d.Weekday())) 364 case 'A': 365 output.WriteString(getLongWeekday(d.Weekday())) 366 case 'b', 'h': 367 output.WriteString(getShortMonth(d.Month())) 368 case 'B': 369 output.WriteString(getLongMonth(d.Month())) 370 case 'c': 371 zn, _ := d.Zone() 372 fmt.Fprintf(output, "%s %02d %s %d %02d:%02d:%02d %s %s", 373 getShortWeekday(d.Weekday()), 374 d.Day(), 375 getShortMonth(d.Month()), 376 d.Year(), 377 getAMPMHour(d), 378 d.Minute(), 379 d.Second(), 380 getAMPM(d, true), 381 zn, 382 ) 383 case 'C', 'g': 384 output.WriteString(strconv.Itoa(d.Year())[0:2]) 385 case 'd': 386 fmt.Fprintf(output, "%02d", d.Day()) 387 case 'D': 388 fmt.Fprintf(output, "%02d/%02d/%s", d.Month(), d.Day(), strconv.Itoa(d.Year())[2:4]) 389 case 'e': 390 fmt.Fprintf(output, "%2d", d.Day()) 391 case 'F': 392 fmt.Fprintf(output, "%d-%02d-%02d", d.Year(), d.Month(), d.Day()) 393 case 'G': 394 fmt.Fprintf(output, "%02d", d.Year()) 395 case 'H': 396 fmt.Fprintf(output, "%02d", d.Hour()) 397 case 'I': 398 fmt.Fprintf(output, "%02d", getAMPMHour(d)) 399 case 'j': 400 fmt.Fprintf(output, "%03d", d.YearDay()) 401 case 'k': 402 fmt.Fprintf(output, "%2d", d.Hour()) 403 case 'K': 404 output.WriteString(fmt.Sprintf("%03d", d.Nanosecond())[:3]) 405 case 'l': 406 output.WriteString(strconv.Itoa(getAMPMHour(d))) 407 case 'm': 408 fmt.Fprintf(output, "%02d", d.Month()) 409 case 'M': 410 fmt.Fprintf(output, "%02d", d.Minute()) 411 case 'n': 412 output.WriteString("\n") 413 case 'N': 414 fmt.Fprintf(output, "%09d", d.Nanosecond()) 415 case 'p': 416 output.WriteString(getAMPM(d, false)) 417 case 'P': 418 output.WriteString(getAMPM(d, true)) 419 case 'r': 420 fmt.Fprintf(output, "%02d:%02d:%02d %s", getAMPMHour(d), d.Minute(), d.Second(), getAMPM(d, true)) 421 case 'R': 422 fmt.Fprintf(output, "%02d:%02d", d.Hour(), d.Minute()) 423 case 's': 424 output.WriteString(strconv.FormatInt(d.Unix(), 10)) 425 case 'S': 426 fmt.Fprintf(output, "%02d", d.Second()) 427 case 'T': 428 fmt.Fprintf(output, "%02d:%02d:%02d", d.Hour(), d.Minute(), d.Second()) 429 case 'u': 430 output.WriteString(strconv.Itoa(getWeekdayNum(d))) 431 case 'V': 432 _, wn := d.ISOWeek() 433 fmt.Fprintf(output, "%02d", wn) 434 case 'w': 435 fmt.Fprintf(output, "%d", d.Weekday()) 436 case 'y': 437 output.WriteString(strconv.Itoa(d.Year())[2:4]) 438 case 'Y': 439 output.WriteString(strconv.Itoa(d.Year())) 440 case 'z': 441 output.WriteString(getTimezone(d, false)) 442 case ':': 443 input.ReadRune() 444 output.WriteString(getTimezone(d, true)) 445 case 'Z': 446 zn, _ := d.Zone() 447 output.WriteString(zn) 448 default: 449 output.WriteRune('%') 450 output.WriteRune(r) 451 } 452 } 453 454 // codebeat:enable[LOC,ABC] 455 456 func getShortWeekday(d time.Weekday) string { 457 long := getLongWeekday(d) 458 459 if long == "" { 460 return "" 461 } 462 463 return long[:3] 464 } 465 466 func getLongWeekday(d time.Weekday) string { 467 switch int(d) { 468 case 0: 469 return "Sunday" 470 case 1: 471 return "Monday" 472 case 2: 473 return "Tuesday" 474 case 3: 475 return "Wednesday" 476 case 4: 477 return "Thursday" 478 case 5: 479 return "Friday" 480 case 6: 481 return "Saturday" 482 } 483 484 return "" 485 } 486 487 func getShortMonth(m time.Month) string { 488 long := getLongMonth(m) 489 490 if long == "" { 491 return "" 492 } 493 494 return long[:3] 495 } 496 497 func getLongMonth(m time.Month) string { 498 switch int(m) { 499 case 1: 500 return "January" 501 case 2: 502 return "February" 503 case 3: 504 return "March" 505 case 4: 506 return "April" 507 case 5: 508 return "May" 509 case 6: 510 return "June" 511 case 7: 512 return "July" 513 case 8: 514 return "August" 515 case 9: 516 return "September" 517 case 10: 518 return "October" 519 case 11: 520 return "November" 521 case 12: 522 return "December" 523 } 524 525 return "" 526 } 527 528 func getAMPMHour(d time.Time) int { 529 h := d.Hour() 530 531 switch { 532 case h == 0 || h == 12: 533 return 12 534 535 case h < 12: 536 return h 537 538 default: 539 return h - 12 540 } 541 } 542 543 func getAMPM(d time.Time, caps bool) string { 544 if d.Hour() < 12 { 545 switch caps { 546 case true: 547 return "AM" 548 default: 549 return "am" 550 } 551 } else { 552 switch caps { 553 case true: 554 return "PM" 555 default: 556 return "pm" 557 } 558 } 559 } 560 561 func getWeekdayNum(d time.Time) int { 562 r := int(d.Weekday()) 563 564 if r == 0 { 565 r = 7 566 } 567 568 return r 569 } 570 571 func getTimezone(d time.Time, separator bool) string { 572 negative := false 573 _, tzofs := d.Zone() 574 575 if tzofs < 0 { 576 negative = true 577 tzofs *= -1 578 } 579 580 hours := int64(tzofs) / _HOUR 581 minutes := int64(tzofs) % _HOUR 582 583 switch negative { 584 case true: 585 if separator { 586 return fmt.Sprintf("-%02d:%02d", hours, minutes) 587 } 588 589 return fmt.Sprintf("-%02d%02d", hours, minutes) 590 591 default: 592 if separator { 593 return fmt.Sprintf("+%02d:%02d", hours, minutes) 594 } 595 596 return fmt.Sprintf("+%02d%02d", hours, minutes) 597 } 598 } 599 600 func getShortDuration(dur time.Duration, highPrecision bool) string { 601 var h, m int64 602 603 s := dur.Seconds() 604 d := int64(s) 605 606 if d >= 3600 { 607 h = d / 3600 608 d = d % 3600 609 } 610 611 if d >= 60 { 612 m = d / 60 613 d = d % 60 614 } 615 616 if h > 0 { 617 return fmt.Sprintf("%d:%02d:%02d", h, m, d) 618 } 619 620 if highPrecision && s < 10.0 { 621 ms := fmt.Sprintf("%.3f", s-math.Floor(s)) 622 ms = strings.ReplaceAll(ms, "0.", "") 623 return fmt.Sprintf("%d:%02d.%s", m, d, ms) 624 } 625 626 return fmt.Sprintf("%d:%02d", m, d) 627 } 628 629 // It's ok to have so nested blocks in this method 630 // codebeat:disable[BLOCK_NESTING] 631 632 func getPrettyLongDuration(dur time.Duration) string { 633 d := int64(dur.Seconds()) 634 635 var result []string 636 637 MAINLOOP: 638 for i := 0; i < 5; i++ { 639 switch { 640 case d >= _WEEK: 641 weeks := d / _WEEK 642 d = d % _WEEK 643 result = append(result, pluralize.PS(pluralize.En, "%d %s", weeks, "week", "weeks")) 644 case d >= _DAY: 645 days := d / _DAY 646 d = d % _DAY 647 result = append(result, pluralize.PS(pluralize.En, "%d %s", days, "day", "days")) 648 case d >= _HOUR: 649 hours := d / _HOUR 650 d = d % _HOUR 651 result = append(result, pluralize.PS(pluralize.En, "%d %s", hours, "hour", "hours")) 652 case d >= _MINUTE: 653 minutes := d / _MINUTE 654 d = d % _MINUTE 655 result = append(result, pluralize.PS(pluralize.En, "%d %s", minutes, "minute", "minutes")) 656 case d >= 1: 657 result = append(result, pluralize.PS(pluralize.En, "%d %s", d, "second", "seconds")) 658 break MAINLOOP 659 case d <= 0 && len(result) == 0: 660 return "< 1 second" 661 } 662 } 663 664 resultLen := len(result) 665 666 if resultLen > 1 { 667 return strings.Join(result[:resultLen-1], " ") + " and " + result[resultLen-1] 668 } 669 670 return result[0] 671 } 672 673 // codebeat:enable[BLOCK_NESTING] 674 675 func getPrettyShortDuration(d time.Duration) string { 676 var duration float64 677 678 switch { 679 case d > time.Millisecond: 680 duration = float64(d) / float64(time.Millisecond) 681 return fmt.Sprintf("%g ms", formatFloat(duration)) 682 683 case d > time.Microsecond: 684 duration = float64(d) / float64(time.Microsecond) 685 return fmt.Sprintf("%g μs", formatFloat(duration)) 686 687 default: 688 return fmt.Sprintf("%d ns", d.Nanoseconds()) 689 690 } 691 } 692 693 func appendDur(value int64, buf *bytes.Buffer, mod int64) (int64, error) { 694 v, err := strconv.ParseInt(buf.String(), 10, 64) 695 696 if err != nil { 697 return 0, err 698 } 699 700 buf.Reset() 701 702 return value + (v * mod), nil 703 } 704 705 func formatFloat(f float64) float64 { 706 if f < 10.0 { 707 return mathutil.Round(f, 2) 708 } 709 710 return mathutil.Round(f, 1) 711 }