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 }