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  }