diff --git a/strftime/strftime.go b/strftime/strftime.go index 1cb1af7..b337fe5 100644 --- a/strftime/strftime.go +++ b/strftime/strftime.go @@ -1,46 +1,238 @@ // Package strftime wraps the C stdlib strftime and strptime functions. package strftime -/* -#include -#include -#include -int read_errno() { return errno; } -*/ -import "C" import ( - "syscall" + "fmt" + "regexp" + "strconv" + "strings" "time" - "unsafe" + "unicode/utf8" ) -// Note: The use of errno below is not thread-safe. -// -// Even if we added a mutex, it would not be thread-safe -// relative to other C stdlib calls that don't use that mutex. -// -// OTOH, I can't find a format string that C strftime thinks is illegal -// to test this with, so maybe this is a non-issue - -// Strftime wraps the C Strftime function +// Strftime clones Ruby's Time.strftime func Strftime(format string, t time.Time) (string, error) { - var ( - _, offset = t.Zone() - tz = t.Location() - secs = t.Sub(time.Date(1970, 1, 1, 0, 0, 0, 0, tz)).Seconds() - float64(offset) - tt = C.time_t(secs) - tm = C.struct_tm{} - cFormat = C.CString(format) - cOut [256]C.char - ) - defer C.free(unsafe.Pointer(cFormat)) // nolint: gas - C.localtime_r(&tt, &tm) - size := C.strftime(&cOut[0], C.size_t(len(cOut)), cFormat, &tm) - if size == 0 { - // If size == 0 there *might* be an error. - if errno := C.read_errno(); errno != 0 { - return "", syscall.Errno(errno) + return re.ReplaceAllStringFunc(format, func(directive string) string { + var ( + m = re.FindAllStringSubmatch(directive, 1)[0] + flags = m[1] + width = m[2] + conversion, _ = utf8.DecodeRuneInString(m[3]) + c = replaceComponent(t, conversion, flags, width) + pad, w = '0', 2 + ) + if s, ok := c.(string); ok { + return s } - } - return C.GoString(&cOut[0]), nil + if f, ok := padding[conversion]; ok { + pad, w = f.c, f.w + } + switch flags { + case "-": + w = 0 + case "_": + pad = ' ' + case "0": + pad = '0' + } + if len(width) > 0 { + w, _ = strconv.Atoi(width) // nolint: gas + } + fm := fmt.Sprintf("%%%c%dd", pad, w) + if pad == '-' { + fm = fmt.Sprintf("%%%dd", w) + } + s := fmt.Sprintf(fm, c) + switch flags { + case "^": + return strings.ToUpper(s) + // case "#": + default: + return s + } + }), nil +} + +var re = regexp.MustCompile(`%([-_0]|::?)?(\d+)?[EO]?([a-zA-Z\+nt%])`) + +var amPmTable = map[bool]string{true: "AM", false: "PM"} +var amPmLowerTable = map[bool]string{true: "am", false: "pm"} + +var padding = map[rune]struct { + c rune + w int +}{ + 'e': {'-', 2}, + 'f': {'0', 6}, + 'j': {'0', 3}, + 'k': {'-', 2}, + 'L': {'0', 3}, + 'l': {'-', 2}, + 'N': {'0', 9}, + 'u': {'-', 0}, + 'w': {'-', 0}, + 'Y': {'0', 4}, +} + +func replaceComponent(t time.Time, c rune, flags, width string) interface{} { // nolint: gocyclo + switch c { + + // Date + case 'Y': + return t.Year() + case 'y': + return t.Year() % 100 + case 'C': + return t.Year() / 100 + + case 'm': + return t.Month() + case 'B': + return t.Month().String() + case 'b', 'h': + return t.Month().String()[:3] + + case 'd', 'e': + return t.Day() + + case 'j': + return t.YearDay() + + // Time + case 'H', 'k': + return t.Hour() + case 'I', 'l': + return (t.Hour()+11)%12 + 1 + case 'M': + return t.Minute() + case 'S': + return t.Second() + case 'L': + return t.Nanosecond() / 1e6 + case 'N': + ns := t.Nanosecond() + if len(width) > 0 { + w, _ := strconv.Atoi(width) // nolint: gas + if w <= 9 { + return fmt.Sprintf("%09d", ns)[:w] + } + return fmt.Sprintf(fmt.Sprintf("%%09d%%0%dd", w-9), ns, 0) + } + return ns + + case 'P': + return amPmLowerTable[t.Hour() < 12] + case 'p': + return amPmTable[t.Hour() < 12] + + // Time zone + case 'z': + _, offset := t.Zone() + var ( + h = offset / 3600 + m = (offset / 60) % 60 + ) + switch flags { + case ":": + return fmt.Sprintf("%+03d:%02d", h, m) + case "::": + return fmt.Sprintf("%+03d:%02d:%02d", h, m, offset%60) + default: + return fmt.Sprintf("%+03d%02d", h, m) + } + case 'Z': + z, _ := t.Zone() + return z + + // Weekday + case 'A': + return t.Weekday().String() + case 'a': + return t.Weekday().String()[:3] + case 'u': + return (t.Weekday()+6)%7 + 1 + case 'w': + return t.Weekday() + + // ISO Year + case 'G': + y, _ := t.ISOWeek() + return y + case 'g': + y, _ := t.ISOWeek() + return y % 100 + case 'V': + _, wn := t.ISOWeek() + return wn + + // ISO Week + case 'U': + t = t.Add(24 * time.Hour) + y, wn := t.ISOWeek() + if y < t.Year() { + wn = 0 + } + return wn + case 'W': + y, wn := t.ISOWeek() + if y < t.Year() { + wn = 0 + } + return wn + + // Epoch seconds + case 's': + return t.Unix() + case 'Q': + return t.UnixNano() / 1000 + + // Literals + case 'n': + return "\n" + case 't': + return "\t" + case '%': + return "%" + + // Combinations + case 'c': + // date and time (%a %b %e %T %Y) + h, m, s := t.Clock() + return fmt.Sprintf("%s %s %2d %02d:%02d:%02d %04d", t.Weekday().String()[:3], t.Month().String()[:3], t.Day(), h, m, s, t.Year()) + case 'D', 'x': + // Date (%m/%d/%y) + y, m, d := t.Date() + return fmt.Sprintf("%02d/%02d/%02d", m, d, y%100) + case 'F': + // The ISO 8601 date format (%Y-%m-%d) + y, m, d := t.Date() + return fmt.Sprintf("%04d-%02d-%02d", y, m, d) + case 'v': + // VMS date (%e-%b-%Y) + return fmt.Sprintf("%2d-%s-%04d", t.Day(), t.Month().String()[:3], t.Year()) + case 'f': + return t.Nanosecond() / 1e3 + case 'r': + // 12-hour time (%I:%M:%S %p) + h, m, s := t.Clock() + h12 := (h+11)%12 + 1 + return fmt.Sprintf("%02d:%02d:%02d %s", h12, m, s, amPmTable[h < 12]) + case 'R': + // 24-hour time (%H:%M) + h, m, _ := t.Clock() + return fmt.Sprintf("%02d:%02d", h, m) + case 'T', 'X': + // 24-hour time (%H:%M:%S) + h, m, s := t.Clock() + return fmt.Sprintf("%02d:%02d:%02d", h, m, s) + case '+': + // date(1) (%a %b %e %H:%M:%S %Z %Y) + s, err := Strftime("%a %b %e %H:%M:%S %Z %Y", t) + if err != nil { + panic(err) + } + return s + default: + return fmt.Sprintf("%%%c", c) + } } diff --git a/strftime/strftime_test.go b/strftime/strftime_test.go index 26004be..49befef 100644 --- a/strftime/strftime_test.go +++ b/strftime/strftime_test.go @@ -19,17 +19,39 @@ func timeMustParse(f, s string) time.Time { } var conversionTests = []struct{ format, expect string }{ - // {"%1N"}, - // {"%3N"}, - // {"%6N"}, - // {"%9N"}, + {"%1N", "1"}, + {"%3N", "123"}, + {"%6N", "123456"}, + {"%9N", "123456789"}, + {"%12N", "123456789000"}, {"%v", " 2-Jan-2006"}, {"%Z", "EST"}, - // {"%:z", "-05:00"}, - // {"%::z", "-05:00:00"}, + {"%:z", "-05:00"}, + {"%::z", "-05:00:00"}, {"%%", "%"}, } +var dayOfWeekTests = []string{ + "%A=Sunday %a=Sun %u=7 %w=0 %d=01 %e= 1 %j=001 %U=01 %V=52 %W=00", + "%A=Monday %a=Mon %u=1 %w=1 %d=02 %e= 2 %j=002 %U=01 %V=01 %W=01", + "%A=Tuesday %a=Tue %u=2 %w=2 %d=03 %e= 3 %j=003 %U=01 %V=01 %W=01", + "%A=Wednesday %a=Wed %u=3 %w=3 %d=04 %e= 4 %j=004 %U=01 %V=01 %W=01", + "%A=Thursday %a=Thu %u=4 %w=4 %d=05 %e= 5 %j=005 %U=01 %V=01 %W=01", + "%A=Friday %a=Fri %u=5 %w=5 %d=06 %e= 6 %j=006 %U=01 %V=01 %W=01", + "%A=Saturday %a=Sat %u=6 %w=6 %d=07 %e= 7 %j=007 %U=01 %V=01 %W=01", +} + +var hourTests = []struct { + hour int + expect string +}{ + {0, "%H=00 %k= 0 %I=12 %l=12 %P=am %p=AM"}, + {1, "%H=01 %k= 1 %I=01 %l= 1 %P=am %p=AM"}, + {12, "%H=12 %k=12 %I=12 %l=12 %P=pm %p=PM"}, + {13, "%H=13 %k=13 %I=01 %l= 1 %P=pm %p=PM"}, + {23, "%H=23 %k=23 %I=11 %l=11 %P=pm %p=PM"}, +} + func TestStrftime(t *testing.T) { require.NoError(t, os.Setenv("TZ", "America/New_York")) @@ -63,28 +85,13 @@ func TestStrftime(t *testing.T) { if skip[format] { continue } - name := fmt.Sprintf("Strftime %q (cf. Ruby)", format) + name := fmt.Sprintf("Strftime %q", format) actual, err := Strftime(format, dt) require.NoErrorf(t, err, name) require.Equalf(t, expect, actual, name) } - ins := []struct{ source, expect string }{ - {"02 Jan 06 15:04 UTC", "02 Jan 06 10:04 EST"}, - {"02 Jan 06 15:04 EST", "02 Jan 06 15:04 EST"}, - {"02 Jan 06 15:04 EDT", "02 Jan 06 14:04 EST"}, - // {"02 Jan 06 15:04 MST", "02 Jan 06 10:04 EST"}, - {"14 Mar 16 12:00 UTC", "14 Mar 16 08:00 EDT"}, - // {"14 Mar 16 00:00 UTC", "14 Mar 16 00:00 UTC"}, - } - for _, test := range ins { - rt := timeMustParse(time.RFC822, test.source) - actual, err := Strftime("%d %b %y %H:%M %Z", rt) - require.NoErrorf(t, err, test.source) - require.Equalf(t, test.expect, actual, test.source) - } - - dt = timeMustParse(time.RFC822, "02 Jan 06 15:04 MST") + dt = timeMustParse(time.RFC1123Z, "Mon, 02 Jan 2006 15:04:05 -0500") tests := []struct{ format, expect string }{ {"%a, %b %d, %Y", "Mon, Jan 02, 2006"}, {"%Y/%m/%d", "2006/01/02"}, @@ -100,3 +107,42 @@ func TestStrftime(t *testing.T) { require.Equalf(t, test.expect, s, test.format) } } + +func TestStrftime_dow(t *testing.T) { + require.NoError(t, os.Setenv("TZ", "America/New_York")) + for day, expect := range dayOfWeekTests { + dt := time.Date(2006, 01, day+1, 15, 4, 5, 0, time.UTC) + format := "%%A=%A %%a=%a %%u=%u %%w=%w %%d=%d %%e=%e %%j=%j %%U=%U %%V=%V %%W=%W" + name := fmt.Sprintf("%s.Strftime", dt) + actual, err := Strftime(format, dt) + require.NoErrorf(t, err, name) + require.Equalf(t, expect, actual, name) + } +} + +func TestStrftime_hours(t *testing.T) { + require.NoError(t, os.Setenv("TZ", "America/New_York")) + for _, test := range hourTests { + dt := time.Date(2006, 01, 2, test.hour, 4, 5, 0, time.UTC) + format := "%%H=%H %%k=%k %%I=%I %%l=%l %%P=%P %%p=%p" + name := fmt.Sprintf("%s.Strftime", dt) + actual, err := Strftime(format, dt) + require.NoErrorf(t, err, name) + require.Equalf(t, test.expect, actual, name) + } +} + +func TestStrftime_zones(t *testing.T) { + require.NoError(t, os.Setenv("TZ", "America/New_York")) + ins := []struct{ source, expect string }{ + {"02 Jan 06 15:04 UTC", "%z=+0000 %Z=UTC"}, + {"02 Jan 06 15:04 EST", "%z=-0500 %Z=EST"}, + {"02 Jul 06 15:04 EDT", "%z=-0400 %Z=EDT"}, + } + for _, test := range ins { + rt := timeMustParse(time.RFC822, test.source) + actual, err := Strftime("%%z=%z %%Z=%Z", rt) + require.NoErrorf(t, err, test.source) + require.Equalf(t, test.expect, actual, test.source) + } +}