khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 1 | package humanize |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "math" |
| 6 | "sort" |
| 7 | "time" |
| 8 | ) |
| 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 | |
| 19 | // Time formats a time into a relative string. |
| 20 | // |
| 21 | // Time(someT) -> "3 weeks ago" |
| 22 | func Time(then time.Time) string { |
| 23 | return RelTime(then, time.Now(), "ago", "from now") |
| 24 | } |
| 25 | |
| 26 | // A RelTimeMagnitude struct contains a relative time point at which |
| 27 | // the relative format of time will switch to a new format string. A |
| 28 | // slice of these in ascending order by their "D" field is passed to |
| 29 | // CustomRelTime to format durations. |
| 30 | // |
| 31 | // The Format field is a string that may contain a "%s" which will be |
| 32 | // replaced with the appropriate signed label (e.g. "ago" or "from |
| 33 | // now") and a "%d" that will be replaced by the quantity. |
| 34 | // |
| 35 | // The DivBy field is the amount of time the time difference must be |
| 36 | // divided by in order to display correctly. |
| 37 | // |
| 38 | // e.g. if D is 2*time.Minute and you want to display "%d minutes %s" |
| 39 | // DivBy should be time.Minute so whatever the duration is will be |
| 40 | // expressed in minutes. |
| 41 | type RelTimeMagnitude struct { |
| 42 | D time.Duration |
| 43 | Format string |
| 44 | DivBy time.Duration |
| 45 | } |
| 46 | |
| 47 | var defaultMagnitudes = []RelTimeMagnitude{ |
| 48 | {time.Second, "now", time.Second}, |
| 49 | {2 * time.Second, "1 second %s", 1}, |
| 50 | {time.Minute, "%d seconds %s", time.Second}, |
| 51 | {2 * time.Minute, "1 minute %s", 1}, |
| 52 | {time.Hour, "%d minutes %s", time.Minute}, |
| 53 | {2 * time.Hour, "1 hour %s", 1}, |
| 54 | {Day, "%d hours %s", time.Hour}, |
| 55 | {2 * Day, "1 day %s", 1}, |
| 56 | {Week, "%d days %s", Day}, |
| 57 | {2 * Week, "1 week %s", 1}, |
| 58 | {Month, "%d weeks %s", Week}, |
| 59 | {2 * Month, "1 month %s", 1}, |
| 60 | {Year, "%d months %s", Month}, |
| 61 | {18 * Month, "1 year %s", 1}, |
| 62 | {2 * Year, "2 years %s", 1}, |
| 63 | {LongTime, "%d years %s", Year}, |
| 64 | {math.MaxInt64, "a long while %s", 1}, |
| 65 | } |
| 66 | |
| 67 | // RelTime formats a time into a relative string. |
| 68 | // |
| 69 | // It takes two times and two labels. In addition to the generic time |
| 70 | // delta string (e.g. 5 minutes), the labels are used applied so that |
| 71 | // the label corresponding to the smaller time is applied. |
| 72 | // |
| 73 | // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" |
| 74 | func RelTime(a, b time.Time, albl, blbl string) string { |
| 75 | return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) |
| 76 | } |
| 77 | |
| 78 | // CustomRelTime formats a time into a relative string. |
| 79 | // |
| 80 | // It takes two times two labels and a table of relative time formats. |
| 81 | // In addition to the generic time delta string (e.g. 5 minutes), the |
| 82 | // labels are used applied so that the label corresponding to the |
| 83 | // smaller time is applied. |
| 84 | func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { |
| 85 | lbl := albl |
| 86 | diff := b.Sub(a) |
| 87 | |
| 88 | if a.After(b) { |
| 89 | lbl = blbl |
| 90 | diff = a.Sub(b) |
| 91 | } |
| 92 | |
| 93 | n := sort.Search(len(magnitudes), func(i int) bool { |
| 94 | return magnitudes[i].D > diff |
| 95 | }) |
| 96 | |
| 97 | if n >= len(magnitudes) { |
| 98 | n = len(magnitudes) - 1 |
| 99 | } |
| 100 | mag := magnitudes[n] |
| 101 | args := []interface{}{} |
| 102 | escaped := false |
| 103 | for _, ch := range mag.Format { |
| 104 | if escaped { |
| 105 | switch ch { |
| 106 | case 's': |
| 107 | args = append(args, lbl) |
| 108 | case 'd': |
| 109 | args = append(args, diff/mag.DivBy) |
| 110 | } |
| 111 | escaped = false |
| 112 | } else { |
| 113 | escaped = ch == '%' |
| 114 | } |
| 115 | } |
| 116 | return fmt.Sprintf(mag.Format, args...) |
| 117 | } |