Added tests and fixed timestamp handling
All checks were successful
/ publish (push) Successful in 47s
All checks were successful
/ publish (push) Successful in 47s
This commit is contained in:
parent
5c89c44ff7
commit
e74906e0c4
|
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -10,51 +9,66 @@ type TimeComponent struct {
|
||||||
TS *string `parser:"@Time"`
|
TS *string `parser:"@Time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TimeComponent) Time() (time.Time, error) {
|
func (t TimeComponent) Time(loc *time.Location) (time.Time, error) {
|
||||||
if strings.HasSuffix(*t.TS, "Z") {
|
return time.ParseInLocation("15:04:05", *t.TS, loc)
|
||||||
return time.ParseInLocation("15:04:05Z", *t.TS, time.UTC)
|
|
||||||
}
|
|
||||||
return time.ParseInLocation("15:04:05", *t.TS, time.Local)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TimeComponent) RequiresRefreshing() bool {
|
func (t TimeComponent) RequiresRefreshing() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimeInZoneComponent struct {
|
||||||
|
TS *string `parser:"@Time"`
|
||||||
|
InUTC bool `parser:"@Z?"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimeInZoneComponent) Time() (time.Time, error) {
|
||||||
|
loc := time.Local
|
||||||
|
if t.InUTC {
|
||||||
|
loc = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.ParseInLocation("15:04:05", *t.TS, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TimeInZoneComponent) RequiresRefreshing() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type DateWithTimeComponents struct {
|
type DateWithTimeComponents struct {
|
||||||
Date *string `parser:"@Date"`
|
Date *string `parser:"@Date"`
|
||||||
MaybeTime *TimeComponent `parser:" @@?"`
|
MaybeTime *TimeComponent `parser:"(T? @@)?"`
|
||||||
|
InUTC bool `parser:"(@Z)?"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DateWithTimeComponents) Time() (time.Time, error) {
|
func (d DateWithTimeComponents) Time() (time.Time, error) {
|
||||||
t, err := time.Parse("2006-01-02", *d.Date)
|
loc := time.Local
|
||||||
|
if d.InUTC {
|
||||||
|
loc = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.ParseInLocation("2006-01-02", *d.Date, loc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
if d.MaybeTime != nil {
|
if d.MaybeTime != nil {
|
||||||
tt, err := d.MaybeTime.Time()
|
tt, err := d.MaybeTime.Time(loc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
t = t.Add(tt.Sub(tt.Truncate(24 * time.Hour)))
|
t = time.Date(t.Year(), t.Month(), t.Day(), tt.Hour(), tt.Minute(), tt.Second(), 0, loc)
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Timestamp struct {
|
type Timestamp struct {
|
||||||
Full *string `parser:"@Timestamp"`
|
DT *DateWithTimeComponents `parser:"@@"`
|
||||||
DT *DateWithTimeComponents `parser:"| @@"`
|
TimeToday *TimeInZoneComponent `parser:" | @@"`
|
||||||
TimeToday *TimeComponent `parser:" | @@"`
|
|
||||||
Int *int `parser:" | @Int"`
|
Int *int `parser:" | @Int"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Timestamp) Time() (time.Time, error) {
|
func (t Timestamp) Time() (time.Time, error) {
|
||||||
switch {
|
switch {
|
||||||
case t.Full != nil:
|
|
||||||
if strings.HasSuffix(*t.Full, "Z") {
|
|
||||||
return time.ParseInLocation("2006-01-02T15:04:05Z", *t.Full, time.UTC)
|
|
||||||
}
|
|
||||||
return time.ParseInLocation("2006-01-02T15:04:05", *t.Full, time.Local)
|
|
||||||
case t.DT != nil:
|
case t.DT != nil:
|
||||||
return t.DT.Time()
|
return t.DT.Time()
|
||||||
case t.TimeToday != nil:
|
case t.TimeToday != nil:
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var basicLexer = lexer.MustSimple([]lexer.SimpleRule{
|
var basicLexer = lexer.MustSimple([]lexer.SimpleRule{
|
||||||
{"Identifier", `[a-zA-Z_][a-zA-Z0-9_]*`},
|
{"Identifier", `[a-z][a-zA-Z0-9_]*`},
|
||||||
{"Date", `\d{4}-\d{2}-\d{2}`},
|
{"Date", `\d{4}-\d{2}-\d{2}`},
|
||||||
{"Timestamp", `\d{4}-\d{2}-\d{2}T?\d{2}:\d{2}:\d{2}Z?`},
|
{"Time", `\d{2}:\d{2}:\d{2}`},
|
||||||
{"Time", `\d{2}:\d{2}:\d{2}Z?`},
|
|
||||||
{"Int", `\d+`},
|
{"Int", `\d+`},
|
||||||
|
{"T", `T`},
|
||||||
|
{"Z", `Z`},
|
||||||
{"Whitespace", `[ ]+`},
|
{"Whitespace", `[ ]+`},
|
||||||
})
|
})
|
||||||
|
|
||||||
var parser = participle.MustBuild[Atom](
|
var parser = participle.MustBuild[Atom](
|
||||||
participle.Lexer(basicLexer),
|
participle.Lexer(basicLexer),
|
||||||
participle.Elide("Whitespace"),
|
participle.Elide("Whitespace"),
|
||||||
|
participle.UseLookahead(2),
|
||||||
)
|
)
|
||||||
|
|
||||||
func Parse(expr string) (TimeResult, error) {
|
func Parse(expr string) (TimeResult, error) {
|
||||||
|
|
|
||||||
113
cmds/timestamps/input_test.go
Normal file
113
cmds/timestamps/input_test.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
// Reference time: 2025-11-19T11:07:13Z
|
||||||
|
// Unix: 1763550433
|
||||||
|
refTime := time.Date(2025, 11, 19, 11, 7, 13, 0, time.UTC)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected func() time.Time
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "1763550433",
|
||||||
|
expected: func() time.Time {
|
||||||
|
return refTime
|
||||||
|
},
|
||||||
|
desc: "Unix timestamp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "2025-11-19T11:07:13Z",
|
||||||
|
expected: func() time.Time {
|
||||||
|
return refTime
|
||||||
|
},
|
||||||
|
desc: "ISO 8601 UTC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "2025-11-19T11:07:13",
|
||||||
|
expected: func() time.Time {
|
||||||
|
return time.Date(2025, 11, 19, 11, 7, 13, 0, time.Local)
|
||||||
|
},
|
||||||
|
desc: "ISO 8601 Local",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "2025-11-19 11:07:13Z",
|
||||||
|
expected: func() time.Time {
|
||||||
|
return refTime
|
||||||
|
},
|
||||||
|
desc: "Date Space Time Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "2025-11-19",
|
||||||
|
expected: func() time.Time {
|
||||||
|
return time.Date(2025, 11, 19, 0, 0, 0, 0, time.Local)
|
||||||
|
},
|
||||||
|
desc: "Date only Local",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "2025-11-19Z",
|
||||||
|
expected: func() time.Time {
|
||||||
|
return time.Date(2025, 11, 19, 0, 0, 0, 0, time.UTC)
|
||||||
|
},
|
||||||
|
desc: "Date only UTC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "11:07:13",
|
||||||
|
expected: func() time.Time {
|
||||||
|
now := time.Now()
|
||||||
|
return time.Date(now.Year(), now.Month(), now.Day(), 11, 7, 13, 0, time.Local)
|
||||||
|
},
|
||||||
|
desc: "Time only Local",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "11:07:13Z",
|
||||||
|
expected: func() time.Time {
|
||||||
|
now := time.Now().In(time.UTC)
|
||||||
|
return time.Date(now.Year(), now.Month(), now.Day(), 11, 7, 13, 0, time.UTC)
|
||||||
|
},
|
||||||
|
desc: "Time only UTC",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
res, err := Parse(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse(%q) error: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
got, err := res.Time()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("res.Time() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := tc.expected()
|
||||||
|
|
||||||
|
// For "Time only" tests, there might be a slight race condition if the day changes between test execution and expected value generation.
|
||||||
|
// But practically it's fine.
|
||||||
|
|
||||||
|
if !got.Truncate(time.Second).Equal(want) {
|
||||||
|
t.Errorf("Parse(%q) = %v, want %v", tc.input, got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("now", func(t *testing.T) {
|
||||||
|
res, err := Parse("now")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse('now') error: %v", err)
|
||||||
|
}
|
||||||
|
got, err := res.Time()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("res.Time() error: %v", err)
|
||||||
|
}
|
||||||
|
if time.Since(got) > time.Second {
|
||||||
|
t.Errorf("Parse('now') = %v, want close to now", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<title>Tools</title>
|
<title>Tools</title>
|
||||||
<link
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
|
|
||||||
>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="container">
|
<body class="container">
|
||||||
<header>
|
<header>
|
||||||
<hgroup>
|
<hgroup>
|
||||||
<h1>Tools</h1>
|
<h1>Tools</h1>
|
||||||
<p>Collection of online tools</p>
|
<p>Collection of online tools</p>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/clocks/">Clocks</a></li>
|
<li><a href="/clocks/">Clocks</a></li>
|
||||||
|
|
@ -24,7 +23,9 @@
|
||||||
<li><a href="/gotemplate/">Go Template Playground</a></li>
|
<li><a href="/gotemplate/">Go Template Playground</a></li>
|
||||||
<li><a href="/gradient-bands/">Gradient Bands</a></li>
|
<li><a href="/gradient-bands/">Gradient Bands</a></li>
|
||||||
<li><a href="/2lcc/">Two-letter Country Codes</a></li>
|
<li><a href="/2lcc/">Two-letter Country Codes</a></li>
|
||||||
|
<li><a href="/timestamps/">Timestamp Converter</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in a new issue