Neaten strptime.go (#950)

This commit is contained in:
John Kerl 2022-02-20 00:29:35 -05:00 committed by GitHub
parent 8cf7de2b74
commit 43ff9108ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 55 deletions

View file

@ -113,111 +113,144 @@ func Check(format string) error {
return nil
}
func strptime_tz(value, format string, ignoreUnsupported bool, useTZ bool, location *time.Location) (time.Time, error) {
format = expandShorthands(format)
func strptime_tz(
strptime_input, strptime_format string, ignoreUnsupported bool, useTZ bool, location *time.Location,
) (time.Time, error) {
parseStr := ""
parseFmt := ""
vi := 0
// E.g. re-write "%F" to "%Y-%m-%d".
strptime_format = expandShorthands(strptime_format)
parts := strings.Split(format, "%")
for pi, ps := range parts {
if pi == 0 {
// check prefix string
if value[:len(ps)] != ps {
// The job of strptime is to map "format strings" like "%Y-%m-%d %H:%M:%S" to
// Go-library "templates" like "2006 01 02 15 04 05".
//
// The way this works within pbnjay/strptime is to split the format string on "%", then walk
// through and modify the input string as well.
//
// Example:
// * strptime("2015-08-28T13:33:21Z", "%Y-%m-%dT%H:%M:%SZ")
// * strptime input "2015-08-28T13:33:21Z"
// * strptime format "%Y-%m-%dT%H:%M:%SZ"
// * go-lib input "2015 08 28 13 33 21"
// * go-lib template "2006 01 02 15 04 05"
//
// Note that since we split the strptime-style format string on "%", the first character in each
// part is a format character like 'Y', 'm', etc -- except for the very start of the format
// string which may have some prefix text before its very first percent sign.
goLibInput := ""
goLibTemplate := ""
// sii and sil are start index and length of components in the strptime-style input string,
// i.e. the caller's original date/time string to be parsed.
sii := 0
partsBetweenPercentSigns := strings.Split(strptime_format, "%")
for partsIndex, partBetweenPercentSigns := range partsBetweenPercentSigns {
if partsIndex == 0 {
// Check for prefix text. It must be an exact match, e.g. with input "foo 2021" and
// format "foo %Y", "foo " == "foo ". Or, if the format starts with a "%", we're
// checking "" == "".
if strptime_input[:len(partBetweenPercentSigns)] != partBetweenPercentSigns {
return time.Time{}, ErrFormatMismatch
}
vi += len(ps)
continue
}
// since we split on '%', this is the format code
c := int(ps[0])
if c == '%' { // handle %% quickly
if ps != value[vi:vi+len(ps)] {
return time.Time{}, ErrFormatMismatch
}
vi += len(ps)
sii += len(partBetweenPercentSigns)
continue
}
// Check if format is supported and get the time.Parse translation
f, supported := formatMap[c]
// Since we split on '%', this is the format code
formatCode := int(partBetweenPercentSigns[0])
// TODO: I don't think this is right. And, needs a unit-test case.
if formatCode == '%' { // Handle %% straight off, as this is just a text-match.
if partBetweenPercentSigns != strptime_input[sii:sii+len(partBetweenPercentSigns)] {
return time.Time{}, ErrFormatMismatch
}
sii += len(partBetweenPercentSigns)
continue
}
// Check if the format code is supported, and map the strptime-style format code to the
// Go-library (time.Parse) template component, e.g. 'Y' -> "2006".
templateComponent, supported := formatMap[formatCode]
if !supported && !ignoreUnsupported {
return time.Time{}, ErrFormatUnsupported
}
// Check the intervening text between format strings.
// There may be some edge cases where this isn't quite right
// but if that's the case you've got other problems...
vj := len(ps) - 1
if vj > 0 {
vj = strings.Index(value[vi:], ps[1:])
// Check the intervening text between format strings, e.g. the ":" in "%Y:%m". There may be
// some edge cases where this isn't quite right but if that's the case you've got other
// problems ...
// Subtract 1 for the format code itself. E.g. with "%Y:%m", splitting on "%", one piece
// is "Y:". sil is the length of the ":" part.
sil := len(partBetweenPercentSigns) - 1
// Now sil becomes the offset of this part within the strptime-style input.
if sil > 0 {
sil = strings.Index(strptime_input[sii:], partBetweenPercentSigns[1:])
}
if vj == -1 {
if sil == -1 {
return time.Time{}, ErrFormatMismatch
}
if supported {
// Build up a new format and date string
if vj == 0 { // no intervening text
if c == 'f' {
vj = len(value) - vi
// Accumulate the go-lib style template and input strings.
if sil == 0 { // No intervening text, e.g. "%Y%m%d"
if formatCode == 'f' {
sil = len(strptime_input) - sii
} else {
vj = len(f)
if vj > len(value)-vi {
sil = len(templateComponent)
if sil > len(strptime_input)-sii {
return time.Time{}, ErrFormatMismatch
}
}
}
if c == 'f' {
parseFmt += "." + f
parseStr += "." + value[vi:vi+vj]
} else if c == 'p' {
parseFmt += " " + f
parseStr += " " + strings.ToUpper(value[vi:vi+vj])
if formatCode == 'f' {
goLibTemplate += "." + templateComponent
goLibInput += "." + strptime_input[sii:sii+sil]
} else if formatCode == 'p' {
goLibTemplate += " " + templateComponent
goLibInput += " " + strings.ToUpper(strptime_input[sii:sii+sil])
} else {
parseFmt += " " + f
parseStr += " " + value[vi:vi+vj]
goLibTemplate += " " + templateComponent
goLibInput += " " + strptime_input[sii:sii+sil]
}
}
if !supported && vj == 0 {
// ignore to the end of the string
vi = len(value)
if !supported && sil == 0 {
// Ignore to the end of the string
sii = len(strptime_input)
} else {
vi += (len(ps) - 1) + vj
sii += (len(partBetweenPercentSigns) - 1) + sil
}
}
if vi < len(value) {
// extra text on end of value
if sii < len(strptime_input) {
// Extra text on end of strptime_input
return time.Time{}, ErrFormatMismatch
}
// Now call the Go time library with template and input formatted the way it wants.
if useTZ {
if location != nil {
return time.ParseInLocation(parseFmt, parseStr, location)
return time.ParseInLocation(goLibTemplate, goLibInput, location)
} else {
tz := os.Getenv("TZ")
if tz == "" {
return time.Parse(parseFmt, parseStr)
return time.Parse(goLibTemplate, goLibInput)
} else {
location, err := time.LoadLocation(tz)
if err != nil {
return time.Time{}, err
}
return time.ParseInLocation(parseFmt, parseStr, location)
return time.ParseInLocation(goLibTemplate, goLibInput, location)
}
}
} else {
return time.Parse(parseFmt, parseStr)
return time.Parse(goLibTemplate, goLibInput)
}
}
// expandShorthands handles some shorthands that the C library uses, which we can easily
// replicate -- e.g. "%T" is "%Y-%m-%d".
// replicate -- e.g. "%F" is "%Y-%m-%d".
func expandShorthands(format string) string {
// TODO: mem cache
format = strings.ReplaceAll(format, "%T", "%H:%M:%S")

View file

@ -1,7 +1,9 @@
=============================================================== RELEASES
* plan 6.1.0
o strptime/882
- UT-per-se cases
m strptime/strftime tabulate options
- UT case for %% matching
? https://github.com/bykof/gostradamus
? https://golangrepo.com/repo/leekchan-timeutil-go-date-time
? port mlr5 c -> go?