times.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package humanize
  2. import (
  3. "fmt"
  4. "kitty/tools/wcswidth"
  5. "math"
  6. "sort"
  7. "strings"
  8. "time"
  9. )
  10. // Seconds-based time units
  11. const (
  12. Day = 24 * time.Hour
  13. Week = 7 * Day
  14. Month = 30 * Day
  15. Year = 12 * Month
  16. LongTime = 37 * Year
  17. )
  18. // Time formats a time into a relative string.
  19. //
  20. // Time(someT) -> "3 weeks ago"
  21. func Time(then time.Time) string {
  22. return RelTime(then, time.Now(), "ago", "from now")
  23. }
  24. // A RelTimeMagnitude struct contains a relative time point at which
  25. // the relative format of time will switch to a new format string. A
  26. // slice of these in ascending order by their "D" field is passed to
  27. // CustomRelTime to format durations.
  28. //
  29. // The Format field is a string that may contain a "%s" which will be
  30. // replaced with the appropriate signed label (e.g. "ago" or "from
  31. // now") and a "%d" that will be replaced by the quantity.
  32. //
  33. // The DivBy field is the amount of time the time difference must be
  34. // divided by in order to display correctly.
  35. //
  36. // e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
  37. // DivBy should be time.Minute so whatever the duration is will be
  38. // expressed in minutes.
  39. type RelTimeMagnitude struct {
  40. D time.Duration
  41. Format string
  42. DivBy time.Duration
  43. }
  44. var defaultMagnitudes = []RelTimeMagnitude{
  45. {time.Second, "now", time.Second},
  46. {2 * time.Second, "1 second %s", 1},
  47. {time.Minute, "%d seconds %s", time.Second},
  48. {2 * time.Minute, "1 minute %s", 1},
  49. {time.Hour, "%d minutes %s", time.Minute},
  50. {2 * time.Hour, "1 hour %s", 1},
  51. {Day, "%d hours %s", time.Hour},
  52. {2 * Day, "1 day %s", 1},
  53. {Week, "%d days %s", Day},
  54. {2 * Week, "1 week %s", 1},
  55. {Month, "%d weeks %s", Week},
  56. {2 * Month, "1 month %s", 1},
  57. {Year, "%d months %s", Month},
  58. {18 * Month, "1 year %s", 1},
  59. {2 * Year, "2 years %s", 1},
  60. {LongTime, "%d years %s", Year},
  61. {math.MaxInt64, "a long while %s", 1},
  62. }
  63. // RelTime formats a time into a relative string.
  64. //
  65. // It takes two times and two labels. In addition to the generic time
  66. // delta string (e.g. 5 minutes), the labels are used applied so that
  67. // the label corresponding to the smaller time is applied.
  68. //
  69. // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
  70. func RelTime(a, b time.Time, albl, blbl string) string {
  71. return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
  72. }
  73. // CustomRelTime formats a time into a relative string.
  74. //
  75. // It takes two times two labels and a table of relative time formats.
  76. // In addition to the generic time delta string (e.g. 5 minutes), the
  77. // labels are used applied so that the label corresponding to the
  78. // smaller time is applied.
  79. func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
  80. lbl := albl
  81. diff := b.Sub(a)
  82. if a.After(b) {
  83. lbl = blbl
  84. diff = a.Sub(b)
  85. }
  86. n := sort.Search(len(magnitudes), func(i int) bool {
  87. return magnitudes[i].D > diff
  88. })
  89. if n >= len(magnitudes) {
  90. n = len(magnitudes) - 1
  91. }
  92. mag := magnitudes[n]
  93. args := []interface{}{}
  94. escaped := false
  95. for _, ch := range mag.Format {
  96. if escaped {
  97. switch ch {
  98. case 's':
  99. args = append(args, lbl)
  100. case 'd':
  101. args = append(args, diff/mag.DivBy)
  102. }
  103. escaped = false
  104. } else {
  105. escaped = ch == '%'
  106. }
  107. }
  108. return fmt.Sprintf(mag.Format, args...)
  109. }
  110. func optional_cut(x string, sep string) (string, string) {
  111. a, b, found := strings.Cut(x, sep)
  112. if found {
  113. return a, b
  114. }
  115. return "00", a
  116. }
  117. func zero_pad(x string) string {
  118. if len(x) < 2 {
  119. x = strings.Repeat("0", 2-len(x)) + x
  120. }
  121. return x
  122. }
  123. // Render the duration in exactly 8 visual chars
  124. func ShortDuration(val time.Duration) (ans string) {
  125. if val >= time.Second {
  126. if val > Day {
  127. days := int(val.Hours() / 24)
  128. if days > 99 {
  129. ans = `∞`
  130. } else {
  131. if days == 1 {
  132. ans = ">1 day"
  133. } else {
  134. ans = fmt.Sprintf(">%d days", int(days))
  135. }
  136. }
  137. } else {
  138. ans = val.String()
  139. hr, rest := optional_cut(ans, `h`)
  140. min, rest := optional_cut(rest, `m`)
  141. secs, _, _ := strings.Cut(rest, ".")
  142. secs = strings.Replace(secs, `s`, ``, 1)
  143. ans = zero_pad(hr) + `:` + zero_pad(min) + `:` + zero_pad(secs)
  144. }
  145. } else {
  146. ans = "<1 sec"
  147. }
  148. if w := wcswidth.Stringwidth(ans); w < 8 {
  149. ans = strings.Repeat(" ", 8-w) + ans
  150. } else if w > 8 {
  151. ans = wcswidth.TruncateToVisualLength(ans, 8)
  152. }
  153. return
  154. }