diff --git a/internal/pkg/pbnjay-strptime/strptime.go b/internal/pkg/pbnjay-strptime/strptime.go index 5e1a33a91..e2d590288 100644 --- a/internal/pkg/pbnjay-strptime/strptime.go +++ b/internal/pkg/pbnjay-strptime/strptime.go @@ -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") diff --git a/todo.txt b/todo.txt index dea394559..4ce4e19d2 100644 --- a/todo.txt +++ b/todo.txt @@ -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?