Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve os.date / strftime #465

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion stringlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
104 changes: 86 additions & 18 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...) }
Expand All @@ -85,38 +88,103 @@ 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
}
}
}
}
}
}
fs.Pos++
return c, false
}

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"}
var cDateFlagToGo = map[string]string{
// Formatting
"n": "\n",
"t": "\t",

// Year
"Y": "2006", "y": "06",

// Month
"b": "Jan", "B": "January",
"m": "01", "-m": "1",

// Day of the year/month
"j": "002",
"d": "02", "-d": "2", "e": "_2",

// Day of the week
"a": "Mon", "A": "Monday",

// 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)
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 {
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:
sc.AppendChar('%')
if sc.HasModifier {
sc.AppendChar(sc.Modifier)
}
sc.AppendChar(c)
}
}
Expand Down
73 changes: 73 additions & 0 deletions utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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", "Feb February"},
{t1, "%m %-m", "02 2"},
{t1, "%V", "5"},
{t1, "%w", "3"},
{t1, "%j", "034"},
{t1, "%d %-d %e", "03 3 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", "Sep September"},
{t2, "%m %-m", "09 9"},
{t2, "%V", "36"},
{t2, "%w", "4"},
{t2, "%j", "249"},
{t2, "%d %-d %e", "06 6 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"},

{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) {
actual := strftime(c.T, c.Fmt)
if actual != c.Expected {
t.Errorf("bad strftime: expected \"%s\" but got \"%s\"", c.Expected, actual)
}
})
}
}