Skip to content

Commit

Permalink
Strftime (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Visness <bvisness@gmail.com>
  • Loading branch information
tsukinoko-kun and bvisness authored Apr 2, 2024
1 parent 09ee001 commit 0ea1e3a
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 19 deletions.
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 @@ -47,19 +47,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 @@ -84,38 +87,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)
}
})
}
}

0 comments on commit 0ea1e3a

Please sign in to comment.