github.com/Aoi-hosizora/ahlib@v1.5.1-0.20230404072829-241b93cf91c7/xtime/strftime.go (about)

     1  package xtime
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/Aoi-hosizora/ahlib/xstring"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  const (
    13  	fsmNormal       = 0
    14  	fsmPercent      = 1
    15  	fsmPercentMinus = 2
    16  )
    17  
    18  // StrftimeInBytes formats given time.Time and strftime pattern to bytes, returns error when using invalid pattern, such as trailing '%'
    19  // or wrong character after '%' or '%-', please visit https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
    20  // and https://man7.org/linux/man-pages/man3/strftime.3.html#DESCRIPTION and https://github.com/lestrrat-go/strftime for more details.
    21  func StrftimeInBytes(pattern []byte, t time.Time) ([]byte, error) {
    22  	bs := make([]byte, 0, len(pattern))
    23  	fsm := fsmNormal
    24  	for _, b := range pattern {
    25  		if fsm == fsmNormal {
    26  			if b == '%' {
    27  				fsm = fsmPercent
    28  			} else {
    29  				bs = append(bs, b)
    30  			}
    31  		} else {
    32  			var result string
    33  			var err error
    34  			if fsm == fsmPercent {
    35  				if b == '-' {
    36  					fsm = fsmPercentMinus
    37  					continue
    38  				} else {
    39  					fsm = fsmNormal
    40  					result, err = strftimeFormatVerbatim(b, t, false)
    41  				}
    42  			} else if fsm == fsmPercentMinus {
    43  				fsm = fsmNormal
    44  				result, err = strftimeFormatVerbatim(b, t, true)
    45  			}
    46  			if err != nil {
    47  				return nil, err
    48  			}
    49  			bs = append(bs, result...)
    50  		}
    51  	}
    52  
    53  	if fsm != fsmNormal {
    54  		return nil, fmt.Errorf("invalid pattern '%s'", pattern)
    55  	}
    56  	return bs, nil
    57  }
    58  
    59  // StrftimeInString formats given time.Time and strftime pattern to string, returns error when using invalid pattern, such as trailing '%'
    60  // or wrong character after '%' or '%-', please visit https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
    61  // and https://man7.org/linux/man-pages/man3/strftime.3.html#DESCRIPTION and https://github.com/lestrrat-go/strftime for more details.
    62  func StrftimeInString(pattern string, t time.Time) (string, error) {
    63  	bs, err := StrftimeInBytes(xstring.FastStob(pattern), t)
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  	return xstring.FastBtos(bs), nil
    68  }
    69  
    70  // strftimeFormatVerbatim formats given character to result string in verbatim.
    71  func strftimeFormatVerbatim(ch byte, t time.Time, nopad bool) (result string, err error) {
    72  	var formatted string
    73  	if !nopad {
    74  		switch ch {
    75  		case '%':
    76  			formatted = "%"
    77  		case 'n':
    78  			formatted = "\n"
    79  		case 't':
    80  			formatted = "\t"
    81  		case 'Y': // fourDigitYearZeroPad
    82  			formatted = t.Format("2006")
    83  		case 'y': // twoDigitYearZeroPad, 00-99
    84  			formatted = t.Format("06")
    85  		case 'C': // centuryDecimalZeroPad
    86  			formatted = _padItoa(t.Year()/100, 2, false)
    87  		case 'm': // monthNumberZeroPad, 01-12
    88  			formatted = t.Format("01")
    89  		case 'B': // fullMonthName
    90  			formatted = t.Format("January")
    91  		case 'b', 'h': // abbrMonthName
    92  			formatted = t.Format("Jan")
    93  		case 'd': // dayOfMonthZeroPad, 01-31
    94  			formatted = t.Format("02")
    95  		case 'e': // dayOfMonthSpacePad, _1-31
    96  			formatted = t.Format("_2")
    97  		case 'A': // fullWeekDayName
    98  			formatted = t.Format("Monday")
    99  		case 'a': // abbrWeekDayName
   100  			formatted = t.Format("Mon")
   101  		case 'H': // twentyFourHourClockZeroPad, 00-23
   102  			formatted = t.Format("15")
   103  		case 'k': // twentyFourHourClockSpacePad, _0-23
   104  			formatted = _padItoa(t.Hour(), 2, true)
   105  		case 'I': // twelveHourClockZeroPad, 01-12
   106  			formatted = t.Format("03")
   107  		case 'l': // twelveHourClockSpacePad, _1-12
   108  			formatted = _padItoa(_twelveHour(t), 2, true)
   109  		case 'p': // capitalAmpm
   110  			formatted = t.Format("PM")
   111  		case 'P': // lowercaseAmpm
   112  			formatted = _lowercaseAmpm(t.Format("PM"))
   113  		case 'M': // minutesZeroPad, 00-59
   114  			formatted = t.Format("04")
   115  		case 'S': // secondsNumberZeroPad, 00-60
   116  			formatted = t.Format("05")
   117  		case 'Z': // timezone
   118  			formatted = t.Format("MST")
   119  		case 'z': // timezoneOffset
   120  			formatted = t.Format("-0700")
   121  		case 's': // secondsSinceEpoch
   122  			formatted = strconv.FormatInt(t.Unix(), 10)
   123  		case 'j': // dayOfYearZeroPad, 001-366
   124  			formatted = _padItoa(t.YearDay(), 3, false)
   125  		case 'w': // weekdaySundayOrigin, 0-6
   126  			formatted = strconv.Itoa(int(t.Weekday()))
   127  		case 'u': // weekdayMondayOrigin, 1-7
   128  			formatted = strconv.Itoa(_weekDayOffset(t, 1))
   129  		case 'U': // weekNumberSundayOriginZeroPad, 00-53
   130  			formatted = _padItoa(_weekNumberOffset(t, false), 2, false)
   131  		case 'W': // weekNumberMondayOriginZeroPad, 00-53
   132  			formatted = _padItoa(_weekNumberOffset(t, true), 2, false)
   133  		case 'G': // fourDigitISO8601YearZeroPad
   134  			formatted = _padItoa(_isoYear(t), 4, false)
   135  		case 'g': // twoDigitISO8601YearZeroPad, 01-99
   136  			formatted = _padItoa(_isoYear(t)%100, 2, false)
   137  		case 'V': // iso8601WeekNumberZeroPad, 01-53
   138  			formatted = _padItoa(_isoWeek(t), 2, false)
   139  		case 'c': // timeAndDate
   140  			formatted = t.Format("Mon Jan _2 15:04:05 2006")
   141  		case 'D': // mdy
   142  			formatted = t.Format("01/02/06")
   143  		case 'F': // ymd
   144  			formatted = t.Format("2006-01-02")
   145  		case 'R': // hm
   146  			formatted = t.Format("15:04")
   147  		case 'r': // imsp
   148  			formatted = t.Format("03:04:05 PM")
   149  		case 'T': // hms
   150  			formatted = t.Format("15:04:05")
   151  		case 'v': // eby
   152  			formatted = t.Format("_2-Jan-2006")
   153  		case 'X': // natReprTime
   154  			formatted = t.Format("15:04:05")
   155  		case 'x': // natReprDate
   156  			formatted = t.Format("01/02/06")
   157  		default:
   158  			err = fmt.Errorf("invalid pattern '%%%s'", string(ch))
   159  		}
   160  	} else {
   161  		switch ch {
   162  		case 'Y': // fourDigitYearNoPad
   163  			formatted = strconv.Itoa(t.Year())
   164  		case 'y': // twoDigitYearNoPad, 0-99
   165  			formatted = strconv.Itoa(t.Year() % 100)
   166  		case 'C': // centuryDecimalNoPad
   167  			formatted = strconv.Itoa(t.Year() / 100)
   168  		case 'm': // monthNumberNoPad, 1-12
   169  			formatted = t.Format("1")
   170  		case 'd': // dayOfMonthNoPad, 1-31
   171  			formatted = t.Format("2")
   172  		case 'H': // twentyFourHourClockNoPad, 0-23
   173  			formatted = strconv.Itoa(t.Hour())
   174  		case 'I': // twelveHourClockNoPad, 1-12
   175  			formatted = t.Format("3")
   176  		case 'M': // minutesNoPad, 0-59
   177  			formatted = t.Format("4")
   178  		case 'S': // secondsNumberNoPad, 0-60
   179  			formatted = t.Format("5")
   180  		case 'j': // dayOfYearNoPad, 1-366
   181  			formatted = strconv.Itoa(t.YearDay())
   182  		case 'U': // weekNumberSundayOriginNoPad, 0-53
   183  			formatted = strconv.Itoa(_weekNumberOffset(t, false))
   184  		case 'W': // weekNumberMondayOriginNoPad, 0-53
   185  			formatted = strconv.Itoa(_weekNumberOffset(t, true))
   186  		case 'G': // fourDigitISO8601YearNoPad
   187  			formatted = strconv.Itoa(_isoYear(t))
   188  		case 'g': // twoDigitISO8601YearNoPad, 1-99
   189  			formatted = strconv.Itoa(_isoYear(t) % 100)
   190  		case 'V': // iso8601WeekNumberNoPad, 1-53
   191  			formatted = strconv.Itoa(_isoWeek(t))
   192  		default:
   193  			err = fmt.Errorf("invalid pattern '%%-%s'", string(ch))
   194  		}
   195  	}
   196  	if err != nil {
   197  		return "", err
   198  	}
   199  	return formatted, nil
   200  }
   201  
   202  func _padItoa(num int, digit int, space bool) string {
   203  	s := strconv.Itoa(num)
   204  	switch {
   205  	case num >= 1000, num >= 100 && digit < 4, num >= 10 && digit < 3, num >= 0 && digit < 2:
   206  		return s
   207  	case num >= 100 && digit >= 4, num >= 10 && digit == 3, num >= 0 && digit == 2:
   208  		if space {
   209  			return " " + s
   210  		}
   211  		return "0" + s
   212  	case num >= 10 && digit >= 4, num >= 0 && digit == 3:
   213  		if space {
   214  			return "  " + s
   215  		}
   216  		return "00" + s
   217  	case num >= 0 && digit >= 4:
   218  		if space {
   219  			return "   " + s
   220  		}
   221  		return "000" + s
   222  	default:
   223  		return "???"
   224  	}
   225  }
   226  
   227  func _twelveHour(t time.Time) int {
   228  	h := t.Hour() % 12
   229  	if h == 0 {
   230  		h = 12
   231  	}
   232  	return h
   233  }
   234  
   235  func _lowercaseAmpm(s string) string {
   236  	if s == "AM" {
   237  		return "am"
   238  	}
   239  	return "pm"
   240  }
   241  
   242  // Note: some following code are referred from https://github.com/lestrrat-go/strftime.
   243  
   244  func _weekDayOffset(t time.Time, offset int) int {
   245  	wd := int(t.Weekday())
   246  	if wd < offset {
   247  		wd += 7
   248  	}
   249  	return wd
   250  }
   251  
   252  func _weekNumberOffset(t time.Time, mondayFirst bool) int {
   253  	// https://github.com/lestrrat-go/strftime/blob/547681d557/appenders.go#L248
   254  	// https://opensource.apple.com/source/Libc/Libc-167/string.subproj/strftime.c.auto.html
   255  	// https://github.com/arnoldrobbins/strftime/blob/03825f7dbd/strftime.c
   256  	yd := t.YearDay()
   257  	wd := int(t.Weekday())
   258  	if !mondayFirst {
   259  		return (yd + 7 - wd) / 7
   260  	}
   261  	if wd == 0 {
   262  		return (yd + 7 - 6) / 7
   263  	}
   264  	return (yd + 7 - (wd - 1)) / 7
   265  }
   266  
   267  func _isoYear(t time.Time) int {
   268  	n, _ := t.ISOWeek()
   269  	return n
   270  }
   271  
   272  func _isoWeek(t time.Time) int {
   273  	_, n := t.ISOWeek()
   274  	return n
   275  }
   276  
   277  var (
   278  	strftime2GlobPatternRe1 = regexp.MustCompile(`%-?[A-Za-z]`)
   279  	strftime2GlobPatternRe2 = regexp.MustCompile(`\*+`)
   280  )
   281  
   282  // StrftimeToGlobPattern returns a corresponding glob pattern from strftime pattern.
   283  func StrftimeToGlobPattern(pattern string) string {
   284  	pattern = strings.ReplaceAll(pattern, "%%", "%")
   285  	s := strftime2GlobPatternRe1.ReplaceAllString(pattern, "*")
   286  	s = strftime2GlobPatternRe2.ReplaceAllString(s, "*")
   287  	return s
   288  }