Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
834395c050
|
||
|
|
ef56898d6b
|
||
|
|
006cf39fa1
|
1
go.sum
1
go.sum
@@ -13,3 +13,4 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
r00t2.io/sysutils v1.15.0 h1:FSnREfbXDhBQEO7LMpnRQeKlPshozxk9XHw3YgWRgRg=
|
||||
r00t2.io/sysutils v1.15.0/go.mod h1:28qB0074EIRQ8Sy/ybaA5jC3qA32iW2aYLkMCRhyAFM=
|
||||
r00t2.io/sysutils v1.15.1/go.mod h1:T0iOnaZaSG5NE1hbXTqojRZc0ia/u8TB73lV7zhMz58=
|
||||
|
||||
10
remap/doc.go
10
remap/doc.go
@@ -1,4 +1,12 @@
|
||||
/*
|
||||
Package remap provides convenience functions around regular expressions, primarily offering maps for named capture groups.
|
||||
Package remap provides convenience functions around regular expressions,
|
||||
primarily offering maps for named capture groups.
|
||||
|
||||
It offers convenience equivalents of the following:
|
||||
|
||||
* [regexp.Compile] ([Compile])
|
||||
* [regexp.CompilePOSIX] ([CompilePOSIX])
|
||||
* [regexp.MustCompile] ([MustCompile])
|
||||
* [regexp.MustCompilePOSIX] ([MustCompilePOSIX])
|
||||
*/
|
||||
package remap
|
||||
|
||||
113
remap/funcs.go
Normal file
113
remap/funcs.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package remap
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
/*
|
||||
Compile is a convenience shorthand for:
|
||||
|
||||
var err error
|
||||
var r *remap.ReMap = new(remap.ReMap)
|
||||
|
||||
if r.Regexp, err = regexp.Compile(expr); err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
It corresponds to [regexp.Compile].
|
||||
*/
|
||||
func Compile(expr string) (r *ReMap, err error) {
|
||||
|
||||
var p *regexp.Regexp
|
||||
|
||||
if p, err = regexp.Compile(expr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CompilePOSIX is a convenience shorthand for:
|
||||
|
||||
var err error
|
||||
var r *remap.ReMap = new(remap.ReMap)
|
||||
|
||||
if r.Regexp, err = regexp.CompilePOSIX(expr); err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
It corresponds to [regexp.CompilePOSIX].
|
||||
*/
|
||||
func CompilePOSIX(expr string) (r *ReMap, err error) {
|
||||
|
||||
var p *regexp.Regexp
|
||||
|
||||
if p, err = regexp.CompilePOSIX(expr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MustCompile is a convenience shorthand for:
|
||||
|
||||
var r *remap.ReMap = &remap.ReMap{
|
||||
Regexp: regexp.MustCompile(expr),
|
||||
}
|
||||
|
||||
It corresponds to [regexp.MustCompile].
|
||||
*/
|
||||
func MustCompile(expr string) (r *ReMap) {
|
||||
|
||||
var err error
|
||||
var p *regexp.Regexp
|
||||
|
||||
// We panic ourselves instead of wrapping regexp.MustCompile.
|
||||
// Makes debuggers a little more explicit.
|
||||
if p, err = regexp.Compile(expr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MustCompilePOSIX is a convenience shorthand for:
|
||||
|
||||
var r *remap.ReMap = &remap.ReMap{
|
||||
Regexp: regexp.MustCompilePOSIX(expr),
|
||||
}
|
||||
|
||||
It corresponds to [regexp.MustCompilePOSIX].
|
||||
*/
|
||||
func MustCompilePOSIX(expr string) (r *ReMap) {
|
||||
|
||||
var err error
|
||||
var p *regexp.Regexp
|
||||
|
||||
// We panic ourselves instead of wrapping regexp.MustCompilePOSIX.
|
||||
// Makes debuggers a little more explicit.
|
||||
if p, err = regexp.CompilePOSIX(expr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -5,6 +5,8 @@ Map returns a map[string][]<match bytes> for regexes with named capture groups m
|
||||
Note that this supports non-unique group names; [regexp.Regexp] allows for patterns with multiple groups
|
||||
using the same group name (though your IDE might complain; I know GoLand does).
|
||||
|
||||
It will panic if the embedded [regexp.Regexp] is nil.
|
||||
|
||||
Each match for each group is in a slice keyed under that group name, with that slice
|
||||
ordered by the indexing done by the regex match itself.
|
||||
|
||||
@@ -87,7 +89,7 @@ In detail, matches and/or its values may be nil or empty under the following con
|
||||
IF inclNoMatch is true
|
||||
IF inclNoMatchStrict is true
|
||||
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
|
||||
(matches[<group name>] == [][]byte{nil[, nil...]})
|
||||
(matches[<group name>] == [][]byte{nil[, nil, ...]})
|
||||
ELSE
|
||||
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
|
||||
ELSE
|
||||
@@ -109,7 +111,7 @@ func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (m
|
||||
return
|
||||
}
|
||||
|
||||
names = r.Regexp.SubexpNames()
|
||||
names = r.Regexp.SubexpNames()[:]
|
||||
matchBytes = r.Regexp.FindSubmatch(b)
|
||||
|
||||
if matchBytes == nil {
|
||||
@@ -204,13 +206,15 @@ func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (m
|
||||
}
|
||||
|
||||
/*
|
||||
MapString is exactly like ReMap.Map(), but operates on (and returns) strings instead.
|
||||
MapString is exactly like [ReMap.Map], but operates on (and returns) strings instead.
|
||||
(matches will always be nil if s == “.)
|
||||
|
||||
A small deviation, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
|
||||
It will panic if the embedded [regexp.Regexp] is nil.
|
||||
|
||||
A small deviation and caveat, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
|
||||
This unfortunately *does not provide any indication* if an empty string positively matched the pattern (a "hit") or if it was simply
|
||||
not matched at all (a "miss"). If you need definitive determination between the two conditions, it is instead recommended to either
|
||||
*not* use inclNoMatchStrict or to use ReMap.Map() instead and convert any non-nil values to strings after.
|
||||
*not* use inclNoMatchStrict or to use [ReMap.Map] instead and convert any non-nil values to strings after.
|
||||
|
||||
Particularly:
|
||||
|
||||
@@ -233,7 +237,7 @@ is provided but s does not match then matches will be:
|
||||
# inclNoMatchStrict
|
||||
|
||||
If true (and inclNoMatch is true), instead of a single nil the group's values will be
|
||||
a slice of eempty string values explicitly matching the number of times the group name is specified
|
||||
a slice of empty string values explicitly matching the number of times the group name is specified
|
||||
in the pattern.
|
||||
|
||||
For example, if a pattern:
|
||||
@@ -290,8 +294,8 @@ In detail, matches and/or its values may be nil or empty under the following con
|
||||
IF <group name> does not have a match
|
||||
IF inclNoMatch is true
|
||||
IF inclNoMatchStrict is true
|
||||
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
|
||||
(matches[<group name>] == []string{""[, ""...]})
|
||||
THEN matches[<group name>] is defined and non-nil, but populated with placeholder strings
|
||||
(matches[<group name>] == []string{""[, "", ...]})
|
||||
ELSE
|
||||
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
|
||||
ELSE
|
||||
@@ -334,7 +338,8 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
|
||||
return
|
||||
}
|
||||
/*
|
||||
I'm not entirely sure how serious they are about "the slice should not be modified"...
|
||||
I'm not entirely sure how serious they are about
|
||||
"the slice should not be modified"...
|
||||
|
||||
DO NOT sort or dedupe `names`! If the same name for groups is duplicated,
|
||||
it will be duplicated here in proper order and the ordering is tied to
|
||||
@@ -351,7 +356,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
|
||||
return
|
||||
}
|
||||
|
||||
if names == nil || len(names) <= 1 {
|
||||
if names == nil || len(names) == 0 || len(names) == 1 {
|
||||
/*
|
||||
No named capture groups;
|
||||
technically only the last condition would be the case,
|
||||
@@ -363,6 +368,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
|
||||
}
|
||||
return
|
||||
}
|
||||
names = names[1:]
|
||||
|
||||
if len(matchIndices) == 0 || len(matchIndices) == 1 {
|
||||
/*
|
||||
@@ -385,15 +391,15 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
|
||||
}
|
||||
return
|
||||
}
|
||||
matchIndices = matchIndices[2:]
|
||||
|
||||
/*
|
||||
A reslice of `matchIndices` could technically start at 2 (as long as `names` is sliced [1:])
|
||||
A reslice of `matchIndices` starts at 2 (as long as `names` is sliced [1:])
|
||||
because they're in pairs: []int{<start>, <end>, <start>, <end>, ...}
|
||||
and the first pair is the entire pattern match (un-resliced names[0]).
|
||||
Thus the len(matchIndices) == 2*len(names), *even* if you
|
||||
Thus the len(matchIndices) == 2*len(names), *even* if you reslice.
|
||||
Keep in mind that since the first element of names is removed,
|
||||
the first pair here is skipped.
|
||||
This provides a bit more consistent readability, though.
|
||||
we reslices matchIndices as well (above).
|
||||
*/
|
||||
idxChunks = make([][]int, len(names))
|
||||
chunkIdx = 0
|
||||
@@ -411,6 +417,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
|
||||
// group did not match
|
||||
chunkIndices = nil
|
||||
} else {
|
||||
// single character
|
||||
if chunkIndices[0] == chunkIndices[1] {
|
||||
chunkIndices = []int{chunkIndices[0]}
|
||||
} else {
|
||||
@@ -432,6 +439,7 @@ func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bo
|
||||
(which is either an *unnamed* capture group
|
||||
OR the first element in `names`, which is always
|
||||
the entire match).
|
||||
(We reslice out the latter.)
|
||||
*/
|
||||
if grpNm == "" {
|
||||
continue
|
||||
|
||||
279
remap/funcs_remap_test.go
Normal file
279
remap/funcs_remap_test.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package remap
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`reflect`
|
||||
`regexp`
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
testMatcher struct {
|
||||
Nm string
|
||||
S string
|
||||
M *ReMap
|
||||
Expected map[string][][]byte
|
||||
ExpectedStr map[string][]string
|
||||
ParamInclNoMatch bool
|
||||
ParamInclNoMatchStrict bool
|
||||
ParamInclMustMatch bool
|
||||
}
|
||||
)
|
||||
|
||||
func TestRemap(t *testing.T) {
|
||||
|
||||
var matches map[string][][]byte
|
||||
|
||||
for midx, m := range []testMatcher{
|
||||
testMatcher{
|
||||
Nm: "No matches",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "Single mid match",
|
||||
S: "This contains a single match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match)\s+`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{[]byte("match")},
|
||||
},
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "multi mid match",
|
||||
S: "This contains a single match and another match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another (?P<g1>match)\s+`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{
|
||||
[]byte("match"),
|
||||
[]byte("match"),
|
||||
},
|
||||
},
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "line match",
|
||||
S: "This\ncontains a\nsingle\nmatch\non a dedicated line",
|
||||
M: &ReMap{regexp.MustCompile(`(?m)^(?P<g1>match)$`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{
|
||||
[]byte("match"),
|
||||
},
|
||||
},
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "multiline match",
|
||||
S: "This\ncontains a\nsingle match and another\nmatch\nin the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another\s+(?P<g1>match)\s+`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{
|
||||
[]byte("match"),
|
||||
[]byte("match"),
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
matches = m.M.Map([]byte(m.S), false, false, false)
|
||||
t.Logf(
|
||||
"#%d:\n\tsrc:\t'%s'\n\tptrn:\t'%s'\n\tmatch:\t%s\n",
|
||||
midx+1,
|
||||
m.S,
|
||||
m.M.Regexp.String(),
|
||||
testBmapToStrMap(matches),
|
||||
)
|
||||
if !reflect.DeepEqual(matches, m.Expected) {
|
||||
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.Expected, matches)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRemapParams(t *testing.T) {
|
||||
|
||||
var matches map[string][][]byte
|
||||
|
||||
for midx, m := range []testMatcher{
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
} {
|
||||
matches = m.M.Map([]byte(m.S), m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch)
|
||||
t.Logf(
|
||||
"%d: %v/%v/%v: %#v\n",
|
||||
midx+1, m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch, matches,
|
||||
)
|
||||
if !reflect.DeepEqual(matches, m.Expected) {
|
||||
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.ExpectedStr, matches)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRemapString(t *testing.T) {
|
||||
|
||||
var matches map[string][]string
|
||||
|
||||
for midx, m := range []testMatcher{
|
||||
testMatcher{
|
||||
Nm: "No matches",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
ExpectedStr: nil,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "Single mid match",
|
||||
S: "This contains a single match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match)\s+`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{"match"},
|
||||
},
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "multi mid match",
|
||||
S: "This contains a single match and another match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another (?P<g1>match)\s+`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{
|
||||
"match",
|
||||
"match",
|
||||
},
|
||||
},
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "line match",
|
||||
S: "This\ncontains a\nsingle\nmatch\non a dedicated line",
|
||||
M: &ReMap{regexp.MustCompile(`(?m)^(?P<g1>match)$`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{
|
||||
"match",
|
||||
},
|
||||
},
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "multiline match",
|
||||
S: "This\ncontains a\nsingle match and another\nmatch\nin the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another\s+(?P<g1>match)\s+`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{
|
||||
"match",
|
||||
"match",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
matches = m.M.MapString(m.S, false, false, false)
|
||||
t.Logf(
|
||||
"#%d:\n\tsrc:\t'%s'\n\tptrn:\t'%s'\n\tmatch:\t%s\n",
|
||||
midx+1,
|
||||
m.S,
|
||||
m.M.Regexp.String(),
|
||||
testSmapToStrMap(matches),
|
||||
)
|
||||
if !reflect.DeepEqual(matches, m.ExpectedStr) {
|
||||
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.ExpectedStr, matches)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testBmapToStrMap(bmap map[string][][]byte) (s string) {
|
||||
|
||||
if bmap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = "\n"
|
||||
for k, v := range bmap {
|
||||
s += fmt.Sprintf("\t%s\n", k)
|
||||
for _, i := range v {
|
||||
s += fmt.Sprintf("\t\t%s\n", string(i))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func testSmapToStrMap(smap map[string][]string) (s string) {
|
||||
|
||||
if smap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = "\n"
|
||||
for k, v := range smap {
|
||||
s += fmt.Sprintf("\t%s\n", k)
|
||||
for _, i := range v {
|
||||
s += fmt.Sprintf("\t\t%s\n", i)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// ReMap provides some map-related functions around a regexp.Regexp.
|
||||
// ReMap provides some map-related functions around a [regexp.Regexp].
|
||||
ReMap struct {
|
||||
*regexp.Regexp
|
||||
}
|
||||
|
||||
4
timex/doc.go
Normal file
4
timex/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package timex provides some handy [time]-related functions.
|
||||
*/
|
||||
package timex
|
||||
35
timex/funcs.go
Normal file
35
timex/funcs.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package timex
|
||||
|
||||
import (
|
||||
`time`
|
||||
)
|
||||
|
||||
/*
|
||||
F64Seconds returns [time.Time] `t` as a 64-bit float of <seconds>.<nanoseconds>
|
||||
(where <nanoseconds> is the number of nanoseconds since <seconds>,
|
||||
and <seconds> is the number of seconds since the UNIX epoch).
|
||||
|
||||
This can be used to represent a UNIX Epoch timestamp as seconds but with nanosecond precision.
|
||||
*/
|
||||
func F64Seconds(t time.Time) (f64 float64) {
|
||||
return F64Nanoseconds(t) / float64(time.Second)
|
||||
}
|
||||
|
||||
/*
|
||||
F64Milliseconds is like [F64Seconds] but with a millisecond integer.
|
||||
*/
|
||||
func F64Milliseconds(t time.Time) (f64 float64) {
|
||||
return F64Nanoseconds(t) / float64(time.Millisecond)
|
||||
}
|
||||
|
||||
/*
|
||||
F64Microseconds is like [F64Seconds] but with a microsecond integer.
|
||||
*/
|
||||
func F64Microseconds(t time.Time) (f64 float64) {
|
||||
return F64Nanoseconds(t) / float64(time.Microsecond)
|
||||
}
|
||||
|
||||
// F64Nanoseconds returns [time.Time.UnixNano] as a float64.
|
||||
func F64Nanoseconds(t time.Time) (f64 float64) {
|
||||
return float64(t.UnixNano())
|
||||
}
|
||||
30
timex/funcs_test.go
Normal file
30
timex/funcs_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package timex
|
||||
|
||||
import (
|
||||
"testing"
|
||||
`time`
|
||||
)
|
||||
|
||||
func TestF64(t *testing.T) {
|
||||
|
||||
var tmNano float64 = 1766533329999999999
|
||||
var tmSeconds float64 = 1766533329.999999999
|
||||
var tmMilli float64 = 1766533329999.999999
|
||||
var tmMicro float64 = 1766533329999999.999
|
||||
// 2025-12-23 23:42:09.999999999 +0000 UTC
|
||||
var tm time.Time = time.Unix(1766533329, int64(time.Second-1))
|
||||
|
||||
if F64Seconds(tm) != tmSeconds {
|
||||
t.Fatalf("Failed seconds: %f != %f", F64Seconds(tm), tmSeconds)
|
||||
}
|
||||
if F64Milliseconds(tm) != tmMilli {
|
||||
t.Fatalf("Failed milliseconds: %f != %f", F64Milliseconds(tm), tmMilli)
|
||||
}
|
||||
if F64Microseconds(tm) != tmMicro {
|
||||
t.Fatalf("Failed microseconds: %f != %f", F64Microseconds(tm), tmMicro)
|
||||
}
|
||||
if F64Nanoseconds(tm) != tmNano {
|
||||
t.Fatalf("Failed nanoseconds: %f != %f", F64Nanoseconds(tm), tmNano)
|
||||
}
|
||||
|
||||
}
|
||||
6
tplx/consts.go
Normal file
6
tplx/consts.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package tplx
|
||||
|
||||
const (
|
||||
TplTypeText tplType = iota
|
||||
TplTypeHtml
|
||||
)
|
||||
4
tplx/doc.go
Normal file
4
tplx/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package tplx provides some "shortcuts" to [text/template] and [html/template] rendering.
|
||||
*/
|
||||
package tplx
|
||||
9
tplx/errs.go
Normal file
9
tplx/errs.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidTplType = errors.New("unknown/invalid template type")
|
||||
)
|
||||
235
tplx/funcs.go
Normal file
235
tplx/funcs.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
htmlTpl `html/template`
|
||||
txtTpl `text/template`
|
||||
)
|
||||
|
||||
// MustTplStrToStr wraps [TplStrToStr] but will panic on a non-nil error instead of returning it.
|
||||
func MustTplStrToStr(tplStr string, typ tplType, obj any) (s string) {
|
||||
|
||||
var err error
|
||||
|
||||
if s, err = TplStrToStr(tplStr, typ, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MustTplToStr wraps [TplToStr] but will panic on error instead of returning it.
|
||||
func MustTplToStr[T Template](tpl T, obj any) (s string) {
|
||||
|
||||
var err error
|
||||
|
||||
if s, err = TplToStr(tpl, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MustTplToStrWith wraps [TplToStrWith] but will panic on error instead of returning it.
|
||||
func MustTplToStrWith[T Template](tpl T, tplNm string, obj any) (s string) {
|
||||
|
||||
var err error
|
||||
|
||||
if s, err = TplToStrWith(tpl, tplNm, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TplStrToStr takes in a template string, a template type (see i.e. [TplTypeText], [TplTypeHtml]),
|
||||
and an object and renders to a string.
|
||||
|
||||
This is obviously quite inflexible - there's no way to provide a [text/template.FuncMap]/[html/template.FuncMap],
|
||||
for instance, but if more advanced template features aren't needed then this might just do the trick.
|
||||
|
||||
If you need something more flexible, see [TplToStr] instead.
|
||||
*/
|
||||
func TplStrToStr(tplStr string, typ tplType, obj any) (out string, err error) {
|
||||
|
||||
var ttpl *txtTpl.Template
|
||||
var htpl *htmlTpl.Template
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
switch typ {
|
||||
case TplTypeText:
|
||||
if ttpl, err = txtTpl.New("").Parse(tplStr); err != nil {
|
||||
return
|
||||
}
|
||||
if err = ttpl.Execute(buf, obj); err != nil {
|
||||
return
|
||||
}
|
||||
case TplTypeHtml:
|
||||
if htpl, err = htmlTpl.New("").Parse(tplStr); err != nil {
|
||||
return
|
||||
}
|
||||
if err = htpl.Execute(buf, obj); err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = ErrInvalidTplType
|
||||
return
|
||||
}
|
||||
|
||||
out = buf.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TplToStr takes in an [html/template] or [text/template] and an object and executes it.
|
||||
|
||||
PLEASE NOTE that it is expected that `tpl` has already had at least one template string `.Parse()`'d in.
|
||||
|
||||
If you haven't used generics in Golang yet, this function would be used via something like the following complete example
|
||||
for both a [text/template.Template] (import-aliased as `txtT.Template`) and
|
||||
an [html/template.Template] (import-aliased as `htmlT.Template`).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
txtT "text/template"
|
||||
htmlT "html/template"
|
||||
|
||||
`r00t2.io/goutils/tplx`
|
||||
)
|
||||
|
||||
type (
|
||||
S struct {
|
||||
Name string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
tTpl *txtT.Template
|
||||
hTpl *htmlT.Template
|
||||
)
|
||||
|
||||
const tTplStr string = "Greetings, {{ .Name }}!\n"
|
||||
const hTplStr string = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hello, {{ .Name }}!</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, {{ .Name }}. Good to see you.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var s string
|
||||
var o *S
|
||||
|
||||
o = &S{
|
||||
Name: "Bob",
|
||||
}
|
||||
|
||||
// A text template.
|
||||
if tTpl, err = txtT.
|
||||
New("my_txt_template").
|
||||
Parse(tTplStr); err != nil {
|
||||
log.Panicf("Failed to parse text template string '%s': %v\n", tTplStr, err)
|
||||
}
|
||||
if s, err = tplx.TplToStr[*txtT.Template](tTpl, o); err != nil {
|
||||
log.Panicf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
fmt.Println(s)
|
||||
|
||||
// An HTML template.
|
||||
if hTpl, err = htmlT.
|
||||
New("index.html").
|
||||
Parse(hTplStr); err != nil {
|
||||
log.Panicf("Failed to parse HTML template string '%s': %v\n", hTplStr, err)
|
||||
}
|
||||
if s, err = tplx.TplToStr[*htmlT.Template](hTpl, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
Additionally, because this function uses a union type [Template],
|
||||
you can even leave the type indicator off.
|
||||
For example:
|
||||
|
||||
// ...
|
||||
if s, err = tplx.TplToStr(tTpl, o); err != nil {
|
||||
log.Panicf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
// ...
|
||||
if s, err = tplx.TplToStr(hTpl, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
// ...
|
||||
|
||||
However, this is not recommended for readability purposes - including
|
||||
the type indicator indicates (heh heh) to others reading your code
|
||||
what type `tTpl` and `hTpl` are without needing to cross-reference
|
||||
their declaration/assignment/definition.
|
||||
|
||||
For more information on generics in Golang, see:
|
||||
|
||||
* The introductory [blog post]
|
||||
* The official [tutorial]
|
||||
* The syntax [reference doc]
|
||||
* The (community-maintained/unofficial) [Go by Example: Generics]
|
||||
|
||||
[blog post]: https://go.dev/blog/intro-generics
|
||||
[tutorial]: https://go.dev/doc/tutorial/generics
|
||||
[reference doc]: https://go.dev/ref/spec#Instantiations
|
||||
[Go by Example: Generics]: https://gobyexample.com/generics
|
||||
*/
|
||||
func TplToStr[T Template](tpl T, obj any) (out string, err error) {
|
||||
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
if err = tpl.Execute(buf, obj); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
out = buf.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TplToStrWith functions the exact same as [TplToStr] but allows you to specify the
|
||||
template entry point (template name) named `nm`.
|
||||
|
||||
For example (see [TplToStr] for a full example):
|
||||
|
||||
// ...
|
||||
var tplNm string = "index.html"
|
||||
|
||||
if s, err = tplx.TplToStrWith(tTpl, tplNm, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template '%s' to string: %v\n", tplNm, err)
|
||||
}
|
||||
// ...
|
||||
|
||||
would call the equivalent of:
|
||||
|
||||
// ...
|
||||
if err = tpl.ExecuteTemplate(<internal buffer>, tplNm, o); err != nil {
|
||||
// ...
|
||||
}
|
||||
*/
|
||||
func TplToStrWith[T Template](tpl T, tplNm string, obj any) (out string, err error) {
|
||||
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
if err = tpl.ExecuteTemplate(buf, tplNm, obj); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
out = buf.String()
|
||||
|
||||
return
|
||||
}
|
||||
103
tplx/funcs_test.go
Normal file
103
tplx/funcs_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
htmlT `html/template`
|
||||
`log`
|
||||
"testing"
|
||||
txtT `text/template`
|
||||
)
|
||||
|
||||
const (
|
||||
txtTplNm string = "my_txt_template"
|
||||
htmlTplNm string = "index.html"
|
||||
tgtTxt string = "Greetings, Bob!\n"
|
||||
tgtHtml string = "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Hello, Bob!</title>\n\t</head>\n\t<body>\n\t\t<p>Hello, Bob. Good to see you.</p>\n\t</body>\n</html>\n"
|
||||
tTplStr string = "Greetings, {{ .Name }}!\n"
|
||||
hTplStr string = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hello, {{ .Name }}!</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, {{ .Name }}. Good to see you.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
tTpl *txtT.Template = txtT.Must(txtT.New(txtTplNm).Parse(tTplStr))
|
||||
hTpl *htmlT.Template = htmlT.Must(htmlT.New(htmlTplNm).Parse(hTplStr))
|
||||
o struct{ Name string } = struct{ Name string }{
|
||||
Name: "Bob",
|
||||
}
|
||||
)
|
||||
|
||||
func TestTpl(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
|
||||
// if s, err = TplToStr[*txtT.Template](tTpl, o); err != nil {
|
||||
if s, err = TplToStr(tTpl, o); err != nil {
|
||||
t.Fatalf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("Text template (%#v): '%s'", s, s)
|
||||
if s != tgtTxt {
|
||||
t.Fatalf("Mismatch on text template '%s'", s)
|
||||
}
|
||||
|
||||
// if s, err = TplToStr[*htmlT.Template](hTpl, o); err != nil {
|
||||
if s, err = TplToStr(hTpl, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("HTML template (%#v):\n%s", s, s)
|
||||
if s != tgtHtml {
|
||||
t.Fatalf("Mismatch on HTML template '%s'", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTplStr(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
|
||||
if s, err = TplStrToStr(tTplStr, TplTypeText, o); err != nil {
|
||||
t.Fatalf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("Text template (%#v): '%s'", s, s)
|
||||
if s != tgtTxt {
|
||||
t.Fatalf("Mismatch on text template '%s'", s)
|
||||
}
|
||||
|
||||
if s, err = TplStrToStr(hTplStr, TplTypeHtml, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("HTML template (%#v):\n%s", s, s)
|
||||
if s != tgtHtml {
|
||||
t.Fatalf("Mismatch on HTML template '%s'", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTplWith(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
|
||||
if s, err = TplToStrWith(tTpl, txtTplNm, o); err != nil {
|
||||
t.Fatalf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("Text template (%#v): '%s'", s, s)
|
||||
if s != tgtTxt {
|
||||
t.Fatalf("Mismatch on text template '%s'", s)
|
||||
}
|
||||
|
||||
if s, err = TplToStrWith(hTpl, htmlTplNm, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("HTML template (%#v):\n%s", s, s)
|
||||
if s != tgtHtml {
|
||||
t.Fatalf("Mismatch on HTML template '%s'", s)
|
||||
}
|
||||
}
|
||||
19
tplx/types.go
Normal file
19
tplx/types.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
htmlTpl `html/template`
|
||||
`io`
|
||||
txtTpl `text/template`
|
||||
)
|
||||
|
||||
type (
|
||||
tplType uint8
|
||||
)
|
||||
|
||||
type (
|
||||
Template interface {
|
||||
*txtTpl.Template | *htmlTpl.Template
|
||||
Execute(w io.Writer, obj any) (err error)
|
||||
ExecuteTemplate(w io.Writer, tplNm string, obj any) (err error)
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user