From 6920514e97df96ddc5a7536a051215959b7f096d Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Sun, 10 Dec 2023 13:38:53 -0600 Subject: [PATCH 1/2] Add lots more strftime flags --- utils.go | 52 +++++++++++++++++++++++++++++++++++--- utils_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 utils_test.go diff --git a/utils.go b/utils.go index 2df68dc7..1f39fa26 100644 --- a/utils.go +++ b/utils.go @@ -100,10 +100,47 @@ func (fs *flagScanner) Next() (byte, bool) { } var cDateFlagToGo = map[byte]string{ - 'a': "mon", 'A': "Monday", 'b': "Jan", 'B': "January", 'c': "02 Jan 06 15:04 MST", 'd': "02", - 'F': "2006-01-02", 'H': "15", 'I': "03", 'm': "01", 'M': "04", 'p': "PM", 'P': "pm", 'S': "05", - 'x': "15/04/05", 'X': "15:04:05", 'y': "06", 'Y': "2006", 'z': "-0700", 'Z': "MST"} + // Formatting + 'n': "\n", + 't': "\t", + // Year + 'Y': "2006", 'y': "06", + + // Month + 'b': "Jan", 'B': "January", // TODO: %^B, %^b + 'm': "01", // TODO: %-m, %_m + + // Day of the year/month + 'j': "002", + 'd': "02", 'e': "_2", // TODO: %-d + + // Day of the week + 'a': "Mon", 'A': "Monday", // TODO: %^A, %^a + + // Hour, minute, second + 'H': "15", + 'I': "03", 'l': "3", + 'M': "04", + 'S': "05", + + // Other + 'c': "02 Jan 06 15:04 MST", + 'x': "01/02/06", 'X': "15:04:05", + 'D': "01/02/06", + 'F': "2006-01-02", + 'r': "03:04:05 PM", 'R': "15:04", + 'T': "15:04:05", + 'p': "PM", 'P': "pm", + 'z': "-0700", 'Z': "MST", + + // Many other flags are handled in the body of strftime since they cannot + // be represented in Go format strings. +} + +// This implementation of strftime is inspired by both the C spec and Ruby's +// extensions. This allows for flags like %-d, which provides the day of the +// month without padding (1..31 instead of 01..31). func strftime(t time.Time, cfmt string) string { sc := newFlagScanner('%', "", "", cfmt) for c, eos := sc.Next(); !eos; c, eos = sc.Next() { @@ -113,6 +150,15 @@ func strftime(t time.Time, cfmt string) string { sc.AppendString(t.Format(v)) } else { switch c { + case 'G': + isoYear, _ := t.ISOWeek() + sc.AppendString(fmt.Sprint(isoYear)) + case 'g': + isoYear, _ := t.ISOWeek() + sc.AppendString(fmt.Sprint(isoYear)[2:]) + case 'V': + _, isoWeek := t.ISOWeek() + sc.AppendString(fmt.Sprint(isoWeek)) case 'w': sc.AppendString(fmt.Sprint(int(t.Weekday()))) default: diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 00000000..3d90b412 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,70 @@ +package lua + +import ( + "fmt" + "testing" + "time" +) + +func TestStrftime(t *testing.T) { + type testCase struct { + T time.Time + Fmt string + Expected string + } + + t1 := time.Date(2016, time.February, 3, 13, 23, 45, 123, time.FixedZone("Plus2", 60*60*2)) + t2 := time.Date(1945, time.September, 6, 7, 35, 4, 989, time.FixedZone("Minus5", 60*60*-5)) + + cases := []testCase{ + {t1, "foo%nbar%tbaz 100%% cool", "foo\nbar\tbaz 100% cool"}, + + {t1, "%Y %y", "2016 16"}, + {t1, "%G %g", "2016 16"}, + {t1, "%b %B %m", "Feb February 02"}, + {t1, "%V", "5"}, + {t1, "%w", "3"}, + {t1, "%j", "034"}, + {t1, "%d", "03"}, + {t1, "%e", " 3"}, + {t1, "%a %A", "Wed Wednesday"}, + {t1, "%H %I %l", "13 01 1"}, + {t1, "%M", "23"}, + {t1, "%S", "45"}, + {t1, "%c", "03 Feb 16 13:23 Plus2"}, + {t1, "%D %x", "02/03/16 02/03/16"}, + {t1, "%F", "2016-02-03"}, + {t1, "%r", "01:23:45 PM"}, + {t1, "%R %T %X", "13:23 13:23:45 13:23:45"}, + {t1, "%p %P", "PM pm"}, + {t1, "%z %Z", "+0200 Plus2"}, + + {t2, "%Y %y", "1945 45"}, + {t2, "%G %g", "1945 45"}, + {t2, "%b %B %m", "Sep September 09"}, + {t2, "%V", "36"}, + {t2, "%w", "4"}, + {t2, "%j", "249"}, + {t2, "%d", "06"}, + {t2, "%e", " 6"}, + {t2, "%a %A", "Thu Thursday"}, + {t2, "%H %I %l", "07 07 7"}, + {t2, "%M", "35"}, + {t2, "%S", "04"}, + {t2, "%c", "06 Sep 45 07:35 Minus5"}, + {t2, "%D %x", "09/06/45 09/06/45"}, + {t2, "%F", "1945-09-06"}, + {t2, "%r", "07:35:04 AM"}, + {t2, "%R %T %X", "07:35 07:35:04 07:35:04"}, + {t2, "%p %P", "AM am"}, + {t2, "%z %Z", "-0500 Minus5"}, + } + for i, c := range cases { + t.Run(fmt.Sprintf("Case %d (\"%s\")", i, c.Fmt), func(t *testing.T) { + actual := strftime(c.T, c.Fmt) + if actual != c.Expected { + t.Errorf("bad strftime: expected \"%s\" but got \"%s\"", c.Expected, actual) + } + }) + } +} From 90501ab9848b2814fbb6f807229065250ec6f7a4 Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Sun, 10 Dec 2023 14:28:55 -0600 Subject: [PATCH 2/2] Support trimming and padding modifiers --- stringlib.go | 2 +- utils.go | 92 +++++++++++++++++++++++++++++++-------------------- utils_test.go | 15 +++++---- 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/stringlib.go b/stringlib.go index f484c2b3..c7ff34c6 100644 --- a/stringlib.go +++ b/stringlib.go @@ -222,7 +222,7 @@ func strGsubStr(L *LState, str string, repl string, matches []*pm.MatchData) str infoList := make([]replaceInfo, 0, len(matches)) for _, match := range matches { start, end := match.Capture(0), match.Capture(1) - sc := newFlagScanner('%', "", "", repl) + sc := newFlagScanner('%', "", "", "", repl) for c, eos := sc.Next(); !eos; c, eos = sc.Next() { if !sc.ChangeFlag { if sc.HasFlag { diff --git a/utils.go b/utils.go index 1f39fa26..e9acd641 100644 --- a/utils.go +++ b/utils.go @@ -48,19 +48,22 @@ func defaultFormat(v interface{}, f fmt.State, c rune) { } type flagScanner struct { - flag byte - start string - end string - buf []byte - str string - Length int - Pos int - HasFlag bool - ChangeFlag bool + flag byte + modifiers []byte + start string + end string + buf []byte + str string + Length int + Pos int + HasFlag bool + ChangeFlag bool + HasModifier bool + Modifier byte } -func newFlagScanner(flag byte, start, end, str string) *flagScanner { - return &flagScanner{flag, start, end, make([]byte, 0, len(str)), str, len(str), 0, false, false} +func newFlagScanner(flag byte, modifiers, start, end, str string) *flagScanner { + return &flagScanner{flag, []byte(modifiers), start, end, make([]byte, 0, len(str)), str, len(str), 0, false, false, false, 0} } func (fs *flagScanner) AppendString(str string) { fs.buf = append(fs.buf, str...) } @@ -85,13 +88,24 @@ func (fs *flagScanner) Next() (byte, bool) { fs.AppendChar(fs.flag) fs.Pos += 2 return fs.Next() - } else if fs.Pos != fs.Length-1 { + } else if fs.Pos < fs.Length-1 { if fs.HasFlag { fs.AppendString(fs.end) } fs.AppendString(fs.start) fs.ChangeFlag = true fs.HasFlag = true + fs.HasModifier = false + fs.Modifier = 0 + if fs.Pos < fs.Length-2 { + for _, modifier := range fs.modifiers { + if fs.str[fs.Pos+1] == modifier { + fs.HasModifier = true + fs.Modifier = modifier + fs.Pos += 1 + } + } + } } } } @@ -99,40 +113,40 @@ func (fs *flagScanner) Next() (byte, bool) { return c, false } -var cDateFlagToGo = map[byte]string{ +var cDateFlagToGo = map[string]string{ // Formatting - 'n': "\n", - 't': "\t", + "n": "\n", + "t": "\t", // Year - 'Y': "2006", 'y': "06", + "Y": "2006", "y": "06", // Month - 'b': "Jan", 'B': "January", // TODO: %^B, %^b - 'm': "01", // TODO: %-m, %_m + "b": "Jan", "B": "January", + "m": "01", "-m": "1", // Day of the year/month - 'j': "002", - 'd': "02", 'e': "_2", // TODO: %-d + "j": "002", + "d": "02", "-d": "2", "e": "_2", // Day of the week - 'a': "Mon", 'A': "Monday", // TODO: %^A, %^a + "a": "Mon", "A": "Monday", // Hour, minute, second - 'H': "15", - 'I': "03", 'l': "3", - 'M': "04", - 'S': "05", + "H": "15", + "I": "03", "l": "3", + "M": "04", + "S": "05", // Other - 'c': "02 Jan 06 15:04 MST", - 'x': "01/02/06", 'X': "15:04:05", - 'D': "01/02/06", - 'F': "2006-01-02", - 'r': "03:04:05 PM", 'R': "15:04", - 'T': "15:04:05", - 'p': "PM", 'P': "pm", - 'z': "-0700", 'Z': "MST", + "c": "02 Jan 06 15:04 MST", + "x": "01/02/06", "X": "15:04:05", + "D": "01/02/06", + "F": "2006-01-02", + "r": "03:04:05 PM", "R": "15:04", + "T": "15:04:05", + "p": "PM", "P": "pm", + "z": "-0700", "Z": "MST", // Many other flags are handled in the body of strftime since they cannot // be represented in Go format strings. @@ -142,11 +156,16 @@ var cDateFlagToGo = map[byte]string{ // extensions. This allows for flags like %-d, which provides the day of the // month without padding (1..31 instead of 01..31). func strftime(t time.Time, cfmt string) string { - sc := newFlagScanner('%', "", "", cfmt) + sc := newFlagScanner('%', "-", "", "", cfmt) for c, eos := sc.Next(); !eos; c, eos = sc.Next() { if !sc.ChangeFlag { if sc.HasFlag { - if v, ok := cDateFlagToGo[c]; ok { + flag := string(c) + if sc.HasModifier { + flag = string(sc.Modifier) + flag + } + + if v, ok := cDateFlagToGo[flag]; ok { sc.AppendString(t.Format(v)) } else { switch c { @@ -163,6 +182,9 @@ func strftime(t time.Time, cfmt string) string { sc.AppendString(fmt.Sprint(int(t.Weekday()))) default: sc.AppendChar('%') + if sc.HasModifier { + sc.AppendChar(sc.Modifier) + } sc.AppendChar(c) } } diff --git a/utils_test.go b/utils_test.go index 3d90b412..321ebc43 100644 --- a/utils_test.go +++ b/utils_test.go @@ -21,12 +21,12 @@ func TestStrftime(t *testing.T) { {t1, "%Y %y", "2016 16"}, {t1, "%G %g", "2016 16"}, - {t1, "%b %B %m", "Feb February 02"}, + {t1, "%b %B", "Feb February"}, + {t1, "%m %-m", "02 2"}, {t1, "%V", "5"}, {t1, "%w", "3"}, {t1, "%j", "034"}, - {t1, "%d", "03"}, - {t1, "%e", " 3"}, + {t1, "%d %-d %e", "03 3 3"}, {t1, "%a %A", "Wed Wednesday"}, {t1, "%H %I %l", "13 01 1"}, {t1, "%M", "23"}, @@ -41,12 +41,12 @@ func TestStrftime(t *testing.T) { {t2, "%Y %y", "1945 45"}, {t2, "%G %g", "1945 45"}, - {t2, "%b %B %m", "Sep September 09"}, + {t2, "%b %B", "Sep September"}, + {t2, "%m %-m", "09 9"}, {t2, "%V", "36"}, {t2, "%w", "4"}, {t2, "%j", "249"}, - {t2, "%d", "06"}, - {t2, "%e", " 6"}, + {t2, "%d %-d %e", "06 6 6"}, {t2, "%a %A", "Thu Thursday"}, {t2, "%H %I %l", "07 07 7"}, {t2, "%M", "35"}, @@ -58,6 +58,9 @@ func TestStrftime(t *testing.T) { {t2, "%R %T %X", "07:35 07:35:04 07:35:04"}, {t2, "%p %P", "AM am"}, {t2, "%z %Z", "-0500 Minus5"}, + + {t1, "not real flags: %-Q %_J %^^ %-", "not real flags: %-Q %_J %^^ %-"}, + {t1, "end in flag: %", "end in flag: %"}, } for i, c := range cases { t.Run(fmt.Sprintf("Case %d (\"%s\")", i, c.Fmt), func(t *testing.T) {