timeutil.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. package timeutil
  2. import (
  3. "errors"
  4. "fmt"
  5. "regexp"
  6. "strconv"
  7. "strings"
  8. "sync"
  9. "time"
  10. )
  11. const (
  12. // SecsPerMin expresses the amount of seconds in a minute
  13. SecsPerMin = 60.0
  14. // SecsPerHour expresses the amount of seconds in a minute
  15. SecsPerHour = 3600.0
  16. // SecsPerDay expressed the amount of seconds in a day
  17. SecsPerDay = 86400.0
  18. // MinsPerHour expresses the amount of minutes in an hour
  19. MinsPerHour = 60.0
  20. // MinsPerDay expresses the amount of minutes in a day
  21. MinsPerDay = 1440.0
  22. // HoursPerDay expresses the amount of hours in a day
  23. HoursPerDay = 24.0
  24. // HoursPerMonth expresses the amount of hours in a month
  25. HoursPerMonth = 730.0
  26. // DaysPerMonth expresses the amount of days in a month
  27. DaysPerMonth = 30.42
  28. // Day expresses 24 hours
  29. Day = time.Hour * 24.0
  30. )
  31. // DurationString converts a duration to a Prometheus-compatible string in
  32. // terms of days, hours, minutes, or seconds.
  33. func DurationString(duration time.Duration) string {
  34. durSecs := int64(duration.Seconds())
  35. durStr := ""
  36. if durSecs > 0 {
  37. if durSecs%SecsPerDay == 0 {
  38. // convert to days
  39. durStr = fmt.Sprintf("%dd", durSecs/SecsPerDay)
  40. } else if durSecs%SecsPerHour == 0 {
  41. // convert to hours
  42. durStr = fmt.Sprintf("%dh", durSecs/SecsPerHour)
  43. } else if durSecs%SecsPerMin == 0 {
  44. // convert to mins
  45. durStr = fmt.Sprintf("%dm", durSecs/SecsPerMin)
  46. } else if durSecs > 0 {
  47. // default to secs, as long as duration is positive
  48. durStr = fmt.Sprintf("%ds", durSecs)
  49. }
  50. }
  51. return durStr
  52. }
  53. // DurationToPromOffsetString returns a Prometheus formatted string with leading offset or empty string if given a negative duration
  54. func DurationToPromOffsetString(duration time.Duration) string {
  55. dirStr := DurationString(duration)
  56. if dirStr != "" {
  57. dirStr = fmt.Sprintf("offset %s", dirStr)
  58. }
  59. return dirStr
  60. }
  61. // DurationOffsetStrings converts a (duration, offset) pair to Prometheus-
  62. // compatible strings in terms of days, hours, minutes, or seconds.
  63. func DurationOffsetStrings(duration, offset time.Duration) (string, string) {
  64. return DurationString(duration), DurationString(offset)
  65. }
  66. // FormatStoreResolution provides a clean notation for ETL store resolutions.
  67. // e.g. daily => 1d; hourly => 1h
  68. func FormatStoreResolution(dur time.Duration) string {
  69. if dur >= 24*time.Hour {
  70. return fmt.Sprintf("%dd", int(dur.Hours()/24.0))
  71. } else if dur >= time.Hour {
  72. return fmt.Sprintf("%dh", int(dur.Hours()))
  73. }
  74. return fmt.Sprint(dur)
  75. }
  76. // ParseDuration parses a duration string.
  77. // A duration string is a possibly signed sequence of
  78. // decimal numbers, each with optional fraction and a unit suffix,
  79. // such as "300ms", "-1.5h" or "2h45m".
  80. // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d"
  81. func ParseDuration(duration string) (time.Duration, error) {
  82. duration = CleanDurationString(duration)
  83. return goParseDuration(duration)
  84. }
  85. // unitMap contains a list of units that can be parsed by ParseDuration
  86. var unitMap = map[string]int64{
  87. "ns": int64(time.Nanosecond),
  88. "us": int64(time.Microsecond),
  89. "µs": int64(time.Microsecond), // U+00B5 = micro symbol
  90. "μs": int64(time.Microsecond), // U+03BC = Greek letter mu
  91. "ms": int64(time.Millisecond),
  92. "s": int64(time.Second),
  93. "m": int64(time.Minute),
  94. "h": int64(time.Hour),
  95. "d": int64(time.Hour * 24),
  96. }
  97. // goParseDuration is time.ParseDuration lifted from the go std library and enhanced with the ability to
  98. // handle the "d" (day) unit. The contents of the function itself are identical to the std library, it is
  99. // only the unitMap above that contains the added unit.
  100. func goParseDuration(s string) (time.Duration, error) {
  101. // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
  102. orig := s
  103. var d int64
  104. neg := false
  105. // Consume [-+]?
  106. if s != "" {
  107. c := s[0]
  108. if c == '-' || c == '+' {
  109. neg = c == '-'
  110. s = s[1:]
  111. }
  112. }
  113. // Special case: if all that is left is "0", this is zero.
  114. if s == "0" {
  115. return 0, nil
  116. }
  117. if s == "" {
  118. return 0, errors.New("time: invalid duration " + quote(orig))
  119. }
  120. for s != "" {
  121. var (
  122. v, f int64 // integers before, after decimal point
  123. scale float64 = 1 // value = v + f/scale
  124. )
  125. var err error
  126. // The next character must be [0-9.]
  127. if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
  128. return 0, errors.New("time: invalid duration " + quote(orig))
  129. }
  130. // Consume [0-9]*
  131. pl := len(s)
  132. v, s, err = leadingInt(s)
  133. if err != nil {
  134. return 0, errors.New("time: invalid duration " + quote(orig))
  135. }
  136. pre := pl != len(s) // whether we consumed anything before a period
  137. // Consume (\.[0-9]*)?
  138. post := false
  139. if s != "" && s[0] == '.' {
  140. s = s[1:]
  141. pl := len(s)
  142. f, scale, s = leadingFraction(s)
  143. post = pl != len(s)
  144. }
  145. if !pre && !post {
  146. // no digits (e.g. ".s" or "-.s")
  147. return 0, errors.New("time: invalid duration " + quote(orig))
  148. }
  149. // Consume unit.
  150. i := 0
  151. for ; i < len(s); i++ {
  152. c := s[i]
  153. if c == '.' || '0' <= c && c <= '9' {
  154. break
  155. }
  156. }
  157. if i == 0 {
  158. return 0, errors.New("time: missing unit in duration " + quote(orig))
  159. }
  160. u := s[:i]
  161. s = s[i:]
  162. unit, ok := unitMap[u]
  163. if !ok {
  164. return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig))
  165. }
  166. if v > (1<<63-1)/unit {
  167. // overflow
  168. return 0, errors.New("time: invalid duration " + quote(orig))
  169. }
  170. v *= unit
  171. if f > 0 {
  172. // float64 is needed to be nanosecond accurate for fractions of hours.
  173. // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
  174. v += int64(float64(f) * (float64(unit) / scale))
  175. if v < 0 {
  176. // overflow
  177. return 0, errors.New("time: invalid duration " + quote(orig))
  178. }
  179. }
  180. d += v
  181. if d < 0 {
  182. // overflow
  183. return 0, errors.New("time: invalid duration " + quote(orig))
  184. }
  185. }
  186. if neg {
  187. d = -d
  188. }
  189. return time.Duration(d), nil
  190. }
  191. // CleanDurationString removes prometheus formatted prefix "offset " allong with leading a trailing whitespace
  192. // from duration string, leaving behind a string with format [0-9+](s|m|d|h)
  193. func CleanDurationString(duration string) string {
  194. duration = strings.TrimSpace(duration)
  195. duration = strings.TrimPrefix(duration, "offset ")
  196. return duration
  197. }
  198. // ParseTimeRange returns a start and end time, respectively, which are converted from
  199. // a duration and offset, defined as strings with Prometheus-style syntax.
  200. func ParseTimeRange(duration, offset time.Duration) (time.Time, time.Time) {
  201. // endTime defaults to the current time, unless an offset is explicity declared,
  202. // in which case it shifts endTime back by given duration
  203. endTime := time.Now()
  204. if offset > 0 {
  205. endTime = endTime.Add(-1 * offset)
  206. }
  207. startTime := endTime.Add(-1 * duration)
  208. return startTime, endTime
  209. }
  210. // FormatDurationStringDaysToHours converts string from format [0-9+]d to [0-9+]h
  211. func FormatDurationStringDaysToHours(param string) (string, error) {
  212. //check that input matches format
  213. ok, err := regexp.MatchString("[0-9+]d", param)
  214. if !ok {
  215. return param, fmt.Errorf("FormatDurationStringDaysToHours: input string (%s) not formatted as [0-9+]d", param)
  216. }
  217. if err != nil {
  218. return "", err
  219. }
  220. // convert days to hours
  221. if param[len(param)-1:] == "d" {
  222. count := param[:len(param)-1]
  223. val, err := strconv.ParseInt(count, 10, 64)
  224. if err != nil {
  225. return "", err
  226. }
  227. val = val * 24
  228. param = fmt.Sprintf("%dh", val)
  229. }
  230. return param, nil
  231. }
  232. // JobTicker is a ticker used to synchronize the next run of a repeating
  233. // process. The designated use-case is for infinitely-looping selects,
  234. // where a timeout or an exit channel might cancel the process, but otherwise
  235. // the intent is to wait at the select for some amount of time until the
  236. // next run. This differs from a standard ticker, which ticks without
  237. // waiting and drops any missed ticks; rather, this ticker must be kicked
  238. // off manually for each tick, so that after the current run of the job
  239. // completes, the timer starts again.
  240. type JobTicker struct {
  241. Ch <-chan time.Time
  242. ch chan time.Time
  243. closed bool
  244. mx sync.Mutex
  245. }
  246. // NewJobTicker instantiates a new JobTicker.
  247. func NewJobTicker() *JobTicker {
  248. c := make(chan time.Time)
  249. return &JobTicker{
  250. Ch: c,
  251. ch: c,
  252. closed: false,
  253. }
  254. }
  255. // Close closes the JobTicker channels
  256. func (jt *JobTicker) Close() {
  257. jt.mx.Lock()
  258. defer jt.mx.Unlock()
  259. if jt.closed {
  260. return
  261. }
  262. jt.closed = true
  263. close(jt.ch)
  264. }
  265. // TickAt schedules the next tick of the ticker for the given time in the
  266. // future. If the time is not in the future, the ticker will tick immediately.
  267. func (jt *JobTicker) TickAt(t time.Time) {
  268. go func(t time.Time) {
  269. n := time.Now()
  270. if t.After(n) {
  271. time.Sleep(t.Sub(n))
  272. }
  273. jt.mx.Lock()
  274. defer jt.mx.Unlock()
  275. if !jt.closed {
  276. jt.ch <- time.Now()
  277. }
  278. }(t)
  279. }
  280. // TickIn schedules the next tick of the ticker for the given duration into
  281. // the future. If the duration is less than or equal to zero, the ticker will
  282. // tick immediately.
  283. func (jt *JobTicker) TickIn(d time.Duration) {
  284. go func(d time.Duration) {
  285. if d > 0 {
  286. time.Sleep(d)
  287. }
  288. jt.mx.Lock()
  289. defer jt.mx.Unlock()
  290. if !jt.closed {
  291. jt.ch <- time.Now()
  292. }
  293. }(d)
  294. }
  295. // NOTE: The following functions were lifted from the go std library to support the ParseDuration enhancement
  296. // NOTE: described above.
  297. const (
  298. lowerhex = "0123456789abcdef"
  299. runeSelf = 0x80
  300. runeError = '\uFFFD'
  301. )
  302. // quote is lifted from the go std library to support the custom ParseDuration enhancement
  303. func quote(s string) string {
  304. buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes
  305. buf[0] = '"'
  306. for i, c := range s {
  307. if c >= runeSelf || c < ' ' {
  308. // This means you are asking us to parse a time.Duration or
  309. // time.Location with unprintable or non-ASCII characters in it.
  310. // We don't expect to hit this case very often. We could try to
  311. // reproduce strconv.Quote's behavior with full fidelity but
  312. // given how rarely we expect to hit these edge cases, speed and
  313. // conciseness are better.
  314. var width int
  315. if c == runeError {
  316. width = 1
  317. if i+2 < len(s) && s[i:i+3] == string(runeError) {
  318. width = 3
  319. }
  320. } else {
  321. width = len(string(c))
  322. }
  323. for j := 0; j < width; j++ {
  324. buf = append(buf, `\x`...)
  325. buf = append(buf, lowerhex[s[i+j]>>4])
  326. buf = append(buf, lowerhex[s[i+j]&0xF])
  327. }
  328. } else {
  329. if c == '"' || c == '\\' {
  330. buf = append(buf, '\\')
  331. }
  332. buf = append(buf, string(c)...)
  333. }
  334. }
  335. buf = append(buf, '"')
  336. return string(buf)
  337. }
  338. // leadingFraction consumes the leading [0-9]* from s.
  339. // It is used only for fractions, so does not return an error on overflow,
  340. // it just stops accumulating precision.
  341. func leadingFraction(s string) (x int64, scale float64, rem string) {
  342. i := 0
  343. scale = 1
  344. overflow := false
  345. for ; i < len(s); i++ {
  346. c := s[i]
  347. if c < '0' || c > '9' {
  348. break
  349. }
  350. if overflow {
  351. continue
  352. }
  353. if x > (1<<63-1)/10 {
  354. // It's possible for overflow to give a positive number, so take care.
  355. overflow = true
  356. continue
  357. }
  358. y := x*10 + int64(c) - '0'
  359. if y < 0 {
  360. overflow = true
  361. continue
  362. }
  363. x = y
  364. scale *= 10
  365. }
  366. return x, scale, s[i:]
  367. }
  368. var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
  369. // leadingInt consumes the leading [0-9]* from s.
  370. func leadingInt(s string) (x int64, rem string, err error) {
  371. i := 0
  372. for ; i < len(s); i++ {
  373. c := s[i]
  374. if c < '0' || c > '9' {
  375. break
  376. }
  377. if x > (1<<63-1)/10 {
  378. // overflow
  379. return 0, "", errLeadingInt
  380. }
  381. x = x*10 + int64(c) - '0'
  382. if x < 0 {
  383. // overflow
  384. return 0, "", errLeadingInt
  385. }
  386. }
  387. return x, s[i:], nil
  388. }