mirror of
https://github.com/danog/liquid.git
synced 2024-12-02 12:47:45 +01:00
Replace extern "C" strftime by go implementation
This commit is contained in:
parent
8d53a6b4a8
commit
85bd1ddfe1
@ -1,46 +1,238 @@
|
|||||||
// Package strftime wraps the C stdlib strftime and strptime functions.
|
// Package strftime wraps the C stdlib strftime and strptime functions.
|
||||||
package strftime
|
package strftime
|
||||||
|
|
||||||
/*
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <errno.h>
|
|
||||||
int read_errno() { return errno; }
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Note: The use of errno below is not thread-safe.
|
// Strftime clones Ruby's Time.strftime
|
||||||
//
|
|
||||||
// Even if we added a mutex, it would not be thread-safe
|
|
||||||
// relative to other C stdlib calls that don't use that mutex.
|
|
||||||
//
|
|
||||||
// OTOH, I can't find a format string that C strftime thinks is illegal
|
|
||||||
// to test this with, so maybe this is a non-issue
|
|
||||||
|
|
||||||
// Strftime wraps the C Strftime function
|
|
||||||
func Strftime(format string, t time.Time) (string, error) {
|
func Strftime(format string, t time.Time) (string, error) {
|
||||||
|
return re.ReplaceAllStringFunc(format, func(directive string) string {
|
||||||
var (
|
var (
|
||||||
_, offset = t.Zone()
|
m = re.FindAllStringSubmatch(directive, 1)[0]
|
||||||
tz = t.Location()
|
flags = m[1]
|
||||||
secs = t.Sub(time.Date(1970, 1, 1, 0, 0, 0, 0, tz)).Seconds() - float64(offset)
|
width = m[2]
|
||||||
tt = C.time_t(secs)
|
conversion, _ = utf8.DecodeRuneInString(m[3])
|
||||||
tm = C.struct_tm{}
|
c = replaceComponent(t, conversion, flags, width)
|
||||||
cFormat = C.CString(format)
|
pad, w = '0', 2
|
||||||
cOut [256]C.char
|
|
||||||
)
|
)
|
||||||
defer C.free(unsafe.Pointer(cFormat)) // nolint: gas
|
if s, ok := c.(string); ok {
|
||||||
C.localtime_r(&tt, &tm)
|
return s
|
||||||
size := C.strftime(&cOut[0], C.size_t(len(cOut)), cFormat, &tm)
|
}
|
||||||
if size == 0 {
|
if f, ok := padding[conversion]; ok {
|
||||||
// If size == 0 there *might* be an error.
|
pad, w = f.c, f.w
|
||||||
if errno := C.read_errno(); errno != 0 {
|
}
|
||||||
return "", syscall.Errno(errno)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return C.GoString(&cOut[0]), nil
|
|
||||||
}
|
|
||||||
|
@ -19,17 +19,39 @@ func timeMustParse(f, s string) time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var conversionTests = []struct{ format, expect string }{
|
var conversionTests = []struct{ format, expect string }{
|
||||||
// {"%1N"},
|
{"%1N", "1"},
|
||||||
// {"%3N"},
|
{"%3N", "123"},
|
||||||
// {"%6N"},
|
{"%6N", "123456"},
|
||||||
// {"%9N"},
|
{"%9N", "123456789"},
|
||||||
|
{"%12N", "123456789000"},
|
||||||
{"%v", " 2-Jan-2006"},
|
{"%v", " 2-Jan-2006"},
|
||||||
{"%Z", "EST"},
|
{"%Z", "EST"},
|
||||||
// {"%:z", "-05:00"},
|
{"%:z", "-05:00"},
|
||||||
// {"%::z", "-05:00: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) {
|
func TestStrftime(t *testing.T) {
|
||||||
require.NoError(t, os.Setenv("TZ", "America/New_York"))
|
require.NoError(t, os.Setenv("TZ", "America/New_York"))
|
||||||
|
|
||||||
@ -63,28 +85,13 @@ func TestStrftime(t *testing.T) {
|
|||||||
if skip[format] {
|
if skip[format] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := fmt.Sprintf("Strftime %q (cf. Ruby)", format)
|
name := fmt.Sprintf("Strftime %q", format)
|
||||||
actual, err := Strftime(format, dt)
|
actual, err := Strftime(format, dt)
|
||||||
require.NoErrorf(t, err, name)
|
require.NoErrorf(t, err, name)
|
||||||
require.Equalf(t, expect, actual, name)
|
require.Equalf(t, expect, actual, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
ins := []struct{ source, expect string }{
|
dt = timeMustParse(time.RFC1123Z, "Mon, 02 Jan 2006 15:04:05 -0500")
|
||||||
{"02 Jan 06 15:04 UTC", "02 Jan 06 10:04 EST"},
|
|
||||||
{"02 Jan 06 15:04 EST", "02 Jan 06 15:04 EST"},
|
|
||||||
{"02 Jan 06 15:04 EDT", "02 Jan 06 14:04 EST"},
|
|
||||||
// {"02 Jan 06 15:04 MST", "02 Jan 06 10:04 EST"},
|
|
||||||
{"14 Mar 16 12:00 UTC", "14 Mar 16 08:00 EDT"},
|
|
||||||
// {"14 Mar 16 00:00 UTC", "14 Mar 16 00:00 UTC"},
|
|
||||||
}
|
|
||||||
for _, test := range ins {
|
|
||||||
rt := timeMustParse(time.RFC822, test.source)
|
|
||||||
actual, err := Strftime("%d %b %y %H:%M %Z", rt)
|
|
||||||
require.NoErrorf(t, err, test.source)
|
|
||||||
require.Equalf(t, test.expect, actual, test.source)
|
|
||||||
}
|
|
||||||
|
|
||||||
dt = timeMustParse(time.RFC822, "02 Jan 06 15:04 MST")
|
|
||||||
tests := []struct{ format, expect string }{
|
tests := []struct{ format, expect string }{
|
||||||
{"%a, %b %d, %Y", "Mon, Jan 02, 2006"},
|
{"%a, %b %d, %Y", "Mon, Jan 02, 2006"},
|
||||||
{"%Y/%m/%d", "2006/01/02"},
|
{"%Y/%m/%d", "2006/01/02"},
|
||||||
@ -100,3 +107,42 @@ func TestStrftime(t *testing.T) {
|
|||||||
require.Equalf(t, test.expect, s, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user