diff --git a/internal/strftime/parser.go b/internal/strftime/parser.go index bd0dbea..4146f7a 100644 --- a/internal/strftime/parser.go +++ b/internal/strftime/parser.go @@ -56,10 +56,15 @@ func (p *parser) parse() error { } switch state { + default: + return nil case specifier: return p.writeLit('%') case nopadding: + err := p.writeLit('%') + if err != nil { + return err + } return p.writeLit('-') } - return nil } diff --git a/internal/strftime/specifiers.go b/internal/strftime/specifiers.go index 7802b03..de1896b 100644 --- a/internal/strftime/specifiers.go +++ b/internal/strftime/specifiers.go @@ -1,5 +1,10 @@ package strftime +import ( + "strconv" + "time" +) + func (p *parser) goSpecifiers() { // https://strftime.org/ p.specs = map[byte]string{ @@ -28,6 +33,7 @@ func (p *parser) goSpecifiers() { '+': "Mon Jan _2 15:04:05 MST 2006", 'c': "Mon Jan _2 15:04:05 2006", + 'v': "_2-Jan-2006", 'F': "2006-01-02", 'D': "01/02/06", 'x': "01/02/06", @@ -50,7 +56,8 @@ func (p *parser) goSpecifiers() { } } -func (p *parser) uts35Specifiers() { // https://nsdateformatter.com/ +func (p *parser) uts35Specifiers() { + // https://nsdateformatter.com/ p.specs = map[byte]string{ 'B': "MMMM", 'b': "MMM", @@ -78,6 +85,7 @@ func (p *parser) uts35Specifiers() { // https://nsdateformatter.com/ '+': "E MMM d HH:mm:ss zzz yyyy", 'c': "E MMM d HH:mm:ss yyyy", + 'v': "d-MMM-yyyy", 'F': "yyyy-MM-dd", 'D': "MM/dd/yy", 'x': "MM/dd/yy", @@ -101,3 +109,29 @@ func (p *parser) uts35Specifiers() { // https://nsdateformatter.com/ 'S': "s", } } + +func weekNumber(t time.Time, pad, monday bool) string { + day := t.YearDay() + offset := int(t.Weekday()) + if monday { + if offset == 0 { + offset = 6 + } else { + offset-- + } + } + + if day < offset { + if pad { + return "00" + } else { + return "0" + } + } + + n := (day-offset)/7 + 1 + if n < 10 && pad { + return "0" + strconv.Itoa(n) + } + return strconv.Itoa(n) +} diff --git a/internal/strftime/strftime.go b/internal/strftime/strftime.go index 620e138..b0ddeb9 100644 --- a/internal/strftime/strftime.go +++ b/internal/strftime/strftime.go @@ -29,7 +29,11 @@ func Format(fmt string, t time.Time) string { parser.fallback = func(spec byte, pad bool) error { switch spec { default: - return errors.New("strftime: unsupported specifier: %" + string(spec)) + res.WriteByte('%') + if !pad { + res.WriteByte('-') + } + res.WriteByte(spec) case 'C': s := t.Format("2006") res.WriteString(s[:len(s)-2]) @@ -45,6 +49,10 @@ func Format(fmt string, t time.Time) string { res.WriteByte('0') } res.WriteString(strconv.Itoa(w)) + case 'W': + res.WriteString(weekNumber(t, pad, true)) + case 'U': + res.WriteString(weekNumber(t, pad, false)) case 'w': w := int(t.Weekday()) res.WriteString(strconv.Itoa(w)) @@ -54,6 +62,16 @@ func Format(fmt string, t time.Time) string { } else { res.WriteString(strconv.Itoa(w)) } + case 'k': + res.WriteString(strconv.Itoa(t.Hour())) + case 'l': + h := t.Hour() + if h == 0 { + h = 12 + } else if h > 12 { + h -= 12 + } + res.WriteString(strconv.Itoa(h)) } return nil } diff --git a/internal/strftime/strftime_test.go b/internal/strftime/strftime_test.go index 4ba4e8a..adde4c3 100644 --- a/internal/strftime/strftime_test.go +++ b/internal/strftime/strftime_test.go @@ -18,16 +18,18 @@ var timeTests = []struct { {"%c", time.ANSIC, "E MMM d HH:mm:ss yyyy", "Fri Aug 7 06:05:04 2009"}, {"%+", time.UnixDate, "E MMM d HH:mm:ss zzz yyyy", "Fri Aug 7 06:05:04 UTC 2009"}, {"%FT%TZ", time.RFC3339[:20], "yyyy-MM-dd'T'HH:mm:ss'Z'", "2009-08-07T06:05:04Z"}, - {"%a %b %e %H:%M:%S %Y", time.ANSIC, "", "Fri Aug 7 06:05:04 2009"}, - {"%a %b %e %H:%M:%S %Z %Y", time.UnixDate, "", "Fri Aug 7 06:05:04 UTC 2009"}, - {"%a %b %d %H:%M:%S %z %Y", time.RubyDate, "E MMM dd HH:mm:ss Z yyyy", "Fri Aug 07 06:05:04 +0000 2009"}, - {"%a, %d %b %Y %H:%M:%S %Z", time.RFC1123, "E, dd MMM yyyy HH:mm:ss zzz", "Fri, 07 Aug 2009 06:05:04 UTC"}, - {"%a, %d %b %Y %H:%M:%S GMT", http.TimeFormat, "E, dd MMM yyyy HH:mm:ss 'GMT'", "Fri, 07 Aug 2009 06:05:04 GMT"}, + {"%a %b %e %T %Y", time.ANSIC, "", "Fri Aug 7 06:05:04 2009"}, + {"%a %b %e %T %Z %Y", time.UnixDate, "", "Fri Aug 7 06:05:04 UTC 2009"}, + {"%a %b %d %T %z %Y", time.RubyDate, "E MMM dd HH:mm:ss Z yyyy", "Fri Aug 07 06:05:04 +0000 2009"}, + {"%a, %d %b %Y %T %Z", time.RFC1123, "E, dd MMM yyyy HH:mm:ss zzz", "Fri, 07 Aug 2009 06:05:04 UTC"}, + {"%a, %d %b %Y %T GMT", http.TimeFormat, "E, dd MMM yyyy HH:mm:ss 'GMT'", "Fri, 07 Aug 2009 06:05:04 GMT"}, {"%Y-%m-%dT%H:%M:%SZ", time.RFC3339[:20], "yyyy-MM-dd'T'HH:mm:ss'Z'", "2009-08-07T06:05:04Z"}, // Date formats + {"%v", "_2-Jan-2006", "d-MMM-yyyy", " 7-Aug-2009"}, {"%F", "2006-01-02", "yyyy-MM-dd", "2009-08-07"}, {"%D", "01/02/06", "MM/dd/yy", "08/07/09"}, {"%x", "01/02/06", "MM/dd/yy", "08/07/09"}, + {"%e-%b-%Y", "_2-Jan-2006", "", " 7-Aug-2009"}, {"%Y-%m-%d", "2006-01-02", "yyyy-MM-dd", "2009-08-07"}, {"%m/%d/%y", "01/02/06", "MM/dd/yy", "08/07/09"}, // Time formats @@ -49,11 +51,11 @@ var timeTests = []struct { // Parsing {"", "", "", ""}, {"%", "%", "%", "%"}, - {"%-", "-", "-", "-"}, + {"%-", "%-", "%-", "%-"}, {"%n", "\n", "\n", "\n"}, {"%t", "\t", "\t", "\t"}, - {"%q", "", "", ""}, - {"%-q", "", "", ""}, + {"%q", "", "", "%q"}, + {"%-q", "", "", "%-q"}, {"'", "'", "''", "'"}, {"100%", "", "100%", "100%"}, {"Monday", "", "'Monday'", "Monday"}, @@ -92,3 +94,129 @@ func TestUTS35(t *testing.T) { } } } + +func TestFormat_ruby(t *testing.T) { + // https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-i-strftime + reference := time.Date(2007, 11, 19, 8, 37, 48, 0, time.FixedZone("", -6*3600)) + tests := []struct { + format string + time string + }{ + {"Printed on %m/%d/%Y", "Printed on 11/19/2007"}, + {"at %I:%M%p", "at 08:37AM"}, + // Various ISO 8601 formats: + {"%Y%m%d", "20071119"}, // Calendar date (basic) + {"%F", "2007-11-19"}, // Calendar date (extended) + {"%Y-%m", "2007-11"}, // Calendar date, reduced accuracy, specific month + {"%Y", "2007"}, // Calendar date, reduced accuracy, specific year + {"%C", "20"}, // Calendar date, reduced accuracy, specific century + {"%Y%j", "2007323"}, // Ordinal date (basic) + {"%Y-%j", "2007-323"}, // Ordinal date (extended) + {"%GW%V%u", "2007W471"}, // Week date (basic) + {"%G-W%V-%u", "2007-W47-1"}, // Week date (extended) + {"%GW%V", "2007W47"}, // Week date, reduced accuracy, specific week (basic) + {"%G-W%V", "2007-W47"}, // Week date, reduced accuracy, specific week (extended) + {"%H%M%S", "083748"}, // Local time (basic) + {"%T", "08:37:48"}, // Local time (extended) + {"%H%M", "0837"}, // Local time, reduced accuracy, specific minute (basic) + {"%H:%M", "08:37"}, // Local time, reduced accuracy, specific minute (extended) + {"%H", "08"}, // Local time, reduced accuracy, specific hour + {"%H%M%S,%L", "083748,000"}, // Local time with decimal fraction, comma as decimal sign (basic) + {"%T,%L", "08:37:48,000"}, // Local time with decimal fraction, comma as decimal sign (extended) + {"%H%M%S.%L", "083748.000"}, // Local time with decimal fraction, full stop as decimal sign (basic) + {"%T.%L", "08:37:48.000"}, // Local time with decimal fraction, full stop as decimal sign (extended) + {"%H%M%S%z", "083748-0600"}, // Local time and the difference from UTC (basic) + {"%Y%m%dT%H%M%S%z", "20071119T083748-0600"}, // Date and time of day for calendar date (basic) + {"%Y%jT%H%M%S%z", "2007323T083748-0600"}, // Date and time of day for ordinal date (basic) + {"%GW%V%uT%H%M%S%z", "2007W471T083748-0600"}, // Date and time of day for week date (basic) + {"%Y%m%dT%H%M", "20071119T0837"}, // Calendar date and local time (basic) + {"%FT%R", "2007-11-19T08:37"}, // Calendar date and local time (extended) + {"%Y%jT%H%MZ", "2007323T0837Z"}, // Ordinal date and UTC of day (basic) + {"%Y-%jT%RZ", "2007-323T08:37Z"}, // Ordinal date and UTC of day (extended) + {"%GW%V%uT%H%M%z", "2007W471T0837-0600"}, // Week date and local time and difference from UTC (basic) + // {"%T%:z", "08:37:48-06:00"}, // Local time and the difference from UTC (extended) + // {"%FT%T%:z", "2007-11-19T08:37:48-06:00"}, // Date and time of day for calendar date (extended) + // {"%Y-%jT%T%:z", "2007-323T08:37:48-06:00"}, // Date and time of day for ordinal date (extended) + // {"%G-W%V-%uT%T%:z", "2007-W47-1T08:37:48-06:00"}, // Date and time of day for week date (extended) + // {"%G-W%V-%uT%R%:z", "2007-W47-1T08:37-06:00"}, // Week date and local time and difference from + } + + for _, test := range tests { + if got := Format(test.format, reference); got != test.time { + t.Errorf("Format(%q) = %q, want %q", test.format, got, test.time) + } + } +} + +func TestFormat_tebeka(t *testing.T) { + // https://github.com/tebeka/strftime + // https://github.com/hhkbp2/go-strftime + reference := time.Date(2009, time.November, 10, 23, 1, 2, 3, time.UTC) + tests := []struct { + format string + time string + }{ + {"%a", "Tue"}, + {"%A", "Tuesday"}, + {"%b", "Nov"}, + {"%B", "November"}, + {"%c", "Tue Nov 10 23:01:02 2009"}, // we use a different format + {"%d", "10"}, + {"%H", "23"}, + {"%I", "11"}, + {"%j", "314"}, + {"%m", "11"}, + {"%M", "01"}, + {"%p", "PM"}, + {"%S", "02"}, + {"%U", "45"}, + {"%w", "2"}, + {"%W", "45"}, + {"%x", "11/10/09"}, + {"%X", "23:01:02"}, + {"%y", "09"}, + {"%Y", "2009"}, + {"%Z", "UTC"}, + {"%L", "000"}, // we use a different specifier + {"%f", "000000"}, // we use a different specifier + {"%N", "000000003"}, // we use a different specifier + + // Escape + {"%%%Y", "%2009"}, + {"%3%%", "%3%"}, + {"%3%L", "%3000"}, // we use a different specifier + {"%3xy%L", "%3xy000"}, // we use a different specifier + + // Embedded + {"/path/%Y/%m/report", "/path/2009/11/report"}, + + // Empty + {"", ""}, + } + + for _, test := range tests { + if got := Format(test.format, reference); got != test.time { + t.Errorf("Format(%q) = %q, want %q", test.format, got, test.time) + } + } +} + +func TestFormat_lestrrat(t *testing.T) { + // https://github.com/lestrrat-go/strftime + reference := time.Unix(1136239445, 123456789).UTC() + tests := []struct { + format string + time string + }{ + { + `%A %a %B %b %C %c %D %d %e %F %H %h %I %j %k %l %M %m %n %p %R %r %S %T %t %U %u %V %v %W %w %X %x %Y %y %Z %z`, + "Monday Mon January Jan 20 Mon Jan 2 22:04:05 2006 01/02/06 02 2 2006-01-02 22 Jan 10 002 22 10 04 01 \n PM 22:04 10:04:05 PM 05 22:04:05 \t 01 1 01 2-Jan-2006 01 1 22:04:05 01/02/06 2006 06 UTC +0000", + }, + } + + for _, test := range tests { + if got := Format(test.format, reference); got != test.time { + t.Errorf("Format(%q) = %q, want %q", test.format, got, test.time) + } + } +}