diff --git a/filters/standard_filters.go b/filters/standard_filters.go index 493aa90..4687099 100644 --- a/filters/standard_filters.go +++ b/filters/standard_filters.go @@ -14,8 +14,8 @@ import ( "unicode" "unicode/utf8" - "github.com/osteele/liquid/strftime" "github.com/osteele/liquid/values" + strftime "github.com/osteele/rbstrftime" ) // A FilterDictionary holds filters. diff --git a/strftime/strftime.go b/strftime/strftime.go deleted file mode 100644 index 977ac06..0000000 --- a/strftime/strftime.go +++ /dev/null @@ -1,238 +0,0 @@ -// Package strftime implements a Strftime function that is compatible with Ruby's Time.strftime. -package strftime - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "time" - "unicode/utf8" -) - -// Strftime is compatible with Ruby's Time.strftime. -func Strftime(format string, t time.Time) (string, error) { - 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 - } - 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 deleted file mode 100644 index 49befef..0000000 --- a/strftime/strftime_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package strftime - -import ( - "encoding/csv" - "fmt" - "os" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func timeMustParse(f, s string) time.Time { - t, err := time.ParseInLocation(f, s, time.Local) - if err != nil { - panic(err) - } - return t -} - -var conversionTests = []struct{ format, expect string }{ - {"%1N", "1"}, - {"%3N", "123"}, - {"%6N", "123456"}, - {"%9N", "123456789"}, - {"%12N", "123456789000"}, - {"%v", " 2-Jan-2006"}, - {"%Z", "EST"}, - {"%: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")) - - dt := timeMustParse(time.RFC3339Nano, "2006-01-02T15:04:05.123456789-05:00") - for _, test := range conversionTests { - name := fmt.Sprintf("Strftime %q", test.format) - actual, err := Strftime(test.format, dt) - require.NoErrorf(t, err, name) - require.Equalf(t, test.expect, actual, name) - } - - skip := map[string]bool{} - f, err := os.Open("testdata/skip.csv") - require.NoError(t, err) - defer f.Close() // nolint: errcheck - r := csv.NewReader(f) - rows, err := r.ReadAll() - require.NoError(t, err) - for _, row := range rows { - skip[row[0]] = true - } - - f, err = os.Open("testdata/data.csv") - require.NoError(t, err) - defer f.Close() // nolint: errcheck - r = csv.NewReader(f) - rows, err = r.ReadAll() - require.NoError(t, err) - for _, row := range rows { - format, expect := row[0], row[1] - if skip[format] { - continue - } - name := fmt.Sprintf("Strftime %q", format) - actual, err := Strftime(format, dt) - require.NoErrorf(t, err, name) - require.Equalf(t, expect, actual, name) - } - - 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"}, - {"%Y/%m/%e", "2006/01/ 2"}, - {"%Y/%-m/%-d", "2006/1/2"}, - {"%a, %b %d, %Y %z", "Mon, Jan 02, 2006 -0500"}, - {"%a, %b %d, %Y %Z", "Mon, Jan 02, 2006 EST"}, - // {"", ""}, this errors on Travis - } - for _, test := range tests { - s, err := Strftime(test.format, dt) - require.NoErrorf(t, err, test.format) - 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) - } -} diff --git a/strftime/testdata/data.csv b/strftime/testdata/data.csv deleted file mode 100644 index aefaed1..0000000 --- a/strftime/testdata/data.csv +++ /dev/null @@ -1,83 +0,0 @@ -%A,Monday -%B,January -%C,20 -%D,01/02/06 -%F,2006-01-02 -%G,2006 -%H,15 -%I,03 -%L,000 -%M,04 -%N,000000000 -%P,pm -%R,15:04 -%S,05 -%T,15:04:05 -%U,01 -%V,01 -%W,01 -%X,15:04:05 -%Y,2006 -%Z,"" -%a,Mon -%b,Jan -%c,Mon Jan 2 15:04:05 2006 -%d,02 -%e, 2 -%g,06 -%h,Jan -%j,002 -%k,15 -%l, 3 -%m,01 -%n," -" -%p,PM -%r,03:04:05 PM -%s,1136232245 -%t, -%u,1 -%v, 2-JAN-2006 -%w,1 -%x,01/02/06 -%y,06 -%z,-0500 -%-I,3 -%-M,4 -%-S,5 -%-U,1 -%-V,1 -%-W,1 -%-d,2 -%-e,2 -%-g,6 -%-j,2 -%-l,3 -%-m,1 -%-y,6 -%_I, 3 -%_M, 4 -%_S, 5 -%_U, 1 -%_V, 1 -%_W, 1 -%_d, 2 -%_g, 6 -%_j, 2 -%_m, 1 -%_y, 6 -%_z, -500 -%^A,MONDAY -%^B,JANUARY -%^P,PM -%^a,MON -%^b,JAN -%^c,MON JAN 2 15:04:05 2006 -%^h,JAN -%#A,MONDAY -%#B,JANUARY -%#P,PM -%#a,MON -%#b,JAN -%#h,JAN -%#p,pm diff --git a/strftime/testdata/gen.rb b/strftime/testdata/gen.rb deleted file mode 100644 index f8f9879..0000000 --- a/strftime/testdata/gen.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Create a file data.csv in the current directory, containing format strings, -# and the reference date thus formatted. -# -# This is run manually rather than on go generate, so that go generate doesn't -# require a Ruby installation. - -require "CSV" - -rt = Time.new(2006, 1, 2, 15, 4, 5, "-05:00") -CSV.open(File.join(File.dirname(__FILE__), "data.csv"), "w") do |csv| - for mod in ['', '-', '_', '^', '#'] do - for c in ('A'..'Z').to_a + ('a'..'z').to_a do - fmt = "%#{mod}#{c}" - out = rt.strftime(fmt) - next if out == fmt - next if mod != '' && out == rt.strftime("%#{c}") - csv << [fmt, out] - end - end -end diff --git a/strftime/testdata/skip.csv b/strftime/testdata/skip.csv deleted file mode 100644 index 42b7947..0000000 --- a/strftime/testdata/skip.csv +++ /dev/null @@ -1,27 +0,0 @@ -%L -%N -%P -%Z -%v -%-g -%-v -%-y -%-z -%_g -%_v -%_y -%_z -%^a -%^b -%^c -%^h -%#a -%#b -%#h -%#p -%^A -%^B -%^P -%#A -%#B -%#P