miller/pkg/bifs/datetime_test.go
John Kerl 72ffcb3414
Support %a %A %e %h %c %x %X in strptime (#1518) (#2179)
Miller's strftime delegates to the full-featured lestrrat-go/strftime
library, but strptime uses an in-tree fork of a small subset-only
package, so strftime output couldn't always be parsed back by
strptime with the same format string. This adds the format codes
needed to round-trip common formats like "%a %b %e %T %Y":

* %a / %A (weekday name) and %h (alias of %b): straightforward
  formatMap entries, same pattern as the existing %b/%B.
* %e (space-padded day of month): needed dedicated width-detection
  logic, since %e's own optional leading pad space is otherwise
  indistinguishable from a literal separator space when the code
  searches for the next literal text to bound a field -- this was
  the root cause of the round-trip failure in the linked issue.
* %c, %x, %X: shorthand aliases (expanding to %a %b %e %H:%M:%S %Y,
  %m/%d/%y, and %H:%M:%S respectively), same mechanism already used
  for %T/%D/%F/%R/%r.

Also documents the %D/%F/%r/%R/%T shorthands in the strptime table
in reference-dsl-time.md.in, which were already supported but
missing from the docs -- this was the exact confusion reported in
the discussion linked from #1518.

Round-trip tests added in pkg/bifs/datetime_test.go assert
strptime(strftime(t, fmt), fmt) == t across the newly-supported
formats, plus table-driven cases in
pkg/pbnjay-strptime/strptime_test.go covering %e's edge cases
(padded/unpadded single digit, double digit, adjacent to another
code with no separator, at end of string).

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:42:03 -04:00

98 lines
3.7 KiB
Go

package bifs
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/johnkerl/miller/v6/pkg/mlrval"
)
// TestBIF_strftime_strptime_roundtrip checks that for each of a set of formats
// exercising the newly-added %a %A %e %h %x %X %c strptime codes,
// strptime(strftime(t, fmt), fmt) == t. This is the exact failure mode from
// https://github.com/johnkerl/miller/issues/1518, where strptime accepted fewer
// format codes than strftime produced.
func TestBIF_strftime_strptime_roundtrip(t *testing.T) {
formats := []string{
"%a %b %e %T %Y",
"%A %b %e %T %Y",
"%a %h %e %T %Y",
"%A %h %e %T %Y",
"%c",
"%x %X",
"%Y-%m-%d %H:%M:%S",
}
epochSecondsValues := []int64{
0, // 1970-01-01, single-digit day
1709552468, // 2024-03-04 11:41:08, single-digit day
1729555200, // 2024-10-22 00:00:00, double-digit day
1735732805, // 2025-01-01 12:00:05, single-digit day, new year
}
for _, format := range formats {
for _, epochSeconds := range epochSecondsValues {
formatted := BIF_strftime(mlrval.FromInt(epochSeconds), mlrval.FromString(format))
formattedString, isString := formatted.GetStringValue()
assert.True(t, isString, "strftime(%d, %q) produced non-string %v", epochSeconds, format, formatted)
parsed := BIF_strptime(mlrval.FromString(formattedString), mlrval.FromString(format))
parsedSeconds, isNumeric := parsed.GetNumericToFloatValue()
assert.True(t, isNumeric, "strptime(%q, %q) produced non-numeric %v", formattedString, format, parsed)
assert.Equal(
t, float64(epochSeconds), parsedSeconds,
"round-trip mismatch for format %q: strftime(%d) = %q, strptime(...) = %v",
format, epochSeconds, formattedString, parsedSeconds,
)
}
}
}
// TestBIF_strptime_new_format_codes exercises the newly-supported %a %A %e %h
// %x %X %c strptime format codes directly, including %e's space-padding edge
// cases (single-digit day with/without padding, double-digit day, %e at the
// end of the string, and %e directly adjacent to another format code).
func TestBIF_strptime_new_format_codes(t *testing.T) {
type testCase struct {
input string
format string
want float64
}
cases := []testCase{
{"Mon Mar 4 11:41:08 2024", "%a %b %e %T %Y", 1709552468},
{"Monday Mar 4 11:41:08 2024", "%A %b %e %T %Y", 1709552468},
{"Mon Mar 4 11:41:08 2024", "%a %h %e %T %Y", 1709552468},
{"Tue Oct 22 00:00:00 2024", "%a %b %e %T %Y", 1729555200},
{"Mon Mar 4 11:41:08 2024", "%c", 1709552468},
{"03/04/24 11:41:08", "%x %X", 1709552468},
// %e with a single (non-padded) space before a single-digit day.
{"Mar 4 2024", "%b %e %Y", 1709510400},
// %e with two-digit day.
{"Mar 14 2024", "%b %e %Y", 1710374400},
// %e directly adjacent to another format code, no intervening text.
{"4Mar2024", "%e%b%Y", 1709510400},
{"14Mar2024", "%e%b%Y", 1710374400},
// %e at the very end of the string.
{"2024-03-4", "%Y-%m-%e", 1709510400},
{"2024-03-14", "%Y-%m-%e", 1710374400},
}
for _, c := range cases {
output := BIF_strptime(mlrval.FromString(c.input), mlrval.FromString(c.format))
seconds, isNumeric := output.GetNumericToFloatValue()
assert.True(t, isNumeric, "strptime(%q, %q) produced non-numeric %v", c.input, c.format, output)
assert.Equal(t, c.want, seconds, "strptime(%q, %q)", c.input, c.format)
}
}
// TestBIF_strptime_still_unsupported_format_code checks that a format code
// which remains unsupported (%U, week-of-year) still errors, i.e. that adding
// the new codes didn't accidentally cause unsupported codes to be silently
// ignored.
func TestBIF_strptime_still_unsupported_format_code(t *testing.T) {
output := BIF_strptime(mlrval.FromString("2024-03-04 09"), mlrval.FromString("%Y-%m-%d %U"))
assert.True(t, output.IsError())
}